<?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: stepinto.dev</title>
    <description>The latest articles on DEV Community by stepinto.dev (@stepinto).</description>
    <link>https://dev.to/stepinto</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%2F4186945%2F27bce636-dcde-4827-83aa-0f2f1f2a14e9.png</url>
      <title>DEV Community: stepinto.dev</title>
      <link>https://dev.to/stepinto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stepinto"/>
    <language>en</language>
    <item>
      <title>Latent State Is the Fields You Never Logged</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:23:35 +0000</pubDate>
      <link>https://dev.to/stepinto/latent-state-is-the-fields-you-never-logged-23ip</link>
      <guid>https://dev.to/stepinto/latent-state-is-the-fields-you-never-logged-23ip</guid>
      <description>&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/world-models-103/" rel="noopener noreferrer"&gt;last post&lt;/a&gt; ended with a challenge. Four weights fitted from logs can learn a room, but give that room a door that opens and closes on a schedule the logger never records, and the best a linear model can do is average over the door.&lt;/p&gt;

&lt;p&gt;The log has three columns: the room temperature, the outside temperature and whether the heater is on. When the door opens the room cools faster, and every row written while it is open looks like a row from a room that is simply worse at holding heat. The model sees the effect and never the cause, so fitting the same three columns harder gets it nowhere. What the log does still have is order. A room that was cooling steadily and then drops three degrees in one step has told you something, even if no column says what.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/world-models-102/" rel="noopener noreferrer"&gt;102 post&lt;/a&gt; described how we might work around this: give the model a private field it keeps for itself, and let it decide what to put there. It also promised to explain how such a field can be learned when nobody ever sees it.&lt;/p&gt;

&lt;p&gt;This post builds the room with the door, watches the four-weight model fail on it, gives the model one private field, and watches the field turn into the door. The maths comes at the end, because by then it describes something you have watched run. The &lt;a href="https://stepinto.dev/heater/?scenario=door" rel="noopener noreferrer"&gt;playground&lt;/a&gt; has the door in it now, so everything below can be rerun in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  An invisible door (according to the data)
&lt;/h2&gt;

&lt;p&gt;I'm using the same simulated room from the last post, with one addition. Someone keeps opening the door, and while it is open the room leaks heat three and a half times faster.&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;LEAK_CLOSED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// the 103 room&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LEAK_OPEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;doorOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// the room's secret; never written to the log&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;someone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// opens the door about once every 25 steps, leaves it open about 8&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doorOpen&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="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;doorOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;doorOpen&lt;/span&gt; &lt;span class="o"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;realRoom&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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heaterAfter&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;action&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;leak&lt;/span&gt; &lt;span class="o"&gt;=&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;outsideTemp&lt;/span&gt; &lt;span class="o"&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;roomTemp&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;doorOpen&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;LEAK_OPEN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LEAK_CLOSED&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;heat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;REAL_HEAT&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&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;roomTemp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;heat&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;gauss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;NOISE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&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;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&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 rule from the last post still holds: nothing below this line looks at &lt;code&gt;doorOpen&lt;/code&gt;. The logger calls &lt;code&gt;someone()&lt;/code&gt; before every step and writes down the same three fields it always did, so a row from the new log is indistinguishable from a row from the old one. Eighty episodes of sixty steps gives 4,800 transitions, and the door is open for 22 percent of them. Nothing in the log says which.&lt;/p&gt;

&lt;p&gt;If you run devices in the field, this is every column your telemetry does not have. The window someone left open, the second heater in the room that is not on your network, the pump that was switched to manual for an afternoon. The dynamics change and the log does not say why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What four weights do with a door
&lt;/h2&gt;

&lt;p&gt;First I refitted the four-weight model on the new log, with the same &lt;code&gt;features&lt;/code&gt;, the same twenty-line training loop and ten thousand epochs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;weight&lt;/th&gt;
&lt;th&gt;sealed room (103)&lt;/th&gt;
&lt;th&gt;with the door&lt;/th&gt;
&lt;th&gt;truth, door closed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;room&lt;/td&gt;
&lt;td&gt;-0.074&lt;/td&gt;
&lt;td&gt;-0.037&lt;/td&gt;
&lt;td&gt;-0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outside&lt;/td&gt;
&lt;td&gt;0.069&lt;/td&gt;
&lt;td&gt;0.040&lt;/td&gt;
&lt;td&gt;0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;heater&lt;/td&gt;
&lt;td&gt;1.19&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bias&lt;/td&gt;
&lt;td&gt;0.077&lt;/td&gt;
&lt;td&gt;-0.41&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I expected the loss to get worse, and it flattens at 0.29 against 0.01 in the sealed room. I didn't expect the shape of the damage. The model has not averaged over the door, which would have given a room weight somewhere between the closed and open leak rates. It has halved the room weight and gone looking for the lost heat in the bias.&lt;/p&gt;

&lt;p&gt;The reason is that the door is not independent of the things the model can see. An open door drags the room down, so in the log a cold room and a big drop arrive together, and the only lever the model has for "cold room" is the room weight. It pushes that weight towards zero to account for the drops, then pulls the bias negative to make the sums balance. A hidden variable that moves with the visible ones bends the fit, which is rather worse than adding noise to it.&lt;/p&gt;

&lt;p&gt;You can see the bend when the model is asked to forecast. Later in the post there is a room at minus four with the heater on, where the door opens at step 10. Ask this model at step 13, with the room at 7.8 degrees and falling fast, what the next five steps look like, and it says the room warms to 8.3. The real room goes to 2.7.&lt;/p&gt;

&lt;h2&gt;
  
  
  One private field
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/world-models-102/" rel="noopener noreferrer"&gt;102 post&lt;/a&gt; sketched the shape of the fix as a class with a field nobody outside can read:&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;class&lt;/span&gt; &lt;span class="nc"&gt;WorldModel&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="nb"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// s_t lives here, and nowhere in your logs&lt;/span&gt;
  &lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fold the new reading into #state */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Distribution&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Observation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* predict, given #state and a */&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;Here is the smallest version of that I could make work. The private field is one number. &lt;code&gt;act&lt;/code&gt; is the four-weight model from 103, plus a second weighted sum that the field switches on. &lt;code&gt;observe&lt;/code&gt; folds what just happened into the field, using a weighted sum of its own, squashed through &lt;code&gt;tanh&lt;/code&gt; so the field stays between minus one and one.&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;type&lt;/span&gt; &lt;span class="nx"&gt;Weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// act: change in room temperature = base + field * extra&lt;/span&gt;
  &lt;span class="na"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;fieldRoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fieldOutside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fieldBias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// observe: next field = tanh(a weighted sum of the field and what just happened)&lt;/span&gt;
  &lt;span class="nl"&gt;uField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uRoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uOutside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uHeater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uBias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WorldModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// s_t lives here, and nowhere in the logs&lt;/span&gt;
  &lt;span class="nl"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Weights&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="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Weights&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;reset&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;#field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;act&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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inputs&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;action&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;w&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="nx"&gt;w&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&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;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldRoom&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldOutside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldBias&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="na"&gt;roomTemp&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;roomTemp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;base&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="nx"&gt;#field&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&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;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;observe&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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextState&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inputs&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;action&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;w&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="nx"&gt;w&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;change&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&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;roomTemp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&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;#field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tanh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uField&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="nx"&gt;#field&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uRoom&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uOutside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uHeater&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uChange&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uBias&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;Thirteen numbers, against four. Seven of them say what the field does to the prediction; six say what the model should write into the field after each step. Nothing in there mentions a door. The field is allowed to be anything between minus one and one, and the model gets to decide what it means.&lt;/p&gt;

&lt;p&gt;Two small things changed alongside. &lt;code&gt;inputs&lt;/code&gt; is &lt;code&gt;features&lt;/code&gt; with the temperatures centred and scaled (room temperature minus 17, outside minus 7, both divided by five), and the change the model observes is divided by five too. In the last post I left the inputs raw and paid for it with ten thousand epochs down a long thin valley. This time the loop below is more expensive per epoch, so I took the shortcut the 103 post mentioned, and it converges in three thousand.&lt;/p&gt;

&lt;p&gt;The other change is to the &lt;code&gt;observe&lt;/code&gt; signature. The 102 sketch took an observation; this one takes the whole transition, because the thing the field most needs to know is how the room moved compared with what the model expected, and that needs both ends of the step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training through the field
&lt;/h2&gt;

&lt;p&gt;The 102 post promised to explain how this gets trained. The field is never in the log, so there is no column to compare it against. The thirteen weights have to be judged by the only thing that is in the log, the next temperature, and six of those weights only affect the next temperature indirectly, by changing what gets written into the field, which changes the prediction a step later, and the step after that.&lt;/p&gt;

&lt;p&gt;So the gradient has to flow through the field. In 103 each row of the log was its own small problem, and the loop could visit rows in any order. Now a row's prediction depends on the field, the field depends on the previous row, and so on back to the start of the episode. The training loop walks each episode forwards, remembering what the field was at every step, and then walks it backwards, asking at each step how much that step's field mattered to everything that came after it.&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;function&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;[][],&lt;/span&gt; &lt;span class="nx"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;Weights&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u&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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;losses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;epoch&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;Weights&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &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;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &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;episode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// forwards through the episode, remembering what the field was at every step&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;for &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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;episode&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;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&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;roomTemp&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&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;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldRoom&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldOutside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldBias&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;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;err&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;change&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tanh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uField&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uRoom&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uOutside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uHeater&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uChange&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uBias&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;steps&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;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// backwards through the episode: how much did each step's field matter to everything after it?&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// d loss / d (field after this step)&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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;dPre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dField&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// through the tanh&lt;/span&gt;
        &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uField&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uRoom&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uOutside&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uHeater&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uChange&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uBias&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldRoom&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldOutside&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fieldBias&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;dField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dPre&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uField&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// on to the step before&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for &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;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;losses&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;loss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&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="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;losses&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;losses&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The forwards half is &lt;code&gt;act&lt;/code&gt; and &lt;code&gt;observe&lt;/code&gt; inlined. The backwards half is the chain rule, and two lines of it carry the whole idea. &lt;code&gt;dPre&lt;/code&gt; is how much the loss would change if the field written at this step were nudged, pulled back through the &lt;code&gt;tanh&lt;/code&gt;. The last line passes that on to the step before, because the step before chose the field this step started with. Everything else is the &lt;code&gt;err * input&lt;/code&gt; pattern from 103, applied to whichever weight the line is about.&lt;/p&gt;

&lt;p&gt;This is backpropagation through time, and it is the reason recurrent models are trained on episodes rather than on rows. Three thousand epochs over 4,800 rows takes about half a second in Node 23. The six &lt;code&gt;u&lt;/code&gt; weights start at small random values rather than zero, because with every &lt;code&gt;u&lt;/code&gt; at zero the field never moves, nothing depends on it, and the gradient for those weights is exactly zero forever. The seven prediction weights start at zero as before.&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%2F0jiyq22rcx83hni1qwum.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%2F0jiyq22rcx83hni1qwum.png" alt="Mean squared error while training the latent model, three thousand epochs" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mean squared error per epoch for the latent model, with three reference lines. The four-weight model stopped at 0.29. A model that could see the door would reach 0.01, the level of the draughts. The same model told the door one step late gets 0.17, and the latent model settles just under it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/heater-door-loss.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two dashed lines show what the model is being asked to do. I fitted a seven-weight model that is told the door state, which is cheating, to find out what the best possible fit looks like. Scored normally it reaches the noise floor. Then I gave the same model the door state one step late, so it only learns the door has moved after seeing the step on which it moved, and it gets 0.174. The step the door moves on is a surprise by definition, and the 310 steps where it moved account for 0.165 of that on their own. The latent model gets 0.172.&lt;/p&gt;

&lt;p&gt;That second line is a reference rather than a floor, and the latent model sitting under it is the clue. The late-door model insists the door is wherever it was a step ago. A model that knows the door a step late and also knows how often it moves (about one step in eight while it is open, one in twenty-five while it is shut) can hedge, adding a little leak to every closed step in case the door has just opened, and that scores 0.163. Refitting the seven weights on the late door state lands in the same place. So the latent model is about a hundredth above what noticing the door can reach with this shape of model. None of these numbers is a true lower bound, either: all of them are scored on the 4,800 rows the models were fitted to, and a model with a better shape could go lower.&lt;/p&gt;

&lt;p&gt;Here is what it learned, with the temperature weights divided by five so they read per degree.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;weight&lt;/th&gt;
&lt;th&gt;learned&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;weight&lt;/th&gt;
&lt;th&gt;learned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;room&lt;/td&gt;
&lt;td&gt;-0.111&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;uField&lt;/td&gt;
&lt;td&gt;0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outside&lt;/td&gt;
&lt;td&gt;0.078&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;uRoom&lt;/td&gt;
&lt;td&gt;-0.049&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;heater&lt;/td&gt;
&lt;td&gt;1.21&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;uOutside&lt;/td&gt;
&lt;td&gt;0.031&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bias&lt;/td&gt;
&lt;td&gt;-0.92&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;uHeater&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fieldRoom&lt;/td&gt;
&lt;td&gt;0.010&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;uChange&lt;/td&gt;
&lt;td&gt;-2.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fieldOutside&lt;/td&gt;
&lt;td&gt;0.015&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;uBias&lt;/td&gt;
&lt;td&gt;-0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fieldBias&lt;/td&gt;
&lt;td&gt;-1.83&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the right-hand column as the recipe for the field. The weight on &lt;code&gt;uChange&lt;/code&gt; is by far the largest: the field is mostly a function of how much the room moved last step, with the heater and the temperatures subtracted off so that a drop the base model already expected does not count. The weight on &lt;code&gt;uField&lt;/code&gt;, the field's memory of itself, is almost nothing. For this room the model decided it did not need memory, because one step of an open door is loud enough to hear on its own. That changes when the door gets quieter, further down.&lt;/p&gt;

&lt;p&gt;The left-hand column is less tidy than in 103. The room and outside weights are no longer equal and opposite, and the field's effect is almost all in &lt;code&gt;fieldBias&lt;/code&gt;. The model is not obliged to factor the room into "closed door physics plus a door term" the way I would, and the gradient found a different split that fits the log just as well. A private field has no label on it. It holds whatever the model found useful, and you judge it by the predictions, as the 102 post said you would have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field finds the door
&lt;/h2&gt;

&lt;p&gt;The test is the same unseen room as in the last post: 22 degrees inside, minus four outside, colder than anything in the log. The heater is on throughout. The door opens at step 10 and closes at step 20, and the models are not told.&lt;/p&gt;

&lt;p&gt;Two things are different about how the models are run. First, each one sees the real reading at every step and predicts the next one, the way a thermostat would, rather than running thirty steps blind from the start. With a door it cannot see, no model could run thirty steps blind, and the point of the field is that the model notices. Second, at three chosen steps, each model is asked to run five steps blind from where it is, which is the "what if" that world models are for.&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%2Fc5tfyl40y3b0q9u3d29u.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%2Fc5tfyl40y3b0q9u3d29u.png" alt="Thirty steps with the heater on at minus four outside, the door open from step 10 to 20" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Top: the real room, with five-step forecasts from both models branching off at steps 5, 13 and 23. Bottom: the private field after each step, with the door state it never saw dashed over it. The field is near zero for ten steps, 0.91 the step after the door opens, and 0.12 the step after it closes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/heater-door-unseen.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The strip at the bottom is the result I was after. The field sits a little below zero while the door is closed, jumps to 0.9 on the first step after it opens, stays high while it is open, and drops back the step after it closes. The model was never told there was a door, and the field is a door anyway, because a door was the thing in the room that the log could not explain and a single number could.&lt;/p&gt;

&lt;p&gt;Step by step at the two moments that matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;step&lt;/th&gt;
&lt;th&gt;door&lt;/th&gt;
&lt;th&gt;real room&lt;/th&gt;
&lt;th&gt;four-weight, one step ahead&lt;/th&gt;
&lt;th&gt;latent, one step ahead&lt;/th&gt;
&lt;th&gt;field after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;17.19&lt;/td&gt;
&lt;td&gt;17.16&lt;/td&gt;
&lt;td&gt;16.97&lt;/td&gt;
&lt;td&gt;-0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;13.14&lt;/td&gt;
&lt;td&gt;16.96&lt;/td&gt;
&lt;td&gt;16.90&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;10.10&lt;/td&gt;
&lt;td&gt;13.06&lt;/td&gt;
&lt;td&gt;11.16&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;7.76&lt;/td&gt;
&lt;td&gt;10.13&lt;/td&gt;
&lt;td&gt;8.52&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;1.94&lt;/td&gt;
&lt;td&gt;2.69&lt;/td&gt;
&lt;td&gt;2.27&lt;/td&gt;
&lt;td&gt;0.59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;2.83&lt;/td&gt;
&lt;td&gt;2.27&lt;/td&gt;
&lt;td&gt;1.77&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;3.55&lt;/td&gt;
&lt;td&gt;3.13&lt;/td&gt;
&lt;td&gt;3.56&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Step 11 is wrong for both models and has to be. Nobody could have predicted the door. From step 12 the latent model is within a degree and closing, while the four-weight model is three degrees adrift and stays there. Step 21 is the same story in reverse. Over the thirty steps the one-step error is 1.16 degrees for the four-weight model and 0.78 for the latent one; leave out the two steps the door moved on and it is 0.95 against 0.32.&lt;/p&gt;

&lt;p&gt;At step 13 the latent model has had two steps to notice the door, and its field reads 0.81. Asked for five steps blind, it says the room will fall from 7.8 to 2.7, and the room falls to 2.7. The four-weight model, from the same reading, says 8.3. It has no way to represent "the room is currently leaking faster than usual", so it cannot condition on it, and its bent physics sends the forecast the wrong way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;forecast from&lt;/th&gt;
&lt;th&gt;real, five steps on&lt;/th&gt;
&lt;th&gt;four-weight&lt;/th&gt;
&lt;th&gt;latent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;step 5, door closed&lt;/td&gt;
&lt;td&gt;17.2&lt;/td&gt;
&lt;td&gt;17.7&lt;/td&gt;
&lt;td&gt;17.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;step 13, door open&lt;/td&gt;
&lt;td&gt;2.7&lt;/td&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;td&gt;2.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;step 23, door just closed&lt;/td&gt;
&lt;td&gt;6.8&lt;/td&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;td&gt;6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The strip also shows the field drifting down from 0.91 to about 0.55 over the ten open steps. It is reading the size of the drop, and the drop shrinks as the room gets closer to the temperature an open room would settle at. The model has no memory to hold the field up because, as the weights table said, it did not learn any. Make the door quieter and that changes. Rerunning the whole script with the open-door leak at 0.15 instead of 0.25 brings the memory weight from 0.007 to 0.08, and at 0.12 it is 0.26: when one step is no longer enough to tell, the model learns to add up several. At 0.10, where the door only adds a few hundredths to the leak, the field stops tracking the door at all, and the latent model's loss sits above the one-step-late line. There is a signal it cannot find in 4,800 rows, and the answer, as with most problems in this series, is more log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the notation
&lt;/h2&gt;

&lt;p&gt;Everything above fits in two lines. The private field is (h), the readings the logger can see are (o), and the action is (a).&lt;/p&gt;

&lt;p&gt;$$h_t = f(h_{t-1},\ o_{t-1},\ a_{t-1},\ o_t)$$&lt;/p&gt;

&lt;p&gt;$$\hat{o}_{t+1} = g(h_t,\ o_t,\ a_t)$$&lt;/p&gt;

&lt;p&gt;The first line is &lt;code&gt;observe&lt;/code&gt;: the new field is a function of the old field and the step that just happened. The second is &lt;code&gt;act&lt;/code&gt;: the predicted next reading is a function of the field, the current reading and the action. In this post (f) is a &lt;code&gt;tanh&lt;/code&gt; of a weighted sum and (g) is two weighted sums, and training them was gradient descent with the chain rule run backwards through the first line.&lt;/p&gt;

&lt;p&gt;Put the field and the readings together and call the pair (s_t), and the second line is the world model equation from the &lt;a href="https://stepinto.dev/posts/world-models-102/" rel="noopener noreferrer"&gt;102 post&lt;/a&gt;, (P(s_{t+1} \mid s_t, a_t)), with the probability dropped because the model predicts a single number. The state the equation conditions on was always meant to include the fields you never logged. That is why it is called latent: the field is hidden from the logger, but the maths had a place for it all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  How DreamerV3 does the same job
&lt;/h2&gt;

&lt;p&gt;The part of this post that carries over most directly to the research systems is the one it spent longest on. Nobody tells DreamerV3 what to keep in its recurrent state either. The state has to hold whatever a single observation cannot show, such as how fast something is moving or where an object went when it left the frame, and it learns to hold those things for the same reason the field learned the door: the predictions were wrong without them, and the gradient, walking back through each sequence, found which numbers would fix that.&lt;/p&gt;

&lt;p&gt;The architecture carries over less neatly. DreamerV3 calls its world model a recurrent state-space model, and it has more parts than my two methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a sequence model, a GRU, that carries a recurrent state (h_t) forward from (h_{t-1}), the last action and the last latent (z_{t-1})&lt;/li&gt;
&lt;li&gt;an encoder that reads the new observation alongside (h_t) and produces (z_t), a stochastic latent made of 32 categorical variables with 32 classes each&lt;/li&gt;
&lt;li&gt;a dynamics predictor that guesses (z_t) from (h_t) alone, before the observation arrives&lt;/li&gt;
&lt;li&gt;a decoder, with heads for reward and for whether the episode carries on, that reads (h_t) and (z_t) together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My &lt;code&gt;observe&lt;/code&gt; does the work of the sequence model and the encoder in one line, and my &lt;code&gt;act&lt;/code&gt; is closest to the decoder. Three of the differences matter, and each of them shows up somewhere in this post.&lt;/p&gt;

&lt;p&gt;The first is where the readings go. My &lt;code&gt;act&lt;/code&gt; is handed the current reading, so the field only has to carry what the reading leaves out, and one number covers the door. DreamerV3's decoder has nothing but the state to work from, so the state has to carry everything, the equivalent of the temperature included. That is why it is hundreds or thousands of numbers wide, and it is what lets the model imagine many steps ahead with no observations at all. My five-step forecasts get away with feeding the predicted temperature back in as the next reading, which works only because the temperature is the whole of what the logger sees.&lt;/p&gt;

&lt;p&gt;The second is the guess made before the reading. DreamerV3 keeps two versions of (z_t) at every step: the dynamics predictor's guess from (h_t), and the encoder's answer once the observation is in. Part of its training loss is the gap between the two. Step 11 in the table above is that gap in miniature, where the model expected a shut door and the reading said otherwise. My model only ever has the answer, so when it forecasts blind its field stays wherever it was. It cannot imagine the door shutting at step 20, or opening in the first place. DreamerV3's guess is a distribution, so an imagined rollout can open the door in some samples and leave it shut in others.&lt;/p&gt;

&lt;p&gt;The third is how the training is fed. The backward walk through each sequence is the same, but DreamerV3 cuts its experience into chunks of 64 steps drawn from a replay buffer that keeps filling while it acts, and its loss has more than one term: how well it reconstructs the observation, the reward and the continuation, plus two terms that pull the guess and the answer towards each other, weighted 0.5 and 0.1. Mine is one squared error over eighty fixed episodes.&lt;/p&gt;

&lt;p&gt;The 101 post joked about representation collapse, where a model scores perfectly by declaring every state identical. That failure needs the model's target to be something the model produces itself. JEPA-style models, from LeCun's line of work, predict the representation of the next observation rather than the observation, so a representation that never changes predicts itself perfectly, and they keep a separate, slowly updated target encoder behind a stop-gradient to prevent it. My model's target is a thermometer reading, which it has no way to change. A constant field earns it nothing, and the model falls back to the four-weight fit and its 0.29. DreamerV3 has the same protection, because it reconstructs its observations, and a quieter risk from the other side: the term that pulls the encoder's answer towards the guess would, pushed far enough, teach the encoder to ignore the observation. DreamerV3 stops penalising that gap once it is below one nat, which it calls free bits.&lt;/p&gt;

&lt;p&gt;The nearest thing this post has to collapse is a field that goes unused, and that is less final than it sounds. With every &lt;code&gt;u&lt;/code&gt; weight at exactly zero the gradient is exactly zero, which is why they start small and random, but near zero it is small and still there. When the open-door leak came down to 0.10, the field stopped tracking the door because the signal was too faint to find in 4,800 rows, which is a shortage of log and not a trap the training fell into.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://stepinto.dev/heater/?scenario=door" rel="noopener noreferrer"&gt;playground&lt;/a&gt; has a scenario switch at the top. In the door scenario, recording adds the door, training runs the four-weight model and then the latent one, and the right-hand pane runs the room at minus four with a slider for where the five-step forecast starts. Drag that slider across step 10 and watch the orange forecast turn the wrong way. The open-door leak has a slider too; bring it down towards the closed rate and watch the memory weight in the latent model's table climb, then watch the field stop finding the door altogether.&lt;/p&gt;

&lt;p&gt;The playground runs the same code with &lt;code&gt;Math.random&lt;/code&gt; where the script's seed is, so your numbers will differ from the ones above by a little and agree with them in shape. The data behind each chart is linked under it.&lt;/p&gt;

&lt;p&gt;The next post puts this model next to an LLM. One predicts the next token of text, the other predicts the next state of a room, and the interesting part is where an agent would want both.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>worldmodels</category>
      <category>typescript</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>A Piece That Breaks Another Piece</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Thu, 29 Oct 2026 19:30:12 +0000</pubDate>
      <link>https://dev.to/stepinto/a-piece-that-breaks-another-piece-3m7h</link>
      <guid>https://dev.to/stepinto/a-piece-that-breaks-another-piece-3m7h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;It was all broken by me really, or built like a clumsy caveman&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;&lt;a href="https://stepinto.dev/words/trying-to-have-something-right-afterwards.html" rel="noopener noreferrer"&gt;Trying to have something right afterwards&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've been building a series of pages where the computer generates the pattern: &lt;a href="https://stepinto.dev/posts/tiles-painted-by-rule/" rel="noopener noreferrer"&gt;tin-glazed tiles painted by rule&lt;/a&gt;, then &lt;a href="https://stepinto.dev/posts/cut-tile-and-poured-cement/" rel="noopener noreferrer"&gt;cut tile and poured cement&lt;/a&gt;, then &lt;a href="https://stepinto.dev/posts/heraldry-by-rule/" rel="noopener noreferrer"&gt;heraldry&lt;/a&gt; and &lt;a href="https://stepinto.dev/posts/knotwork-by-rule/" rel="noopener noreferrer"&gt;knotwork&lt;/a&gt;. They've been travelling round Europe, borrowing from the art and the history of each place, and trying to capture something about how patterns repeat across the whole of the artistic world: the same rule turning up in a different material somewhere else. This one is Barcelona. I've stood in front of Casa Batlló, and walked a good deal of that city in Gaudí's footprints, and the thing you notice about his surfaces once you're close is that none of them are made of anything whole. The bench at Park Güell, the dragon on the stair below it, the chimneys on the Pedrera roof and the pinnacles of the Sagrada Família are all trencadís: tiles that somebody broke with a hammer and set back, shard by shard, in lime.&lt;/p&gt;

&lt;p&gt;So &lt;a href="https://stepinto.dev/art/trencadis.html" rel="noopener noreferrer"&gt;Trencadís&lt;/a&gt; is a page that starts with a wall it didn't paint. It takes &lt;a href="https://stepinto.dev/art/azulejeria.html" rel="noopener noreferrer"&gt;Azulejería&lt;/a&gt;'s tiles, grown from a seed by the same engine, and breaks them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5okkfkg2e4nhozc0asev.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5okkfkg2e4nhozc0asev.webp" alt="A mosaic of broken tile shards in white, cobalt, yellow, green and orange, set in pale lime mortar, with fragments of blue painted rosettes among the plain pieces, shaded as if lying on a surface that swells and dips" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The banc preset. Park Güell's bench: white, cobalt and yellow, a third of the tiles painted, the surface undulating.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A piece that breaks another piece
&lt;/h2&gt;

&lt;p&gt;Every page in this series so far has made its own picture. This one consumes somebody else's. The source wall is Azulejería's flora, the rosette quartered on one corner, the bud on the opposite one, half a flower on each edge, painted by the engine's &lt;code&gt;makeTile&lt;/code&gt; with the same drifting brush and the same wash laid thin twice. The page asks the engine for four paintings of the design at the pixel size they'll be seen at, and keeps them as the tiles the wall was laid from.&lt;/p&gt;

&lt;p&gt;I copied the flora motif and its four painters into the page rather than importing them. Sixty lines. It felt wrong for about a minute, and then it didn't: the engine holds the brush, each page holds what it paints with it. Azulejería's motifs belong to Azulejería. If Trencadís wants a rosette it draws one with the engine, the same way Jerez draws a vine, and the fact that it draws the same rosette is the point of the piece rather than a reason to share code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crack
&lt;/h2&gt;

&lt;p&gt;A broken tile is a Voronoi diagram. Not exactly, but close enough that nobody at the bench will argue. Scatter points, give each point the region of the plane nearer to it than to any other, and you get convex cells with straight edges meeting at threes, which is what a sheet of glaze does when it goes under a hammer.&lt;/p&gt;

&lt;p&gt;The points sit on a grid of cells about a third of a tile across, each one jittered nearly half a cell in either direction, and one in twelve is missing, so its neighbours grow into the gap and some shards come out bigger than others. The Voronoi itself is twenty lines: for each site, start with a square and clip it by the bisector with every neighbour, nearest first, stopping once the next neighbour is more than twice as far as the cell's farthest corner, because it can't touch the cell after that.&lt;/p&gt;

&lt;p&gt;Then every cell goes through the cutter. It's shifted and turned by the fraction a hand allows, its corners land a touch off the drawing, its long edges bow, and it's inset from the joint by a mitred offset so the mortar between two shards is the same width along an edge as at a point. That cutter was &lt;a href="https://stepinto.dev/art/alicatado.html" rel="noopener noreferrer"&gt;Alicatado&lt;/a&gt;'s, and more on that below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxflaixmderun6ceuysr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxflaixmderun6ceuysr.webp" alt="Two mosaics side by side. On the left, Alicatado's eight-point stars and kites in blue, gold, green, red, black and white, cut from glazed tile and bedded in lime. On the right, Trencadís's shards in red, yellow, white, green and cobalt, broken rather than cut, in the same lime" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same cutter on two kinds of piece. Alicatado's alcázar preset on the left, Trencadís's pinacles on the right. Cut on one side, broken on the other, mitred and bedded the same way.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In the bucket, or where it fell
&lt;/h2&gt;

&lt;p&gt;A shard is a piece of a tile, so the page has to decide which tile and which piece. There are two honest answers. Either the shard is still where it broke, in which case the piece of the wall underneath it is simply the piece of the wall underneath it, and the rosettes read through the cracks. Or it went in the bucket with everything else, and came out somewhere else, turned some other way.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;intact&lt;/strong&gt; slider is the share of shards that stayed put. At one, the wall is only cracked. At zero, every shard is a random piece of a random tile at a random angle, which is what a mason does with a bucket of pieces and no particular plan. In between you get a wall where the pattern keeps half-surfacing, a scalloped ring that runs across three shards and then stops, which is what the bench actually looks like when you get your face close to it.&lt;/p&gt;

&lt;p&gt;I've had a year of breaking things and of things being broken, so a slider for how much of the original survives was always going to get built. It turns out the honest setting is somewhere around a third.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvefudcyln5frt4a132t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvefudcyln5frt4a132t.webp" alt="Three panels of the same broken painted wall in cobalt and ochre on cream. On the left the shards are scrambled and no pattern reads. In the middle fragments of rings and rosettes surface and stop. On the right the wall is only cracked and the rings and rosettes are whole" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same seed and the same crack at intact 0, 0.5 and 1. Left, everything in the bucket. Right, the wall as it broke.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwr4rf3cfzv1cg6ogd5u4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwr4rf3cfzv1cg6ogd5u4.webp" alt="A close view of a cobalt and white painted tile wall of rosettes and rings, cracked into shards with fine lime joints, the pattern whole across the cracks" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The sencer preset, up close. Azulejería's wall, broken in place. The joints are the only thing this page added.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sampling a shard in place is a clip and a &lt;code&gt;drawImage&lt;/code&gt; of the tile under it, mirrored the way the module repeat lays it. A displaced shard is the same clip with the tile drawn around a random point at a random turn. The one surprise was the rim of the tile: the engine thickens the glaze at the edge of every tile with a faint square stroke, and on a wall that is only cracked those squares showed through as a grid. Real shards don't carry their edges, they went in the bucket with everything else, so the page crops three per cent off every side of the tile before it samples.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bench
&lt;/h2&gt;

&lt;p&gt;Trencadís is a curved-surface technique. You can't bend a tile, so you break it and lay the pieces on the curve, and that's the whole reason it exists. A flat trencadís wall is a mosaic with a bad excuse.&lt;/p&gt;

&lt;p&gt;The page's surface is a height field: a long sine wave along the wall, bent by a slower one across it, with an amplitude the &lt;strong&gt;curve&lt;/strong&gt; slider sets. The normal of the surface at each shard's centre is taken against the light (the same light angle Alicatado uses, with a height to it now), and the difference from flat lights the shard or shades it. The mortar follows the same surface, sampled coarsely into a small canvas and drawn smoothly over the lime before the shards go on, so the joints dip into the troughs with the pieces. The troughs also sit a little in their own shadow, which is cheap and does more than the lighting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsm10mrlk4p70m7qqrfdd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsm10mrlk4p70m7qqrfdd.webp" alt="Two panels of cream, grey and white marble shards with the odd bottle-green piece, in lime. The left is flat and evenly lit. The right has a broad soft band of shadow curving down through it, with a lit swell either side" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pedrera preset at curve 0 and curve 1. The chimneys on the roof of La Pedrera are broken marble with a few broken bottles, and they are nothing but curve.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first shading was far too strong and the Pedrera chimneys looked like they'd been left out in the rain. The shadow is cooler and lighter now, a lime-coloured surface throwing back a bluish shade rather than going black, and the tin-white shards on the bench stay white on the lit side, which is what the bench does at midday.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cutter moves house
&lt;/h2&gt;

&lt;p&gt;Trencadís was the second page to want Alicatado's cutter, which is the point at which something stops being one page's and becomes the engine's. &lt;code&gt;offsetPoly&lt;/code&gt; (the mitred inset), &lt;code&gt;cutPiece&lt;/code&gt; (the shift, the turn, the nicked corners, the bowed edges), the edge relief that lights one side of a standing piece and shades the other, the grain for the mortar, and the small polygon maths every page kept rewriting all moved out of Alicatado into the package as &lt;code&gt;azulejeria/pieces&lt;/code&gt;, with the Voronoi beside them for pieces that are broken rather than cut.&lt;/p&gt;

&lt;p&gt;Moving a function that draws random numbers is the dangerous kind of refactor on these pages, because every wall is deterministic per seed and an extra &lt;code&gt;rng()&lt;/code&gt; call anywhere shifts everything after it. So before touching anything I rendered all nine of Alicatado's presets, main canvas and the card thumbnails, and hashed the bytes. Then moved the cutter, rendered them again, and compared. Identical, which is the only acceptable answer. The same check ran on every engine page for the two small engine changes Delft needed, below, and came back identical each time.&lt;/p&gt;

&lt;p&gt;The next customer for the cutter is Sutton Hoo, garnets in gold cells, which was blocked on this move and now isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delft
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/art/delft.html" rel="noopener noreferrer"&gt;Delft&lt;/a&gt; is the engine sent north, to see whether it works for someone else's country with no help. A Delft tile is a picture: a ship, a bird on a branch, a windmill, a tulip, a pikeman or a child bowling a hoop in the middle of a tin-white square, with a corner motif in each corner (the ossenkop, the spider's head, the fleur-de-lis) that completes where four tiles meet. Cobalt on white, or manganese when the painter had it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rzrltvcnmh9cv55f95n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rzrltvcnmh9cv55f95n.webp" alt="A wall of Delft tiles, each a small cobalt figure in a roundel on greyish white: ships under sail, birds on branches, windmills, tulips, pikemen with their pikes and children running with hoops, with small ox-head motifs in the corners between tiles" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The schouw preset: everything mixed, in roundels, the way a stove surround came. Every tile is one of eight paintings, and each painting picked its own figure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It nearly worked with no help. The engine's wall mirrors or turns every other tile so that quarter designs complete across the joints, and a picture of a ship can't be turned upside down. The fallthrough branch of the wall already laid tiles the same way up for any repeat it didn't recognise, so a &lt;code&gt;whole&lt;/code&gt; repeat was a word in a list. The second thing was that a wall draws from four paintings of its design, which is plenty when the design is a rosette, and makes a wall of ships four ships. A &lt;code&gt;variants&lt;/code&gt; option on &lt;code&gt;createWall&lt;/code&gt; lets Delft ask for eight. The figure is chosen in the painter from the painting's own random stream, not when the design is grown, so each of the eight is a different ship. That was the whole engine change, and both pages' bytes were checked before and after.&lt;/p&gt;

&lt;p&gt;Then the figures. I drew them from parts, the way I'd drawn Leonor's first lions and Knotwork's first hound: a trapezoid for the mill, a drop for the bird, a hat and a coat for the pikeman. They looked, for the third time in this series, like toys. This is now a rule rather than a lesson: a figure that is a convention copied by hand for three hundred years gets traced from a drawing, not built from rules.&lt;/p&gt;

&lt;p&gt;So I asked an image model for six, on the terms the tracer wants: one solid gold fill, a thick even black line around every part, red for the parts to wash lighter, nothing else in the frame. It gave me a fluyt with red sails, a robin with a red breast, a post mill with a red cap, a tulip, a pikeman with a red feather in his hat, and a boy with a red hoop. The forty-line tracer from the heraldry post turned each one into loops. Gold became the cobalt wash, laid twice so it pools; red became a lighter wash over the glaze; black became the ink, laid last. The mill's lattice sails keep their holes because the silhouette is filled even-odd.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fat5x6j33tae45n9stnxg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fat5x6j33tae45n9stnxg.webp" alt="Two rows. The top row is six flat drawings on white: a gold ship with red sails, a gold robin with a red breast, a gold windmill with a red cap, a red tulip with gold leaves, a gold pikeman with red sleeves and feather, and a gold boy running with a red hoop. Below, the same six as Delft tiles, each a cobalt or manganese figure on tin white with corner motifs" width="798" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The six drawings, and the six tiles traced from them. The soldier is in manganese, the tulip in the early polychrome.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One thing the traced figures couldn't do at first was listen to the sliders. The brush slider is the engine's line weight, and a traced line is a filled shape at the drawing's own width. So the brush object the engine hands every painter now carries &lt;code&gt;hand&lt;/code&gt; and &lt;code&gt;brush&lt;/code&gt; as the state has them, and the traced ink drifts with the hand like the wash does, and is stroked wider when the brush is loaded past the drawing's line. A lighter brush can't thin a traced line without painting a halo over the wash outside it, so below one it stays the drawing's weight, and the page says so. Jerez's horse and Rosa y Granada's pomegranate got the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the package
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/azulejeria" rel="noopener noreferrer"&gt;azulejeria&lt;/a&gt; is at 2.3.1. The cutter is its own module, next to the knots, and uses nothing from the wall except the seeded random.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install azulejeria
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;voronoi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cutPiece&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relief&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;azulejeria/pieces&lt;/span&gt;&lt;span class="dl"&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;cutPiece&lt;/code&gt; takes a polygon and how far it may stray and gives you back the piece as laid and its glazed face inside the joint. &lt;code&gt;voronoi&lt;/code&gt; gives you the cells of a scatter of points. &lt;code&gt;relief&lt;/code&gt; lights the edges. The README has the rest, and the &lt;code&gt;whole&lt;/code&gt; repeat and &lt;code&gt;variants&lt;/code&gt; are in the wall's options for anyone whose tiles are pictures.&lt;/p&gt;

&lt;p&gt;Sebka is next, the brick lattice of the Giralda, a piece that is entirely light. Then Sutton Hoo.&lt;/p&gt;

</description>
      <category>art</category>
      <category>generative</category>
      <category>canvas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A Claude Client in One Go File</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Thu, 29 Oct 2026 07:24:31 +0000</pubDate>
      <link>https://dev.to/stepinto/a-claude-client-in-one-go-file-po8</link>
      <guid>https://dev.to/stepinto/a-claude-client-in-one-go-file-po8</guid>
      <description>&lt;p&gt;Last week I wrote a Claude client in &lt;a href="https://stepinto.dev/posts/claude-hot-path-bash/" rel="noopener noreferrer"&gt;bash&lt;/a&gt;, parsed the stream with regular expressions, and pinned it as the thing to beat. This week it's Go, which has it rather easy by comparison.&lt;/p&gt;

&lt;p&gt;Go is the first language in the series where the standard library covers everything the spec asks for: an HTTP client with TLS, a JSON codec, a buffered line reader, and a timezone database baked into the binary. The whole client is 175 lines in one file, and it runs from a &lt;code&gt;scratch&lt;/code&gt; image.&lt;/p&gt;

&lt;p&gt;The code is at &lt;a href="https://github.com/AndyCross/claude-hot-path/tree/main/clients/go" rel="noopener noreferrer"&gt;github.com/AndyCross/claude-hot-path&lt;/a&gt; and the leaderboard is at &lt;a href="https://stepinto.dev/claude-hot-path/" rel="noopener noreferrer"&gt;/claude-hot-path/&lt;/a&gt;. The spec, which every client has to pass, is on that page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the stream
&lt;/h2&gt;

&lt;p&gt;An SSE stream is lines. &lt;code&gt;event:&lt;/code&gt; lines, &lt;code&gt;data:&lt;/code&gt; lines, blank separators, and the occasional &lt;code&gt;ping&lt;/code&gt;. The client only acts on &lt;code&gt;data:&lt;/code&gt; lines, so a &lt;code&gt;bufio.Scanner&lt;/code&gt; over the response body with a prefix check is the entire transport layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CutPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"data: "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="c"&gt;// event:, ping bodies, blank separators&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&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="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"content_block_start"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;partial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"content_block_delta"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"text_delta"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"input_json_delta"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartialJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"content_block_stop"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"tool_use"&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;s&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;case&lt;/span&gt; &lt;span class="s"&gt;"message_delta"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StopReason&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;sc.Buffer&lt;/code&gt; call matters. The default scanner token limit is 64 KB and a single &lt;code&gt;data:&lt;/code&gt; line can be longer than that if a tool argument is large, so the limit goes up to a megabyte. Bash had no such limit and no such safety either.&lt;/p&gt;

&lt;p&gt;One struct does for every event. The payloads share a shape, &lt;code&gt;type&lt;/code&gt; and &lt;code&gt;index&lt;/code&gt; at the top and a &lt;code&gt;delta&lt;/code&gt; or a &lt;code&gt;content_block&lt;/code&gt; underneath, so a single &lt;code&gt;event&lt;/code&gt; type with all the fields and the unused ones left zero is simpler than a type per event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Type&lt;/span&gt;         &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"type"`&lt;/span&gt;
    &lt;span class="n"&gt;Index&lt;/span&gt;        &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="s"&gt;`json:"index"`&lt;/span&gt;
    &lt;span class="n"&gt;ContentBlock&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="s"&gt;`json:"content_block"`&lt;/span&gt;
    &lt;span class="n"&gt;Delta&lt;/span&gt;        &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Type&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"type"`&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"text"`&lt;/span&gt;
        &lt;span class="n"&gt;PartialJSON&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"partial_json"`&lt;/span&gt;
        &lt;span class="n"&gt;StopReason&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"stop_reason"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"delta"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Echoing the assistant turn
&lt;/h2&gt;

&lt;p&gt;Tool use is a conversation. After the first stream ends with &lt;code&gt;stop_reason: "tool_use"&lt;/code&gt;, the second request has to carry the user message, the assistant's whole turn including the &lt;code&gt;tool_use&lt;/code&gt; block with its assembled input, and then a user message holding the &lt;code&gt;tool_result&lt;/code&gt;. The &lt;code&gt;block&lt;/code&gt; type that &lt;code&gt;content_block_start&lt;/code&gt; decodes into is also the type that gets marshalled back out, which is where &lt;code&gt;omitzero&lt;/code&gt; earns its place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Type&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"type"`&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"text,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"id,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"name,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Input&lt;/span&gt;     &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt; &lt;span class="s"&gt;`json:"input,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;ToolUseID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"tool_use_id,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Content&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;          &lt;span class="s"&gt;`json:"content,omitzero"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A text block marshals as &lt;code&gt;type&lt;/code&gt; and &lt;code&gt;text&lt;/code&gt;, a tool use as &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;input&lt;/code&gt;, and a tool result as &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;tool_use_id&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt;, all from the one struct. &lt;code&gt;Input&lt;/code&gt; is a &lt;code&gt;json.RawMessage&lt;/code&gt; because the fragments arrive as text and there is no reason to decode them into a map only to encode them again. The tool itself reads the timezone out of it with a one-field anonymous struct.&lt;/p&gt;

&lt;p&gt;The loop in &lt;code&gt;main&lt;/code&gt; is then the agentic loop in its smallest form: send, print, and if the stop reason was &lt;code&gt;tool_use&lt;/code&gt;, run every tool block, append two messages, go round again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing the standard library does not have
&lt;/h2&gt;

&lt;p&gt;Timezones. &lt;code&gt;time.LoadLocation("Europe/London")&lt;/code&gt; reads the system's zoneinfo, and a &lt;code&gt;scratch&lt;/code&gt; image has no system. Go ships its own copy of the database at &lt;code&gt;$GOROOT/lib/time/zoneinfo.zip&lt;/code&gt; and &lt;code&gt;LoadLocation&lt;/code&gt; falls back to it, so the Dockerfile copies that one file into the final image, next to the CA bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; client.go ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod init client &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-trimpath&lt;/span&gt; &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /client client.go

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /client /app/client&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app/client"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The alternative is &lt;code&gt;import _ "time/tzdata"&lt;/code&gt;, which embeds the database in the binary and adds about 450 KB to it. For a leaderboard that ranks on size, the zip file in the image is the honest choice because the harness measures the artefact, and either way the dependency is paid for somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it measures as
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;th&gt;bash and curl&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Artefact&lt;/td&gt;
&lt;td&gt;5.8 MB&lt;/td&gt;
&lt;td&gt;3.7 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;23.7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship&lt;/td&gt;
&lt;td&gt;5.8 MB&lt;/td&gt;
&lt;td&gt;23.7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start, median of 100&lt;/td&gt;
&lt;td&gt;1.7 ms&lt;/td&gt;
&lt;td&gt;7.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turnaround, median of 100&lt;/td&gt;
&lt;td&gt;1.2 ms&lt;/td&gt;
&lt;td&gt;19.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client total, median of 100&lt;/td&gt;
&lt;td&gt;4.8 ms&lt;/td&gt;
&lt;td&gt;35.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;6 MB&lt;/td&gt;
&lt;td&gt;11 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of code&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;go1.27.2, linux/arm64, Apple M4 Max under Docker. The binary is built with &lt;code&gt;-s -w&lt;/code&gt; and &lt;code&gt;-trimpath&lt;/code&gt;, and nothing else.&lt;/p&gt;

&lt;p&gt;The 5.8 MB binary is the number people will argue about. Most of it is &lt;code&gt;net/http&lt;/code&gt; and its dependency on &lt;code&gt;crypto/tls&lt;/code&gt;, and the rest is &lt;code&gt;encoding/json&lt;/code&gt;, which since 1.27 is the v2 implementation underneath. You cannot get a Go HTTPS client much below five megabytes without leaving the standard library, and the series rules say a language with HTTP in its standard library uses it. Next week's .NET client, published with Native AOT, comes in at 5.4 MB for the binary and then pays 2.4 MB for the C library it links, which is the comparison I most wanted to see when I started this.&lt;/p&gt;

&lt;p&gt;The cold start is where Go earns its place. Under two milliseconds from spawn to the first request on the wire, with no runtime to initialise and no shared libraries to load. The turnaround, which is the time between the first reply finishing and the second request arriving, is just over a millisecond: parse the tail of the stream, look up a timezone, build the second body, send it down the connection &lt;code&gt;net/http&lt;/code&gt; kept open. I assumed most of that millisecond was &lt;code&gt;time.LoadLocation&lt;/code&gt; opening the zoneinfo zip, and measured it: a variant that reads the TZif file straight from &lt;code&gt;/usr/share/zoneinfo&lt;/code&gt; came out the same. What is left is the runtime. Every read on the socket is a wake-up through the network poller and a goroutine switch, where Rust and Zig make a blocking syscall and carry on, and at this scale that is the whole difference. Under five milliseconds of client time across the whole exchange, with nothing tuned and nothing worth tuning, is the number the others have to beat, and two of them do. The six megabytes of peak memory is the Go runtime's heap arena rather than anything the client allocated, and it varies by a couple of megabytes between runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AndyCross/claude-hot-path
&lt;span class="nb"&gt;cd &lt;/span&gt;claude-hot-path
./bench/bench.sh go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker and Python 3, no key. The Rust client is next, which is the first language in the series without HTTP in its standard library, and so the first one that has to pick a dependency and pay for it on the leaderboard.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>go</category>
      <category>sse</category>
    </item>
    <item>
      <title>Where AI Research Actually Happens, in Six Charts</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:24:20 +0000</pubDate>
      <link>https://dev.to/stepinto/where-ai-research-actually-happens-in-six-charts-1chi</link>
      <guid>https://dev.to/stepinto/where-ai-research-actually-happens-in-six-charts-1chi</guid>
      <description>&lt;p&gt;&lt;a href="https://stepinto.dev/posts/compute-isnt-the-constraint/" rel="noopener noreferrer"&gt;Compute Isn't the Constraint&lt;/a&gt; argued that the UK's sovereign AI fund is buying the wrong input. That post was prose. This one is the numbers behind it, drawn so you can check them, with the source data under every chart as CSV. The sources are at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who publishes
&lt;/h2&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%2F3htvp877bn1qjyxi02bt.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%2F3htvp877bn1qjyxi02bt.png" alt="Share of global AI publications, 2000 to 2025" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Share of global AI publications by fractional count, OpenAlex via OECD.AI. China's dip between 2010 and 2015 is in the source data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/research-publications.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The United States and the EU together produced over half of the world's AI papers in 2000. They produce under a quarter now. Volume moved east and it is not coming back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who gets cited
&lt;/h2&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%2Ffxuhhhv5fbeewmq2wupc.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%2Ffxuhhhv5fbeewmq2wupc.png" alt="Papers in the 100 most-cited AI papers of the year, by country" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Papers in the year's 100 most-cited AI papers, by country of the authors' institutions, Zeta Alpha. The UK figure is only available for 2022.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/research-top100-cited.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Volume and influence are different things. The US has never had fewer than 46 of the hundred most-cited papers in a year, and in 2022 had 68. The UK's 16 in 2022 is a respectable third place, and 11 of those 16 came from one laboratory, Google DeepMind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the people are trained, and where they work
&lt;/h2&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%2F7ku0phq23p5ajasnzptb.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%2F7ku0phq23p5ajasnzptb.png" alt="Where elite AI researchers were educated versus where they work" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Elite AI researchers, defined as authors at NeurIPS, ICML and ICLR, by country of undergraduate degree and country of current employer. MacroPolo Global AI Talent Tracker 3.0.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/research-talent.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The American lead is imported. US universities produce a quarter of the world's top AI researchers and US institutions employ six in ten of them. China produces the largest share of anyone and keeps about a tenth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which way they move
&lt;/h2&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%2Fz7z44ur35t9ohjfyjwl0.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%2Fz7z44ur35t9ohjfyjwl0.png" alt="Net flow of elite AI researchers" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Net flow of elite AI researchers, employer country minus undergraduate country. Same source as above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/research-talent.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UK is on the right side of this chart by 92 people. It is the smallest positive number that still counts as a win, and the 65% retention rate behind it is the figure a fund should be trying to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the money looks like
&lt;/h2&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%2Ftmdvbz419t4n58of2xs5.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%2Ftmdvbz419t4n58of2xs5.png" alt="Private AI investment in 2025, billions of US dollars" width="799" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Private AI investment in 2025, Stanford AI Index 2026. The Europe figure covers generative AI only and is the comparable number the Index reports.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/research-investment.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the gravity. A lab funded from a £500m pot competes for people against an industry that raised two hundred and eighty-six billion dollars last year. The researchers it trains are worth more to the companies on the left of the chart than to the state that paid for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How far behind the published frontier sits
&lt;/h2&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%2F4eolmoidn7x4su5hj5wf.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%2F4eolmoidn7x4su5hj5wf.png" alt="How far the best open-weight models trail the closed frontier, in months" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lag between the best open-weight models and the closed frontier, by how you measure it. Epoch AI for the index and compute rows; Håvard Tveit Ihle's analysis of Epoch's benchmark data for the public and private benchmark rows, 2024 to 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/data/research-open-weight-lag.csv" rel="noopener noreferrer"&gt;Download the data (CSV)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pick the measure that flatters you and open weights are four months behind. Pick the one you cannot game and it is closer to ten. Measure by the compute actually deployed and it is sixteen. A team that trains on the public literature is a generation behind by construction, which is the whole argument of the earlier post in one bar.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Update, November 2026: the public and private benchmark rows are from &lt;a href="https://www.lesswrong.com/posts/rJcCrXyEsJKmmDpWG/how-far-behind-are-open-models" rel="noopener noreferrer"&gt;Håvard Tveit Ihle's analysis&lt;/a&gt; of Epoch's benchmark data, not from Epoch itself; the caption and sources now say so. &lt;a href="https://stepinto.dev/posts/how-far-behind-are-open-weights/" rel="noopener noreferrer"&gt;How Far Behind Are Open Weights, Measured&lt;/a&gt; draws the lag over time rather than as one bar.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Every chart above has a CSV link. The charts and the CSV files come out of the same script, so the numbers in the pictures and the numbers in the files cannot drift apart.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/html/2509.25298v1" rel="noopener noreferrer"&gt;Trajectories and Comparative Analysis of Global Countries Dominating AI Publications&lt;/a&gt;, arXiv 2509.25298, using OpenAlex data from OECD.AI with fractional counting.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.zeta-alpha.com/post/must-read-the-100-most-cited-ai-papers-in-2022" rel="noopener noreferrer"&gt;The 100 most cited AI papers in 2022&lt;/a&gt;, Zeta Alpha's annual lists, which also give the 2021 and 2024 counts.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://archivemacropolo.org/interactive/digital-projects/the-global-ai-talent-tracker" rel="noopener noreferrer"&gt;The Global AI Talent Tracker 3.0&lt;/a&gt;, MacroPolo, Paulson Institute.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hai.stanford.edu/ai-index/2026-ai-index-report" rel="noopener noreferrer"&gt;The 2026 AI Index Report&lt;/a&gt;, Stanford HAI.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://epoch.ai/data-insights/open-closed-eci-gap" rel="noopener noreferrer"&gt;Open models lag state-of-the-art closed models by 4 months&lt;/a&gt; and &lt;a href="https://epoch.ai/publications/open-models-report" rel="noopener noreferrer"&gt;Open vs. closed AI: how behind are open models?&lt;/a&gt;, Epoch AI.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.lesswrong.com/posts/rJcCrXyEsJKmmDpWG/how-far-behind-are-open-models" rel="noopener noreferrer"&gt;How far behind are open models?&lt;/a&gt;, Håvard Tveit Ihle, May 2026, for the public and private benchmark lags.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.verdict.co.uk/uks-over-dependence-on-googles-deepmind-harms-its-global-ai-competitiveness/" rel="noopener noreferrer"&gt;UK's over-dependence on Google's DeepMind harms its global AI competitiveness&lt;/a&gt;, Verdict, for the DeepMind share of UK top-cited papers.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>uk</category>
      <category>sovereignty</category>
      <category>research</category>
    </item>
    <item>
      <title>Knotwork by Rule</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:40:52 +0000</pubDate>
      <link>https://dev.to/stepinto/knotwork-by-rule-46eb</link>
      <guid>https://dev.to/stepinto/knotwork-by-rule-46eb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;All the plans turn into old tickets, half unredeemed.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;&lt;a href="https://stepinto.dev/words/old-tickets.html" rel="noopener noreferrer"&gt;Old Tickets&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The series has been working its way north. Andalusian tile, then English heraldry, then a medieval English floor, and now the furthest north it's going to get: the carpet pages of the Lindisfarne Gospels, the Book of Durrow and the Book of Kells. Whole pages of vellum given over to ribbons that go over, under, over, under, round every corner and back, in red lead, orpiment yellow and verdigris green, with the odd dog's head biting its own tail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/art/knotwork.html" rel="noopener noreferrer"&gt;Knotwork&lt;/a&gt; grows those pages from a seed. It's the most rule-bound piece I've made so far, and it turned out the monks had done most of the thinking already.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwq22bsg67ym872p9donr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwq22bsg67ym872p9donr.webp" alt="A wall of manuscript pages, each a plus-shaped cross of green and blue interlace on red, set in a channel, with a second knot of red, yellow and green ribbons filling the bare vellum around it, and small hound heads at the edges" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The lindisfarne preset. Seed 698, the year Eadfrith became bishop and, so the story goes, started painting.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Over, under, over, under
&lt;/h2&gt;

&lt;p&gt;If you look closely at the back of a Lindisfarne page you can still find the prick marks and the ruled grid. Interlace was laid out on squares, and once you see that, the rule is short.&lt;/p&gt;

&lt;p&gt;Take a grid of squares. Every edge has a midpoint. A strand runs diagonally from one midpoint to the next, so at every midpoint two strands cross. Do nothing else and you get a plain plait. Then pick some edges and make them breaks: a strand that reaches a break turns back off it instead of crossing. The rim of the panel is all breaks, so every strand closes up on itself. Breaks are what turn a plait into a knot, and I chose them by the panel's mirrors so the page stays symmetric, often in runs of two or three along a line, which is where the knot's sharp turns come from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhkqkh09zfqqxjff9lmzk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhkqkh09zfqqxjff9lmzk.webp" alt="Two rectangles of interlace on cream. On the left an even orange plait with every strand crossing; on the right the same grid in blue with some edges turned into breaks, so the strands loop back into knots" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same six by seven grid. On the left, no breaks, one plait. On the right, three in ten interior edges are breaks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bit I was pleased about is over and under. In &lt;a href="https://stepinto.dev/art/cini.html" rel="noopener noreferrer"&gt;Çini&lt;/a&gt; the strapwork walks each strand from end to end and flips over and under at every crossing it meets. Here there's no walking at all. A crossing on a horizontal edge takes the rising strand over and a crossing on a vertical edge takes the falling one. Every step along a strand lands on the other kind of edge, and every bounce off a break flips its direction, so the alternation round every strand is guaranteed by the grid. In code it's one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;over&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&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;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;x&lt;/code&gt; is in doubled coordinates, where an odd &lt;code&gt;x&lt;/code&gt; means a horizontal edge. That's the whole of the weave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points in the corners
&lt;/h2&gt;

&lt;p&gt;My first corners were round, and they looked wrong in a way I couldn't name until I went back to the facsimiles. Where a strand turns in a corner of the panel, the scribes draw it up to a sharp point aimed into the corner and back out again. So the page does that now. There's also a "pointed" setting that sharpens every bounce off the rim, which I use for the Kells preset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffr5yca8hgtqg8m0x0psy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffr5yca8hgtqg8m0x0psy.webp" alt="Three copies of the same blue and green knot. The first has rounded turns everywhere, the second has sharp points in its four corners, the third has points on every turn along the rim as well" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Round, corners, pointed. Same knot, three ways to turn at the edge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The cross-carpet page in the first picture comes from letting a knot fill any region of squares, not just a rectangle. The cross is a plus of knotwork painted on a coloured ground inside a ruled channel, and a second knot fills the bare vellum around it. Then come the frame, its two rules and the red-lead dots. Lindisfarne has a page with over ten thousand of those dots on it, which is the sort of thing that makes you put the pen down for a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The beasts
&lt;/h2&gt;

&lt;p&gt;Insular strands don't always close. Sometimes a ribbon ends in a head, and the head bites something, usually another ribbon or its own tail. On the grid that's a third kind of edge, a cut, where the strand stops instead of turning. Two strand ends always arrive at a cut from the same side, so they pair up neatly: one gets a head, the other becomes the tail it bites.&lt;/p&gt;

&lt;p&gt;The head was the problem. I drew mine from curves the way I'd drawn the first lions for &lt;a href="https://stepinto.dev/art/leonor.html" rel="noopener noreferrer"&gt;Leonor&lt;/a&gt;, and it came out like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw7mb6onna0wsavk4h7d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw7mb6onna0wsavk4h7d.webp" alt="A green ribbon ending in a crude head with an eye, two spikes on top like horns and a flap sticking out at the back like a tongue" width="718" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My drawn head. The question it got was "what is this meant to be?", which was fair.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I did what fixed the lions. I asked an image model for a flat drawing of an Insular hound's head, one gold fill with an even black outline, a red eye, and a piece of blue ribbon gripped in its jaws so I'd know where the bitten strand goes. The neck was to come in from the left as a straight band, cut square, so I could bolt it onto a strand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ethc9rj2wcwt7zry1py.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ethc9rj2wcwt7zry1py.webp" alt="A flat gold hound's head in profile with a red almond eye, a curled lappet off the back of the skull and a long snout, holding a blue ribbon in its open jaws" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The drawing I traced. The blue ribbon hanging below the jaw was cut away first, and the bit inside the mouth kept.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The tracer is the forty lines of Node from the heraldry post. I erased the ribbon below the jaw before tracing and kept the piece inside the mouth, which now gets painted in the tail's colour. The rest was placement. My first go pointed the head straight on along its strand, and every beast stuck out of the knot like a dog through a fence. The version that works lets the neck turn off the strand and narrow, so the head lies along the cut facing its tail, and the tail hooks round under the jaw and up into the mouth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnmtqrlofbit6sp677ay.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnmtqrlofbit6sp677ay.webp" alt="A close view of a knot in split ribbons of red, purple, green and blue on vellum, with four hound heads around it, each lying along the edge with the end of another ribbon curled up into its jaws" width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The kells preset, close up. Four hounds, each with a tail in its mouth.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two heads on neighbouring cuts sat on top of each other and looked like a scrum, so no cut is allowed beside another now. A mirror copy that would collide just stays a plain break. The heads are still big for the grid and lie over a strand or two, and at very high zoom there's a faint seam where the neck meets the traced head. I can live with both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the painter
&lt;/h2&gt;

&lt;p&gt;My favourite preset has no colour at all. It's the page as the scribe would have left it for the painter: ink on vellum, ruled frame, a few hounds along the rims.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5skhkhfxcql59l0nll1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5skhkhfxcql59l0nll1.webp" alt="Panels of interlace drawn in brown ink on uncoloured vellum, each in a ruled double frame, with small hound heads along the edges of some panels" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The drawing preset. Ink only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqacfjxsgj8i8uk129ndb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqacfjxsgj8i8uk129ndb.webp" alt="A large square page of faded red, yellow, green and purple interlace on vellum, with a bare field in the middle where a few hound heads face each other" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The lichfield preset, seed 730, the colour gone soft the way that book's has.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In the package
&lt;/h2&gt;

&lt;p&gt;The knots now live in &lt;a href="https://www.npmjs.com/package/azulejeria" rel="noopener noreferrer"&gt;azulejeria&lt;/a&gt; as their own module, next to the tile engine but not using any of it. It draws on any 2D canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install azulejeria
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildKnot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paintKnot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;azulejeria/knots&lt;/span&gt;&lt;span class="dl"&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 README covers the options: masks for crosses and fields, symmetry, cuts, bends, and the traced head with its narrow neck. The knot's grid is the same one under the Mudéjar lacería, so I'd like to see somebody use it for that. Next on the list is Sutton Hoo, which is garnets set in gold cells, and &lt;a href="https://stepinto.dev/art/alicatado.html" rel="noopener noreferrer"&gt;Alicatado&lt;/a&gt;'s cutter will do most of that work.&lt;/p&gt;

</description>
      <category>art</category>
      <category>generative</category>
      <category>canvas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Heraldry by Rule</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Sat, 24 Oct 2026 08:57:06 +0000</pubDate>
      <link>https://dev.to/stepinto/heraldry-by-rule-3251</link>
      <guid>https://dev.to/stepinto/heraldry-by-rule-3251</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The man who boarded at Gatwick never got off in Seville.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;&lt;a href="https://stepinto.dev/words/teleporter.html" rel="noopener noreferrer"&gt;Teleporter&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Until now the art pages have stayed on the peninsula: Islamic stars, Talavera cobalt, the Alhambra's cut tile, Barcelona's cement floors, Roman pavements under Seville. I'm English, and I wanted the series to come home without changing the subject. The link turns out to be seven hundred years old and made of exactly the right stuff. Eleanor of Castile married Edward I in 1254 and brought her arms with her, the castle of Castile quartered with the lion of León. They're carved on her tomb in Westminster Abbey and on the crosses Edward put up wherever her coffin rested on the way to London. Two centuries later Catherine of Aragon's badge, the pomegranate of Granada beside the Tudor rose, got carved and painted over half of Tudor England. Heraldry is how Spain got into English stone, so heraldry is how England gets into this series.&lt;/p&gt;

&lt;p&gt;Those of you who know me, or regular visitors, will maybe know of my deep love for Spain, my friends there and from there, and my dreams, intact and in ruins. Anyway. I wanted to bring this series home, and there are a few more steps I'll flesh out on the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stepinto.dev/art/leonor.html" rel="noopener noreferrer"&gt;Leonor&lt;/a&gt; is that page. The shield in the middle is hers, or what hers would have looked like on a wall at Westminster: England impaling Castile and León.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iy9ks6id91zvtywbisn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iy9ks6id91zvtywbisn.webp" alt="A hall of shields hung on dark oak panelling, each with its blazon lettered beneath. At the centre, three gold lions on red impaled with the quartered castle and lion of Castile and León" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The hall. Every shield but the centre one is a random legal blazon, generated from its position on the wall.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A sentence that paints
&lt;/h2&gt;

&lt;p&gt;A blazon is a sentence that describes a shield exactly, in a grammar that hasn't really moved since the thirteenth century. "Gules three lions passant guardant in pale Or armed and langued Azure" is England. Read it in order: a red field, three lions walking and looking at you, one above another, gold, with blue claws and tongues. Every word changes the picture, and two heralds handed the same sentence paint the same shield, which I rather love.&lt;/p&gt;

&lt;p&gt;So the page just has to read the sentence. The parser is a recursive descent over a chunk of the grammar: a field, which is a tincture, a division (per pale, per fess, quarterly) or a variation (barry, paly, chequy, lozengy); then ordinaries (chief, fess, pale, bend, chevron, cross, saltire, bordure, pile, canton), each of which can carry charges of its own; then charges by number and arrangement, with their attitudes and the tinctures of their parts; then marshalling, which is impaling and quartering, which recurse back into the whole thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;field&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="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;per&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;DIVISIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;peek&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;take&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;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DIVISIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;take&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tincture&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="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;take&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tincture&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="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;argent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gules&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;VARIATIONS&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;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VARIATIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&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="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;of&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;take&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NUMBERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;take&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tincture&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="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;take&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tincture&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="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;argent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gules&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tincture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;argent&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;It's a lot more forgiving than a herald would be. Unknown words are skipped, a missing tincture defaults, "gardant" and "guardant" both work. The sentence is the shareable thing: &lt;code&gt;?blazon=Or a lion rampant Gules&lt;/code&gt; is Scotland, and whatever you type into the box becomes the URL.&lt;/p&gt;

&lt;p&gt;The generator goes the other way. Every shield in the hall apart from the centre one is a sentence written by a seeded random generator that knows the rule of tincture (metal on colour, colour on metal, never gold on silver or red on blue). It picks a field, then an ordinary, an ordinary with charges on it, a single charge, or three, and writes it out properly with the capitals in the right places. Then it hands the sentence to the same parser you'd type into. The generator can't paint at all. It can only write, which keeps the hall honest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4d6yfhvnb7pyrgsarjo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4d6yfhvnb7pyrgsarjo.webp" alt="Shields on a wall of dressed grey stone: Castile and León at the centre, purple towers on gold, black lions on green and gold stripes, a blue roundel on a gold and black chequer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Castile preset on dressed stone. The hall's palette is the medieval one: Or, Argent, Gules, Azure, Sable, Vert and Purpure, and nothing else.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enamel and gilt
&lt;/h2&gt;

&lt;p&gt;The material is a painted board. Each tincture goes down flat and then gets whatever that pigment would have had. Gold gets a gradient from leaf to shadow and back, and over it a tooled grain, fine diagonal lines punched into the leaf. Silver gets a cool fall-off from top to bottom. The whole shield gets a gloss across it, light at the top left and a touch of dark in the base, and then a black line round the heater shape with a pale line just inside it for the bevel. The shield casts a shadow on the panelling. The oak itself is boards with a grain, and there are stone and red velvet halls for when the oak gets too brown.&lt;/p&gt;

&lt;p&gt;The charges are painted like the tinctures, plus one thing. Every charge gets a single outline round the whole of itself, stroked wide in black before the parts are filled, so a lion built from a body and a tail and a leg reads as one animal. That was the theory, anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lions were toys
&lt;/h2&gt;

&lt;p&gt;I drew the first lions from parts. A body polygon, a tail polygon, a raised leg on top, a scalloped mane, little triangle claws, dot eyes. An afternoon of nudging coordinates got me this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrklmw3dv85kzcckwjo7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrklmw3dv85kzcckwjo7.webp" alt="A shield with three crude gold lions, their manes drawn as spiky sunbursts and their legs as stubs, next to a quartered shield where the purple lion is a shapeless blob" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first lions. England's three should be the thing you recognise first, and they weren't.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A heraldic lion is a particular drawing. Tufted mane, tail curling up over the back into a brush, tongue out, claws splayed, and for passant guardant the head turned full-face so you see both eyes. You can't write that down as a rule and get a lion out. It's a convention copied by hand from roll to roll since the 1250s, and the honest way to get it is to copy a drawing too.&lt;/p&gt;

&lt;p&gt;So I asked an image model for one. The prompt was about what I needed to trace, not what I wanted to look at: a flat vector illustration, one solid gold fill, a thick even black outline around every part, red tongue, blue claws, no shading, no texture, white background. It gave me a lion that a thirteenth-century herald would have recognised, and a purple rampant one to match, and then a pomegranate and a castle on the same terms.&lt;/p&gt;

&lt;p&gt;The tracing is forty lines of Node, and it's pretty dumb. Every pixel is classed by its nearest colour out of white, black, gold, red, blue and green. Each class becomes a mask, each mask becomes closed loops by walking the boundary edges between a pixel that is in and a pixel that is out, each loop is simplified with Douglas-Peucker to a tolerance of two and a half pixels, and the points are normalised to the silhouette's bounding box. The silhouette is one layer. Each colour is a layer. The black lines are a layer. The page fills them in that order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roles&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;P&lt;/span&gt; &lt;span class="o"&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="nf"&gt;loops&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="k"&gt;for &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;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;red&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;blue&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;green&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;P&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;loops&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;detail&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="nx"&gt;P&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;loops&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;detail&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="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;traced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;P&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;DRAW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;lion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&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;traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attitude&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rampant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;TRACED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rampant&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TRACED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;armed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;armed&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;pomegranate&lt;/span&gt;&lt;span class="p"&gt;()&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;traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TRACED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pomegranate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;seeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;green&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leaved&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;castle&lt;/span&gt;&lt;span class="p"&gt;()&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;traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TRACED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;castle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;armed&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="c1"&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 colours in the drawing are labels, and the shield doesn't use them. Red in the lion means "armed", and armed resolves to whatever the blazon said, so the same traced lion paints in Or, Purpure, Gules or Sable, with blue claws or red ones. Red in the pomegranate means "seeded" and green means "leaved". The silhouette is filled even-odd so the loop of the tail stays open and the field shows through. The black line network goes on last, and because the drawing's own outline is in it, the page's outline stroke drops to half its width for traced charges so the lion doesn't go muddy at the size of a quarter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5sdvvhdmsai3vh1xcxym.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5sdvvhdmsai3vh1xcxym.webp" alt="A close view of the centre shield: three gold lions passant guardant with tufted manes and curled tails on red, impaled with the quartered castle of Castile and purple lion rampant of León" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same blazon, traced. The lions are the drawings; the colours are the sentence's.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It costs about ninety kilobytes of numbers, more than the rest of the page put together, and it was worth every byte. The procedural charges are rules and the lions are a tradition, and if you pretend a tradition is a rule you get a sunburst for a mane.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33mtw5m1mmj7dupyw4bl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33mtw5m1mmj7dupyw4bl.webp" alt="A gold pomegranate split to show a cluster of red seeds, with green leaves, on a silver shield" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Argent a pomegranate proper seeded Gules slipped and leaved Vert. Catherine's badge, traced on the same terms as the lions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Small corrections
&lt;/h2&gt;

&lt;p&gt;Two things got fixed along the way that any herald would have spotted straight off. A crescent has its horns pointing up, to chief; mine pointed down, which is a crescent reversed and a different charge. And charges on a bend lie bendwise, turned with it and sized to the band, rather than standing upright in a column across it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyginzp0jx41d5sbewsjw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyginzp0jx41d5sbewsjw.webp" alt="A silver shield with a blue diagonal band carrying three red lions rampant, each turned to lie along the band" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Argent on a bend Azure three lions rampant Gules. They lie bendwise, as they should.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;The tower is still drawn from parts and looks it, so it gets the same treatment when there's a drawing. Two of the pieces this page pointed at have since been made. &lt;a href="https://stepinto.dev/art/rosaygranada.html" rel="noopener noreferrer"&gt;Rosa y Granada&lt;/a&gt; is Catherine's rose and pomegranate as a medieval English floor, white clay inlaid in red, with the pomegranate traced here doing double duty. &lt;a href="https://stepinto.dev/art/knotwork.html" rel="noopener noreferrer"&gt;Knotwork&lt;/a&gt; is interlace on vellum after Lindisfarne and Kells, the same over-and-under as the Mudéjar lacería, and its beasts' heads came out of the same trace-a-drawing trick as these lions. Sutton Hoo's garnet cloisonné is still in the queue, which is Alicatado's cutter in gold. The English pieces are the same rules in a colder light.&lt;/p&gt;

</description>
      <category>art</category>
      <category>generative</category>
      <category>canvas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Cut Tile and Poured Cement</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:16:36 +0000</pubDate>
      <link>https://dev.to/stepinto/cut-tile-and-poured-cement-12en</link>
      <guid>https://dev.to/stepinto/cut-tile-and-poured-cement-12en</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;It was all broken by me really, or built like a clumsy caveman&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;&lt;a href="https://stepinto.dev/words/trying-to-have-something-right-afterwards.html" rel="noopener noreferrer"&gt;Trying to have something right afterwards&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two tile walls I wrote up &lt;a href="https://stepinto.dev/posts/tiles-painted-by-rule/" rel="noopener noreferrer"&gt;last time&lt;/a&gt; are painted: a brush drifts, a wash pools, an ink line goes on top. These two aren't painted at all. &lt;a href="https://stepinto.dev/art/alicatado.html" rel="noopener noreferrer"&gt;Alicatado&lt;/a&gt; is cut: every piece is chipped out of a fired, glazed tile with a hammer and bedded in lime next to its neighbours, the way the dados of the Alhambra were made. &lt;a href="https://stepinto.dev/art/hidraulico.html" rel="noopener noreferrer"&gt;Hidráulico&lt;/a&gt; is pressed: pigmented cement poured into a brass divider, lifted, pressed and left to cure, never fired, the way every hallway in Spain was floored between 1880 and 1960.&lt;/p&gt;

&lt;p&gt;I keep asking the same question from different sides. The first two pages were about what a rule looks like once a hand has painted it, and these are about what it looks like once a material has been through it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frirthmxvlndhhf9bdu0n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frirthmxvlndhhf9bdu0n.webp" alt="A wall of eight-point stars in blue, black, honey and purple glazed tile, with white crosses and small green squares between, every piece edged by pale mortar" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The mexuar preset. Octagons and squares underneath, θ at 45 degrees, and every region a piece.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The geometry is borrowed
&lt;/h2&gt;

&lt;p&gt;Alicatado draws nothing new. The pattern comes straight from &lt;a href="https://stepinto.dev/art/cini.html" rel="noopener noreferrer"&gt;Çini&lt;/a&gt;, which puts a contact point on every edge of a hidden tiling, fires two rays from each at an angle θ, and draws the pattern where the rays collide. Walk the faces of that line web and you have a list of polygons, which Çini colours by class and calls a mosaic.&lt;/p&gt;

&lt;p&gt;That was the bit I kept coming back to. Çini's mosaic mode already insets every face a little and calls the gap grout, and its Alhambra palette already cycles glazes by position. It was most of the way to cut tile without ever treating a face as a thing you could pick up. So the new page uses the same polygons, and all the work is in what happens to each one afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cutter
&lt;/h2&gt;

&lt;p&gt;A piece on a real alicatado wall is cut by hand to a drawing, and it misses the drawing in four ways. It's set a fraction off where it should be, and turned a fraction. Its corners are nicked. Its long edges bow, because a hammer doesn't cut straight. So every face gets a shift, a turn, a jitter on each vertex and a bow on any edge longer than half a unit, all scaled by a slider called cut, and all drawn from a random generator seeded by the face's position, so the same piece is wrong the same way every time you pan back to it.&lt;/p&gt;

&lt;p&gt;Then there's the mortar. Çini shrinks a face towards its centroid to make the joint, which is fine for a square and wrong for a star. Scaling a star towards its centre moves the points a long way and the inner corners hardly at all, so the mortar is wide at the tips and thin in the notches. A real joint is the same width everywhere. The fix is a mitred offset: at each vertex, move inward along the bisector of the two edge normals by the joint width over the cosine of the half angle, clamped so a very sharp point can't shoot off to infinity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;offsetPoly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="c1"&gt;// unit edge directions, then their inward normals (the polygon is CCW)&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;e1x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&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="nx"&gt;e1y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&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="nx"&gt;l1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e1x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e1y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;let&lt;/span&gt; &lt;span class="nx"&gt;e2x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;v&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="nx"&gt;e2y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;v&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="nx"&gt;l2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e2x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e2y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;e1x&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="nx"&gt;l1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;e1y&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="nx"&gt;l1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;e2x&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="nx"&gt;l2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;e2y&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="nx"&gt;l2&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;n1x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;e1y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n1y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e1x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n2x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;e2y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n2y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e2x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n1x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n2x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n1y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n2y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;by&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="nx"&gt;bl&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n1x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;bx&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="nx"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="nx"&gt;bl&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;cosHalf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n1x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n1y&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cosHalf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;l2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.49&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;out&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;v&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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's the whole cutter, twenty lines, and it's what makes a twelve-point wheel look set into a wall rather than drawn on one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each piece came from a different tile
&lt;/h2&gt;

&lt;p&gt;The pieces of a cut-tile wall were chipped from many tiles, fired in many batches, so no two are quite the same colour. Every piece shifts its hue, saturation and lightness a touch from its class colour. On top of that goes a mottle, a soft radial cloud of the same glaze darker or lighter where it ran thick. Then the lustre: a glazed surface is glossy, so each piece gets a small highlight on the side nearer the light, with the light's direction on a slider.&lt;/p&gt;

&lt;p&gt;The pieces also stand proud of the mortar, and the edges are what sell that. For each side of the inset polygon, take its outward normal and dot it with the light direction. Facing the light, stroke a thin pale line; facing away, a thin dark one, with the alpha scaled by the dot product. Nothing is actually raised, but at reading distance the wall has relief, and if you drag the light slider round the relief moves the way it should.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbl33gqo0bo0q7x4qjw7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbl33gqo0bo0q7x4qjw7.webp" alt="A close view of cut tile: white crosses with green centres, blue and black stars, fine crazing in the white glaze, and one piece missing to show the combed mortar bed beneath" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Close, with wear on. The crazing is in the white glaze, where it shows. The missing piece shows the bed, combed by a trowel, with a shadow cast onto it by the pieces still standing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Wear is the last layer, and I got it wrong the first time. A missing piece was a flat patch of mortar colour and a repaired piece was a random glaze from the palette, and both read as mistakes rather than history. Now a missing piece is sunk: darker, grainy, combed, with a shadow along the edges that face the light. A repair keeps its own colour and just looks newer, a touch brighter and glassier, with no crazing. The crazing itself only goes on about a third of the pieces, which is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pajarita
&lt;/h2&gt;

&lt;p&gt;The Alhambra's most famous alicatado isn't a star pattern at all. It's the pajarita, the wall of birds, hexagons and stars in the p3 symmetry group with no mirrors, so every bird is the same hand. Çini draws it directly, with one S-shaped Bézier curve splitting every rhombus into two crescents. For Alicatado I sample each curve into ten points, so a crescent is a polygon like everything else and goes through the same cutter. The hexagons and stars sit over the birds, so they paint their own mortar first: a wide stroke of lime around the piece, then the inset fill on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4u0ogy5lrtp8x1k0lw70.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4u0ogy5lrtp8x1k0lw70.webp" alt="Interlocking bird shapes in cream, black, honey, green and blue around white hexagons and small stars in purple, black and green" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pajaritos. Every curve is a polygon now, and every bird a piece.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Then cement
&lt;/h2&gt;

&lt;p&gt;Hidráulico is the opposite kind of page. Alicatado used none of the azulejeria engine and Hidráulico uses all of it, exactly as Azulejería and Jerez do. The only thing that changes is what the tile is made of.&lt;/p&gt;

&lt;p&gt;A cement tile isn't painted, so there's no wash and no ink line. The pigment goes into a brass divider and comes out flat, matte, and butted straight against the next colour with no outline between them. The divider is metal, so edges are straight. The only hand in it is the bleed: a fraction of a millimetre where one pigment crept under the divider before it lifted. The surface is cement, so it has grain and cures a little unevenly. It doesn't craze, but it wears, and it wears where people walk.&lt;/p&gt;

&lt;p&gt;So the engine now takes a material, an object of optional hooks that change what the tile is made of without touching what is drawn on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ampOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hand&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0015&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// the mould is brass; the hand is only bleed&lt;/span&gt;
  &lt;span class="nf"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                  &lt;span class="c1"&gt;// flat fill, then the pigment that crept under&lt;/span&gt;
    &lt;span class="nf"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pts&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="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&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;bleed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&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="nx"&gt;bleed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0005&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bleed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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="nf"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* grain, and the cast of an uneven cure */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* a checker's plain tile is solid pigment */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.003&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;      &lt;span class="c1"&gt;// laid tight, machine square&lt;/span&gt;
  &lt;span class="na"&gt;gapOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="c1"&gt;// a hairline joint&lt;/span&gt;
  &lt;span class="nf"&gt;age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                            &lt;span class="c1"&gt;// the pigment dulls along the line people walk&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;walk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.6&lt;/span&gt;&lt;span class="p"&gt;))&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;walk&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hexA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;glaze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&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="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&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="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// then scuffs, the odd crack, a corner chipped to the grey beneath&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;shape&lt;/code&gt;, &lt;code&gt;line&lt;/code&gt; and &lt;code&gt;dot&lt;/code&gt; replace the brush's strokes. &lt;code&gt;surface&lt;/code&gt; lays the ground, &lt;code&gt;plain&lt;/code&gt; paints the tile a checker alternates with, &lt;code&gt;lay&lt;/code&gt; is how far a tile strays from its cell, &lt;code&gt;gapOf&lt;/code&gt; is the joint, and &lt;code&gt;age&lt;/code&gt; is drawn over each tile in its own frame when age is on, with the tile's world position passed in so wear can follow a line through the floor rather than a line through the screen. Without a material, every hook falls back to the glazed behaviour.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlz82vn2b8cj669utd9m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlz82vn2b8cj669utd9m.webp" alt="A floor of burgundy cement tiles on cream: octagonal frames around four-petal flowers, concentric rings at the corners, a grid of burgundy lines through every join" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The zaguán preset. Flat burgundy and grey, straight edges, and a joint you can barely see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2ozzpodus4v4p9rrvqq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2ozzpodus4v4p9rrvqq.webp" alt="A grey cement floor of nested squares, paler and scuffed along a band through the middle where it has been walked, with a few cracks" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bodega, with wear on. The paler band through the middle is the walked line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The design never finds out. None of the hooks read the design's random generator, so a material can't move a motif, and the old walls are untouched by construction. I checked anyway: I hashed the canvas bytes of every Azulejería and Jerez preset, two query-string walls with age and checker on, and every thumbnail in every panel, before and after the change, and all twenty-two were identical. The package is at 1.1.0 and the README has the hooks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgf4n0yl64bhi79obqkt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgf4n0yl64bhi79obqkt.webp" alt="Cream cement tiles each carrying an eight-point star in ochre inside a green diamond and a brown ring, with brown stars where four tiles meet" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Casino. Every tile whole, the star at its centre, and a second star made by four corners.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two pages taught the engine
&lt;/h2&gt;

&lt;p&gt;Alicatado is the more interesting of the two for the package, because it doesn't use it and still ended up rewriting half of it. Not the tile half: the drag, the pinch, the wheel, the double tap, the buffered repaint that lets you pan without recomputing the world, the panel with its tabs and thumbnails, the query string, the seeded random and the colour helpers. All of that is the same on every page, and Alicatado has its own copy because the engine only offers it bundled with a grid of square tiles.&lt;/p&gt;

&lt;p&gt;So the next version of the engine draws its line somewhere else. A viewer: a canvas you paint a world into, with all the interaction and the panel. And on top of it, a tile wall, which is one paint function and the materials. Alicatado becomes the viewer plus its own paint function. So would a trencadís page, or a Roman mosaic, which is where the cutter would get reused. That's a breaking change, so I waited until there were three materials to design it against instead of one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1avp12yjpr3x3zdgtue.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1avp12yjpr3x3zdgtue.webp" alt="A dense zellige of small cut pieces on a near-black ground: stars in gold, turquoise, terracotta and green, with pale mortar between" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The fez preset. Small pieces, dark ground, the mortar doing most of the drawing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>art</category>
      <category>generative</category>
      <category>canvas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Tiles Painted by Rule</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:07:51 +0000</pubDate>
      <link>https://dev.to/stepinto/tiles-painted-by-rule-442o</link>
      <guid>https://dev.to/stepinto/tiles-painted-by-rule-442o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The roses and white lilies, their petals curl and tap the azulejería,&lt;br&gt;
The sounds of a future unrealised.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;&lt;a href="https://stepinto.dev/words/sounds-of-a-future-unrealised.html" rel="noopener noreferrer"&gt;Sounds of a Future Unrealised&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two of the art pages here are walls of tin-glazed tiles. &lt;a href="https://stepinto.dev/art/azulejeria.html" rel="noopener noreferrer"&gt;Azulejería&lt;/a&gt; is the cobalt rosette of a Talavera dado and the strapwork of a Seville patio. &lt;a href="https://stepinto.dev/art/jerez.html" rel="noopener noreferrer"&gt;Jerez&lt;/a&gt; is the same wall sent south: grapes and vine leaves, horses' heads, a sherry glass sitting across every join. Neither page has a picture in it. Each grows a design from a seed and paints it the way a brush would, then keeps repeating it until the screen is full.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhm919885tqr126ttjavw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhm919885tqr126ttjavw.webp" alt="A wall of blue and white tiles, each four forming a rosette with a leaf at every corner" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Design number 71, cobalt on white. Every tile is a quarter of the flower.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Jerez started life as Azulejería with the motifs swapped, and by the time it worked the two files shared about six hundred lines. So I pulled the shared part out into a package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install azulejeria
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how the wall gets made, and what I had to change to pull the engine out from under the motifs.&lt;/p&gt;

&lt;h2&gt;
  
  
  One square, mirrored across its diagonal
&lt;/h2&gt;

&lt;p&gt;The módulo, the four-tile repeat every Andalusian dado is laid in, is a pattern that only completes where four tiles meet. The tile on its own is a quarter of a flower, a quarter of a star, and the wall supplies the rest by laying each neighbour mirrored. The whole design lives in one unit square, and the trick only works because that square is symmetric across its diagonal. Swap x and y and you get the same picture.&lt;/p&gt;

&lt;p&gt;Corner (0,0) holds the main motif. Corner (1,1) holds a small one. The midpoint of each edge holds half of something, so the tile next door finishes it. A leaf or a strap runs up the diagonal, which is its own mirror. Everything else is placed in pairs, at (x,y) and (y,x).&lt;/p&gt;

&lt;p&gt;With that one constraint the same list of elements serves four repeats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Module, mirrored.&lt;/strong&gt; The design fills the tile with its main corner at bottom right. Odd columns are flipped left to right, odd rows top to bottom, so four tiles make one great rosette, and the far corners make a smaller one between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module, turned.&lt;/strong&gt; Same tile, rotated by quarter turns instead of flipped. The motif completes the same way, but the brush strokes now run in four directions, which reads less like a stamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single.&lt;/strong&gt; The design is laid four times inside one tile, mirrored about its centre, so every tile carries the whole motif. The motif is drawn at 1.35 times the size to fill it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checker.&lt;/strong&gt; Single tiles alternate with plain ones, a ring and four dots in the middle, the way an ajedrezado floor does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgaaxhmv6mjp8hn2si4z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgaaxhmv6mjp8hn2si4z.webp" alt="One painted tile on its own, then the same design laid as a mirrored module, a turned module, single whole tiles, and a checker of painted and plain tiles" width="799" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same design five ways. On the left, one tile alone, with the rosette's corner at bottom right. Then mirrored, turned, every tile whole, and checkered with plain ones.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the page grows a design it has no idea which repeat you'll ask for, and it doesn't need one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growing a design from a seed
&lt;/h2&gt;

&lt;p&gt;A design is a list of elements, and each element names a painter and carries its parameters. The seed feeds a small deterministic random generator (mulberry32, thirty lines, good enough), and a motif function reads from it in a fixed order. The same seed always grows the same design, so &lt;code&gt;?seed=4242&lt;/code&gt; is a wall you can send to someone.&lt;/p&gt;

&lt;p&gt;This is the rosette, as the Azulejería page defines it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;flora&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;density&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dots&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;els&lt;/span&gt; &lt;span class="o"&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;els&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rosette&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.07&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rOut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.26&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="na"&gt;wd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&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="na"&gt;scallop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;role&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="nx"&gt;density&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;els&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&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="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;role&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="nx"&gt;density&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;els&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;half&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;role&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="nx"&gt;density&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;els&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leaf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;r0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;wd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wash&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;bands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;els&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;band&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle&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;diamond&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;circle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.035&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;role&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="nx"&gt;density&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;els&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ring&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lobes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&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="nx"&gt;density&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;els&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="nf"&gt;dots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;els&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 density slider is just how far down that list you get to go. One is the rosette alone. Five adds a scalloped ring and a scatter of dots in the spare space. &lt;code&gt;role()&lt;/code&gt; decides whether an element is painted in the main wash or in one of the palette's accents, and a palette with no accents gets everything in cobalt, which is how a cobalt-only palette stays blue and white while Seville goes polychrome from the same motif.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdztddbxlb3wuls5vilam.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdztddbxlb3wuls5vilam.webp" alt="Five four-tile modules of the same rosette, gaining a bud, leaves, a band, a scalloped ring and dots as the density rises" width="799" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Design number 71 at density one to five. Nothing moves, the list just gets longer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Jerez motifs are the same thing with different nouns. The horse motif starts with two heads, adds a horseshoe near the far corner (so four cluster at every grout crossing), then a feria cockade half on each edge, then reins as a lattice. The bodega motif crosses two venencias at the corner and lies a bota up the diagonal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0swmfbaqbzz1bd0185ba.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0swmfbaqbzz1bd0185ba.webp" alt="Three whole tiles: a ring of grape bunches with vine leaves, four grey horses' heads meeting at the centre with horseshoes in the corners, and crossed venencias with botas on the diagonals" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The three Jerez motifs as whole tiles: viña, caballo, bodega. Same corners, same edges, different nouns.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can smell fino just looking at those generated azulejos.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;Juanka&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Painting like a hand
&lt;/h2&gt;

&lt;p&gt;Draw a design with clean arcs and straight lines and you get clip art. The pages look painted because every line drifts.&lt;/p&gt;

&lt;p&gt;The drift is a wobble function: two sines of different pitch and random phase, summed, applied along the normal of whatever is being drawn. An arc becomes a polyline whose radius breathes a little as it goes round. A straight line bows. A petal gets a different wobble on each side so it is never quite symmetric. The hand slider scales the amplitude. At zero the brush turns into a plotter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bqbcu5ztnuwt82x6xgn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bqbcu5ztnuwt82x6xgn.webp" alt="The same four-tile rosette painted three times, the lines growing more uneven from left to right" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Hand at zero, a half and one. Same seed, same design, and the brush gets less sure of itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Colour goes on the way a glaze does. A wash is filled at about half opacity, then filled again shifted by a hair at a fifth opacity, so the colour pools at one edge of every shape. The ink line on top varies its width by up to twenty percent per stroke. Under all of it the tile's white is three soft radial blooms of the palette's tint colour, placed by the tile's own seed, with a faint darker rim where a real glaze thickens at the edge.&lt;/p&gt;

&lt;p&gt;Then the wall adds its own imperfection. Each cell of the grid hashes its position with the seed and uses that to pick one of four painted variants of the tile, nudge it by up to one and a half percent of its width, and turn it by up to a degree. Switch on age and half the tiles get a warm cast, a fifth get a craze line or two, and one in twenty loses a corner to the grout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5tidw6aa1igt7rbieun.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5tidw6aa1igt7rbieun.webp" alt="A wall of cream and mahogany tiles, venencias crossed on every one, some tiles darker, a few with fine crack lines" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The oloroso preset with age on. The crazing and the warm cast are the wall's doing, not the tile's.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall
&lt;/h2&gt;

&lt;p&gt;Tiles are expensive to paint and cheap to draw, so each one gets painted once into an offscreen canvas at the current zoom and the wall is just &lt;code&gt;drawImage&lt;/code&gt; in a loop. Four variants of the painted tile, three of the plain one for checker, and the hash decides which goes where. Pan is a translation. Zoom scales the cached tiles immediately so it feels live, then repaints them at the new size once the wheel has been still for a sixth of a second.&lt;/p&gt;

&lt;p&gt;Double-tap picks a new seed. A design number shows in the panel so you can find it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling the engine out
&lt;/h2&gt;

&lt;p&gt;When I laid the two files side by side the shared part was obvious: the random generator, every geometry helper, the ink and wash strokes, the tile painter, the wall renderer, the pointer handling, and the panel wiring with its tabs, thumbnails, sliders and query string. What differed was the palettes, the motif functions, the painters those motifs name, the presets, and the colours of the panel.&lt;/p&gt;

&lt;p&gt;So that's where the package draws its line. &lt;code&gt;createWall(canvas, { palettes, motifs, painters, presets })&lt;/code&gt; is the engine. A motif is a function of the seeded context that returns elements. A painter is a function of an element and a brush:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;bud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;petal&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;el&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="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;ink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;arcPts&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.035&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;lw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&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 brush carries the scaled canvas context, the palette, the line weight, the wobble amplitude, the design's random generator, the colour the element's role resolved to, and the strokes every motif reaches for: &lt;code&gt;petal&lt;/code&gt;, &lt;code&gt;kite&lt;/code&gt;, &lt;code&gt;shape&lt;/code&gt; (wash then ink), &lt;code&gt;dot&lt;/code&gt;, &lt;code&gt;lobes&lt;/code&gt;. Four painters ship with the engine because both pages used them unchanged: the band across the far corner, the scalloped ring, the lattice line, the dots. Everything else, from the horse's head (a nineteen-point polygon traced once and placed twice) to the half catavino whose stem runs along the tile edge, stays in the page that owns it.&lt;/p&gt;

&lt;p&gt;The panel's CSS went the same way. The two pages' stylesheets differed in fourteen colour values and nothing else, so the package ships the stylesheet with custom properties and each page sets a dozen of them.&lt;/p&gt;

&lt;p&gt;I was most careful about the random generator's call order. A painter that reads &lt;code&gt;rng()&lt;/code&gt; one extra time shifts every wobble after it, and the wall for seed 71 is no longer the wall for seed 71. So I checked the refactor at the pixel. I rendered every preset on both pages before and after, plus a couple of query-string walls with age and checker on, and every thumbnail in every panel, and compared the canvas bytes. All identical. The pages look exactly as they did, and for a refactor of an art piece that's the only result I'd accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving the same file twice
&lt;/h2&gt;

&lt;p&gt;The pages import the engine from &lt;code&gt;/art/lib/azulejeria.js&lt;/code&gt;. That path is a Hugo mount of the package's source directory, so the art pages run the same file npm ships. There's no build step because there's nothing to build: one ES module, one stylesheet, no dependencies.&lt;/p&gt;

&lt;p&gt;If you fancy a wall of your own, the &lt;a href="https://www.npmjs.com/package/azulejeria" rel="noopener noreferrer"&gt;README&lt;/a&gt; has a forty-line example, and the two pages are the long versions. The motifs are the fun bit to write. Mine are a Talavera rosette, Mudéjar strapwork, a vine, a horse and a bodega, and I would rather like to see what someone else's corner of the world looks like as a quarter tile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbddixlslnfg333jauj4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbddixlslnfg333jauj4.webp" alt="A wall of grey and white tiles where eight horses' heads meet around each point and four horseshoes cluster at every grout crossing" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The cartujano preset. Eight heads around a point, four shoes at every crossing, and not one of them drawn by hand.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>art</category>
      <category>generative</category>
      <category>canvas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Hot Path to Claude Is a Shell Script</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:35:54 +0000</pubDate>
      <link>https://dev.to/stepinto/the-hot-path-to-claude-is-a-shell-script-29h3</link>
      <guid>https://dev.to/stepinto/the-hot-path-to-claude-is-a-shell-script-29h3</guid>
      <description>&lt;p&gt;The hot path to Claude is one HTTPS POST and a stream of six event types. Every SDK wraps that in thousands of lines, and the wrapping costs you: in cold start, in memory, and in the milliseconds between the model finishing a thought and your code acting on it. For a chat window none of that matters. For a CLI that should feel instant, a Lambda that is billed from the first millisecond, or an agent loop making ten thousand calls a day, it is the only part of the latency you control.&lt;/p&gt;

&lt;p&gt;This series writes the path by hand, one language a week, one file each, and measures what each one adds. This post is the floor: a complete client in bash and curl, in one file, with no jq, that streams the reply as it arrives, completes a tool call round trip, and reports API errors properly. Everything that follows is measured against it, and against the official SDKs.&lt;/p&gt;

&lt;p&gt;The code and the harness are at &lt;a href="https://github.com/AndyCross/claude-hot-path" rel="noopener noreferrer"&gt;github.com/AndyCross/claude-hot-path&lt;/a&gt;, and the leaderboard lives at &lt;a href="https://stepinto.dev/claude-hot-path/" rel="noopener noreferrer"&gt;/claude-hot-path/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec
&lt;/h2&gt;

&lt;p&gt;Every client in the series has to pass the same seven points, and a harness checks them rather than me reading the code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the key from &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; and the base URL from &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;. Nothing else from the environment.&lt;/li&gt;
&lt;li&gt;Read the prompt from stdin.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /v1/messages&lt;/code&gt; with the &lt;code&gt;x-api-key&lt;/code&gt;, &lt;code&gt;anthropic-version: 2023-06-01&lt;/code&gt; and &lt;code&gt;content-type: application/json&lt;/code&gt; headers, and a body with &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;, &lt;code&gt;stream: true&lt;/code&gt;, &lt;code&gt;messages&lt;/code&gt; and &lt;code&gt;tools&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Print each &lt;code&gt;text_delta&lt;/code&gt; as it arrives. The harness checks that the first visible byte reached stdout before the mock finished sending the stream, so buffering the reply, or flushing only on newlines, fails.&lt;/li&gt;
&lt;li&gt;Declare one tool, &lt;code&gt;local_time&lt;/code&gt;, which takes an IANA timezone. When the stream ends with &lt;code&gt;stop_reason: "tool_use"&lt;/code&gt;, assemble the &lt;code&gt;input_json_delta&lt;/code&gt; fragments, run the tool, send the &lt;code&gt;tool_result&lt;/code&gt; back in a second request with the assistant turn echoed, and stream that reply too.&lt;/li&gt;
&lt;li&gt;On a non-2xx response, print the body's &lt;code&gt;error.message&lt;/code&gt; to stderr and exit non-zero.&lt;/li&gt;
&lt;li&gt;One source file. HTTP and TLS from the platform where it has them. Where it does not (C, Zig), one named library, which counts towards size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 5 is the one that separates a client from a curl wrapper. Tool use is where the protocol stops being request and response and becomes a conversation you have to maintain, and the tool's arguments arrive as fragments of JSON that you assemble yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes down the wire
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;stream: true&lt;/code&gt; the response is &lt;code&gt;text/event-stream&lt;/code&gt;. Each event is an &lt;code&gt;event:&lt;/code&gt; line, a &lt;code&gt;data:&lt;/code&gt; line carrying JSON, and a blank line. Here is the shape of the first turn, from a real recording, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event: message_start
data: {"type":"message_start","message":{"id":"msg_...","model":"claude-opus-5-5",...}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_...","name":"local_time","input":{}}}

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"timezone\""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":": \"Europe/London\"}"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":56}}

event: message_stop
data: {"type":"message_stop"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things I had wrong before I recorded it. The model went straight to the tool call with no preamble text, so a client that assumes a text block always comes first is wrong. And there is a &lt;code&gt;ping&lt;/code&gt; event in the middle that the documentation mentions and nobody's mental model includes. A client has to ignore event types it does not know.&lt;/p&gt;

&lt;p&gt;The six you act on are &lt;code&gt;message_start&lt;/code&gt;, &lt;code&gt;content_block_start&lt;/code&gt;, &lt;code&gt;content_block_delta&lt;/code&gt;, &lt;code&gt;content_block_stop&lt;/code&gt;, &lt;code&gt;message_delta&lt;/code&gt; and &lt;code&gt;message_stop&lt;/code&gt;. Deltas come in two kinds, &lt;code&gt;text_delta&lt;/code&gt; with a &lt;code&gt;text&lt;/code&gt; field and &lt;code&gt;input_json_delta&lt;/code&gt; with a &lt;code&gt;partial_json&lt;/code&gt; field, and the &lt;code&gt;index&lt;/code&gt; says which content block they belong to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client
&lt;/h2&gt;

&lt;p&gt;The whole thing is 93 lines. The interesting parts follow, and the rest is in the repo.&lt;/p&gt;

&lt;p&gt;JSON without jq means encoding strings yourself. The prompt comes from stdin and could contain anything, so backslash, quote and the control characters have to be escaped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;json_str&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\\/\\\\&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\&lt;/span&gt;&lt;span class="s2"&gt;"/&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;}
  s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/\\n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/\\r&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/\\t&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  printf '"&lt;/span&gt;&lt;span class="p"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"' "&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding goes the other way. The text deltas arrive as JSON strings, escapes and all, and bash's &lt;code&gt;printf '%b'&lt;/code&gt; happens to understand &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\t&lt;/code&gt;, &lt;code&gt;\\&lt;/code&gt; and &lt;code&gt;\uXXXX&lt;/code&gt;, so after swapping &lt;code&gt;\"&lt;/code&gt; for &lt;code&gt;"&lt;/code&gt; you can hand it the string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;json_unstr&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\\\&lt;/span&gt;&lt;span class="s2"&gt;"/&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}; s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\\\//\/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  printf '%b' "&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;\uXXXX&lt;/code&gt; support needs a UTF-8 locale. In a slim Debian image there is none set, &lt;code&gt;printf&lt;/code&gt; prints the escape literally, and "café" comes out as &lt;code&gt;caf\u00E9&lt;/code&gt;. The Dockerfile sets &lt;code&gt;LC_ALL=C.UTF-8&lt;/code&gt; and that is the kind of thing this series is for.&lt;/p&gt;

&lt;p&gt;The stream loop is curl with &lt;code&gt;-N&lt;/code&gt; to turn off its output buffering, read line by line, and a regex per event type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; line &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; data:&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; event:&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; errbody+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;#data&lt;/span&gt;:&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;text_delta&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;text&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;.|[^&lt;span class="se"&gt;\"\\&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;json_unstr &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf &lt;/span&gt;x&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;%x&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; ASSISTANT_TEXT+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;input_json_delta&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;partial_json&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;.|[^&lt;span class="se"&gt;\"\\&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;TOOL_INPUT+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;json_unstr &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;tool_use&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;^&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;name&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;^&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;TOOL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;TOOL_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[2]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;stop_reason&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;a-z_]+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;STOP_REASON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hdr&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"anthropic-version: 2023-06-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data-binary&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| [[ -n $line ]]&lt;/code&gt; on the &lt;code&gt;read&lt;/code&gt; is there because an error response is a single JSON line with no trailing newline, and &lt;code&gt;read&lt;/code&gt; returns false on the last line of a file that lacks one, so without it the loop body never ran for the only line that mattered. Lines that are not &lt;code&gt;data:&lt;/code&gt;, &lt;code&gt;event:&lt;/code&gt; or blank are collected as the error body, and after the loop the status code comes out of the headers file curl wrote with &lt;code&gt;-D&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The regex is doing the job a JSON parser would. It works because the API emits compact JSON with the keys in a known order, and that is the honest caveat: a bash client is parsing a wire format with pattern matching, and the first time the server reorders two keys it breaks. Every other client in the series uses a real parser. This one is the floor, and it is meant to be.&lt;/p&gt;

&lt;p&gt;When the first stream ends with &lt;code&gt;tool_use&lt;/code&gt;, the client runs the tool with &lt;code&gt;TZ="$timezone" date&lt;/code&gt;, builds the assistant turn back out of what it collected (any text, then the &lt;code&gt;tool_use&lt;/code&gt; block with its assembled input) and sends the three-message conversation as a second request. The reply to that is the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it measures as
&lt;/h2&gt;

&lt;p&gt;The harness runs every client in its own container against a mock that replays the recorded exchange with the original gaps between chunks. Nothing is measured over the network. The one real call per client is for conformance, and no timing from it is published.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;bash and curl&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Artefact&lt;/td&gt;
&lt;td&gt;3.7 KB (the script)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;23.7 MB (bash, curl, and every shared library they link)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start, median of 100&lt;/td&gt;
&lt;td&gt;7.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turnaround, median of 100&lt;/td&gt;
&lt;td&gt;19.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client total, median of 100&lt;/td&gt;
&lt;td&gt;35.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;11 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of code&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apple M4 Max under Docker, linux/arm64, bash 5.2.37, curl 8.14.1 with OpenSSL 3.5.7.&lt;/p&gt;

&lt;p&gt;Cold start is process spawn to the mock receiving the first request. Turnaround is the first reply finishing to the second request arriving, which is the client parsing the stream, running the tool and sending again. Client total adds the tail after the last reply, so it is every millisecond the mock was not streaming. All three are differences between timestamps the mock recorded and the runner's spawn and exit, with nothing subtracted. The runtime figure counts the shared libraries because a static binary pays for its TLS and HTTP inside itself, and a dynamically linked curl should pay for libcurl and OpenSSL the same way. Without them bash would show 1.7 MB and win on a technicality.&lt;/p&gt;

&lt;p&gt;Thirty-five milliseconds of client time for two requests is two curl process spawns, a &lt;code&gt;date&lt;/code&gt; process, and a few hundred regex matches in a shell loop. Nineteen of those milliseconds are the turnaround alone. The Go and C# clients that follow do the same work in under a quarter of that, and the interesting question for the rest of the series is whether anything gets much below what Go does, or much smaller than what a Native AOT .NET binary does. I have an opinion about both and the leaderboard will tell me whether it was right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing to beat
&lt;/h2&gt;

&lt;p&gt;Every row on the leaderboard is measured against the official SDK as well as against the other hand-written clients. The SDK rows sit below a line on the board and are not ranked, because they are the reference rather than entries. The first is the Python SDK, &lt;code&gt;anthropic&lt;/code&gt; 1.15.0 on Python 3.14, written the way its documentation says: &lt;code&gt;messages.stream()&lt;/code&gt;, &lt;code&gt;text_stream&lt;/code&gt;, &lt;code&gt;get_final_message()&lt;/code&gt;, and the manual tool loop from its own tool-use page. Fifty-five lines, which is the shortest client on the board, and the slowest by a wide margin. Next to it, so the cost of Python and the cost of the SDK can be told apart, is a client in one Python file on &lt;code&gt;http.client&lt;/code&gt; and &lt;code&gt;json&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;bash and curl&lt;/th&gt;
&lt;th&gt;Python, standard library&lt;/th&gt;
&lt;th&gt;Python SDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On the path, median of 100&lt;/td&gt;
&lt;td&gt;35.3 ms&lt;/td&gt;
&lt;td&gt;140.4 ms&lt;/td&gt;
&lt;td&gt;592.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;7.0 ms&lt;/td&gt;
&lt;td&gt;123.8 ms&lt;/td&gt;
&lt;td&gt;464.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turnaround&lt;/td&gt;
&lt;td&gt;19.1 ms&lt;/td&gt;
&lt;td&gt;1.9 ms&lt;/td&gt;
&lt;td&gt;8.7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tail after the last reply&lt;/td&gt;
&lt;td&gt;9.2 ms&lt;/td&gt;
&lt;td&gt;15.2 ms&lt;/td&gt;
&lt;td&gt;119.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;11 MB&lt;/td&gt;
&lt;td&gt;21 MB&lt;/td&gt;
&lt;td&gt;84 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime shipped&lt;/td&gt;
&lt;td&gt;23.7 MB&lt;/td&gt;
&lt;td&gt;35.5 MB&lt;/td&gt;
&lt;td&gt;71.9 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Python itself costs 140 ms per process, almost all of it the interpreter starting and importing four standard modules. The SDK costs 450 ms on top: pydantic, httpx, anyio and the generated types on the way in, and the HTTP client's pool and the interpreter tearing all of that down on the way out, after the answer has already been printed. In between, while the stream is actually flowing, the SDK is quick. If your process lives for an hour, none of this is a number you would notice. If it lives for one call, it is half a second every time, and that is what this series is about. The &lt;a href="https://stepinto.dev/posts/claude-hot-path-python/" rel="noopener noreferrer"&gt;Python post&lt;/a&gt; has both clients in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AndyCross/claude-hot-path
&lt;span class="nb"&gt;cd &lt;/span&gt;claude-hot-path
./bench/bench.sh bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need Docker and Python 3 and you do not need a key. The fixtures in the repo are real recordings. If you want to re-record them, &lt;code&gt;./bench/bench.sh record bash&lt;/code&gt; runs one exchange through a proxy that reads your key from the shell and never writes it anywhere, and the client under test is handed a placeholder.&lt;/p&gt;

&lt;p&gt;Adding a language is a directory with the source file, a Dockerfile and a four-line &lt;code&gt;meta.env&lt;/code&gt;, and the CI runs the bench on every pull request. If you want to see your language on the leaderboard before I get to it, that is the way.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>bash</category>
      <category>curl</category>
    </item>
    <item>
      <title>The Danger of a Hidden Sun</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Tue, 20 Oct 2026 11:15:50 +0000</pubDate>
      <link>https://dev.to/stepinto/the-danger-of-a-hidden-sun-1456</link>
      <guid>https://dev.to/stepinto/the-danger-of-a-hidden-sun-1456</guid>
      <description>&lt;p&gt;I spent a weekend trying to make a 3D scene look less flat and ended up making it faster instead. The flatness and the frame drops turned out to be the same bug, viewed from opposite sides.&lt;/p&gt;

&lt;p&gt;I was helping out a friend on their app. The scene is a few hundred small buildings on a flat ground plane, rendered with &lt;a href="https://github.com/google/filament" rel="noopener noreferrer"&gt;Filament&lt;/a&gt; through react-native-filament, running on whatever phone the user happens to own. It has a performance budget system with tiers, and the lower tiers get simplified meshes, fewer decorations and smaller render arenas. It is the kind of app where you count draw calls.&lt;/p&gt;

&lt;p&gt;What I had not counted was the sun.&lt;/p&gt;

&lt;h2&gt;
  
  
  An all-emissive pipeline
&lt;/h2&gt;

&lt;p&gt;The models were authored the classic mobile way. Ambient occlusion and colour are baked into one texture per material, and that texture goes in the glTF &lt;strong&gt;emissive&lt;/strong&gt; slot with the base colour set to black. The result is a surface that looks the same from every angle, needs no lights, and is cheap. Artists have done this for years and for good reason. Per-instance tinting works too: you multiply the emissive factor on a material instance and the shared texture picks up the colour.&lt;/p&gt;

&lt;p&gt;I checked the material setup across every GLB the app loads, with a short script that reads the JSON chunk out of each binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&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="n"&gt;gltf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gltf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;materials&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;pbr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pbrMetallicRoughness&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pbr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;baseColorFactor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;emissiveTex&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;emissiveTexture&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unlit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;KHR_materials_unlit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;extensions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out of 480 materials, 477 were black base colour plus emissive texture, which is fine. That is the pipeline working as designed.&lt;/p&gt;

&lt;p&gt;The last column is the problem. Every one of them said &lt;code&gt;lit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a lit material costs when it has nothing to light
&lt;/h2&gt;

&lt;p&gt;Filament's glTF loader picks a shader from the material's declared shading model. A material without the &lt;code&gt;KHR_materials_unlit&lt;/code&gt; extension is lit, and the lit "ubershader" does the full job on every fragment: evaluate the directional light, sample the shadow map, look up the indirect light from the environment, run the BRDF. Then it multiplies the diffuse term by the base colour.&lt;/p&gt;

&lt;p&gt;The base colour is black. So the fragment shader does all of that work, multiplies it by zero, adds the emissive texture on top, and writes the pixel. It does that every frame, for every covered pixel, on a phone.&lt;/p&gt;

&lt;p&gt;And there was a sun. The scene had a directional light at 80,000 lux with shadow casting enabled, because that is what you put in a scene when you set it up, and nobody takes it out. A light that casts shadows makes Filament render a shadow map before the main pass: every caster drawn again from the light's point of view into a depth texture, each time the camera or anything in the scene moves. On a map that scrolls, that is most frames.&lt;/p&gt;

&lt;p&gt;Not one pixel showed any of this. Shadows darken the lit diffuse term, which was zero. The indirect light brightens the lit diffuse term, which was zero. The sun was a hidden workload with no output, sitting in the scene like a process you forgot was running.&lt;/p&gt;

&lt;p&gt;I had been treating lighting as something I could add later, when the tier budget allowed. The budget was already being spent. The sun was my single biggest "SUPER tier" feature, enabled on the cheapest phones the app supports, and nobody knew it was on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding it from the other direction
&lt;/h2&gt;

&lt;p&gt;I only noticed because I was trying to make things look better. I had seen a lovely three.js scene with shaded, matcap-style buildings and wondered why mine looked like paper cutouts. The honest answer was that nothing in my scene responded to light, so of course it looked flat. Chasing that down meant reading the material JSON above and then rendering the models headlessly with Filament's own &lt;code&gt;gltf_viewer&lt;/code&gt;, which ships in the Filament release archive alongside &lt;code&gt;matc&lt;/code&gt; and &lt;code&gt;cmgen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The headless run confirmed it. With the stock materials, shadows on, SSAO on, an environment map loaded, the render was identical to shadows off, SSAO off, no environment. The pipeline had quietly made every lighting feature a no-op, and the lit shader was still running underneath.&lt;/p&gt;

&lt;p&gt;It is worth getting into the habit of rendering your assets outside your app with the engine's reference viewer. The viewer has a batch mode that takes a JSON spec and writes screenshots without a window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gltf_viewer &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;spec.json &lt;span class="nt"&gt;--ibl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./sky model.glb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If toggling a feature in that spec changes nothing in the output, you are paying for that feature and getting nothing back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Light&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sun"&lt;/span&gt; &lt;span class="na"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;castShadows&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Light&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sun"&lt;/span&gt; &lt;span class="na"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes the shadow map pass. The lit shader still runs, but the second geometry pass is gone. I was sceptical it would be noticeable. It was. On a mid-range Android device the camera flyover went from slightly jerky to smooth, enough that I shipped the change on its own the same day. There was no visual difference, because there was no visual to lose.&lt;/p&gt;

&lt;p&gt;If you have a Filament scene with baked, emissive-only materials and a shadow-casting light, go and check. It is a thirty-second fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actually turning the sun off
&lt;/h2&gt;

&lt;p&gt;Removing shadows is the cheap half. The per-fragment lighting maths still runs on the black base colour. To stop that you need the materials declared unlit, so the loader picks the unlit shader.&lt;/p&gt;

&lt;p&gt;The obvious move is to add &lt;code&gt;KHR_materials_unlit&lt;/code&gt; to the materials in the pipeline. I tried that on one model a while ago and it rendered black, and at the time I shrugged and moved on. Now I know why. In Filament's glTF ubershader source (&lt;code&gt;libs/gltfio/materials/base.mat.in&lt;/code&gt;), the entire emissive block sits inside &lt;code&gt;#if !defined(SHADING_MODEL_UNLIT)&lt;/code&gt;. An unlit glTF material in Filament outputs base colour and nothing else. The emissive slot that the whole pipeline depends on is ignored.&lt;/p&gt;

&lt;p&gt;So the transform has to be two steps, not one. Move the texture from the emissive slot to the base colour slot, then flag the material unlit. With gltf-transform it is small:&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;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRoot&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;listMaterials&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;tex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEmissiveTexture&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;tex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBaseColorTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBaseColorFactor&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="mi"&gt;1&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEmissiveTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEmissiveFactor&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="mi"&gt;0&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="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;KHR_materials_unlit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUnlit&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;Per-instance tinting survives this, because the unlit ubershader still has the &lt;code&gt;baseColorFactor&lt;/code&gt; parameter and multiplies it in. The runtime code changes from setting a float3 on &lt;code&gt;emissiveFactor&lt;/code&gt; to setting a float4 on &lt;code&gt;baseColorFactor&lt;/code&gt;. The textures, the instancing and the draw calls are all unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that got me interested
&lt;/h2&gt;

&lt;p&gt;Once the texture is in the base colour slot, the same asset works as a lit material too. Leave the unlit flag off, add a small environment map, and the sun that was doing nothing starts doing something. The buildings cast shadows on each other, the walls read darker than the roofs, and the flatness I set out to fix goes away.&lt;/p&gt;

&lt;p&gt;Which means the lighting decision becomes a per-tier flag in the asset build rather than a rewrite. Low tiers get the unlit variant and stop paying for the sun at all. The top tier gets the lit variant and, for the first time, actually gets what it was paying for. The transform is the same function with one boolean.&lt;/p&gt;

&lt;p&gt;I have not shipped that half yet. It needs on-device measurements, an exposure pass, and a think about dark mode. But it reframed the whole thing for me. I had assumed lighting was a luxury I could not afford on mobile. It turned out I had been buying it all along and leaving it in the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your own scene
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dump the materials from your GLBs and look for the combination of black base colour, emissive texture and no unlit extension. That is the hidden-sun signature.&lt;/li&gt;
&lt;li&gt;If you have a shadow-casting light and no material has a non-black base colour, the shadow map is pure cost. Turn it off today.&lt;/li&gt;
&lt;li&gt;If you want to stop the lighting maths entirely, remember that Filament's unlit path drops emissive. Move the texture first, then flag the material.&lt;/li&gt;
&lt;li&gt;Render your assets with the engine's reference viewer in batch mode and toggle features. Anything that does not change the output is a candidate for removal, or a sign that you have a feature you are not actually using.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sun is a budget user like any other. It just does not show up on the invoice unless you go looking.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>filament</category>
      <category>3d</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Smallest Claude Client Is a Shell Script</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:48:46 +0000</pubDate>
      <link>https://dev.to/stepinto/the-smallest-claude-client-is-a-shell-script-3p8j</link>
      <guid>https://dev.to/stepinto/the-smallest-claude-client-is-a-shell-script-3p8j</guid>
      <description>&lt;p&gt;Every SDK for a language model API is thousands of lines, and that size teaches people that a client is a big thing. It is not. Underneath, the Claude Messages API is one HTTPS POST with a JSON body, and the reply is a stream of Server-Sent Events of which you need to understand six. This post is a complete client in bash and curl, in one file, with no jq. It streams the reply as it arrives, completes a tool call round trip, and reports API errors properly. It is the floor for a series that does the same thing in a different language every Thursday and measures each one against the others.&lt;/p&gt;

&lt;p&gt;The code and the harness are at &lt;a href="https://github.com/AndyCross/smallest-client" rel="noopener noreferrer"&gt;github.com/AndyCross/smallest-client&lt;/a&gt;, and the leaderboard lives at &lt;a href="https://stepinto.dev/smallest-client/" rel="noopener noreferrer"&gt;/smallest-client/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec
&lt;/h2&gt;

&lt;p&gt;Every client in the series has to pass the same seven points, and a harness checks them rather than me reading the code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the key from &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; and the base URL from &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;. Nothing else from the environment.&lt;/li&gt;
&lt;li&gt;Read the prompt from stdin.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /v1/messages&lt;/code&gt; with the &lt;code&gt;x-api-key&lt;/code&gt;, &lt;code&gt;anthropic-version: 2023-06-01&lt;/code&gt; and &lt;code&gt;content-type: application/json&lt;/code&gt; headers, and a body with &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;, &lt;code&gt;stream: true&lt;/code&gt;, &lt;code&gt;messages&lt;/code&gt; and &lt;code&gt;tools&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Print each &lt;code&gt;text_delta&lt;/code&gt; as it arrives. Buffering the whole reply and printing it at the end fails.&lt;/li&gt;
&lt;li&gt;Declare one tool, &lt;code&gt;local_time&lt;/code&gt;, which takes an IANA timezone. When the stream ends with &lt;code&gt;stop_reason: "tool_use"&lt;/code&gt;, assemble the &lt;code&gt;input_json_delta&lt;/code&gt; fragments, run the tool, send the &lt;code&gt;tool_result&lt;/code&gt; back in a second request with the assistant turn echoed, and stream that reply too.&lt;/li&gt;
&lt;li&gt;On a non-2xx response, print the body's &lt;code&gt;error.message&lt;/code&gt; to stderr and exit non-zero.&lt;/li&gt;
&lt;li&gt;One source file. HTTP and TLS from the platform where it has them. Where it does not (C, Zig), one named library, which counts towards size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 5 is the one that separates a client from a curl wrapper. Tool use is where the protocol stops being request and response and becomes a conversation you have to maintain, and the tool's arguments arrive as fragments of JSON that you assemble yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes down the wire
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;stream: true&lt;/code&gt; the response is &lt;code&gt;text/event-stream&lt;/code&gt;. Each event is an &lt;code&gt;event:&lt;/code&gt; line, a &lt;code&gt;data:&lt;/code&gt; line carrying JSON, and a blank line. Here is the shape of the first turn, from a real recording, trimmed:&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="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message_start&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"message_start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"msg_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-5-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content_block_start&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"content_block_start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"content_block"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"toolu_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"local_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{}}}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ping&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&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;"ping"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content_block_delta&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"content_block_delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"input_json_delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"partial_json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;timezone&lt;/span&gt;&lt;span class="se"&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="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content_block_delta&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"content_block_delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"input_json_delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"partial_json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;": &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Europe/London&lt;/span&gt;&lt;span class="se"&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="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content_block_stop&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"content_block_stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message_delta&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"message_delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"stop_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message_stop&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"message_stop"&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;Two things I had wrong before I recorded it. The model went straight to the tool call with no preamble text, so a client that assumes a text block always comes first is wrong. And there is a &lt;code&gt;ping&lt;/code&gt; event in the middle that the documentation mentions and nobody's mental model includes. A client has to ignore event types it does not know.&lt;/p&gt;

&lt;p&gt;The six you act on are &lt;code&gt;message_start&lt;/code&gt;, &lt;code&gt;content_block_start&lt;/code&gt;, &lt;code&gt;content_block_delta&lt;/code&gt;, &lt;code&gt;content_block_stop&lt;/code&gt;, &lt;code&gt;message_delta&lt;/code&gt; and &lt;code&gt;message_stop&lt;/code&gt;. Deltas come in two kinds, &lt;code&gt;text_delta&lt;/code&gt; with a &lt;code&gt;text&lt;/code&gt; field and &lt;code&gt;input_json_delta&lt;/code&gt; with a &lt;code&gt;partial_json&lt;/code&gt; field, and the &lt;code&gt;index&lt;/code&gt; says which content block they belong to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client
&lt;/h2&gt;

&lt;p&gt;The whole thing is 93 lines. The interesting parts follow, and the rest is in the repo.&lt;/p&gt;

&lt;p&gt;JSON without jq means encoding strings yourself. The prompt comes from stdin and could contain anything, so backslash, quote and the control characters have to be escaped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;json_str&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\\/\\\\&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\&lt;/span&gt;&lt;span class="s2"&gt;"/&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;}
  s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/\\n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/\\r&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/\\t&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  printf '"&lt;/span&gt;&lt;span class="p"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"' "&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding goes the other way. The text deltas arrive as JSON strings, escapes and all, and bash's &lt;code&gt;printf '%b'&lt;/code&gt; happens to understand &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\t&lt;/code&gt;, &lt;code&gt;\\&lt;/code&gt; and &lt;code&gt;\uXXXX&lt;/code&gt;, so after swapping &lt;code&gt;\"&lt;/code&gt; for &lt;code&gt;"&lt;/code&gt; you can hand it the string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;json_unstr&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\\\&lt;/span&gt;&lt;span class="s2"&gt;"/&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}; s=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;//\\\//\/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  printf '%b' "&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;\uXXXX&lt;/code&gt; support needs a UTF-8 locale. In a slim Debian image there is none set, &lt;code&gt;printf&lt;/code&gt; prints the escape literally, and "café" comes out as &lt;code&gt;caf\u00E9&lt;/code&gt;. The Dockerfile sets &lt;code&gt;LC_ALL=C.UTF-8&lt;/code&gt; and that is the kind of thing this series is for.&lt;/p&gt;

&lt;p&gt;The stream loop is curl with &lt;code&gt;-N&lt;/code&gt; to turn off its output buffering, read line by line, and a regex per event type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; line &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; data:&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; event:&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; errbody+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;#data&lt;/span&gt;:&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;text_delta&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;text&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;.|[^&lt;span class="se"&gt;\"\\&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;json_unstr &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf &lt;/span&gt;x&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;%x&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; ASSISTANT_TEXT+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;input_json_delta&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;partial_json&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;.|[^&lt;span class="se"&gt;\"\\&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;TOOL_INPUT+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;json_unstr &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;tool_use&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;^&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;,&lt;span class="se"&gt;\"&lt;/span&gt;name&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;^&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;TOOL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;TOOL_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[2]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="se"&gt;\"&lt;/span&gt;stop_reason&lt;span class="se"&gt;\"&lt;/span&gt;:&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;a-z_]+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;STOP_REASON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hdr&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"anthropic-version: 2023-06-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data-binary&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| [[ -n $line ]]&lt;/code&gt; on the &lt;code&gt;read&lt;/code&gt; is there because an error response is a single JSON line with no trailing newline, and &lt;code&gt;read&lt;/code&gt; returns false on the last line of a file that lacks one, so without it the loop body never ran for the only line that mattered. Lines that are not &lt;code&gt;data:&lt;/code&gt;, &lt;code&gt;event:&lt;/code&gt; or blank are collected as the error body, and after the loop the status code comes out of the headers file curl wrote with &lt;code&gt;-D&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The regex is doing the job a JSON parser would. It works because the API emits compact JSON with the keys in a known order, and that is the honest caveat: a bash client is parsing a wire format with pattern matching, and the first time the server reorders two keys it breaks. Every other client in the series uses a real parser. This one is the floor, and it is meant to be.&lt;/p&gt;

&lt;p&gt;When the first stream ends with &lt;code&gt;tool_use&lt;/code&gt;, the client runs the tool with &lt;code&gt;TZ="$timezone" date&lt;/code&gt;, builds the assistant turn back out of what it collected (any text, then the &lt;code&gt;tool_use&lt;/code&gt; block with its assembled input) and sends the three-message conversation as a second request. The reply to that is the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it measures as
&lt;/h2&gt;

&lt;p&gt;The harness runs every client in its own container against a mock that replays the recorded exchange with the original gaps between chunks. Nothing is measured over the network. The one real call per client is for conformance, and no timing from it is published.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;bash and curl&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Artefact&lt;/td&gt;
&lt;td&gt;3.7 KB (the script)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;23.7 MB (bash, curl, and every shared library they link)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start, median of 100&lt;/td&gt;
&lt;td&gt;7.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overhead, median of 100&lt;/td&gt;
&lt;td&gt;56.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;11 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apple M4 Max under Docker, linux/arm64, bash 5.2.37, curl 8.14.1 with OpenSSL 3.5.7.&lt;/p&gt;

&lt;p&gt;Cold start is process spawn to the mock receiving the first request. Overhead is spawn to exit minus the time the mock spent replaying, which is what the client itself cost. The runtime figure counts the shared libraries because a static binary pays for its TLS and HTTP inside itself, and a dynamically linked curl should pay for libcurl and OpenSSL the same way. Without them bash would show 1.7 MB and win on a technicality.&lt;/p&gt;

&lt;p&gt;Fifty-seven milliseconds of overhead for two requests is two curl process spawns plus a few hundred regex matches. The Go and C# clients that follow do the same work in under half of that, and the interesting question for the rest of the series is whether anything gets much below what Go does, or much smaller than what a Native AOT .NET binary does. I have an opinion about both and the leaderboard will tell me whether it was right.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AndyCross/smallest-client
&lt;span class="nb"&gt;cd &lt;/span&gt;smallest-client
./bench/bench.sh bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need Docker and Python 3 and you do not need a key. The fixtures in the repo are real recordings. If you want to re-record them, &lt;code&gt;./bench/bench.sh record bash&lt;/code&gt; runs one exchange through a proxy that reads your key from the shell and never writes it anywhere, and the client under test is handed a placeholder.&lt;/p&gt;

&lt;p&gt;Adding a language is a directory with the source file, a Dockerfile and a four-line &lt;code&gt;meta.env&lt;/code&gt;, and the CI runs the bench on every pull request. If you want to see your language on the leaderboard before I get to it, that is the way.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>bash</category>
      <category>curl</category>
    </item>
    <item>
      <title>What I Got Wrong About Go</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:48:42 +0000</pubDate>
      <link>https://dev.to/stepinto/what-i-got-wrong-about-go-17k3</link>
      <guid>https://dev.to/stepinto/what-i-got-wrong-about-go-17k3</guid>
      <description>&lt;p&gt;The C# to Go series was written against Go 1.22 and 1.23. Go is at 1.27 now and two of those versions are out of support, so this is the Christmas post the &lt;a href="https://stepinto.dev/posts/dotnet-to-go-in-the-right-order/" rel="noopener noreferrer"&gt;landing post&lt;/a&gt; promised: the claims in the series that the toolchain has overtaken, each one checked against a current install rather than remembered. Every snippet here was run on go1.27.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container CPU limits arrived in 1.25, not 1.19
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/dotnet-to-go-kubernetes/" rel="noopener noreferrer"&gt;Kubernetes post&lt;/a&gt; says "Go respects container resource limits automatically since Go 1.19" and its comparison table repeats it. The 1.19 change was &lt;code&gt;GOMEMLIMIT&lt;/code&gt;, which is memory. CPU came much later. The Go 1.25 notes say that on Linux "the runtime considers the CPU bandwidth limit of the cgroup containing the process" and that &lt;code&gt;GOMAXPROCS&lt;/code&gt; now defaults to the lower of that limit and the logical CPU count. It also re-reads the limit periodically, so a vertical autoscaler changing the limit under a running pod takes effect without a restart.&lt;/p&gt;

&lt;p&gt;Anything older than 1.25 in a container with a CPU limit still needs Uber's &lt;code&gt;automaxprocs&lt;/code&gt;, or an explicit &lt;code&gt;GOMAXPROCS&lt;/code&gt; in the pod spec. On the toolchain the post was written against, the log line in its example would have printed the node's core count, not the pod's limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The image pins are two versions out of support
&lt;/h2&gt;

&lt;p&gt;Every Dockerfile in the &lt;a href="https://stepinto.dev/posts/dotnet-to-go-docker/" rel="noopener noreferrer"&gt;containers post&lt;/a&gt; starts with &lt;code&gt;golang:1.22&lt;/code&gt;, and every job in the &lt;a href="https://stepinto.dev/posts/dotnet-to-go-github-actions/" rel="noopener noreferrer"&gt;GitHub Actions post&lt;/a&gt; has &lt;code&gt;go-version: '1.22'&lt;/code&gt;. Go supports the two most recent majors, so 1.22 fell out of support when 1.24 shipped in February 2025, and the supported pair as I write is 1.26 and 1.27.&lt;/p&gt;

&lt;p&gt;The fix is to stop pinning a minor in two places. The module file already names the version, so let the build read it:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v7&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;go-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go.mod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in the Dockerfile, pin to the major and let the &lt;code&gt;go&lt;/code&gt; line in go.mod pull a newer toolchain if it needs one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action pins have moved too. The posts use &lt;code&gt;actions/setup-go@v5&lt;/code&gt; and &lt;code&gt;golangci/golangci-lint-action@v4&lt;/code&gt;. Setup-go is at v7, and the lint action is at v9, which matters more than a bump usually does because v6 of the action was the last to work with golangci-lint v1. The &lt;code&gt;alpine:3.19&lt;/code&gt; runtime image in the same post is also past its end of life, and &lt;code&gt;golang:1-alpine&lt;/code&gt; currently means Alpine 3.24.&lt;/p&gt;

&lt;h2&gt;
  
  
  The golangci-lint config is the old format, and part of it was already invalid
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/dotnet-to-go-linting/" rel="noopener noreferrer"&gt;linting post&lt;/a&gt; ships a &lt;code&gt;.golangci.yml&lt;/code&gt; with &lt;code&gt;linters-settings&lt;/code&gt;, &lt;code&gt;issues.exclude-rules&lt;/code&gt; and &lt;code&gt;govet: check-shadowing: true&lt;/code&gt;. golangci-lint v2 shipped in March 2025 with a new config shape, and the migration tool told me something I had not noticed: the &lt;code&gt;check-shadowing&lt;/code&gt; key was rejected by the v1 schema too. It had been removed in favour of &lt;code&gt;enable: [shadow]&lt;/code&gt; before the post went out.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;golangci-lint migrate --skip-validation&lt;/code&gt; over the post's file produces 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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
&lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gosec&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;misspell&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;errcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;check-type-assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;gocyclo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;min-complexity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
  &lt;span class="na"&gt;exclusions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;generated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lax&lt;/span&gt;
    &lt;span class="na"&gt;presets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;comments&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;common-false-positives&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;legacy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;std-error-handling&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gosec&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_test\.go&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five of the eight linters have gone from the &lt;code&gt;enable&lt;/code&gt; list because &lt;code&gt;errcheck&lt;/code&gt;, &lt;code&gt;govet&lt;/code&gt;, &lt;code&gt;ineffassign&lt;/code&gt;, &lt;code&gt;staticcheck&lt;/code&gt; and &lt;code&gt;unused&lt;/code&gt; are the v2 default set, and &lt;code&gt;gosimple&lt;/code&gt; is gone because it was merged into &lt;code&gt;staticcheck&lt;/code&gt;. The install line changed as well. The module path is now &lt;code&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint&lt;/code&gt;, and the project's own docs say that &lt;code&gt;go install&lt;/code&gt; and the &lt;code&gt;tool&lt;/code&gt; directive "aren't guaranteed to work" and recommend the binary install script. The post's &lt;code&gt;@latest&lt;/code&gt; install would have quietly stayed on v1 forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  b.N loops where b.Loop goes
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/dotnet-to-go-benchmarks/" rel="noopener noreferrer"&gt;benchmarks post&lt;/a&gt; says "that &lt;code&gt;b.N&lt;/code&gt; is key" and writes nine benchmarks around it. Go 1.24 added &lt;code&gt;testing.B.Loop&lt;/code&gt;, and the release notes list two reasons to prefer it: the benchmark function runs exactly once per &lt;code&gt;-count&lt;/code&gt;, so setup runs once, and "function call parameters and results are kept alive, preventing the compiler from fully optimizing away the loop body."&lt;/p&gt;

&lt;p&gt;The second one bit the post's very first example. Here is &lt;code&gt;BenchmarkAdd&lt;/code&gt; both ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;BenchmarkAddN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&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;func&lt;/span&gt; &lt;span class="n"&gt;BenchmarkAddLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;ns/op&lt;/th&gt;
&lt;th&gt;What it measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BenchmarkAddN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;td&gt;an empty loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BenchmarkAddLoop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.58&lt;/td&gt;
&lt;td&gt;the call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;go1.27.2, darwin/arm64, Apple M4 Max, best of three with &lt;code&gt;-count=3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;b.N&lt;/code&gt; version inlines &lt;code&gt;Add&lt;/code&gt;, sees the result is unused and deletes it, so the number is the cost of incrementing &lt;code&gt;i&lt;/code&gt;. The post presented that as a working benchmark. Go 1.26 also removed the one cost of switching: &lt;code&gt;b.Loop&lt;/code&gt; no longer blocks inlining inside the loop body, so there is no reason left to write the old form. The post's &lt;code&gt;b.ResetTimer()&lt;/code&gt; calls go too, since &lt;code&gt;Loop&lt;/code&gt; resets the timer on its first call.&lt;/p&gt;

&lt;h2&gt;
  
  
  omitempty has a replacement, and json has a second package
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/dotnet-to-go-json/" rel="noopener noreferrer"&gt;JSON post&lt;/a&gt; has a whole section called "The omitempty Gotcha" and ends it by reaching for pointers so that zero and absent can be told apart. Go 1.24 added &lt;code&gt;omitzero&lt;/code&gt;. The release notes describe it as "clearer and less error-prone than &lt;code&gt;omitempty&lt;/code&gt; when the intent is to omit zero values" and call out the case that most often hurt: &lt;code&gt;omitempty&lt;/code&gt; never omitted a zero &lt;code&gt;time.Time&lt;/code&gt;, because a struct is not one of the "empty" kinds, so every optional timestamp serialised as &lt;code&gt;0001-01-01T00:00:00Z&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;When&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"when,omitempty"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Zero&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;When&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"when,omitzero"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// Empty{} marshals as {"when":"0001-01-01T00:00:00Z"}&lt;/span&gt;
&lt;span class="c"&gt;// Zero{}  marshals as {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;omitzero&lt;/code&gt; calls &lt;code&gt;IsZero()&lt;/code&gt; if the type has one and falls back to the zero value otherwise, which is also the hook you want for your own types. The pointer trick still works and still has a job when zero is a meaningful value you want to send. For the common case, the section is now a one-liner.&lt;/p&gt;

&lt;p&gt;The other half of this is Go 1.27 switching &lt;code&gt;encoding/json&lt;/code&gt; over to the v2 implementation underneath. I checked what that changes for code written against the old API, because the v2 defaults are stricter, and the answer is less than I had expected. The old &lt;code&gt;encoding/json&lt;/code&gt; on 1.27 still accepts duplicate object keys (last one wins) and still replaces invalid UTF-8 with U+FFFD, exactly as it did on 1.25. The release notes say so: "Behavior is preserved, though error message text may differ." The strict behaviour lives in the new &lt;code&gt;encoding/json/v2&lt;/code&gt; package, which rejects both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jsontext: duplicate object member name "a"
jsontext: invalid UTF-8 after offset 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the migration risk is in choosing to import v2, not in bumping the toolchain. If you consume JSON from systems you do not control, that is the moment to test. Coming from System.Text.Json, which is strict by default, the v2 behaviour will feel like home.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not need correcting
&lt;/h2&gt;

&lt;p&gt;The things I expected to have to walk back held up. Goroutines against async/await, the error handling argument, the single binary deployment story and the escape analysis post all read the same against 1.27 as they did against 1.22. The language has added range-over-func iterators and, this August, generic methods, and the &lt;a href="https://stepinto.dev/posts/dotnet-to-go-iterators/" rel="noopener noreferrer"&gt;iterators post&lt;/a&gt; covers both. Everything else was a version number.&lt;/p&gt;

&lt;p&gt;Each of the six posts above now carries a dated note pointing here, and the code in them stays as it was. The series is a record of what Go looked like when I learned it, and the notes are the diff.&lt;/p&gt;

</description>
      <category>go</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>series</category>
    </item>
  </channel>
</rss>
