<?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: Meinard Francisco</title>
    <description>The latest articles on DEV Community by Meinard Francisco (@nardian).</description>
    <link>https://dev.to/nardian</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%2F4159543%2Fcfe3e5e9-0bbb-4446-9c0d-989948bdc21d.jpg</url>
      <title>DEV Community: Meinard Francisco</title>
      <link>https://dev.to/nardian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nardian"/>
    <language>en</language>
    <item>
      <title>My father prints for the neighbours. I built him something better than guessing.</title>
      <dc:creator>Meinard Francisco</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:44:36 +0000</pubDate>
      <link>https://dev.to/nardian/my-father-prints-for-the-neighbours-i-built-him-something-better-than-guessing-10hf</link>
      <guid>https://dev.to/nardian/my-father-prints-for-the-neighbours-i-built-him-something-better-than-guessing-10hf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My father prints for the neighbours.&lt;/p&gt;

&lt;p&gt;That's the whole setup. Two houses, one inkjet model between them, and every so often someone knocks with a USB stick or an email. A school project. A stack of holiday photos. Someone's wedding invitations that absolutely &lt;em&gt;must not&lt;/em&gt; come out banded.&lt;/p&gt;

&lt;p&gt;For the jobs he knows, the workflow is fine. He made the file, he knows what's on it. But for a neighbour's file, the first thing he used to do was open everything. Every image, every page, one at a time, guessing at paper and orientation by eye. Only then would he commit a sheet.&lt;/p&gt;

&lt;p&gt;That preview pass is the thing I took away from him.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/znarfm/KantoPrint" rel="noopener noreferrer"&gt;KantoPrint&lt;/a&gt; drops that pass. You hand it the customer's file and it tells you, per page, which paper to load, how much ink is on it, whether anything is touching the cut line, and whether to turn it 90°. Before the printer wakes up.&lt;/p&gt;

&lt;p&gt;Every page of a PDF, not just the first. Results stream in page by page so a twenty-page file doesn't go quiet for five minutes. Cards fold away when there's more than one page. Filter a batch by tray, copy a load sheet, stop it if it's taking too long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What he said when I handed it over.&lt;/strong&gt; Nothing quotable, and I'm not going to invent a line. What I got was an observation: he no longer opens all those files. That was the relief, and it was specifically about the opening — not about any one verdict being right.&lt;/p&gt;

&lt;p&gt;That reordered my priorities. I had been treating verdict accuracy as the product. For him, a recommendation he can act on beat a correct one he has to double-check. The tool is only useful if the answer is good enough to trust without opening the file yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&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%2Fm50iwno73hb1j04ibyr6.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%2Fm50iwno73hb1j04ibyr6.png" alt="Initial state of the homepage" width="800" height="450"&gt;&lt;/a&gt;&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%2F0eqod7a81ba2mge27wx7.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%2F0eqod7a81ba2mge27wx7.png" alt="Inputting 3 files at once" width="800" height="450"&gt;&lt;/a&gt;&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%2Foq45hscfpxhf1xad91yh.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%2Foq45hscfpxhf1xad91yh.png" alt="Results" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/znarfm/KantoPrint" rel="noopener noreferrer"&gt;https://github.com/znarfm/KantoPrint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Next.js 16, sharp, pdf-lib, Ollama, Zod, vitest, oxlint and oxfmt. Sixty unit tests over the logic that doesn't need a model, plus an opt-in suite that drives the real one against deterministic fixtures. Wired into CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/znarfm/KantoPrint
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;ollama pull gemma4:e2b
pnpm fixtures &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gemma4:e2b&lt;/code&gt;, running locally through Ollama. It's the only model in the system.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't getting it to classify a page. It was finding out how much of it to let near.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gemma4:e2b&lt;/code&gt; is a 2B model, and on my own test fixtures it was confidently wrong about pixels. It called a wide-margin service contract &lt;em&gt;bleeding&lt;/em&gt;. It called a full-bleed colour invitation &lt;em&gt;safe&lt;/em&gt;. It rated a 0.9%-ink form "medium" risk sitting right next to a gauge reading 0.9%.&lt;/p&gt;

&lt;p&gt;A coin flip would have been about as reliable. So I stopped asking it things pixels can answer. Ink at the trim edge comes from measuring the edge band. The risk band comes from measuring ink load. Rotation comes from the page dimensions. The model keeps the two questions that are genuinely judgement: what is this job, and what paper goes in the tray.&lt;/p&gt;

&lt;p&gt;That split cost some accuracy on paper and bought a lot of it in practice. More interestingly, three fields I originally had the model fill in got deleted outright, because they disagreed with the numbers printed beside them &lt;em&gt;and&lt;/em&gt; they were eating about forty of the fifty-five tokens it generated per page.&lt;/p&gt;

&lt;p&gt;One more thing I'd keep if I rebuilt it: the operator notes. I asked the model to write them, and it wrote "Set up the material for printing," which is not an instruction to anybody. They're composed from the flags now. Duller, and actually useful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It runs on a laptop with no internet.&lt;/strong&gt; Ollama serves the model on localhost. There's no outbound call at all — our router could be unplugged and this would behave the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our neighbours' files never leave the house.&lt;/strong&gt; This is the one that settled it. When you print other people's work — school projects, ID photos, someone's thesis — a hosted API means every one of those files, plus a render of it, sits on hardware you don't control. For a two-house print shop that's not a compliance conversation. It's the deal we made with the people who trusted us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing costs money per job.&lt;/strong&gt; A print job costs pennies. Per-token billing makes no sense at that scale; a one-time 4.6 GB download does.&lt;/p&gt;

&lt;p&gt;Being honest about what that buys, on a Core i5 laptop with no GPU: &lt;strong&gt;11 to 17 seconds per page&lt;/strong&gt;, the first time the model sees it. Same page again is 1.6 to 3.2s, because Ollama caches the vision encoding — which is also why my first benchmark of this project was wrong by about 5x, and why I trust the honest number more than the flattering one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it swaps.&lt;/strong&gt; &lt;code&gt;KANTOPRINT_MODEL&lt;/code&gt; is one environment variable. Anything Ollama can serve, this can be pointed at.&lt;/p&gt;

&lt;p&gt;What open doesn't fix is worth saying too. At that resolution an 11pt line is about 4.6 pixels tall, so the model can't read body copy, and the card says so. It also doesn't measure banded output or colour accuracy — it measures coverage, not how your printer reproduces it.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building KantoPrint: teaching a 2B vision model what it is not allowed to guess&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Build KantoPrint: air-gapped prepress triage for desktop inkjet and cutting workflows, using local Gemma 4 multimodal inference. Every page of a PDF should be analysed, and the card collapsible for multi-page files.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Starting with the schema foundation, since everything else depends on the contract. Moving from a single &lt;code&gt;page&lt;/code&gt; field to &lt;code&gt;pages: PageResult[]&lt;/code&gt; so a PDF produces one analysed result per page.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;lib/prepress.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;prepress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pageResultSchema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inkRiskLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;needsRotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operatorNotes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lowDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;thumbPx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingsMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;modelWarning&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="nx"&gt;jobResultSchema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;pagesSkipped&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Also dropping &lt;code&gt;inkRiskLevel&lt;/code&gt; from the model contract. It was producing a band that disagreed with the gauge printed beside it — a 0.9% ink form rated 'medium'. If pixels measure it, pixels own it. That takes the model schema from 5 fields to 3.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Why does the model keep getting bleed wrong? Is the vision call even working?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Good question to check first. Probing the model directly rather than trusting the pipeline.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;probe: does a vision model correctly describe this image?&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ probe: does a vision model correctly describe this image?&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;output: {"documentType": "sticker"} for a 384px badge  -&amp;gt; vision works&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Vision works. So the misclassification is a capability limit, not a plumbing bug. The model sees a blank-margin contract and calls it bleeding.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The ink load numbers say 11-17 seconds per page now, but earlier you said 3.7-4.4s. Which is it?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;This needs measuring, not arguing. Separating 'first time an image is seen' from 'same image again'.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;run 4 visually distinct PNGs, then re-run the same 4&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ run 4 visually distinct PNGs, then re-run the same 4&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;pass A (fresh): 11071, 14123, 14654, 14739 ms
pass B (repeat): 1625, 1574, 2589, 3191 ms&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;That's a 5x gap and it isn't noise: Ollama caches the vision encoder output per image. Every earlier benchmark in this project re-ran the same fixture files, so it measured the cache, not the encoder.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The honest number is 11-17s for a page the model has never seen. The 3.7-4.4s figure I'd been quoting was wrong and had to come out of the README. A 20-page PDF is 4-6 minutes, which is what makes streaming, the page cap, and Stop necessary rather than decorative.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;OK. Then why does the model report wrong bleed?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Measuring instead of asking. Edge-band ink coverage at ~3% of the short side, which is about 3mm at 150 dpi.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;run the 3 fixtures through /api/analyze&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ run the 3 fixtures through /api/analyze&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;contract: edgeInkPct 0.0  -&amp;gt; model said hasBleedMargins true
invitation: edgeInkPct 63.7 -&amp;gt; model said false&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The model was wrong in both directions on the same run. Edge ink above 5% is bleeding; at or below 0.5% the band is provably blank. Between those the model still decides.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Three fields now get overridden by measurement rather than trusted: bleed, ink risk, rotation. The model keeps only what is genuinely judgement — what the job is, and which paper.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;That's a reasonable split, but operatorNotes was supposed to come from the model.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;It did, for a while. Then I read what it produced:&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;inspect verdict.operatorNotes across fixtures&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ inspect verdict.operatorNotes across fixtures&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;"Set up the material and proceed with cutting."
"Set up the stock and handle the document correctly."
"Set up the material for printing."&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;That's not an instruction to anybody. Composing them from the flags instead: duller, and actually useful. Bonus: those notes were about 25 of the ~55 generated tokens per page, so deleting them was also a latency win.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;There's a deeper version of this. The chips on the card and the sentences in the brief were the same predicates written twice, so a flag could appear in one and go missing from the other. One list now feeds both.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Run the tests. Did they actually catch anything?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Two real bugs in the first hour, both of which I'd have shipped:&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-vitest"&gt;vitest&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;tests/pipeline.test.ts &amp;gt; rejects content that only claims to be an image&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;tests/pipeline.test.ts &amp;gt; rejects content that only claims to be an image&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;FAILED: promise resolved instead of rejecting&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;First: &lt;code&gt;ingest&lt;/code&gt; fell back to the browser's declared MIME when magic bytes didn't match. A text file named &lt;code&gt;.pdf&lt;/code&gt; was accepted and died later inside pdf-lib with a worse message. PDF, PNG and JPEG all have signatures, so the fallback was only ever weakening validation. Removed it.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-vitest"&gt;vitest&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;tests/pipeline.test.ts &amp;gt; rejects an empty file&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;tests/pipeline.test.ts &amp;gt; rejects an empty file&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;FAILED: got 'only PDF, PNG and JPEG are accepted', expected 'empty file'&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Second: the empty-file check ran after the type sniff, so an empty file got the wrong reason. Reordering.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Worth noting the four other failures were my test arithmetic being wrong, not the code. The edge-band one I'd asserted was less than the page average; it's exactly equal, because a black half spanning the full height fills the left edge band completely and the white half fills the right one.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;You said &lt;code&gt;sharp&lt;/code&gt; can't read PDFs. What did you do about it?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;sharp on a pdf-lib generated buffer&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ sharp on a pdf-lib generated buffer&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Error: Input buffer contains unsupported image format&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;sharp has no poppler, so PDF input is out. Two native rasterisers were already on the box, so: pdftoppm first, ghostscript second, with a clear error naming both if neither exists.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Multi-page needed care — poppler zero-pads its filenames to the digit count of the last page rendered, so &lt;code&gt;pg-1.png&lt;/code&gt; and &lt;code&gt;pg-01.png&lt;/code&gt; are both possible depending on the page cap. Collecting the directory and sorting numerically is the only version that survives a config change.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Both get a hard timeout. A malformed PDF could otherwise hang the request forever and strand its temp directory.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Turn on the shadcn lint rules.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;oxlint with the six recommended @shadcn/lint rules&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ oxlint with the six recommended @shadcn/lint rules&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;29 errors across 2 files&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Two of these were worth having. &lt;code&gt;no-raw-colors&lt;/code&gt; caught that the ink gauge had been borrowing raw Tailwind amber and emerald because the theme had no such tokens — ink risk has three bands, so the theme now names all three:&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;app/globals.css&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nt"&gt;globals&lt;/span&gt;&lt;span class="nc"&gt;.css&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--safe&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;oklch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;596&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;145&lt;/span&gt; &lt;span class="err"&gt;163&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;225&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nt"&gt;--warning&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;oklch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;666&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;161&lt;/span&gt; &lt;span class="err"&gt;46&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nt"&gt;with&lt;/span&gt; &lt;span class="nt"&gt;dark-mode&lt;/span&gt; &lt;span class="nt"&gt;variants&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;&lt;code&gt;require-static-classes&lt;/code&gt; caught something subtler: the gauge colour was built from an indexed lookup, which the linter can't follow — meaning no rule could check those classes at all. Now a conditional between complete class strings, so everything about it is readable.&lt;/p&gt;

                &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Used contracts rather than blanket allows where the customisation was intentional, so ProgressLabel can take typography without Progress itself being unrestyleable.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        16 of 16 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — &lt;code&gt;gemma4:e2b&lt;/code&gt; is the only model here, served locally via Ollama. The build leans on Gemma-specific behaviour: vision cost that scales with input resolution, constrained decoding through the &lt;code&gt;format&lt;/code&gt; grammar so the reply cannot be prose, and disabled thinking mode for latency.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>weekendchallenge</category>
      <category>devchallenge</category>
      <category>gemma</category>
    </item>
  </channel>
</rss>
