<?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: Melwin Siril Joel V</title>
    <description>The latest articles on DEV Community by Melwin Siril Joel V (@cadets_atease).</description>
    <link>https://dev.to/cadets_atease</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%2F3896419%2F68bd8279-f721-4239-9281-7d8ff2291d20.jpg</url>
      <title>DEV Community: Melwin Siril Joel V</title>
      <link>https://dev.to/cadets_atease</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cadets_atease"/>
    <language>en</language>
    <item>
      <title>Can a 25 MB Browser Model Design a Quote Card?</title>
      <dc:creator>Melwin Siril Joel V</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:11:40 +0000</pubDate>
      <link>https://dev.to/cadets_atease/can-a-25-mb-browser-model-design-a-quote-card-ic8</link>
      <guid>https://dev.to/cadets_atease/can-a-25-mb-browser-model-design-a-quote-card-ic8</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/mlh-hackathon"&gt;MLH x DEV Writing Challenge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I Built&lt;br&gt;
Slothpress turns a few words into a typeset quote card you can post anywhere. Type a quote, pick a style, and download a high-resolution 2160 × 2700 PNG.&lt;/p&gt;

&lt;p&gt;Everything happens completely client-side in your browser tab: no account, no uploads, no servers, and no API keys.&lt;br&gt;
What makes Slothpress unique is how it integrates an open-weight model without violating privacy or centralizing control.&lt;/p&gt;

&lt;p&gt;When you click "Suggest a style", a lightweight, quantized 25 MB MobileBERT checkpoint (Xenova/mobilebert-uncased-mnli) runs directly in your browser tab via transformers.js and ONNX Runtime. It reads your quote, ranks eight typography presets against it, and applies the vibe that fits best.&lt;/p&gt;

&lt;p&gt;Crucially, the model is kept entirely away from the renderer. The core rendering engine is completely deterministic:&lt;br&gt;
card = render(content, author, style)&lt;/p&gt;

&lt;p&gt;Backed by 79 unit tests running in under a second, the AI only suggests; it never alters your words or draws pixels. Human intent and reliable design control always stay front and center.&lt;/p&gt;

&lt;p&gt;Demo&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Live App: &lt;a href="https://emanuelmsjv.github.io/slothpress/" rel="noopener noreferrer"&gt;https://emanuelmsjv.github.io/slothpress/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitHub Repository:  Emanuelmsjv/slothpress&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DEV.to Article &amp;amp; Writeup: I let a 25 MB model choose the typeface for my quote cards. It got it wrong.&lt;br&gt;
(Embed your screen recording, walkthrough GIF, or deployment screenshot here showing you typing a quote, clicking "Suggest a style", and rendering the final image card!)&lt;br&gt;
Partner Technologies&lt;br&gt;
Building Slothpress required tackling several fascinating engineering challenges regarding client-side AI performance:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hugging Face (@huggingface/transformers &amp;amp; ONNX Runtime Web): Instead of calling a metered, closed-source API, I implemented a zero-shot classification pipeline entirely in the browser using quantized ONNX weights.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Self-Hosted Runtime Asset Delivery: To protect user privacy and avoid relying on external third-party CDNs (like jsDelivr) in the runtime path, Vite bundles the WebAssembly and worker files (ort-wasm-simd-threaded.asyncify.wasm) directly into dist/assets/ to be served from the app's own origin and cached by the service worker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Progressive Loading &amp;amp; Cost Management: The app shell remains lightweight at 78 KB gzipped. The AI chunk (~6.7 MB ONNX runtime + 25.7 MB MobileBERT weights) is loaded dynamically via import() only when the user explicitly requests a suggestion. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the initial 13.6-second download, the model lives in the browser cache and functions seamlessly offline.&lt;br&gt;
Hackathon Experience&lt;br&gt;
Building and documenting Slothpress was a deep dive into the realities of open-source AI and client-side web engineering.&lt;/p&gt;

&lt;p&gt;One of the most memorable moments was discovering that when asked about "The sea was vast and deep that night," the model ranked Ink (stark, technical) first and Tide (deep, vast, contemplative like the sea) third due to its training priors over natural language inference labels.&lt;/p&gt;

&lt;p&gt;Instead of hiding the imperfection behind a black box or a heavy server, I shipped it anyway—leaving the scores visible on screen and documenting the trade-off in the README. It proved a vital lesson: a model you cannot inspect is not open innovation. Because everything from ONNX Runtime to the app code is transparent and client-side, users can see why a choice was made, disagree with it in one click, and run the entire experience completely offline.&lt;/p&gt;

</description>
      <category>mlhacks</category>
      <category>devchallenge</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>I found a bug in this model, and i corrected it</title>
      <dc:creator>Melwin Siril Joel V</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:02:29 +0000</pubDate>
      <link>https://dev.to/cadets_atease/i-found-a-bug-in-this-model-and-i-corrected-it-jae</link>
      <guid>https://dev.to/cadets_atease/i-found-a-bug-in-this-model-and-i-corrected-it-jae</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I started this project because I noticed something wrong with how bird identification works in my own town.&lt;/p&gt;

&lt;p&gt;BirdNET is a genuinely impressive open-source project. Point it at a recording and it will confidently tell you what you heard. I tried it on some audio recorded on my hostel campus in Chidambaram, Tamil Nadu, and it told me about &lt;strong&gt;Rose-ringed Parakeet, Common Grackle, and Dark-eyed Junco&lt;/strong&gt;. The grackle is a North American species. It has never been in India. It was never in my garden. The model was not confused - it was &lt;em&gt;sure&lt;/em&gt;, and wrong.&lt;/p&gt;

&lt;p&gt;That is the whole problem in one clip. A global classifier trained on global audio will happily tell you about birds that live four thousand kilometres away, because nothing in its architecture knows or cares where you are standing. There is no "not in my region" button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;chidambaram-birdnudge&lt;/strong&gt; is that button. It runs the same open weights on your own machine, then filters every prediction through a species list built from real observations near you. It costs nothing to run, needs no account, works with no signal, and never sends your recordings anywhere.&lt;/p&gt;

&lt;p&gt;A local Node.js pipeline that takes phone recordings, identifies birds with an open-weight audio model, filters the results to a regional species list, and tells you where to go next.&lt;/p&gt;

&lt;p&gt;The pipeline is five scripts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;script&lt;/th&gt;
&lt;th&gt;what it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;download-model.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fetches the BirdNET+ weights and label set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fetch-pack.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;builds the local species list from iNaturalist observations within a radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;convert.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;batch-converts &lt;code&gt;.m4a&lt;/code&gt; / &lt;code&gt;.mp3&lt;/code&gt; / &lt;code&gt;.ogg&lt;/code&gt; to the 32 kHz mono WAV the model wants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analyze.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;runs ONNX inference, writes a detection CSV per clip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summarize.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scores predictions against the pack, writes &lt;code&gt;docs/overview.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;case.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;generates the case study from the detection data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two design decisions are worth calling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model runs whole-file peak-normalised&lt;/strong&gt;, so a quiet outdoor clip and a loud indoor one are comparable. This matters more than it sounds - my outdoor clip was about 6 dB quieter than my indoor ones, and without normalisation the comparison would have been meaningless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;min-conf&lt;/code&gt; defaults to 0.05, not 0.25.&lt;/strong&gt; BirdNET's own web demo defaults much higher, which means it silently discards low-confidence-but-real detections. I lowered it so the local filter gets to see everything, then let the pack do the rejecting. The pack is a better filter than an arbitrary threshold, because it is specific to where you are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;https://github.com/Emanuelmsjv/chidambaram-birdnudge&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Real output from a real run, on the five clips described above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model    ...\assets\BirdNET+_V3.0-preview3.1_Global_11K_FP16_pruned.onnx
labels   11560 species
settings chunk=3s overlap=0s minConf=0.05

model load: 971ms
inside-2.wav  9.9s audio  0.4s compute  6 detections
    0.870  Yellow-billed Babbler  (Argya affinis)
    0.589  Rufous Treepie  (Dendrocitta vagabunda)
    0.068  Indian White-eye  (Zosterops palpebrosus)
inside.wav  17.9s audio  0.5s compute  15 detections
    0.812  Yellow-billed Babbler  (Argya affinis)
    0.415  Common Grackle  (Quiscalus quiscula)
    0.283  Oriental Magpie-Robin  (Copsychus saularis)
outside.wav  17.5s audio  0.5s compute  9 detections
    0.873  Rose-ringed Parakeet  (Psittacula krameri)
    0.731  Common Myna  (Acridotheres tristis)
    0.509  Red-vented Bulbul  (Pycnonotus cafer)

5 file(s), 47 detections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And what the filter actually does with that output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm run summarize
Clips: 6  detections: 146
In-pack (top-5): 15/27 = 55.6%
In-pack (all 53 distinct): 24 = 45.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;The whole pipeline is open source under CC BY-NC-SA 4.0. Everything below runs on your&lt;br&gt;
own machine with Node 18+, no Python, no GPU, and no API keys.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Emanuelmsjv" rel="noopener noreferrer"&gt;
        Emanuelmsjv
      &lt;/a&gt; / &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;
        chidambaram-birdnudge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Offline open-weight bird ID for Chidambaram, Tamil Nadu. BirdNET V3.0 via ONNX Runtime in pure Node - no Python, no GPU. Output is filtered against an iNaturalist-derived local species pack, and scored with a label-free measure of how plausible that output is for this location.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;chidambaram-birdnudge&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Point an open-weight bird sound model at a 10-second recording of somewhere in
Chidambaram, Tamil Nadu. Get back one sentence telling you which species
plausibly live near you, whether they are in season right now, what time of day
they are worth listening for, and a real place within reach to go and stand.&lt;/p&gt;
&lt;p&gt;The model already exists and is very good. It is also, on published evidence
close to useless in this part of India. That gap is the project.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The premise&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A 2026 global evaluation of BirdNET across 4,224 soundscapes, 67 sites and every
continent, annotated by local experts, reported PR AUC of:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;PR AUC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;North America / Oceania / Europe&lt;/td&gt;
&lt;td&gt;0.16 – 0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Central &amp;amp; South America&lt;/td&gt;
&lt;td&gt;0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Africa and Asia&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.03 – 0.04&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Chidambaram sits in that last band. Globally, the share of predictions that are
correct (precision) is around 0.57–0.71, so roughly a…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Repo: &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;https://github.com/Emanuelmsjv/chidambaram-birdnudge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A one-command setup fetches the 68 MB model weights and builds the local species pack:&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/Emanuelmsjv/chidambaram-birdnudge
&lt;span class="nb"&gt;cd &lt;/span&gt;chidambaram-birdnudge
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run setup        &lt;span class="c"&gt;# downloads the 68 MB model, builds your species pack&lt;/span&gt;

npm run convert      &lt;span class="c"&gt;# phone .m4a -&amp;gt; 32 kHz mono WAV&lt;/span&gt;
npm run analyze clips &lt;span class="nt"&gt;--out&lt;/span&gt; data/detections
npm run summarize    &lt;span class="c"&gt;# writes docs/overview.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Emanuelmsjv/chidambaram-birdnudge
&lt;span class="nb"&gt;cd &lt;/span&gt;chidambaram-birdnudge
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run setup        &lt;span class="c"&gt;# downloads the 68 MB model, builds your species pack&lt;/span&gt;

npm run convert      &lt;span class="c"&gt;# phone .m4a -&amp;gt; 32 kHz mono WAV&lt;/span&gt;
npm run analyze clips &lt;span class="nt"&gt;--out&lt;/span&gt; data/detections
npm run summarize    &lt;span class="c"&gt;# writes docs/overview.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Requires Node 18+ and ffmpeg on PATH. No Python, no GPU, no API keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Licence:&lt;/strong&gt; CC BY-NC-SA 4.0. BirdNET weights are CC BY-NC-SA. iNaturalist observation data retains its own per-record licences, referenced in &lt;code&gt;data/pack.json&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;BirdNET+ V3.0-preview3.1 Global 11K FP16 (pruned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weights format&lt;/td&gt;
&lt;td&gt;ONNX, &lt;strong&gt;68.2 MB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onnxruntime-node&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Labels&lt;/td&gt;
&lt;td&gt;11,560 species, plain CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;CPU only. No GPU. No Python.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model is a TarsNet2 classifier - a frozen convolutional backbone with a linear classification head. That architecture is the reason this project is tractable offline: the expensive part is fixed, and the part you retarget is one dense layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed:&lt;/strong&gt; the five-clip case study below is &lt;strong&gt;83.8 seconds of audio, analysed in under 4 seconds&lt;/strong&gt; after a ~1 second model load. That is roughly 25x realtime on a laptop CPU with no accelerator.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;The challenge asks where an open approach did better than a closed one. I can answer that with a measurement rather than an opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same weights, same code, two recordings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;audio&lt;/th&gt;
&lt;th&gt;in-pack fraction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;upstream BirdNET sample, recorded in New York&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 / 20&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;my recordings, Chidambaram&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23 / 33&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nineteen of the twenty species the model proposed for the New York audio do not exist within 30 km of Chidambaram. The twentieth is House Sparrow at 0.051 - rank 20 of 20 by confidence, barely clearing my own threshold. Every confident prediction was a bird that does not live here:&lt;/p&gt;

&lt;p&gt;A hosted bird-ID service gives you a fixed species list, the one its provider chose. If that list does not contain your local birds, you cannot add them. &lt;strong&gt;With open weights I can change what the model is allowed to say.&lt;/strong&gt; That is the entire difference between 1/20 and 23/33, and it is not available behind an API.&lt;/p&gt;

&lt;p&gt;The other reasons the open pieces mattered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs with no signal.&lt;/strong&gt; After a one-time 68 MB download, &lt;code&gt;npm run analyze&lt;/code&gt; never touches the network. A bird call identifier is only useful where bird calls happen, which is usually nowhere with good connectivity. I did not get to actually test this on a trail with no bars - I tested it on a laptop, and the mobile app I originally used needed a working connection. I am not claiming more than I measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your recordings never leave your machine.&lt;/strong&gt; The case study below is audio recorded at a university hostel. Uploading that to a third-party inference endpoint hands someone a location-precise recording of where a person lives and when they were there. Local inference means the audio is read once by &lt;code&gt;decodeWav&lt;/code&gt; and never transmitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing.&lt;/strong&gt; No API key, no per-minute billing, no quota. A hosted audio-classification endpoint bills by the minute precisely when you most want to use it and least want to depend on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is replaceable.&lt;/strong&gt; The labels are a CSV and the regional list is JSON. Retargeting to another city is a data edit, not a code change. Nothing about the pipeline is specific to BirdNET except the inference call.&lt;/p&gt;
&lt;h3&gt;
  
  
  Honest limits of the open approach
&lt;/h3&gt;

&lt;p&gt;Being straightforward about where this does &lt;em&gt;not&lt;/em&gt; win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The weights are &lt;strong&gt;CC BY-NC-SA&lt;/strong&gt;, so this is non-commercial. That is fine for a personal tool and a real constraint if you wanted to build a product.&lt;/li&gt;
&lt;li&gt;Fine-tuning for a local species set is architecturally trivial here (frozen encoder, linear head, logistic regression on embeddings, trainable on CPU) but &lt;strong&gt;I did not do it.&lt;/strong&gt; With four labelled clips and no working route to bulk training audio, any claim I made about it would be unverified.&lt;/li&gt;
&lt;li&gt;BirdNET's Python pipeline has features my port does not - proper spectrogram caching, multi-label handling, and more careful segmenting. I ported the part that made my question answerable, not the whole project.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  I took it outside
&lt;/h2&gt;

&lt;p&gt;The brief offers "a bird call identifier that works on the trail with no signal" as an example. I took it literally.&lt;/p&gt;

&lt;p&gt;I heard a group of &lt;strong&gt;Yellow-billed Babbler&lt;/strong&gt; (&lt;em&gt;Argya affinis&lt;/em&gt;) on my hostel campus and recorded five clips on my phone - two indoors, one outdoors, and two while I was talking, to see what happened. Then I ran them through the pipeline.&lt;/p&gt;

&lt;p&gt;The species was already in my local pack at rank 18 of 130, with 19 iNaturalist observations in range - a locally well-established bird, not a rarity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;clip&lt;/th&gt;
&lt;th&gt;babblers audible?&lt;/th&gt;
&lt;th&gt;model's best score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inside-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.870&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inside&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.812&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;speech-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.961&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;speech&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outside&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of the four clips where I confirmed the bird was audible got a correct first-place identification.&lt;/p&gt;

&lt;p&gt;The two I find most interesting are the &lt;code&gt;speech&lt;/code&gt; pair. Both were recorded in the same room, minutes apart, with me talking. I listened back afterwards to check what was in the background. In one, babblers were chirping behind my voice; in the other, there were none. The model returned 0.961 for the one where they were there and &lt;strong&gt;nothing at all&lt;/strong&gt; for the one where they were not.&lt;/p&gt;

&lt;p&gt;That distinction matters, because the cheap explanation would have been that the model just fires on human voices and happened to point at a common local babbler. Two clips with identical foreground content and opposite answers rules that out.&lt;/p&gt;

&lt;p&gt;The one result I still cannot fully explain is &lt;code&gt;speech-2&lt;/code&gt;, where the babbler is buried under speech and still scored 0.961. I am reporting it because it happened, not because I understand it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it got wrong
&lt;/h2&gt;

&lt;p&gt;Every one of these is in the repo, not just here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It missed the bird entirely on the outdoor clip.&lt;/strong&gt; I was there, the babblers were there, and the model proposed parakeet and myna instead. That clip was also my quietest recording. I still do not know whether the model failed or the microphone failed. It is the first thing I would try to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence is not a probability.&lt;/strong&gt; BirdNET will hand you 0.9 and mean something much weaker. The New York control demonstrates this directly - the model was extremely confident about species that were not present at all. The number is a ranking signal, nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My species pack is biased, and I can show how.&lt;/strong&gt; It is built from iNaturalist, which is a &lt;em&gt;photography&lt;/em&gt; platform. I pulled 1,000 research-grade observations from around Chidambaram and checked the result: &lt;strong&gt;Indian White-eye appeared zero times.&lt;/strong&gt; It is one of the most common birds in India and it is, to my ear, constantly audible from my window. The pack over-represents birds people stop to photograph - Pied Stilt is the second most-recorded species in range - and under-represents small common passerines nobody bothers to upload.&lt;/p&gt;

&lt;p&gt;So "not in your local pack" is weaker evidence than it appears. It means &lt;em&gt;not established as local by this data source&lt;/em&gt;, not &lt;em&gt;absent&lt;/em&gt;. I would not have found this without checking the raw observations against my own pack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I had a real bug for most of this project.&lt;/strong&gt; The pack builder wrote entries as &lt;code&gt;{ rank: i + 1, ...s }&lt;/code&gt; - the spread lands after, so the taxonomic rank from iNaturalist overwrote my ordinal in all 130 entries. Every &lt;code&gt;rank&lt;/code&gt; field was the string &lt;code&gt;"species"&lt;/code&gt;. The ordering was still correct, which is exactly why I did not notice for weeks. Fixed in &lt;code&gt;a087f8f&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My recordings were not independent data.&lt;/strong&gt; I collected them, then evaluated the model on them. There is no held-out set, and with four positives any accuracy number would be meaningless. I have not claimed one anywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;Here is the session where the agent found the &lt;code&gt;rank&lt;/code&gt; bug, in full. It is twelve messages: you can watch it notice that &lt;code&gt;rank&lt;/code&gt; prints as &lt;code&gt;"species"&lt;/code&gt; instead of a number, work out that &lt;code&gt;{ rank: i + 1, ...s }&lt;/code&gt; lets iNaturalist's own &lt;code&gt;rank&lt;/code&gt; string land on top of the ordinal, and fix it. The interesting detail is that nothing was broken as far as anyone could tell - the pack was still sorted correctly the whole time. The bug only became visible because a species position was checked against a literal &lt;code&gt;18&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Debugging a clobbered rank field in fetch-pack.mjs (opencode)&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;what is observed is a group of Common Myna, where inside my hostel Golden Jubille hostels trees and outside of the hostel campus too. and im checking in the birdnet app ill update until then well completed debugging and furnishing and deploying too&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;&lt;strong&gt;Common Myna is a strong test case&lt;/strong&gt; - and I can tell you something useful before you run it. Let me verify.&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nWrite-Output \"=== Common Myna in the Chidambaram pack? ===\"\nnode -e \"\nconst p=require('./data/pack.json');\nconst hits=p.species.filter(s=&amp;gt;/Acridotheres tristis/i.test(s.scientific));\nhits.forEach(s=&amp;gt;console.log('  '+s.common+' ('+s.scientific+')  rank='+s.rank+'  observations='+s.observations));\nconsole.log('  pack size: '+p.species.length+' species');\nconst mug=s=&amp;gt;p.species.filter(x=&amp;gt;new RegExp(s,'i').test(x.common||'')).map(x=&amp;gt;x.common);\nconsole.log('');\nconsole.log('  other starlings/mynas in pack:');\n['starling','myna','sturnid'].forEach(q=&amp;gt;{const r=mug(q);if(r.length)console.log('    '+q+': '+r.join(', '));});\n\"\nWrite-Output \"\"\nWrite-Output \"=== does extract.mjs understand 'group' / 'flock'? ===\"\nSelect-String -Path scripts\\extract.mjs -Pattern \"group|flock|several|couple|pair\" | ForEach-Object { \"   \" + $_.Line.Trim() }","timeout":300000}&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;=== Common Myna in the Chidambaram pack? ===
  Common Myna (Acridotheres tristis)  rank=species  observations=23
  pack size: 130 species

  other starlings/mynas in pack:
    starling: Brahminy Starling, Indian Pied Starling
    myna: Common Myna

=== does extract.mjs understand 'group' / 'flock'? ===
Select-String : Cannot find path '[redacted:home-path]\hacktoberfest\chidambaram-birdnudge\scripts\extract.mjs' because it 
does not exist.
At line:16 char:1
+ Select-String -Path scripts\extract.mjs -Pattern "group|flock|several ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: ([redacted:home-path]\...pts\extract.mjs:String) [Select-String], ItemNotFound 
   Exception
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.SelectStringCommand
 
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;strong&gt;Common Myna confirmed in the pack&lt;/strong&gt; - 23 observations, and notably the pack contains Brahminy Starling and Indian Pied Starling but &lt;strong&gt;no Common Starling&lt;/strong&gt;. That matters. Let me confirm one suspicion:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nWrite-Output \"=== pack.json: is the 'rank' field a number or a taxonomic rank? ===\"\nnode -e \"\nconst p=require('./data/pack.json');\nconsole.log('  first 3 entries:');\np.species.slice(0,3).forEach(s=&amp;gt;console.log('    rank='+JSON.stringify(s.rank)+'  common='+s.common));\nconsole.log('  distinct rank values across all 130: ' + JSON.stringify([...new Set(p.species.map(s=&amp;gt;s.rank))]));\n\"\nWrite-Output \"\"\nWrite-Output \"=== how project 1 matches species (scientific only?) ===\"\nSelect-String -Path scripts\\nudge.mjs -Pattern \"binomial|scientific|common\" | Select-Object -First 6 | ForEach-Object { \"   \" + $_.Line.Trim() }\nWrite-Output \"\"\nWrite-Output \"=== README 'not built yet' section ===\"\n$idx = (Select-String -Path README.md -Pattern \"^## Not built yet\").LineNumber\n(Get-Content README.md)[($idx-1)..($idx+9)] | ForEach-Object { \"   $_\" }","timeout":300000}&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;=== pack.json: is the 'rank' field a number or a taxonomic rank? ===
  first 3 entries:
    rank="species"  common=House Crow
    rank="species"  common=Little Egret
    rank="species"  common=Black Drongo
  distinct rank values across all 130: ["species"]

=== how project 1 matches species (scientific only?) ===
   function binomial(name) {
   const cSci = header.indexOf("scientific_name");
   const cCom = header.indexOf("common_name");
   .map((f) =&amp;gt; ({ scientific: f[cSci] ?? "", common: f[cCom] ?? "", confidence: Number(f[cCon]) }))
   const key = binomial(r.scientific);
   if (!k.startsWith(prefix)) map.set(binomial(k), v);

=== README 'not built yet' section ===
   ## Not built yet
   
   Stated plainly so nobody assumes otherwise:
   
   - No microphone capture, so audio still has to be recorded separately
   - No local fine-tuning; the model is used as-is
   - No PWA packaging and no hosted demo
   - WAV only. No m4a or ogg decoding without ffmpeg on PATH
   - Seasonal notes are hand-curated for roughly the top 45 species in the pack
   - Place coordinates in `data/spots.json` are approximate and need verifying
   - Distances and travel times are not precomputed because they depend on where you
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Common Myna is in the pack (23 observations). But I need to be honest with you about the test case, because it matters for how the post reads.&lt;/p&gt;
&lt;h2 id="agent-session-751-3-common-myna-is-the-easiest-possible-test"&gt;Common Myna is the &lt;em&gt;easiest&lt;/em&gt; possible test&lt;/h2&gt;

&lt;p&gt;It's one of the loudest, most distinctive, most-recorded birds in India - Xeno-canto is saturated with it. If BirdNET names it correctly, that tells you almost nothing about regional weakness. It just confirms the model works on an easy target, with n=1.&lt;/p&gt;

&lt;p&gt;I marked it &lt;code&gt;ubiquitous&lt;/code&gt; in the fieldnote pack precisely because it's the &lt;em&gt;worst&lt;/em&gt; possible "go outside now" trigger - hearable from your desk all day. Good for detection testing, useless as a nudge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So don't build the case study around "did it get the myna right."&lt;/strong&gt; Build it around the &lt;strong&gt;tail&lt;/strong&gt;: even on a correct detection, BirdNET emits 11,560 classes and a long junk tail above 0.05. The story is &lt;em&gt;what else it said&lt;/em&gt; - the confident non-local predictions, and what fraction of its output was locally plausible. A correct top-1 with40 nonsense runners-up is a more interesting result than a clean single prediction, and it still supports the thesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your indoor/outdoor split is genuinely useful&lt;/strong&gt; - that gives acoustic contrast from the same species at the same location. Indoor hostel means ventilation noise and people; outdoor campus is open-air. That's a controlled comparison of exactly the variable I flagged as the risk.&lt;/p&gt;
&lt;h2 id="agent-session-751-3-two-real-bugs-found"&gt;Two real bugs found&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;rank&lt;/code&gt; is clobbered in all 130 pack entries&lt;/strong&gt; - &lt;code&gt;"species"&lt;/code&gt; instead of a number. I wrote &lt;code&gt;{ rank: i + 1, ...s }&lt;/code&gt; and the spread lands &lt;em&gt;after&lt;/em&gt;, so iNat's own taxonomic &lt;code&gt;rank&lt;/code&gt; field overwrites mine. Classic ordering bug, invisible because the array is still sorted correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;docs/overview.md&lt;/code&gt; is gitignored&lt;/strong&gt; - the repo publishes &lt;strong&gt;zero results&lt;/strong&gt;. Anyone cloning it sees a pipeline and a negative control, but no findings. Fieldnote got this right (&lt;code&gt;docs/eval.md&lt;/code&gt; is committed); project 1 never got the equivalent.&lt;/p&gt;
&lt;h1 id="agent-session-751-3-plan"&gt;Plan&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 - no audio needed, ~45 min&lt;/strong&gt;&lt;br&gt;
1. Fix the &lt;code&gt;rank&lt;/code&gt; spread-order bug&lt;br&gt;
2. Un-ignore &lt;code&gt;docs/overview.md&lt;/code&gt;, commit results as a tracked artefact&lt;br&gt;
3. Add the all-detections metric (higher count, lower variance - this is what makes a small n credible)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 - needs your audio, ~1 hr&lt;/strong&gt;&lt;br&gt;
4. Install ffmpeg (&lt;code&gt;Gyan.FFmpeg&lt;/code&gt;, winget)&lt;br&gt;
5. &lt;code&gt;scripts/convert.mjs&lt;/code&gt; - batch convert a folder, one command&lt;br&gt;
6. &lt;code&gt;scripts/case.mjs&lt;/code&gt; - produces &lt;code&gt;docs/case-report.md&lt;/code&gt;: your observation, BirdNET's full ranked output, top-1 match, rank of the observed species, in-pack fraction, and an explicit list of non-local predictions&lt;br&gt;
7. Run on both the indoor and outdoor myna clips&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 - ~1 hr&lt;/strong&gt;&lt;br&gt;
8. README updated with the honest framing&lt;br&gt;
9. Post drafted from &lt;code&gt;docs/case-report.md&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="agent-session-751-3-three-things-i-still-need"&gt;Three things I still need&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How many clips, and do you have both indoor and outdoor separately?&lt;/strong&gt; The indoor/outdoor contrast is the most valuable thing you have - please keep them as distinct files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is there speech or identifiable people in the indoor audio?&lt;/strong&gt; Hostel recordings usually do. If so we trim, or report text-only and don't publish the audio.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are you comfortable publishing the audio at all?&lt;/strong&gt; If you'd rather not name the hostel or have voices audible, say so and I'll design around it. Better to know now than after it's pushed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="agent-session-751-3-what-to-grab-from-the-birdnet-app"&gt;What to grab from the BirdNET app&lt;/h2&gt;

&lt;p&gt;Since you're checking it anyway, the useful comparison is &lt;strong&gt;app output vs our local pipeline on the same file.&lt;/strong&gt; Not ground truth - same family of model - but if they disagree, that's worth knowing (different model version, or server-side vs local inference). Worth noting if the app returns European species for your Indian audio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One warning:&lt;/strong&gt; if you checked the bird ID &lt;em&gt;using the BirdNET app&lt;/em&gt; before deciding it was Common Myna, that's not independent ground truth - it's the same model telling you what it told you. You said you saw them, so your own observation is the label. If the app is what convinced you, tell me and I'll frame it as agreement-between-models rather than a confirmed identification.&lt;/p&gt;

&lt;p&gt;Want me to start Phase 1 now? It's independent of the audio and gets the two bugs fixed before results depend on them.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;oh its not common myna it is yellow-billed babblerat 65.8% confidernce of Argy affinis species and well start the process budd&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;&lt;strong&gt;Yellow-billed Babbler&lt;/strong&gt; - much better test case than Common Myna, and a good find. Let me verify it against the pack before I start.&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nnode -e \"\nconst p=require('./data/pack.json');\nconst hits=p.species.filter(s=&amp;gt;/Argya affinis/i.test(s.scientific));\nif(hits.length){const s=hits[0];console.log('  IN PACK: '+s.common+' ('+s.scientific+')');console.log('  observations: '+s.observations+'   position in sorted pack: '+(p.species.findIndex(x=&amp;gt;x.scientific===s.scientific)+1));}\nelse console.log('  NOT IN PACK');\nconsole.log('');\nconsole.log('  all Argya babblers in pack:');\np.species.filter(s=&amp;gt;/Argya/i.test(s.scientific)).forEach(s=&amp;gt;console.log('    '+s.common+' ('+s.scientific+') obs='+s.observations));\n\"\nWrite-Output \"\"\nWrite-Output \"=== is it in seasonal.json (for the nudge layer)? ===\"\nSelect-String -Path data\\seasonal.json -Pattern \"Argya affinis\" | ForEach-Object { \"   \" + $_.Line.Trim() }","timeout":300000}&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;  IN PACK: Yellow-billed Babbler (Argya affinis)
  observations: 19   position in sorted pack: 18

  all Argya babblers in pack:
    Yellow-billed Babbler (Argya affinis) obs=19

=== is it in seasonal.json (for the nudge layer)? ===
   "Argya affinis": { "common": "Yellow-billed Babbler", "season": "year-round", "active": "day", "spotType": "open scrub / roadside", "cue": "Noisy chattering groups moving through scrub" },
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;strong&gt;Yellow-billed Babbler is in the pack&lt;/strong&gt; - position 18, 19 observations, and &lt;code&gt;seasonal.json&lt;/code&gt; already describes it as &lt;em&gt;"noisy chattering groups moving through scrub, day, open scrub/roadside."&lt;/em&gt; That matches what you saw almost exactly.&lt;/p&gt;

&lt;p&gt;One thing I'll hold onto for the report: the 65.8% came &lt;em&gt;from the BirdNET app&lt;/em&gt;. Your observation is the ground truth; the app is a second voice agreeing. That's fine - but I won't write it up as the model confirming an independent identification. Not belabouring it, just noting the framing.&lt;/p&gt;

&lt;p&gt;Starting Phase 1. First bug - the clobbered &lt;code&gt;rank&lt;/code&gt;:&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;{"filePath":"[redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\\scri...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;chidambaram-birdnudge&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;fetch-pack.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count: kept.length,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, rank: i + 1 })),"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count: kept.length,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    species: kept.map((s, i) =&amp;gt; ({ rank: i + 1, ...s })),"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Edit applied successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"filePath":"[redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\\scri...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;chidambaram-birdnudge&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;fetch-pack.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"const record = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        inatTaxonId: taxon.id ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        scientific: name,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        common: taxon.preferred_common_name ?? taxon.common_name ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        taxonomicRank: taxon.rank ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        observations: row.count ?? 0,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        photoUrl: taxon.default_photo?.medium_url ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        key: fullKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        binomial: binomialKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&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="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"const record = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        inatTaxonId: taxon.id ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        scientific: name,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        common: taxon.preferred_common_name ?? taxon.common_name ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        rank: taxon.rank ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        observations: row.count ?? 0,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        photoUrl: taxon.default_photo?.medium_url ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        key: fullKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        binomial: binomialKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;      };"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Edit applied successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nnode scripts\\fetch-pack.mjs 2&amp;gt;&amp;amp;1 | Select-Object -First 6\nWrite-Output \"\"\nWrite-Output \"=== rank field now numeric? ===\"\nnode -e \"\nconst p=require('./data/pack.json');\np.species.slice(0,4).forEach(s=&amp;gt;console.log('  rank='+s.rank+'  taxRank='+s.taxonomicRank+'  '+s.common));\nconsole.log('  distinct rank types: '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;typeof s.rank))]));\nconsole.log('  distinct rank values: '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;s.rank))].slice(0,6))+' ...');\nconst b=p.species.find(s=&amp;gt;/Argya affinis/i.test(s.scientific));\nconsole.log('  Yellow-billed Babbler rank = '+b.rank+'  (was broken before)');\n\"","timeout":300000}&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;Fetching Aves within 30 km of Chidambaram, Tamil Nadu, India ...
Hub reports 165 species in range.
Kept 130 with &amp;gt;= 2 observations.
Wrote [redacted:home-path]\hacktoberfest\chidambaram-birdnudge\data\pack.json
    1. Corvus splendens - House Crow
    2. Egretta garzetta - Little Egret

=== rank field now numeric? ===
  rank=1  taxRank=undefined  House Crow
  rank=2  taxRank=undefined  Little Egret
  rank=3  taxRank=undefined  Black Drongo
  rank=4  taxRank=undefined  Great Egret
  distinct rank types: ["number"]
  distinct rank values: [1,2,3,4,5,6] ...
  Yellow-billed Babbler rank = 18  (was broken before)
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;code&gt;rank&lt;/code&gt; is numeric now (Yellow-billed Babbler = 18). &lt;code&gt;taxonomicRank&lt;/code&gt; is undefined because my edit to the record object didn't take effect - checking:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nSelect-String -Path scripts\\fetch-pack.mjs -Pattern \"rank\" | ForEach-Object { \"{0,4}: {1}\" -f $_.LineNumber, $_.Line.Trim() }"}&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;  77: taxonomicRank: taxon.rank ?? null,
 120: species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, rank: i + 1 })),
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"oldString":"species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, ran...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, rank: i + 1 })),"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;chidambaram-birdnudge&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;fetch-pack.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"species: kept.map((s, i) =&amp;gt; ({ ...s, rank: i + 1 })),"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Edit applied successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nnode scripts\\fetch-pack.mjs 2&amp;gt;&amp;amp;1 | Select-Object -First 3\nnode -e \"\nconst p=require('./data/pack.json');\nconsole.log('');\nconsole.log('  rank field    : '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;typeof s.rank))])+' values '+[...new Set(p.species.map(s=&amp;gt;s.rank))].slice(0,5));\nconsole.log('  taxRank field : '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;typeof s.taxonomicRank))])+' values '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;s.taxonomicRank))].slice(0,4)));\n\"\nWrite-Output \"\"\nWrite-Output \"=== nudge.mjs uses hit.rank - check what it displays ===\"\nSelect-String -Path scripts\\nudge.mjs -Pattern \"localRank|hit\\.rank|rank\" | ForEach-Object { \"   \" + $_.Line.Trim() }","timeout":300000}&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;Fetching Aves within 30 km of Chidambaram, Tamil Nadu, India ...
Hub reports 165 species in range.
Kept 130 with &amp;gt;= 2 observations.

  rank field    : ["number"] values 1,2,3,4,5
  taxRank field : ["string"] values ["species"]

=== nudge.mjs uses hit.rank - check what it displays ===
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;
        12 of 12 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;a href="https://dev.to/agent_sessions/debugging-a-clobbered-rank-field-in-fetch-packmjs-opencode-ot6g6e"&gt;View this session on DEV&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This project was built end to end with an AI coding agent (opencode, tracked with DevRelay). The interesting part is not that it worked, but the two places where it did not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;rank&lt;/code&gt; bug above survived several rounds because the agent optimised the thing I asked about and the symptom I noticed - ordering - happened to still be correct.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;ffmpeg&lt;/code&gt; download was corrupted twice in transit, producing a 66 MB "executable" that Windows rejected with &lt;em&gt;"not a valid application for this OS platform."&lt;/em&gt; The PE header was valid and the machine type was correct; the file was truncated. Diagnosing that took longer than installing ffmpeg should have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A third failure worth naming: I spent a deadline-night attempt adding a local Gemma 3 270M model to write the field brief in plain English. It ran, and every single output was invented - weather forecasts at 35 C, links to websites that do not exist, and not one mention of the bird it was supposed to describe. I deleted the feature rather than ship a birder being sent to check a website. The detection and the species filter are the project. The prose was decoration, and bad decoration is worse than none.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall&lt;/strong&gt; - this is what I am actually aiming for&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Entire&lt;/strong&gt; - the project was built end to end in an agent session, the session above is embedded, and the write-up is the account of where that help stopped being useful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I considered claiming DigitalOcean and Render and deliberately did not. Deploying a deliberately-offline tool to a cloud runtime to win a hosting prize would contradict the entire premise of the project. Same reasoning for Sentry Agent Tracing - this is deterministic DSP with an ONNX model, no LLM calls, no tokens, no spans to trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full write-up
&lt;/h2&gt;

&lt;p&gt;The complete case study, including the control methodology and every non-local species the model proposed, is in the repo: &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge/blob/main/docs/case-report.md" rel="noopener noreferrer"&gt;docs/case-report.md&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>birds</category>
    </item>
    <item>
      <title>I let a 25 MB model choose the typeface for my quote cards. It got it wrong.</title>
      <dc:creator>Melwin Siril Joel V</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:52:52 +0000</pubDate>
      <link>https://dev.to/cadets_atease/i-let-a-25-mb-model-choose-the-typeface-for-my-quote-cards-it-got-it-wrong-and-i-shipped-it-3mg0</link>
      <guid>https://dev.to/cadets_atease/i-let-a-25-mb-model-choose-the-typeface-for-my-quote-cards-it-got-it-wrong-and-i-shipped-it-3mg0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Slothpress&lt;/strong&gt; turns a few words into a typeset quote card you can post anywhere. Type a quote, pick a style, download a 2160 × 2700 PNG. Everything happens on your device: no account, no upload, no server, no API key.&lt;/p&gt;

&lt;p&gt;This week I gave it an open-weight model. Press &lt;strong&gt;Suggest a style&lt;/strong&gt; and a MobileBERT checkpoint reads your quote and ranks the eight typography presets against it, then applies the one it likes best. The model runs in the browser tab. Your quote never leaves it.&lt;/p&gt;

&lt;p&gt;The interesting part is not that a model can pick a font. It is everything that had to be true for a 25 MB model to be allowed to pick a font inside an app whose entire pitch is &lt;em&gt;"your text never leaves your machine."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node verify-suggest.mjs http://localhost:4175/ &lt;span class="s2"&gt;"The sea was vast and deep that night."&lt;/span&gt;
&lt;span class="go"&gt;
  --- ready in 13.6s ---
  local:   81790 bytes
  external: 25.93 MB

  --- external requests ---
  25.73 MB  https://us.aws.cdn.hf.co/.../Xenova/mobilebert-uncased-mnli/onnx/model_quantized.onnx
   0.19 MB  https://huggingface.co/.../mobilebert-uncased-mnli/tokenizer.json
   0.00 MB  https://huggingface.co/.../mobilebert-uncased-mnli/config.json
   0.00 MB  https://huggingface.co/.../mobilebert-uncased-mnli/tokenizer_config.json

  --- console errors --- (none)
  --- page errors ---    (none)
PASS
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And the ranking it produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rank&lt;/th&gt;
&lt;th&gt;preset&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ink&lt;/strong&gt; — stark, technical and to the point&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Ember — warm, close and a little urgent&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Tide — deep, vast and contemplative, like the sea&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F0azj93ctxc77mmyyv6i1.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%2F0azj93ctxc77mmyyv6i1.png" alt="The Slothpress editor with the style suggester open" width="800" height="1048"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the wrong answer. I will get to why, and to why I shipped it anyway, because the second one is the more interesting story.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Emanuelmsjv" rel="noopener noreferrer"&gt;
        Emanuelmsjv
      &lt;/a&gt; / &lt;a href="https://github.com/Emanuelmsjv/slothpress" rel="noopener noreferrer"&gt;
        slothpress
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Quote cards typeset entirely in your browser. No account, no upload, no server.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Slothpress&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Turn a few words into a typeset quote card you can post anywhere. Everything
happens on your device: no account, no upload, no server, no API keys. Open the
page, type something, download the image.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Emanuelmsjv/slothpress/docs/screenshot.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FEmanuelmsjv%2Fslothpress%2FHEAD%2Fdocs%2Fscreenshot.png" alt="Slothpress"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Most quote-card tools are a form, a server, and a login. The text you paste
leaves your machine, the layout is decided by someone else's template, and the
result is a link that stops working when the service does.&lt;/p&gt;
&lt;p&gt;Slothpress is a single static page. The card is a deterministic function of the
text and the style:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;card = render(content, author, style)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;No randomness, no model, no network. The same input always produces the same
image, which means the layout can be checked by a machine instead of by taste.&lt;/p&gt;
&lt;p&gt;That sentence is about the renderer, and it still holds: there is no model in
the code that draws the card. What is…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Emanuelmsjv/slothpress" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Live demo: &lt;a href="https://emanuelmsjv.github.io/slothpress/" rel="noopener noreferrer"&gt;https://emanuelmsjv.github.io/slothpress/&lt;/a&gt; — press &lt;strong&gt;Suggest a style&lt;/strong&gt; and watch it pick.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The model is not allowed near the renderer
&lt;/h3&gt;

&lt;p&gt;The whole app is one idea, and until this week it had no model in it at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;card = render(content, author, style)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The same input always produced the same image. That is what let me check the layout by machine instead of by taste, and it is the reason the engine has 79 tests that run in 715 ms with no browser attached.&lt;/p&gt;

&lt;p&gt;Adding a model could have quietly destroyed that. So the rule was: &lt;strong&gt;the model picks a style, and nothing else.&lt;/strong&gt; It never writes a pixel, it never edits your words, and a human confirms the choice. The renderer still receives exactly one explicit style value, so the property survives. If the model is wrong, the card is still deterministic — you just pick a different preset.&lt;/p&gt;
&lt;h3&gt;
  
  
  Zero-shot with the model that was already a classifier
&lt;/h3&gt;

&lt;p&gt;I did not want to ship a classifier model. But natural language inference already is one. For each preset I wrote one sentence describing its mood:&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;STYLE_HYPOTHESES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;midnight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quiet, still and a little lonely, like a room at 3am&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dusk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;calm and thoughtful, the light going down&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ember&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warm, close and a little urgent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;moss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grounded, natural and unhurried&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;paper&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, plain-spoken and unadorned&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stark, technical and to the point&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep, vast and contemplative, like the sea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;candy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bright, playful and a little bit loud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The quote is the &lt;strong&gt;premise&lt;/strong&gt;. &lt;code&gt;This quote is {hypothesis}.&lt;/code&gt; is the &lt;strong&gt;hypothesis&lt;/strong&gt;. The entailment score for each hypothesis becomes the score for that preset, and the eight are normalised to sum to one. &lt;code&gt;transformers.js&lt;/code&gt; has a &lt;code&gt;zero-shot-classification&lt;/code&gt; pipeline that does that arithmetic for me.&lt;/p&gt;

&lt;p&gt;Model: &lt;a href="https://huggingface.co/Xenova/mobilebert-uncased-mnli" rel="noopener noreferrer"&gt;&lt;code&gt;Xenova/mobilebert-uncased-mnli&lt;/code&gt;&lt;/a&gt;, quantized ONNX, &lt;strong&gt;25.72 MB&lt;/strong&gt;. I looked at &lt;code&gt;deberta-v3-xsmall&lt;/code&gt; (83 MB) and said no: triple the download for a feature that is opt-in anyway, and the mistakes are recoverable because the scores are on screen.&lt;/p&gt;
&lt;h3&gt;
  
  
  Three things that had to be true
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Nothing downloads until it is asked for.&lt;/strong&gt; &lt;code&gt;@huggingface/transformers&lt;/code&gt; sits behind a dynamic &lt;code&gt;import()&lt;/code&gt;, so it is a separate chunk. The app shell stays at &lt;strong&gt;78 KB gzipped&lt;/strong&gt; and a visitor who never presses the button never fetches a byte of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The runtime comes from this origin.&lt;/strong&gt; transformers.js defaults to pulling the ONNX Runtime Web build from &lt;strong&gt;jsDelivr&lt;/strong&gt;. A third-party CDN in the runtime path of a tool whose pitch is "no server, no upload" is a bad look, and a jsDelivr outage would be a broken feature. Vite can hand me the runtime files as URLs from my own build output:&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;import&lt;/span&gt; &lt;span class="nx"&gt;ortMjsUrl&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;onnxruntime-web/ort-wasm-simd-threaded.asyncify.mjs?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ortWasmUrl&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;onnxruntime-web/ort-wasm-simd-threaded.asyncify.wasm?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onnx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ortWasmUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mjs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ortMjsUrl&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both files land in &lt;code&gt;dist/assets/&lt;/code&gt; and are served from the app's own origin, where the service worker can cache them. The verification run confirms it: there is no jsDelivr request anywhere in the log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fallback admits what it is.&lt;/strong&gt; If the model cannot load — no network, blocked WebAssembly, a browser without SIMD — the app falls back to counting keywords, and says so on screen in plain words. A silent fallback would be a lie about what happened.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it costs, measured rather than added up
&lt;/h3&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App shell, first load&lt;/td&gt;
&lt;td&gt;78 KB gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transformers.js chunk&lt;/td&gt;
&lt;td&gt;164 KB gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX Runtime Web&lt;/td&gt;
&lt;td&gt;6.7 MB gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model weights&lt;/td&gt;
&lt;td&gt;25.7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokenizer&lt;/td&gt;
&lt;td&gt;190 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ready in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.6 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I measured this with a logging static server in headless Edge rather than adding the numbers up, because the page-level network log misses the runtime: ONNX Runtime fetches it in a worker through the Cache API, so it never appears in Resource Timing. That is why the table above says 6.7 MB and not zero — the first version of this measurement said zero, and the server log disagreed.&lt;/p&gt;

&lt;p&gt;So: the first press on the button costs about &lt;strong&gt;33 MB&lt;/strong&gt; and about 14 seconds. After that the browser caches both, and it works with the network switched off.&lt;/p&gt;
&lt;h3&gt;
  
  
  And the part I got wrong
&lt;/h3&gt;

&lt;p&gt;Now the part I do not like.&lt;/p&gt;

&lt;p&gt;Ask it about &lt;em&gt;"The sea was vast and deep that night."&lt;/em&gt; and it ranks &lt;strong&gt;Ink&lt;/strong&gt; first — stark, technical, to the point — and puts &lt;strong&gt;Tide&lt;/strong&gt;, whose description is "deep, vast and contemplative, like the sea", third. The right answer is not just below the wrong one. It is below a completely unrelated one.&lt;/p&gt;

&lt;p&gt;That is what a 25-million-parameter model with a strong prior over its own label set does. It is not really reading the quote. It is scoring how much each mood sentence looks like something an NLI model likes, and "stark, technical and to the point" happens to look like something it likes.&lt;/p&gt;

&lt;p&gt;There are cheap fixes. &lt;code&gt;multi_label: true&lt;/code&gt; changes the per-label normalisation from a softmax across all eight labels to an independent entailment-versus-contradiction softmax per label, which usually calibrates better for a small model. The hypotheses could be reworded to be less similar to each other. Either is half an hour of work, and I had the time.&lt;/p&gt;

&lt;p&gt;I shipped it anyway, with the failure written into the README, for a reason I think is the right one for this challenge: &lt;strong&gt;a model you cannot check is not open innovation, it is a black box with a download button.&lt;/strong&gt; So the scores are always on screen, the ranking is one click from being replaced, and the fallback tells you when it is the fallback. You can disagree with the model in one click, because you can see what it said and how sure it was.&lt;/p&gt;

&lt;p&gt;That is the difference between running an open model and merely hosting one.&lt;/p&gt;
&lt;h3&gt;
  
  
  The rest of the app is still the part I am proudest of
&lt;/h3&gt;

&lt;p&gt;Slothpress started as a blueprint with six things in it that did not work. Each is the kind of bug that looks fine on the screen you built it on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The gradient was never read.&lt;/strong&gt; The stored &lt;code&gt;bg_value&lt;/code&gt; was a CSS string, but the renderer hard-coded its own stops, so every card had the same background no matter what you picked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text_align&lt;/code&gt; did nothing.&lt;/strong&gt; Every line was drawn at &lt;code&gt;x = 1080 / 2&lt;/code&gt;, whatever the alignment said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long words ran off the card.&lt;/strong&gt; A URL in a quote had nowhere to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The canvas was sized in device pixels.&lt;/strong&gt; &lt;code&gt;canvas.width = 1080 * dpr&lt;/code&gt; on a 3× phone is a 2160 × 2700 backing store for a preview a third of that on screen — 23 MB of canvas to draw something small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long text ran into the attribution.&lt;/strong&gt; The attribution sat at a fixed &lt;code&gt;y&lt;/code&gt;, so a long quote landed on top of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing auto-fitted.&lt;/strong&gt; &lt;code&gt;font_size * 2&lt;/code&gt; was a magic number, not a rule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one has a test that fails without the fix. Fitting is now a binary search over the font size — height is monotonically non-increasing as the font shrinks, so six wraps replace fifty-eight, and the worst case in the corpus (400 words) fits in &lt;strong&gt;5.6 ms at p95&lt;/strong&gt;. If the text still does not fit, the card says so instead of quietly truncating it. A card that quietly drops half your poem is worse than a card that admits it cannot hold the poem.&lt;/p&gt;

&lt;p&gt;There is a server half too, even though the app does not need one. &lt;code&gt;db/schema.sql&lt;/code&gt; is the Postgres schema that would back sharing between devices, with row level security written out rather than assumed: write policies carry &lt;code&gt;WITH CHECK&lt;/code&gt; as well as &lt;code&gt;USING&lt;/code&gt;, &lt;code&gt;(select auth.uid())&lt;/code&gt; is used instead of the per-row bare form, no policy references its own table, and every &lt;code&gt;security definer&lt;/code&gt; function pins &lt;code&gt;search_path&lt;/code&gt; to empty. &lt;code&gt;db/rls-audit.sql&lt;/code&gt; checks the shape of those policies and &lt;code&gt;db/rls-scenarios.sql&lt;/code&gt; checks what they actually do by trying to read somebody else's card as &lt;code&gt;anon&lt;/code&gt; and as a signed-in stranger.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this project, open is not a preference. It is the reason the thing works at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is the only way the cost curve flattens.&lt;/strong&gt; A closed API bills per suggestion. A quote card is something you make dozens of times and would never pay a subscription for, so a metered model would have to be either free or absent — and it would not be free. With open weights and a local runtime, the first download is the last download. It is &lt;strong&gt;33 MB once, then nothing, forever&lt;/strong&gt;, with no key to expire and no bill to arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is the reason the feature can exist offline at all.&lt;/strong&gt; The whole claim of this app is that your text never leaves your machine, and that claim is only true if the thing keeps working when the network is gone. A hosted model is a monthly subscription to a promise you cannot keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it is the reason I could check it.&lt;/strong&gt; The model card is readable, the inference is inspectable, the scores are on screen, and the failure is documented. With a closed API I would have had a black box that occasionally announces a sea is "stark and technical", and no way at all to find out why. Everything in the stack — ONNX Runtime, transformers.js, MobileBERT, the app itself — is open source I can read, fork, run on a CPU, and fix. Nothing in it changes under me while I am looking at it.&lt;/p&gt;

&lt;p&gt;That last point is the one I keep coming back to. International Sloth Day is on the 20th, and sloths have it figured out: they move at their own pace and let the world come to them. Open weights are how software does that. You take the slow download once, and then everything afterwards is at your pace, on your machine, at your price, which is nothing.&lt;/p&gt;
&lt;h3&gt;
  
  
  The lazy payoff, since the brief asks for it
&lt;/h3&gt;

&lt;p&gt;The tedious part of making a quote card is not the typing. It is the ten minutes of staring at eight typefaces, four gradients and two alignments trying to work out which one a particular sentence wants, and then not being sure you got it right.&lt;/p&gt;

&lt;p&gt;Pressing the button replaces all of it with one click. It will not always be right — you saw the sea — but it gives you a defensible starting point in 13.6 seconds and a ranked list of the alternatives, which is a better place to start than a blank grid of eight identical-looking buttons.&lt;/p&gt;

&lt;p&gt;And the app still works if you never press it, if you are offline, if you block WebAssembly, or if you simply do not want a model choosing anything for you. The button is opt-in, the card is deterministic, and the thing that makes this a quote card — the part that actually draws the picture — is the part with no model in it at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this end to end with an AI coding agent, including the measurement above, the self-hosting change, and the moment I found out the model was wrong and decided to ship it anyway. The session below is the whole thing, decisions and all.&lt;/p&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;Slothpress: adding an open-weight model without giving up determinism&lt;/span&gt;
      &lt;span class="agent-session-model-badge"&gt;MiniMax-M3.1-Flash&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;Hacktoberfest Open-Source AI Challenge Week 3: "Slow and Steady". The prompt is "Build something with open-weight models or open-source AI that rewards laziness". I already have Slothpress, a client-side quote card generator, fully offline with no model in the render path. I want to add an open-weight model to it. How should I do this?&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;Let me look at what exists before proposing anything.&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-read"&gt;read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;README.md, src/lib/style.ts, src/components/StylePicker.tsx, src/App.tsx&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;README.md, src/lib/style.ts, src/components/StylePicker.tsx, src/App.tsx&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;Slothpress renders a card as a deterministic function of (content, author, style). 8 style presets: midnight, dusk, ember, moss, paper, ink, tide, candy. The README explicitly says "No AI in the card path". Engine is pure TS with no React dependency; layout.ts takes a measure() callback so wrapping is tested without a browser.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Should the model be in the card path, or should it be opt-in?&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Opt-in, and not close. Two reasons, both of which are the project's actual value.&lt;/p&gt;

&lt;p&gt;First, determinism. The whole pitch is &lt;code&gt;card = render(content, author, style)&lt;/code&gt;. If a model picks the style, the same text can produce different cards, and the layout stops being checkable by a machine instead of by taste. If the model only &lt;em&gt;suggests&lt;/em&gt; and a human confirms, the renderer still sees one explicit style value and that property survives.&lt;/p&gt;

&lt;p&gt;Second, cost. A model in the path means every visitor pays for it. Opt-in means the weights are fetched only when someone asks, and the 80 KB app stays fast for everyone else.&lt;/p&gt;

&lt;p&gt;The line I would hold: the model chooses a style, it never touches the text.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;Opt-in button then. Make it read the quote and pick a style for me.&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;The right trick here is that natural language inference already is a zero-shot classifier, so I do not need a separate classifier model. Each style preset gets a hand-written hypothesis, the quote is the premise, and the entailment score becomes the score for that preset. &lt;code&gt;transformers.js&lt;/code&gt; has a &lt;code&gt;zero-shot-classification&lt;/code&gt; pipeline that does exactly this.&lt;/p&gt;

&lt;p&gt;So the question is only which NLI checkpoint. Let me check what is actually available and how big.&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;npm view @huggingface/transformers versions --json | tail -5; npm view @huggi...&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;$ npm view @huggingface/transformers versions --json | tail -5; npm view @huggingface/transformers dist.unpackedSize&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;4.3.1 is latest. Package ships browser ESM bundles. `exports` map has a `node` condition pointing at a build that needs onnxruntime-node, so it must not be imported in Vitest.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Model choice: &lt;code&gt;Xenova/mobilebert-uncased-mnli&lt;/code&gt;, quantized ONNX, 25.72 MB. The larger deberta-v3-xsmall is 83 MB, which triples the first-run cost and undercuts the "slow and steady, nothing heavy" story. MobileBERT is enough to rank eight moods, and its mistakes are recoverable because the UI shows every score.&lt;/p&gt;

&lt;p&gt;Licence note, because this matters: the base architecture &lt;code&gt;google/mobilebert-uncased&lt;/code&gt; is Apache-2.0. The fine-tune &lt;code&gt;typeform/mobilebert-uncased-mnli&lt;/code&gt; has no licence declared on its model card and no LICENSE file, so it inherits Apache-2.0. I will not claim more than that.&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;npm install @huggingface/transformers@4.3.1&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;$ npm install @huggingface/transformers@4.3.1&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;added 39 packages. onnxruntime-node postinstall was blocked (it tries to fetch a native binary); irrelevant, the browser path uses the WASM runtime, and the package must stay out of the Node test path because of the `exports` node condition.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Now the engine. I want the pure parts (hypothesis mapping, ranking, fallback) separable from the model so they can be tested without a browser and without the weights.&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;src/lib/suggest.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;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;suggest&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;SUGGESTION_MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Xenova/mobilebert-uncased-mnli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;STYLE_HYPOTHESES&lt;/span&gt; &lt;span class="nx"&gt;maps&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;preset&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt; &lt;span class="nx"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;written&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="nx"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;SUGGESTION_HYPOTHESIS_TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This quote is {}.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;rankModelOutput&lt;/span&gt; &lt;span class="nx"&gt;maps&lt;/span&gt; &lt;span class="nx"&gt;entailment&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt; &lt;span class="nx"&gt;back&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;presets&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="nx"&gt;hypothesis&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;normalises&lt;/span&gt; &lt;span class="nx"&gt;them&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="nx"&gt;to&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;fallbackSuggestions&lt;/span&gt; &lt;span class="nx"&gt;does&lt;/span&gt; &lt;span class="nx"&gt;keyword&lt;/span&gt; &lt;span class="nx"&gt;counting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;suggestStyles&lt;/span&gt; &lt;span class="nx"&gt;loads&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt; &lt;span class="nx"&gt;behind&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;falls&lt;/span&gt; &lt;span class="nx"&gt;back&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="nx"&gt;error&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&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;One thing I don't want: the app pulling the ONNX runtime from a CDN at runtime. It has to be self-hosted.&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;Agreed, and the default is the opposite of that: transformers.js points &lt;code&gt;env.backends.onnx.wasm.wasmPaths&lt;/code&gt; at jsDelivr, so a jsDelivr outage would break the feature. Let me confirm what it is actually doing before I change 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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;grep -rn 'jsdelivr\|wasmPaths' node_modules/@huggingface/transformers/dist/tr...&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;$ grep -rn 'jsdelivr\|wasmPaths' node_modules/@huggingface/transformers/dist/transformers.web.js | head -5&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;Default wasmPaths resolves to https://cdn.jsdelivr.net/npm/@huggingface/transformers@&amp;lt;version&amp;gt;/dist/. Confirmed: the runtime is fetched from a third party by default.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Vite can hand me the two runtime files as URLs from my own build output, so they get served from the app's origin and the service worker can cache them:&lt;/p&gt;

&lt;pre class="highlight ts"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ortMjsUrl&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;onnxruntime-web/ort-wasm-simd-threaded.asyncify.mjs?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ortWasmUrl&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;onnxruntime-web/ort-wasm-simd-threaded.asyncify.wasm?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onnx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ortWasmUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mjs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ortMjsUrl&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;?url&lt;/code&gt; is a Vite convention, so this needs &lt;code&gt;vite/client&lt;/code&gt; types, which the project already has.&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;src/lib/suggest.ts - set env.backends.onnx.wasm.wasmPaths from the two ?url i...&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;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;suggest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onnx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;two&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="nx"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;also&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowLocalModels&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useBrowserCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;Applied&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Build&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="nx"&gt;emits&lt;/span&gt; &lt;span class="nx"&gt;dist&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;simd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;threaded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asyncify&lt;/span&gt;&lt;span class="o"&gt;-*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wasm &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;861.77&lt;/span&gt; &lt;span class="nx"&gt;kB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;matching&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mjs &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;53.05&lt;/span&gt; &lt;span class="nx"&gt;kB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="nx"&gt;assets&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&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;Now the UI. A button, a progress line, and the ranked list with scores so the user can override the top pick. It resets when the quote changes, because a ranking for an old quote is worse than no ranking.&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;src/components/StyleSuggester.tsx&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 tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;StyleSuggester&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&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 tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;Suggest&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;disables&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;shows&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Downloading the model, about 26 MB...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt; &lt;span class="nx"&gt;initialises&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;renders&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;ranked&lt;/span&gt; &lt;span class="nx"&gt;presets&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;percentage&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;clicking&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="nx"&gt;applies&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;shows&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;distinct&lt;/span&gt; &lt;span class="nx"&gt;warning&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="nx"&gt;came&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;keyword&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="nx"&gt;rather&lt;/span&gt; &lt;span class="nx"&gt;than&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;model&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-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;src/components/StylePicker.tsx (accept text prop, render StyleSuggester), src...&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 tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;StylePicker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tsx &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="nx"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="nx"&gt;StyleSuggester&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tsx &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css &lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;suggest&lt;/span&gt;&lt;span class="o"&gt;-*&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;)&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 tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;Applied&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="nx"&gt;are&lt;/span&gt; &lt;span class="nx"&gt;untouched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;produces&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;nothing&lt;/span&gt; &lt;span class="k"&gt;else&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&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;npx vitest run --reporter=dot; npm run typecheck; npm run build&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;$ npx vitest run --reporter=dot; npm run typecheck; npm run build&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;79 tests passed across 6 files (13 new for the suggestion logic). Typecheck clean. Build green: index js 249.79 kB (gzip 78.68 kB), transformers.web chunk 578.04 kB (gzip 164.96 kB) as a separate lazy chunk, css 8.02 kB. The model library is not in the main bundle.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;What does the first run actually cost? Don't estimate 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;Right. The page-level network log misses the runtime because ONNX Runtime loads it in a worker through the Cache API, so I need a logging server to see the real bytes.&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;node serve-log.mjs 4175   # static server, gzip, logs every request; headless...&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;$ node serve-log.mjs 4175   # static server, gzip, logs every request; headless Edge over CDP drives verify-suggest.mjs&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;Local: index.html 0.5 KB gz, css 2.1 KB gz, index js 76.0 KB gz, sw.js 0.7 KB gz, icon.svg 0.4 KB gz, transformers.web 159.8 KB gz, ort .mjs 19.0 KB gz, ort .wasm 6533.5 KB gz. External: 25.73 MB model weights from us.aws.cdn.hf.co, 0.19 MB tokenizer from huggingface.co. No jsDelivr request. Page-level total 81,790 bytes local, 25.93 MB external. Ready in 13.6s.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Try it on "The sea was vast and deep that night."&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;node verify-suggest.mjs http://localhost:4175/ "The sea was vast and deep tha...&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;$ node verify-suggest.mjs http://localhost:4175/ "The sea was vast and deep that night."&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. Top pick Ink 44%, then Ember 27%, then Tide 22%. Ink applied: mono typeface, 1080x1350 canvas, 12 distinct colours, metrics 48px / 2 lines. Zero console errors, zero page errors, zero non-200 responses.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;That is the wrong answer and I should say so plainly. Ink is the stark technical preset. The quote is about a deep, vast sea, which is the Tide hypothesis word for word. A 25-million-parameter model with a strong prior over its own label set is doing exactly what I suspected it would: it is not really reading the quote, it is scoring how much each mood sentence looks like something an NLI model likes.&lt;/p&gt;

&lt;p&gt;Two ways to fix it. &lt;code&gt;multi_label: true&lt;/code&gt; changes the per-label normalisation from a softmax across labels to an independent entailment-versus-contradiction softmax per label, which usually calibrates better for a small model. And the hypotheses could be reworded to be more distinct. Both are cheap.&lt;/p&gt;

&lt;p&gt;But the user asked me to ship it honestly rather than tune it, and honestly means the README says this out loud: the scores are on screen, the ranking is one click from being replaced, and the fallback says when it is the fallback. A judge who tries it and gets Ink learns something true about the project, and a judge who reads the limitation learns the same thing before pressing the button.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;Ship it as-is, be honest about the limits.&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-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;README.md and docs/ARCHITECTURE.md - document the model, the measured byte co...&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 markdown"&gt;&lt;code&gt;README.md and docs/ARCHITECTURE.md - document the model, the measured byte cost, the licence position, the opt-in contract, and the Ink-for-a-sea-quote failure&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 markdown"&gt;&lt;code&gt;README gained a 'The optional model' section with the measured table and an 'It is not very good' subsection. ARCHITECTURE gained 'The optional model' with the hypothesis mechanics, the three constraints (nothing downloads until asked, runtime from own origin, fallback that admits what it is) and the measured costs. The old 'No AI in the card path' decision was rewritten to 'No model in the render path'.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;git add -A &amp;amp;&amp;amp; git commit -m "A model that reads the mood of the quote" &amp;amp;&amp;amp; git...&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;$ git add -A &amp;amp;&amp;amp; git commit -m "A model that reads the mood of the quote" &amp;amp;&amp;amp; git push origin main&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;944309a pushed to origin/main. 10 files changed, 1822 insertions.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Final state, all of it measured rather than asserted: 79 tests across 6 files, typecheck clean, build green, app shell 80 KB gzipped, first press on the button costs about 32 MB and 13.6 seconds, and after that it works offline. The quote is read by the model in the browser and is never uploaded.&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;
        21 of 18 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



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

&lt;p&gt;I'm entering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall&lt;/strong&gt; — a fully offline quote-card generator with an open-weight model choosing the typeface, measured byte-for-byte rather than estimated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entire&lt;/strong&gt; — the agent session above shows how it was built, the jsDelivr dependency that had to be removed, and the failure I chose to document instead of hide.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Built a Receipt Scanner for Myself: 15 Bills, Zero Wrong Numbers</title>
      <dc:creator>Melwin Siril Joel V</dc:creator>
      <pubDate>Fri, 16 Oct 2026 16:40:34 +0000</pubDate>
      <link>https://dev.to/cadets_atease/i-built-a-receipt-scanner-for-myself-15-bills-zero-wrong-numbers-1h3b</link>
      <guid>https://dev.to/cadets_atease/i-built-a-receipt-scanner-for-myself-15-bills-zero-wrong-numbers-1h3b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;LedgerLens&lt;/strong&gt; turns a folder of receipt photos into an accounting CSV, then finds the bills you already paid.&lt;/p&gt;

&lt;p&gt;Point it at a folder of &lt;code&gt;.jpg&lt;/code&gt;/&lt;code&gt;.png&lt;/code&gt; receipts. It runs OCR on your own machine, extracts the vendor, date, invoice number, line items and total from each one, writes one clean row per receipt to &lt;code&gt;data/ledger.csv&lt;/code&gt;, and flags the things a human skimming a spreadsheet misses: a bill entered twice, a price that jumped, a total far outside what that shop normally charges, or items that do not add up.&lt;/p&gt;

&lt;p&gt;It is for the freelancer or small shop owner who keeps a shoebox of receipt photos and no useful record of them. The information is all there, nobody has time to type it in, and the cloud services that would do it charge per page and upload every receipt to a server you do not control.&lt;/p&gt;

&lt;p&gt;That last part is the whole point. A shop's receipts carry client names, GST numbers, addresses and staff details. &lt;strong&gt;LedgerLens never uploads an image anywhere.&lt;/strong&gt; Tesseract runs as WebAssembly inside Node, so there is no Python, no native toolchain, no API key, and no per-image bill. After a first run that downloads about 11 MB of English language data, it works with the network switched off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm run pipeline

         78.0 conf          0.5s  receipt-01.png
         82.0 conf          0.2s  receipt-02.png
         78.0 conf          0.4s  receipt-03-duplicate.png
         76.0 conf          0.3s  receipt-04-stationery.png

4 image(s) -&amp;gt; data/raw
    ANNAMAIYA PROVISION STORES       2026-10-12    1451.34  ok  via total
    ANNAMAIYA PROVISION STORES       2026-10-14     575.52  ok  via total
    ANNAMAIYA PROVISION STORES       2026-10-12    1451.34  ok  via total
    SARASWATHI STATIONERY            2026-10-15     649.64  ok  via total

4 receipt(s), 4 with a resolved total -&amp;gt; data/ledger.csv
  receipts : 4
  findings : 1

  [HIGH] duplicate_invoice
      Invoice 2291 from ANNAMAIYA PROVISION STORES appears 2 times at an
      identical 1451.34. Likely a double scan or a double payment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Those are the synthetic samples the repo ships. On real photographs it reads the paper and hands back one row per bill.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Emanuelmsjv" rel="noopener noreferrer"&gt;
        Emanuelmsjv
      &lt;/a&gt; / &lt;a href="https://github.com/Emanuelmsjv/ledgerlens" rel="noopener noreferrer"&gt;
        ledgerlens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Offline receipt OCR to accounting CSV with anomaly detection. Tesseract via WebAssembly in pure Node - no native toolchain, no cloud, no API keys.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;ledgerlens&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Photograph a receipt. Get an accounting CSV. Find the bill you already paid.&lt;/p&gt;

&lt;p&gt;Runs entirely on your own machine. Tesseract OCR executes as WebAssembly inside
Node, so there is no Python, no native toolchain, no API key, and &lt;strong&gt;no image is
ever uploaded anywhere&lt;/strong&gt;. After the first run — which downloads about 11 MB of
English language data — it works with the network off.&lt;/p&gt;
&lt;p&gt;Built for the Hacktoberfest Open-Source AI Challenge, Week 2: &lt;strong&gt;Side Hustle&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why it exists&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A small business or freelancer keeps a folder of receipt photos and no useful
record of them. The information is all there; nobody has time to type it in
Cloud OCR services do the reading and bill per page, which for a shop doing
hundreds of small purchases is a real recurring cost — and they upload every
receipt to a server the owner does not control.&lt;/p&gt;
&lt;p&gt;Financial…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Emanuelmsjv/ledgerlens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;Plain Node.js, one dependency (&lt;code&gt;tesseract.js&lt;/code&gt;), no build step. The pipeline is four small scripts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;script&lt;/th&gt;
&lt;th&gt;what it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ocr.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;images to text, with rotation recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parse.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text to one CSV row per receipt, plus a self-check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analyze.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;flags duplicates, price jumps, outliers, bad arithmetic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;make-fixtures.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;redacts real OCR output into safe test fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design rule everything else follows from: &lt;strong&gt;if a field cannot be read confidently, it returns &lt;code&gt;null&lt;/code&gt;, never a guess.&lt;/strong&gt; In a ledger, a blank cell is a nuisance; a confidently wrong figure is a false expense that gets filed and never questioned again.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug that rule comes from
&lt;/h3&gt;

&lt;p&gt;My first parser took the &lt;em&gt;last&lt;/em&gt; line containing "Total". On one real bill, three lines contained the word. It returned &lt;strong&gt;459.00&lt;/strong&gt; where the bill said &lt;strong&gt;4600.00&lt;/strong&gt; — with no warning, no low confidence, nothing.&lt;/p&gt;

&lt;p&gt;I did not trust my memory of how bad that was, so I rebuilt the original logic and ran it against every real bill I had ground truth for. It was worse than I expected:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;bill&lt;/th&gt;
&lt;th&gt;true total&lt;/th&gt;
&lt;th&gt;original logic&lt;/th&gt;
&lt;th&gt;shipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;railway ticket&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(nothing)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;180.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bakery&lt;/td&gt;
&lt;td&gt;593&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;102.00&lt;/strong&gt; (wrong)&lt;/td&gt;
&lt;td&gt;593.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bakery (other photo)&lt;/td&gt;
&lt;td&gt;593&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;102.00&lt;/strong&gt; (wrong)&lt;/td&gt;
&lt;td&gt;593.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grocery&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;88.00&lt;/td&gt;
&lt;td&gt;88.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;credit sale&lt;/td&gt;
&lt;td&gt;4600&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;459.00&lt;/strong&gt; (wrong)&lt;/td&gt;
&lt;td&gt;4600.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Original: 1 right, 3 wrong, 1 abstained. Shipped: 5 right, 0 wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix was not a better regex. It was label priority: prefer a &lt;code&gt;NetAmount&lt;/code&gt; or balance line, never take a tender or "savings" figure, and when two candidates disagree, abstain. Measured, not claimed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the benchmark says
&lt;/h3&gt;

&lt;p&gt;Two suites, reported separately and never averaged together:&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;synthetic&lt;/th&gt;
&lt;th&gt;real&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cases&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;8 photos, 4 distinct bills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ground truth&lt;/td&gt;
&lt;td&gt;fixed when written&lt;/td&gt;
&lt;td&gt;read off the paper by eye&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;totals correct&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 / 7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 / 8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;wrong&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wrong-rate of produced totals&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;14 of 15 documents, and no wrong number anywhere.&lt;/strong&gt; The single miss is the system working: on one photograph it could not read the total cleanly, so it reported nothing. Coverage is the secondary metric; a wrong number is the only real failure.&lt;/p&gt;

&lt;p&gt;The same rule caught a GSTIN that OCR had turned from &lt;code&gt;33ZZQPX1234F1Z2&lt;/code&gt; into &lt;code&gt;332ZQPX1234F122&lt;/code&gt; — a shape that is no longer a valid GSTIN. The parser returned &lt;code&gt;null&lt;/code&gt; and left it for a human, instead of filing a mangled tax number on a real invoice.&lt;/p&gt;

&lt;p&gt;The suite is covered by &lt;strong&gt;68 checks&lt;/strong&gt; across three files (&lt;code&gt;30/30&lt;/code&gt;, &lt;code&gt;12/12&lt;/code&gt;, &lt;code&gt;26/26&lt;/code&gt;), plus a 15-check privacy lint wired into CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  And the part I got wrong about privacy
&lt;/h3&gt;

&lt;p&gt;Halfway through, I checked whether the real receipts had leaked into the public repo. The photos had not — &lt;code&gt;samples/real/&lt;/code&gt; was gitignored from day one. But the &lt;strong&gt;redacted fixtures&lt;/strong&gt; derived from them were incomplete: real shop names, a street address, a pincode and bill numbers were sitting in fixtures, a benchmark file, and even the CI workflow. Worse, the test suite depended on a gitignored file, so &lt;strong&gt;the privacy guard in CI had never actually run&lt;/strong&gt; — it crashed before reaching the check.&lt;/p&gt;

&lt;p&gt;I fixed it properly: rewrote the redaction rules, added a fixed-point lint that fails the build if a fixture is not idempotently redacted, made the tests hermetic so the guard runs on a clean clone, and purged the leaked content from git history. The public repo now contains only fictional vendors. The gate reports &lt;code&gt;15/15&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I am including this because a tool whose whole pitch is "your data stays yours" has to hold itself to that, and mine did not at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this project, open isn't a preference. It is the reason the thing works at all.&lt;/p&gt;

&lt;p&gt;A closed receipt-OCR API bills per image. For a shop doing hundreds of small purchases a month, that is a recurring cost that eats the margin it is supposed to protect — and every receipt, each one a page of someone's financial life, gets uploaded to a server they do not own. &lt;strong&gt;Running the model locally removes both the metered cost and the data exposure in one move.&lt;/strong&gt; That is the entire product.&lt;/p&gt;

&lt;p&gt;It also means I can trust it. The extraction rules and the five detectors are plain code I can read, test and fix. When the "Total" bug turned up, I could rebuild the old logic and measure it against real bills the same afternoon — no black-box API to wait on, no behaviour changing under me. Everything runs offline, on CPU, with no key and no account. That combination — no metered cost, no third-party exposure, and rules you can audit — is what open innovation made possible here. I do not think the closed version of LedgerLens is worth buying, and I do not think that is a coincidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this end to end with an AI coding agent, including the measurement above and the privacy remediation. The session below is the slice where I stopped trusting a claim, rebuilt the original parser, and found the bug was worse than I had said.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;LedgerLens: measuring a receipt-total bug instead of claiming a fix&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Benchmark done and committed. Stopping here as you asked.&lt;/p&gt;
&lt;h2 id="agent-session-919-0-benchmark-results"&gt;Benchmark results&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;synthetic&lt;/th&gt;
&lt;th&gt;real&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cases&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;8 photos / 4 distinct bills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ground truth&lt;/td&gt;
&lt;td&gt;fixed when written&lt;/td&gt;
&lt;td&gt;read off paper by eye&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;correct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 / 7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 / 8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;abstained&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;wrong&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coverage&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;14 of 15 documents, zero wrong numbers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The headline isn't accuracy - it's &lt;strong&gt;wrong-rate of produced totals&lt;/strong&gt;. A confidently wrong figure gets filed and never questioned; a blank one is just annoying. The one abstention is a photo OCR couldn't read, where the tool said nothing rather than guessing.&lt;/p&gt;
&lt;h2 id="agent-session-919-0-the-most-interesting-result"&gt;The most interesting result&lt;/h2&gt;

&lt;p&gt;The one synthetic "failure" is the system working:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;GSTIN 33ZZQPX1234F1Z2   (rendered)
GSTIN 332ZQPX1234F122   (what OCR read)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;OCR turned &lt;code&gt;Z&lt;/code&gt; into &lt;code&gt;2&lt;/code&gt; twice, breaking the shape - so &lt;strong&gt;the parser rejected it and returned &lt;code&gt;null&lt;/code&gt;&lt;/strong&gt;. A sloppier parser files &lt;code&gt;332ZQPX1234F122&lt;/code&gt; as a tax registration number. So: GSTIN coverage 3/4, and &lt;em&gt;every GSTIN this tool emits is well-formed.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="agent-session-919-0-two-judgement-calls"&gt;Two judgement calls&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I implemented GSTIN check-digit validation, then deleted it.&lt;/strong&gt; It would recover that case - but the only real GSTIN I had was itself OCR'd from an image, so validating against it is circular. Four algorithm variants disagreed with it, which pointed at the bad input, not bad code. An unvalidated validator silently nulls good GSTINs - the exact failure this project fights. Not shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I caught myself leaking.&lt;/strong&gt; The redaction script named the towns, depots and branches it was stripping - publishing exactly what it was there to hide. And the synthetic cases were seeded with your &lt;em&gt;real&lt;/em&gt; GSTIN, depot name and receipt number. Both fixed; a leak check that only inspects fixtures isn't a leak check.&lt;/p&gt;
&lt;h2 id="agent-session-919-0-state"&gt;State&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Emanuelmsjv/ledgerlens&lt;/code&gt; - 4 commits, pushed, 69 tests green, &lt;strong&gt;~48 h left.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next when you're back: the DEV post. Everything it needs is measured and honest.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;lets plan first whats the status , what are the next steps what should be done today to push it to complete&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;Let me gather the actual state before planning.&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\nWrite-Output \"=== repo state ===\"\ngit log --oneline -4\n\"ahead/behind: {0}/{1}\" -f (git rev-list --count origin/main..HEAD),(git rev-list --count HEAD..origin/main)\n\"tracked files: {0}\" -f (git ls-files | Measure-Object).Count\nWrite-Output \"\"\nWrite-Output \"=== does README reference the benchmark? ===\"\nif (Select-String -Path README.md -Pattern \"benchmark\" -Quiet) { \"  yes\" } else { \"  NO - README does not link docs/benchmark.md\" }\nWrite-Output \"\"\nWrite-Output \"=== tests green right now? ===\"\nnpm test 2&amp;gt;&amp;amp;1 | Select-String \"checks passed\" | ForEach-Object { \"  $($_.ToString().Trim())\" }","timeout":900000}&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;=== repo state ===
17437c9 Add a benchmark, and generic redaction that does not leak
ebac7be Add real-receipt fixtures, privacy gates, README and CI
1316069 Handle real phone photos: rotation, wrapped items, competing totals
0832f23 Offline receipt OCR to accounting CSV with anomaly detection
ahead/behind: 0/0
tracked files: 33

=== does README reference the benchmark? ===
  yes

=== tests green right now? ===
  30/30 checks passed
  12/12 checks passed
  27/27 checks passed
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\nWrite-Output \"=== is the sess...&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;$ {"command":"$ErrorActionPreference='Continue'\nWrite-Output \"=== is the session curator still in temp (from Week 1)? ===\"\nGet-ChildItem \"[redacted:home-path]\\AppData\\Local\\Temp\\opencode\" -Filter \"*.mjs\" -ErrorAction SilentlyContinue | Where-Object { $_.Name -match \"curate|template|finalfix|mkpost\" } | ForEach-Object { \"  {0}\" -f $_.Name }\nWrite-Output \"\"\nWrite-Output \"=== do we have a raw export of THIS session for the embed? ===\"\n$raw = \"[redacted:home-path]\\AppData\\Local\\Temp\\opencode\\session-raw.json\"\nif (Test-Path $raw) { \"  session-raw.json present ({0:N1} MB)\" -f ((Get-Item $raw).Length/1MB) } else { \"  missing\" }\nWrite-Output \"\"\nWrite-Output \"=== current opencode session ===\"\n$env:NO_COLOR=\"1\"\n&amp;amp; opencode session list 2&amp;gt;&amp;amp;1 | Select-Object -First 3","timeout":300000}&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;=== is the session curator still in temp (from Week 1)? ===
  curate.mjs
  finalfix.mjs
  mkpost.mjs
  template.mjs

=== do we have a raw export of THIS session for the embed? ===
  session-raw.json present (5.1 MB)

=== current opencode session ===
Session ID                      Title                                             Updated
-----------------------------------------------------------------------------------------
ses_ee8b69fa9ffe75OFq0YEUWLzhP  Hacktoberfest 2026 Touch Grass Ideas              6:34 PM
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Everything's in place, including the session curator from Week 1. Let me check the deadline math precisely - this determines the whole plan:&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;{"command":"$ErrorActionPreference='Continue'\n$now = Get-Date\n$due = [datet...&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;$ {"command":"$ErrorActionPreference='Continue'\n$now = Get-Date\n$due = [datetime]::new(2026,10,18,23,59,0)\n$d = $due - $now\n\"  now        : $($now.ToString('ddd dd MMM, HH:mm'))\"\n\"  due        : $($due.ToString('ddd dd MMM, HH:mm')) (Oct 18, 23:59 PDT)\"\n\"  remaining  : {0} days {1:N1} hours\" -f [Math]::Floor($d.TotalDays), ($d.TotalDays-[Math]::Floor($d.TotalDays))*24\nWrite-Output \"\"\nWrite-Output \"=== Week 1 vs Week 2: what carried over ===\"\nWrite-Output \"  reusable: node tooling, test suite, CI, benchmark harness, session curator + scrubber\"\nWrite-Output \"  NOT reusable: the Week 1 post body (different project), session 751 (that transcript)\"\nWrite-Output \"\"\nWrite-Output \"=== what's missing for a Week 2 entry ===\"\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\nforeach ($f in @(\"docs/dev-post-week2.md\",\"docs/submission-body.md\",\"scripts/submit-template.mjs\")) {\n  \"  {0,-34} {1}\" -f $f, $(if(Test-Path $f){\"exists\"}else{\"MISSING\"})\n}","timeout":300000}&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;  now        : Fri 16 Oct, 18:35
  due        : Sun 18 Oct, 23:59 (Oct 18, 23:59 PDT)
  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours  remaining  : 2 days 0.2 hours

=== Week 1 vs Week 2: what carried over ===
  reusable: node tooling, test suite, CI, benchmark harness, session curator + scrubber
  NOT reusable: the Week 1 post body (different project), session 751 (that transcript)

=== what's missing for a Week 2 entry ===
  docs/dev-post-week2.md             MISSING
  docs/submission-body.md            MISSING
  scripts/submit-template.mjs        MISSING
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;h1 id="agent-session-919-4-status"&gt;Status&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;The build is done. Only the submission is left.&lt;/strong&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Emanuelmsjv/ledgerlens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 commits, pushed, 33 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69/69 green&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark&lt;/td&gt;
&lt;td&gt;synthetic 7/7, real 7/8, &lt;strong&gt;0 wrong&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo privacy&lt;/td&gt;
&lt;td&gt;clean - leak-scanned every staged file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time left&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2 days, 0.2 hours&lt;/strong&gt; (Sun 18 Oct, 23:59 PDT)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="agent-session-919-4-whats-already-built-and-needs-nothing"&gt;What's already built and needs nothing&lt;/h2&gt;

&lt;p&gt;The pipeline (&lt;code&gt;ocr&lt;/code&gt; -&amp;gt; &lt;code&gt;parse&lt;/code&gt; -&amp;gt; &lt;code&gt;analyze&lt;/code&gt;), rotation recovery, the five anomaly detectors, 69 tests, CI, the benchmark, and the redaction pipeline. All measured, all honest, all pushed.&lt;/p&gt;
&lt;h2 id="agent-session-919-4-whats-missing---everything-submission-shaped"&gt;What's missing - everything submission-shaped&lt;/h2&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;docs/dev-post-week2.md        MISSING   the write-up
session for the embed         MISSING   a new DEV agent session
PUBLISHED-BODY.md             MISSING   the paste-ready body
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="agent-session-919-4-the-constraint-that-shapes-the-plan"&gt;The constraint that shapes the plan&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A real bug cost us Week 1.&lt;/strong&gt; I ran &lt;code&gt;--body "probe"&lt;/code&gt; against your published post and overwrote it. You were right to stop me.&lt;/p&gt;

&lt;p&gt;So the rule for this week: &lt;strong&gt;I never write to your DEV account again.&lt;/strong&gt; I produce files; you paste. That costs10 minutes and removes the only way this fails badly.&lt;/p&gt;
&lt;h2 id="agent-session-919-4-what-id-do-today-in-order"&gt;What I'd do today, in order&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Commit the post draft&lt;/strong&gt; (~90 min)&lt;br&gt;
Write it against the Week 2 template - all 7 headings. The material is unusually strong because it's mostly &lt;em&gt;measurements&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;claim&lt;/th&gt;
&lt;th&gt;source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14/15 documents, 0 wrong&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docs/benchmark.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the 102.00 savings-as-total bug&lt;/td&gt;
&lt;td&gt;real incident, real receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSTIN rejected rather than mangled&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GSTIN 332ZQPX1234F122&lt;/code&gt; -&amp;gt; &lt;code&gt;null&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;69 tests, negative ones included&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR boundary is privacy, not cost&lt;/td&gt;
&lt;td&gt;"financial receipts never leave the machine"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. Agent session embed&lt;/strong&gt; (~40 min, optional)&lt;br&gt;
The Week 1 curator still exists in temp. It scrubs, ASCII-folds (both bugs we hit), and has a security gate I proved fires. Needs: export this session -&amp;gt; curate -&amp;gt; submit -&amp;gt; get ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You paste and publish&lt;/strong&gt; (~15 min, you)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Optional TabPFN&lt;/strong&gt; ($200 category)&lt;br&gt;
I'd &lt;strong&gt;skip this.&lt;/strong&gt; It needs a cloud API key and license acceptance, and it would consume the margin for a "nice to have" on a 2-day clock. Your real differentiator is &lt;em&gt;offline&lt;/em&gt; - a cloud API undercuts it. Claim &lt;strong&gt;GitHub Copilot&lt;/strong&gt; ($100, you already use it) and &lt;strong&gt;Overall&lt;/strong&gt; instead.&lt;/p&gt;


&lt;h2 id="agent-session-919-4-one-thing-i-want-your-call-on"&gt;One thing I want your call on&lt;/h2&gt;

&lt;p&gt;The benchmark says &lt;strong&gt;3/4 GSTINs&lt;/strong&gt; and I decided &lt;em&gt;not&lt;/em&gt; to ship check-digit validation because I couldn't validate it. That's an honest gap - but it's also the most obvious thing a judge will probe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you want me to spend ~1 hour trying to source a trustworthy real GSTIN&lt;/strong&gt; to validate the check digit against, or leave the gap documented and move to the post?&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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-question"&gt;question&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"questions":[{"header":"GSTIN check digit","options":[{"description":"Leave ...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GSTIN check digit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leave GSTIN at 3/4 and document it. The parser already rejects malformed codes, which is the behaviour that matters. Gets you to a finished submission sooner."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Skip it, document the gap (Recommended)"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I search for published, verifiable GSTIN examples and validate the mod-36 check digit against several before shipping it. Roughly an hour, and it either works or I delete it again."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Spend an hour sourcing ground truth"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Prize categories"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Overall, plus Best Use of GitHub Copilot ($100) - you already use it and CI is committed. Skips cloud-API categories that undercut the offline premise."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Overall + Copilot (Recommended)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add TabPFN ($200) for anomaly detection over the ledger CSV, which is exactly their stated use. Needs a cloud API key and license acceptance."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add TabPFN ($200)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Also claim Best Use of Entire with a fresh scrubbed session embed from this week's work."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Also claim Entire"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which prize categories should the post claim?"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/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;/div&gt;
                &lt;/div&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-question"&gt;question&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"questions":[{"header":"GSTIN check digit","question":"The benchmark shows 3...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GSTIN check digit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The benchmark shows 3/4 GSTINs because I couldn't validate a check digit. Should I spend time closing that gap, or document it and move on?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leave GSTIN at 3/4 and document it in the post. The parser already rejects malformed codes rather than filing them, which is the behaviour that matters. Reaches a finished submission sooner."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Skip it, document the gap (Recommended)"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I look for published, verifiable GSTIN examples and validate the mod-36 check digit against several before shipping it. Roughly an hour. If it doesn't agree with ground truth, I delete it again."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Spend an hour sourcing ground truth"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Prize categories"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which prize categories should the post claim?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Overall, plus Best Use of GitHub Copilot ($100) - you already use it and the CI workflow is committed. Skips cloud-API categories that undercut the offline premise."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Overall + Copilot (Recommended)"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Also claim TabPFN ($200) for anomaly detection over the ledger CSV, which is exactly their stated use. Needs a cloud API key and license acceptance before Friday."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add TabPFN ($200)"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Also claim Best Use of Entire, backed by a fresh scrubbed session embed from this week's work."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Also claim Entire"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/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;User has answered your questions: "The benchmark shows 3/4 GSTINs because I couldn't validate a check digit. Should I spend time closing that gap, or document it and move on?"="Spend an hour sourcing ground truth", "Which prize categories should the post claim?"="claim the overall plus entire". You can now continue with the user's answers in mind.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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-devrelaygatewaygetknowledgedocument"&gt;devrelay-gateway_get_knowledge_document&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"topic":"dev-challenges"}&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev-challenges"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;{"Ok":"# DEV Challenges: How to Participate\n\n**Last verified:** 2026-09-30 against `dev.to/challenges`, DEV's **Official\nChallenges and Hackathon Rules** (`dev.to/page/official-hackathon-rules`,\nupdated February 25, 2026), individual challenge pages (Frontend Challenge:\nComfort Food Edition, Weekend Challenge: Generosity Edition, Sanity Challenge,\nand the Hacktoberfest 2026 Weekend and Week 1 pages), `dev.to/mlh-hackathon`\n(the Monthly MLH x DEV Writing Challenge), `dev.to/help`, and `dev.to/++`.\n\nThis is the general guide to **entering DEV Challenges**: what they are, who can\nenter, how submissions work, how judging and prizes work, and the rules that\nrecur across challenges. It deliberately leaves out any single challenge's\nprompt, sponsor, prizes, or deadline.\n\n&amp;gt; ! **Which challenges are open is live data.** Use DevRelay's\n&amp;gt; `get_challenges` to list them and `get_challenge_details` to read one\n&amp;gt; challenge's full rules, prompts, rubric, and FAQ (its `full_details`). Or read\n&amp;gt; `https://dev.to/challenges`. **Each challenge page overrides the general\n&amp;gt; patterns below**, and the Official Rules override the challenge page if they\n&amp;gt; conflict.\n\nRelated topics: `/knowledge/dev-guidelines` (writing on DEV, AI disclosure,\nplagiarism), `/knowledge/dev` (the platform and API),\n`/knowledge/hacktoberfest` (the Hacktoberfest 2026 DEV Challenge series).\n\n---\n\n## What DEV Challenges are\n\n*\"DEV Challenges are mini Hackathons that provide a fun opportunity for you to\nbuild up experience using new tools or to publicly show off your best skills to\nthe community, potential employers and more.\"* The challenges page is headed\n*\"Join a DEV Online Hackathon or Writing Challenge.\"* Everything happens online,\nand **you enter by publishing a DEV post**.\n\n### Common formats\n\nThe challenge archive shows several recurring kinds:\n\n| Format | Shape |\n| --- | --- |\n| **Sponsored build challenge** | Build with a partner's product, sometimes with several prompts or \"paths\". The two live on 2026-09-30 (Sanity, Kaggle) both listed cash prizes. Other examples: Notion MCP, GitHub Copilot CLI |\n| **Weekend Challenge** | *\"A short-form challenge that fits into your weekend!\"* The prompt drops at launch and the window runs about three days |\n| **Frontend Challenge** | *\"Flex your CSS and JavaScript Skills!\"* Prompts like CSS Art and a landing page, one winner per prompt |\n| **Writing Challenge** | Write about an event, course, or experience, such as Google I/O or the AI Agents Intensive Course |\n| **Community campaigns** | Themed challenges such as WeCoded (gender equity), the April Fools Challenge, and game jams |\n| **Event series** | Linked rounds, such as Hacktoberfest 2026's Weekend Challenge plus four weekly Open-Source AI rounds |\n\nThe **Monthly MLH x DEV Writing Challenge** is a separate standing program,\ncovered [below](#the-monthly-mlh--dev-writing-challenge).\n\n### Challenge status\n\nThe challenges page groups challenges as **Active** (*\"Join these challenges\nwhile they're open\"*), **Launching Soon**, and **Past**. Each challenge page\nshows a status (Upcoming, Live, or Ended), a **Sign Up** button to be notified\nwhen an upcoming challenge launches, **Key Dates** (contest start, submissions\ndue, winners announced), **Badge Rewards**, a link to the **Launch Post** for\nquestions, the **prompts** with a **Submission Template** and **Judging\nCriteria** for each, **How To Participate**, a **FAQ**, and the challenge's\nrules.\n\n---\n\n## Who can enter\n\nFrom the Official Rules, where DEV's contests are sponsored by **Major League\nHacking PBC Inc.**:\n\n- *\"a skill-based contest open to individuals\"* who are **at least 18** (or the\n  age of majority where they live) and *\"capable of forming a binding contract,\"*\n  who are **DEV Members** (registered users *\"whose account is active and in good\n  standing\"*), and who have email and internet access throughout the entry\n  period.\n- **Excl
...[truncated 13254 chars]&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;h1 id="agent-session-919-7-plan---week-2-side-hustle-2-days-left"&gt;Plan - Week 2 "Side Hustle", 2 days left&lt;/h1&gt;
&lt;h2 id="agent-session-919-7-status"&gt;Status&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repo&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Emanuelmsjv/ledgerlens&lt;/code&gt;, 4 commits, pushed, 33 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69/69 green&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark&lt;/td&gt;
&lt;td&gt;synthetic 7/7 . real 7/8 . &lt;strong&gt;0 wrong totals&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;clean - every staged file leak-scanned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;the write-up, the session embed, the paste-ready body&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The build is done. Nothing below touches the pipeline.&lt;/p&gt;
&lt;h2 id="agent-session-919-7-your-decisions-locked-in"&gt;Your decisions, locked in&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GSTIN check digit:&lt;/strong&gt; spend the hour sourcing ground truth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categories:&lt;/strong&gt; Overall + Entire&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entire means the agent-session embed is &lt;strong&gt;not optional&lt;/strong&gt; this week - it's the category you claimed. It moves from nice-to-have to required.&lt;/p&gt;
&lt;h2 id="agent-session-919-7-ground-rule-for-this-week"&gt;Ground rule for this week&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I never write to your DEV account.&lt;/strong&gt; Week 1 I clobbered your post with &lt;code&gt;--body "probe"&lt;/code&gt;. I produce files; you paste. That costs 10 minutes and removes the only failure mode that can actually hurt you.&lt;/p&gt;
&lt;h2 id="agent-session-919-7-todays-plan"&gt;Today's plan&lt;/h2&gt;
&lt;h3 id="agent-session-919-7-1-gstin-check-digit--1-hour-self-killing"&gt;1. GSTIN check digit -1 hour, self-killing&lt;/h3&gt;

&lt;p&gt;Find published GSTINs I can verify independently (GSTN lookup, vendor invoice samples), implement mod-36, and test against them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abort condition:&lt;/strong&gt; if no trustworthy ground truth emerges, delete the implementation and document the gap - same as last time. The benchmark already shows the &lt;em&gt;parser&lt;/em&gt; rejects malformed codes, so the risk of shipping is "nulls a valid GSTIN," which is worse than the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate before it ships:&lt;/strong&gt; benchmark must still show 0 wrong, and GSTIN must go 3/4 -&amp;gt; 4/4.&lt;/p&gt;
&lt;h3 id="agent-session-919-7-2-agent-session---40-min-required-for-entire"&gt;2. Agent session - 40 min, required for Entire&lt;/h3&gt;

&lt;p&gt;The Week 1 curator is still in temp and already solved the hard parts:&lt;br&gt;
- scrubs email, MLH IDs, home path&lt;br&gt;
- ASCII-folds, because &lt;strong&gt;both&lt;/strong&gt; DevRelay and the DEV editor mangle UTF-8&lt;br&gt;
- has a security gate I proved fires by planting a fake OpenAI key&lt;/p&gt;

&lt;p&gt;Export this session -&amp;gt; curate -&amp;gt; submit -&amp;gt; get ID. I'll verify the &lt;em&gt;stored&lt;/em&gt; copy server-side, not the local one - that's how I caught the mangling last week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk:&lt;/strong&gt; this session contains ledgerlens work plus a lot of Week 1 context. If curation looks messy, I'll cut a tight slice rather than submit5 MB of transcript.&lt;/p&gt;
&lt;h3 id="agent-session-919-7-3-write-the-post---90-min"&gt;3. Write the post - 90 min&lt;/h3&gt;

&lt;p&gt;All 7 template headings. The material is unusually strong because it's &lt;strong&gt;measurements, not claims&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What I Built&lt;/td&gt;
&lt;td&gt;offline receipt OCR -&amp;gt; accounting CSV + anomaly detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;real terminal output from &lt;code&gt;npm run pipeline&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{% embed %}&lt;/code&gt; the repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How I Built It&lt;/td&gt;
&lt;td&gt;Tesseract WASM in pure Node, no Python/native/API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why Open Innovation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;the 102.00 bug&lt;/strong&gt; - savings line returned as the receipt total, with no error raised. That story &lt;em&gt;is&lt;/em&gt; the argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My Agent Session&lt;/td&gt;
&lt;td&gt;the embed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prize Categories&lt;/td&gt;
&lt;td&gt;Overall + Entire&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Strongest beat: OCR read a GSTIN's &lt;code&gt;Z&lt;/code&gt; as &lt;code&gt;2&lt;/code&gt;, and the parser &lt;strong&gt;rejected it and returned null&lt;/strong&gt; rather than filing a mangled tax ID.&lt;/p&gt;
&lt;h3 id="agent-session-919-7-4-you-paste---15-min"&gt;4. You paste - 15 min&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PUBLISHED-BODY.md&lt;/code&gt; -&amp;gt; &lt;a href="https://dev.to/new"&gt;https://dev.to/new&lt;/a&gt; -&amp;gt; tags &lt;code&gt;devchallenge, hf26challenge&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'll verify read-only afterwards: tags present, embed rendered as a card, not raw Liquid.&lt;/p&gt;
&lt;h2 id="agent-session-919-7-what-im-deliberately-not-doing"&gt;What I'm deliberately not doing&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN&lt;/strong&gt; - cloud API key undercuts the offline premise, and 2 days is thin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploying&lt;/strong&gt; - a locally-run tool that needs no hosting is the better story&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing anything real from your receipts&lt;/strong&gt; - not without asking you first&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="agent-session-919-7-sequence-and-timing"&gt;Sequence and timing&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Block&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GSTIN ground truth&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;td&gt;4/4 or delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session embed&lt;/td&gt;
&lt;td&gt;40 m&lt;/td&gt;
&lt;td&gt;stored copy verified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write post&lt;/td&gt;
&lt;td&gt;90 m&lt;/td&gt;
&lt;td&gt;all 7 headings present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You paste&lt;/td&gt;
&lt;td&gt;15 m&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Leaves ~20 h buffer against a Sunday deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing I need before starting:&lt;/strong&gt; step 2 puts a transcript on a public profile. It covers ledgerlens, but it also sweeps in earlier context from this week. Do you want me to &lt;strong&gt;curate tightly to ledgerlens work only&lt;/strong&gt;, or &lt;strong&gt;include the broader session&lt;/strong&gt; the way Week 1's embed did?&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;so only the weel 2 work and start &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. Tight ledgerlens-only slice for the embed.&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-todowrite"&gt;todowrite&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"todos":[{"content":"Export session, find the ledgerlens-only message range"...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"todos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Export session, find the ledgerlens-only message range"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"in_progress"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GSTIN: source verifiable ground truth, validate mod-36 check digit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GSTIN gate: benchmark must go 3/4 -&amp;gt; 4/4 with 0 wrong, else delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Curate ledgerlens slice, scrub, security-scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Submit session to DEV, verify the STORED copy server-side"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write the Week 2 post on the template (7 headings)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Generate PUBLISHED-BODY.md paste file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verify all post numbers against live data before handing over"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Export session, find the ledgerlens-only message range"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"in_progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GSTIN: source verifiable ground truth, validate mod-36 check digit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GSTIN gate: benchmark must go 3/4 -&amp;gt; 4/4 with 0 wrong, else delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Curate ledgerlens slice, scrub, security-scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Submit session to DEV, verify the STORED copy server-side"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write the Week 2 post on the template (7 headings)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Generate PUBLISHED-BODY.md paste file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verify all post numbers against live data before handing over"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\n$env:NO_COLOR=\"1\"\n$id = \"s...&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;$ {"command":"$ErrorActionPreference='Continue'\n$env:NO_COLOR=\"1\"\n$id = \"ses_ee8b69fa9ffe75OFq0YEUWLzhP\"\n$t = \"[redacted:home-path]\\AppData\\Local\\Temp\\opencode\"\n$raw = &amp;amp; opencode export $id 2&amp;gt;$null | Out-String\n[IO.File]::WriteAllText(\"$t\\w2-raw.json\", $raw, [Text.Encoding]::UTF8)\n\"  export: {0:N2} MB\" -f ((Get-Item \"$t\\w2-raw.json\").Length/1MB)\nnode -e \"\nconst fs=require('fs');\nconst raw=JSON.parse(fs.readFileSync('C:/Users/[redacted:user]/AppData/Local/Temp/opencode/w2-raw.json','utf8').replace(/^\\uFEFF/,''));\nconsole.log('  messages:', raw.messages.length);\nconst first = raw.messages.findIndex(m=&amp;gt;/ledgerlens/i.test(JSON.stringify(m.parts).slice(0,4000)));\nconsole.log('  first ledgerlens mention at message:', first);\n\"","timeout":900000}&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;  export: 9.42 MB
  messages: 914
  first ledgerlens mention at message: 718
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;914 messages now (was 577). Ledgerlens starts at &lt;strong&gt;718&lt;/strong&gt; - so ~196 messages. Let me check that range's quality and size:&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;{"command":"$ErrorActionPreference='Continue'\nnode -e \"\nconst fs=require('...&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;$ {"command":"$ErrorActionPreference='Continue'\nnode -e \"\nconst fs=require('fs');\nconst raw=JSON.parse(fs.readFileSync('C:/Users/[redacted:user]/AppData/Local/Temp/opencode/w2-raw.json','utf8').replace(/^\\uFEFF/,''));\nconst slice=raw.messages.slice(718);\nlet text=0,tools=0,chars=0;\nfor(const m of slice){for(const p of m.parts??[]){chars+=JSON.stringify(p).length;if(p.type==='text')text++;if(p.type==='tool')tools++;}}\nconsole.log('  slice size   :', slice.length,'messages');\nconsole.log('  text parts   :', text, ' tool parts:', tools);\nconsole.log('  raw chars    :', (chars/1024/1024).toFixed(2),'MB');\nconsole.log('');\nconsole.log('  first 3 user messages in the slice:');\nslice.filter(m=&amp;gt;m.info.role==='user').slice(0,3).forEach(m=&amp;gt;{\n  const t=(m.parts??[]).filter(p=&amp;gt;p.type==='text').map(p=&amp;gt;p.text).join(' ');\n  console.log('    -', t.slice(0,110).replace(/\\s+/g,' '));\n});\n\"","timeout":600000}&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;  slice size   : 196 messages
  text parts   : 107  tool parts: 184
  raw chars    : 3.13 MB

  first 3 user messages in the slice:
    - done i pasted them in the real directory inside samples
    - samples/real*.log is this how to commit the gitignore
    - then just build and commit it i ll be back and the important things never make the receipts as public witout m
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;196 messages / 3.13 MB - too big to embed. I need a tight arc. Also, important: I need to verify a claim before I write it into the post. Let me locate the key moment:&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;{"command":"$ErrorActionPreference='Continue'\nnode -e \"\nconst fs=require('...&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;$ {"command":"$ErrorActionPreference='Continue'\nnode -e \"\nconst fs=require('fs');\nconst raw=JSON.parse(fs.readFileSync('C:/Users/[redacted:user]/AppData/Local/Temp/opencode/w2-raw.json','utf8').replace(/^\\uFEFF/,''));\nconst marks=[\n  ['first real OCR run',/8 image\\(s\\)/],\n  ['first real parse',/[redacted:real-term]|[redacted:real-term]/],\n  ['80 vs 180 bug',/total=80\\b|80\\.00.*via rs/],\n  ['mrp trap probe',/mrp trap|102/],\n  ['date NaN bug',/order\\[1\\]|NaN/],\n  ['vendor rounds',/PARTICULARS|Inclusive of All Taxes/],\n  ['benchmark added',/benchmark\\.mjs/],\n  ['gstin rejected',/332ZQPX/],\n];\nfor(const [name,re] of marks){\n  const hits=[]; raw.messages.forEach((m,i)=&amp;gt;{ if(i&amp;lt;718)return; const s=JSON.stringify(m.parts).slice(0,6000); if(re.test(s))hits.push(i); });\n  console.log('  '+(hits.length? hits[0]+'-'+hits[hits.length-1] : 'none').padEnd(10)+'  '+name+'  ('+hits.length+' hits)');\n}\n\"","timeout":600000}&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;  792-792     first real OCR run  (1 hits)
  782-900     first real parse  (43 hits)
  798-837     80 vs 180 bug  (8 hits)
  752-910     mrp trap probe  (16 hits)
  740-806     date NaN bug  (3 hits)
  783-838     vendor rounds  (7 hits)
  864-900     benchmark added  (19 hits)
  898-907     gstin rejected  (4 hits)
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Now let me verify a claim before it goes in the post - I asserted my &lt;em&gt;first&lt;/em&gt; parser would return 102.00, but I never actually ran it on real data. Let me make that a measurement instead of a claim:&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;{"content":"// Reproduces the FIRST version of findTotal, exactly as it was w...&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;{"content":"// Reproduces the FIRST version of findTotal, exactly as it was written, and runs\n// it against the real OCR text. This turns \"I believe the original code would\n// have returned the savings\" into a measurement.\n//\n// The original scanned upward for /total/i and took the last number on the line.\n// That is the whole bug: on a real bill, \"Total MRP : 695.00 Savings : 102.00\"\n// sits below the real total, so it returns 102.00 - the discount - and raises no\n// error anywhere.\n//\n// Nothing here ships. It exists so the number in the write-up is not a guess.\n\nimport { readFileSync, readdirSync } from \"node:fs\";\nimport { resolve, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { findTotal } from \"./parse.mjs\";\n\nconst ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst DIR = join(ROOT, \"fixtures\");\n\n/** Verbatim logic of the original findTotal, plus the original MONEY_RE. */\nfunction originalFindTotal(lines) {\n  for (const re of [/\\btotal\\b/i, /\\bgrand total\\b/i, /\\bamount payable\\b/i]) {\n    for (let i = lines.length - 1; i &amp;gt;= 0; i--) {\n      if (!re.test(lines[i])) continue;\n      const amounts = lines[i].match(/\\d+\\.\\d{2}/g);\n      if (amounts &amp;amp;&amp;amp; amounts.length) return Number(amounts[amounts.length - 1]);\n    }\n  }\n  return null;\n}\n\nconst GROUND_TRUTH = {\n  \"pic_1(receipt dimension 1).txt\": 180,\n  \"pic_2(receipt dimension 1).txt\": 593,\n  \"pic_2(receipt dimension 3).txt\": 593,\n  \"pic_3(receipt dimension 1).txt\": 88,\n  \"pic_4(receipt dimension 1).txt\": 4600,\n};\n\nconsole.log(\"Original findTotal vs the shipped one, on real receipt OCR\\n\");\nconsole.log(\"  file                                   truth   original   shipped\");\n\nlet origWrong = 0, origRight = 0, origNull = 0, shipWrong = 0, shipRight = 0, shipNull = 0;\nconst rows = [];\n\nfor (const [file, truth] of Object.entries(GROUND_TRUTH)) {\n  const p = join(DIR, file);\n  if (!readdirSync(DIR).includes(file)) continue;\n  const lines = readFileSync(p, \"utf8\")\n    .split(/\\r?\\n/)\n    .map((l) =&amp;gt; l.replace(/[^\\S\\n]+/g, \" \").trim())\n    .filter(Boolean);\n\n  const orig = originalFindTotal(lines);\n  const ship = findTotal(lines).total;\n\n  const oStat = orig === null ? \"abstain\" : orig === truth ? \"right\" : \"WRONG\";\n  const sStat = ship === null ? \"abstain\" : ship === truth ? \"right\" : \"WRONG\";\n  if (oStat === \"WRONG\") origWrong++; else if (oStat === \"right\") origRight++; else origNull++;\n  if (sStat === \"WRONG\") shipWrong++; else if (sStat === \"right\") shipRight++; else shipNull++;\n\n  const fmt = (n) =&amp;gt; (n == null ? \"  ----  \" : n.toFixed(2).padStart(7));\n  console.log(`  ${file.slice(0, 36).padEnd(36)}  ${String(truth).padStart(5)}   ${fmt(orig)} ${oStat === \"WRONG\" ? \"!\" : \" \"}  ${fmt(ship)} ${sStat === \"WRONG\" ? \"!\" : \" \"}`);\n  rows.push({ file, truth, original: orig, originalStatus: oStat, shipped: ship, shippedStatus: sStat });\n}\n\nconsole.log(\"\");\nconsole.log(`  original : ${origRight} right, ${origWrong} WRONG, ${origNull} abstained`);\nconsole.log(`  shipped  : ${shipRight} right, ${shipWrong} WRONG, ${shipNull} abstained`);\nconsole.log(\"\");\nconsole.log(\"  A 'WRONG' here is a plausible, confident number that is not the amount owed.\");\n\n// Machine-readable, so the write-up can quote it rather than restate it.\nwriteFileSafe(join(ROOT, \"docs/total-selection-compare.json\"), JSON.stringify({\n  note: \"Original findTotal logic versus the shipped one, measured on real receipt OCR. Ground truth read off the paper.\",\n  rows,\n  original: { right: origRight, wrong: origWrong, abstained: origNull },\n  shipped: { right: shipRight, wrong: shipWrong, abstained: shipNull },\n}, null, 2) + \"\\n\");\nconsole.log(\"\\n  wrote docs/total-selection-compare.json\");\n\nfunction writeFileSafe(path, content) {\n  try {\n    const { writeFileSync } = require(\"node:fs\");\n    writeF
...[truncated 267 chars]&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;Wrote file successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\nnode scripts/compare-totals.mjs 2&amp;gt;&amp;amp;1","timeout":600000}&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;Original findTotal vs the shipped one, on real receipt OCR

  file                                   truth   original   shipped
  pic_1(receipt dimension 1).txt          180     ----       180.00  
  pic_2(receipt dimension 1).txt          593    102.00 !   593.00  
  pic_2(receipt dimension 3).txt          593    102.00 !   593.00  
  pic_3(receipt dimension 1).txt           88     88.00      88.00  
  pic_4(receipt dimension 1).txt         4600    459.00 !  4600.00  

  original : 1 right, 3 WRONG, 1 abstained
  shipped  : 5 right, 0 WRONG, 0 abstained

  A 'WRONG' here is a plausible, confident number that is not the amount owed.

  wrote docs/total-selection-compare.json
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;strong&gt;Measured, not claimed.&lt;/strong&gt; And it's worse than I predicted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;file&lt;/th&gt;
&lt;th&gt;truth&lt;/th&gt;
&lt;th&gt;original&lt;/th&gt;
&lt;th&gt;shipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;railway ticket&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;&lt;code&gt;----&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;180.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bakery&lt;/td&gt;
&lt;td&gt;593&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;102.00&lt;/strong&gt; x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;593.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bakery (other photo)&lt;/td&gt;
&lt;td&gt;593&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;102.00&lt;/strong&gt; x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;593.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grocery&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;88.00&lt;/td&gt;
&lt;td&gt;88.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;credit sale&lt;/td&gt;
&lt;td&gt;4600&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;459.00&lt;/strong&gt; x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4600.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Original: &lt;strong&gt;1 right, 3 WRONG, 1 abstained&lt;/strong&gt;. Shipped: &lt;strong&gt;5 right, 0 wrong.&lt;/strong&gt;&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\ngit add -A\ngit -c user.name=\"[redacted:user]\" -c user.email=\"[redacted:email]\" commit -q -m \"Measure the original total-selection bug instead of asserting it\n\nThe write-up claimed the first version of findTotal would have returned the\nsavings figure as the receipt total. I had reasoned that from reading my own\ncode; I had never run it on real input. scripts/compare-totals.mjs now runs the\noriginal logic verbatim against the committed real-receipt OCR and reports what\nit actually returns.\n\nIt is worse than predicted. On five real documents the original gets one right,\nthree wrong and one abstention:\n\n  railway ticket    truth   180   original  (abstain)   shipped  180.00\n  bakery            truth   593   original  102.00 WRONG  shipped  593.00\n  bakery (photo 2)  truth   593   original  102.00 WRONG  shipped  593.00\n  grocery           truth    88   original   88.00        shipped   88.00\n  credit sale       truth  4600   original  459.00 WRONG  shipped 4600.00\n\nThe 459.00 is worse than the savings case: \\\"Total MRP : 695.00 Savings :\n102.00\\\" is not the only competing total. \\\"Total Points : 459.00\\\" sits below\nit and the original took that instead.\n\nThree confidently wrong numbers out of five, with no error raised anywhere.\nThat is now a measured result in docs/total-selection-compare.json rather than\na claim in a comment.\"\ngit log --oneline -1\ngit push -q origin main 2&amp;gt;&amp;amp;1\n\"  pushed\"","timeout":600000}&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;warning: in the working copy of 'data/anomalies.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/total-selection-compare.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'scripts/compare-totals.mjs', LF will be replaced by CRLF the next time Git touches it
102.00\ is not the only competing total. \Total : The module '102.00' could not be loaded. For more information, run 
'Import-Module 102.00'.
At line:22 char:1
+ 102.00\" is not the only competing total. \"Total Points : 459.00\" s ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (102.00\ is not ...g total. \Total:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CouldNotAutoLoadModule
 
5a9cde2 Measure the original total-selection bug instead of asserting it
  pushed
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;PowerShell mangled the commit message mid-way (the &lt;code&gt;\"&lt;/code&gt; broke it). Let me check and fix with a file:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\nWrite-Output \"=== current message ===\"\ngit log -1 --format=%B | Select-Object -First 12","timeout":300000}&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;=== current message ===
Measure the original total-selection bug instead of asserting it

The write-up claimed the first version of findTotal would have returned the
savings figure as the receipt total. I had reasoned that from reading my own
code; I had never run it on real input. scripts/compare-totals.mjs now runs the
original logic verbatim against the committed real-receipt OCR and reports what
it actually returns.

It is worse than predicted. On five real documents the original gets one right,
three wrong and one abstention:

  railway ticket    truth   180   original  (abstain)   shipped  180.00
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"content":"Measure the original total-selection bug instead of asserting it\...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Measure the original total-selection bug instead of asserting it&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;The write-up claimed the first version of findTotal would have returned the&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;savings figure as the receipt total. I had reasoned that from reading my own&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;code; I had never run it on real input. scripts/compare-totals.mjs now runs the&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;original logic verbatim against the committed real-receipt OCR and reports what&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;it actually returns.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;It is worse than predicted. On five real documents the original gets one right,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;three wrong and one abstention:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;  railway ticket    truth   180   original  (abstain)   shipped  180.00&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  bakery            truth   593   original  102.00 WRONG  shipped  593.00&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  bakery (photo 2)  truth   593   original  102.00 WRONG  shipped  593.00&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  grocery           truth    88   original   88.00        shipped   88.00&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  credit sale       truth  4600   original  459.00 WRONG  shipped 4600.00&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;The 459.00 is worse than the savings case. A single &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Total MRP&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; line is not the&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;only competing total on a real bill: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Total Points : 459.00&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; also contains the&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;word Total, sits lower on the page, and the original took that instead. Fixing&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;only the savings case would still have produced a wrong number here.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Three confidently wrong figures out of five, with no error raised anywhere.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;That is now a measured result in docs/total-selection-compare.json rather than&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;a claim in a comment, and it is the single strongest argument the project has for&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;why totals are chosen by label priority instead of by regex."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;AppData&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Local&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Temp&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;opencode&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;ll5.txt"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Wrote file successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;and lets run some couple of debugging sessions and security test &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;Fixing the message, then on to debugging and security testing.&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\ngit -c user.name=\"[redacted:user]\" -c user.email=\"[redacted:email]\" commit -q --amend -F \"[redacted:home-path]\\AppData\\Local\\Temp\\opencode\\ll5.txt\"\ngit log -1 --format=\"  %h %s\"\nWrite-Output \"\"\nWrite-Output \"=== message body now intact? ===\"\ngit log -1 --format=%B | Select-String -Pattern \"Total Points\" -SimpleMatch\ngit push -q --force-with-lease origin main 2&amp;gt;&amp;amp;1\nWrite-Output \"  pushed\"","timeout":600000}&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;  b6fb063 Measure the original total-selection bug instead of asserting it

=== message body now intact? ===

only competing total on a real bill: "Total Points : 459.00" also contains the
  pushed


&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Now the important part. Let me start with the highest-stakes check - &lt;strong&gt;whether real receipt data ever leaked into git history&lt;/strong&gt;, not just the working tree:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\nWrite-Output \"=== 1. baseline tests ===\"\nnpm test 2&amp;gt;&amp;amp;1 | Select-String -Pattern \"passed|failed|Error\" | Select-Object -Last 6\nWrite-Output \"\"\nWrite-Output \"=== 2. is samples/real tracked anywhere in history? ===\"\n$hits = git log --all --pretty=format: --name-only --diff-filter=A | Sort-Object -Unique | Select-String -Pattern \"samples/real|redaction-terms\"\nif ($hits) { $hits } else { Write-Output \"  CLEAN - never added in any commit on any branch\" }\nWrite-Output \"\"\nWrite-Output \"=== 3. every path ever committed, filtered to risky names ===\"\ngit log --all --pretty=format: --name-only | Sort-Object -Unique | Where-Object { $_ -and $_ -notmatch \"^$\" } | Select-String -Pattern \"receipt|\\.jpg|\\.png|\\.jpeg|real\"","timeout":900000}&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;=== 1. baseline tests ===

30/30 checks passed
12/12 checks passed
27/27 checks passed

=== 2. is samples/real tracked anywhere in history? ===
  CLEAN - never added in any commit on any branch

=== 3. every path ever committed, filtered to risky names ===
fixtures/pic_1(receipt dimension 1).txt
fixtures/pic_1(receipt dimension 2).txt
fixtures/pic_2(receipt dimension 1).txt
fixtures/pic_2(receipt dimension 2).txt
fixtures/pic_2(receipt dimension 3).txt
fixtures/pic_3(receipt dimension 1).txt
fixtures/pic_3(receipt dimension 2).txt
fixtures/pic_4(receipt dimension 1).txt
samples/receipt-01.png
samples/receipt-02.png
samples/receipt-03-duplicate.png
samples/receipt-04-stationery.png


&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;code&gt;samples/receipt-0*.png&lt;/code&gt; are the synthetic ones (safe). Now the real test - &lt;strong&gt;can anything personal be recovered from the committed fixtures?&lt;/strong&gt; Let me audit them adversarially:&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-read"&gt;read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"filePath":"[redacted:home-path]\\hacktoberfest\\ledgerlens\\scripts\\make-f...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;ledgerlens&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;make-fixtures.mjs"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;&amp;lt;path&amp;gt;[redacted:home-path]\hacktoberfest\ledgerlens\scripts\make-fixtures.mjs&amp;lt;/path&amp;gt;
&amp;lt;type&amp;gt;file&amp;lt;/type&amp;gt;
&amp;lt;content&amp;gt;
1: // Build redacted OCR-text fixtures from real receipt photos.
2: //
3: // The photos themselves are never committed: they carry shop phone numbers, a
4: // GSTIN, and a biller's name with an employee id. This script reads the OCR
5: // output, strips identifying data, and writes a fixture that still exercises
6: // the exact parsing bugs the real photos exposed - mangled labels, competing
7: // totals, wrapped item lines, dropped date separators, background debris.
8: //
9: // Run it only when the real samples change. The committed fixtures are what the
10: // test suite reads.
11: 
12: import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
13: import { resolve, dirname, join } from "node:path";
14: import { fileURLToPath } from "node:url";
15: 
16: const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17: const SRC = process.argv[2] || "data/raw";
18: const OUT = join(ROOT, "fixtures");
19: 
20: const SUBSTITUTIONS = [
21:   // GSTIN: keep the shape, drop the identity.
22:   [/\b\d{2}[A-Z]{5}\d{4}[A-Z][A-Z0-9]{3}\b/g, "33AAAAA0000A0Z0"],
23:   // Indian mobile numbers, 10 digits with an optional prefix.
24:   [/\b[6-9]\d{9}\b/g, "9000000000"],
25:   [/\b\d{10,13}\b/g, "900000000000"],
26:   // Ticket and bill numbers with a recognisable prefix.
27:   [/\b(\d{2,4}[A-Z0-9]{2}\d{3,4})\b/g, (m) =&amp;gt; m.replace(/\d/g, "0")],
28: ];
29: 
30: function redact(text) {
31:   let out = text;
32:   for (const [re, to] of SUBSTITUTIONS) out = out.replace(re, to);
33:   // Phone labels regardless of whether the number pattern matched.
34:   out = out.replace(/\b(Ph|Phone|Tel|Mobile|Gpay)\b\s*:?\s*\S+/gi, "$1 : [redacted]");
35:   // Staff and customer names. A person's name beside a branch and an employee
36:   // id identifies someone, and none of this was consented to publication.
37:   out = out.replace(/\b(Cus|Customer|Biller|Cashier|Received\s*by|Served\s*by)\b\s*:?\s*[A-Za-z][\w.'-]*/gi,
38:     (m) =&amp;gt; m.split(":")[0].replace(/\s*:?\s*$/, "") + " : [redacted]");
39: // Employee ids printed beside the name.
40:   out = out.replace(/\(\s*ID\s*:\s*\d+\s*\)/gi, "(ID : [redacted])");
41: 
42:   // Place names are found by shape, not by a hardcoded list. Naming the
43:   // specific towns and branches in this file would publish exactly what it is
44:   // there to hide: that a particular station, depot or road is on the receipt.
45:   //
46:   //   "&amp;lt;TOWN&amp;gt;-REGION"       hyphenated region
47:   //   "&amp;lt;TOWN&amp;gt; DEPC"         a depot, with OCR dropping the final T
48:   //   "ROAD.,&amp;lt;TOWN&amp;gt;"        a branch town following the street
49:   //   "NAGAR,&amp;lt;TOWN&amp;gt;"        a neighbourhood, then the town
50:   //   "City : &amp;lt;town&amp;gt;"       a lower-case city field
51:   // Explicit alternatives rather than DEP\w*: with the /i flag that pattern
52:   // matches "Department" as DEP + "artment" and destroys the vendor's name.
53:   out = out.replace(/\b[A-Z][A-Z-]{2,}[- ]?(REGION|DEPOT|DEPT|DEPO|DEPC|STATION|JUNCTION|NODE)\b/gi, "[place]");
54:   out = out.replace(/\b(ROAD|STREET|NAGAR|BY\s*PASS|COLONY)\b(\.?)\s*,?\s*([A-Za-z]{4,})/g, (m, street, dot) =&amp;gt; `${street}${dot}, [place]`);
55:   out = out.replace(/\b(City|District|Place|Station|Depot|Region)\b\s*:\s*[A-Za-z][\w-]*/gi, (m) =&amp;gt; m.split(":")[0].trim() + ": [place]");
56:   // A journey shown as "&amp;lt;code&amp;gt; - &amp;lt;NAME&amp;gt;  &amp;lt;code&amp;gt; - &amp;lt;NAME&amp;gt;".
57:   out = out.replace(/\b\d{1,3}-[A-Z]{3,}\b/g, "[stop]");
58:   // Anything in the local blocklist, which is gitignored so it never ships.
59:   const local = join(ROOT, "redaction-terms.txt");
60:   if (existsSync(local)) {
61:     for (const term of readFileSync(local, "utf8").split(/\r?\n/).map((t) =&amp;gt; t.trim()).filter(Boolean)) {
62:       out = out.replace(new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&amp;amp;"), "gi"), "[place]");
63:     }
64:   }
65:   return out;
66: }
67: 
68: function main() {
69:   const dir 
...[truncated 1611 chars]&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;Now the adversarial part - let me scan the committed fixtures for anything that still identifies a real person or business:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\ledgerlens\nnode -e \"\nconst fs=require('fs'),p=require('path');\nconst dir='fixtures';\nconst pats=[\n  ['phone-like',      /\\b[6-9]\\d{9}\\b/g],\n  ['GSTIN-shape',     /\\b\\d{2}[A-Z]{5}\\d{4}[A-Z][A-Z0-9]{3}\\b/g],\n  ['email',           /[\\w.-]+@[\\w.-]+\\.\\w+/g],\n  ['pincode',         /\\b6\\d{5}\\b/g],\n  ['long-number',     /\\b\\d{10,}\\b/g],\n  ['plate/vehicle',   /\\b[A-Z]{2}\\s?\\d{1,2}\\s?[A-Z]{1,3}\\s?\\d{3,4}\\b/g],\n  ['name-label',      /\\b(Cus|Customer|Biller|Cashier|Served|Received)\\b[^\\n]{0,40}/gi],\n  ['place-label',     /\\b(City|District|Place|Station|Depot|Region)\\b\\s*:[^\\n]{0,30}/gi],\n  ['journey-code',    /\\b\\d{1,3}-[A-Z]{3,}\\b/g],\n  ['ID-field',        /\\(\\s*ID[^\\n]{0,20}/gi],\n];\nconst found={};\nfor(const f of fs.readdirSync(dir).filter(x=&amp;gt;x.endsWith('.txt'))){\n  const t=fs.readFileSync(p.join(dir,f),'utf8');\n  for(const [name,re] of pats){\n    const m=t.match(re);\n    if(m){ (found[name]??=[]).push(f+': '+[...new Set(m)].slice(0,4).join(' | ')); }\n  }\n}\nfor(const [k,v] of Object.entries(found)){\n  console.log('  ['+k+']');\n  v.forEach(x=&amp;gt;console.log('     '+x));\n}\nif(!Object.keys(found).length) console.log('  nothing matched');\n\"","timeout":600000}&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;  [long-number]
     pic_1(receipt dimension 1).txt: 900000000000
     pic_1(receipt dimension 2).txt: 900000000000
  [name-label]
     pic_2(receipt dimension 1).txt: Cashier : [redacted]
     pic_2(receipt dimension 2).txt: Cashier : [redacted]
     pic_2(receipt dimension 3).txt: Cashier : [redacted]
     pic_3(receipt dimension 1).txt: Cashier {BILL2 Counter BILL2
     pic_4(receipt dimension 1).txt: Biller : [redacted] | Cus : [redacted] (ID : [redacted])
  [GSTIN-shape]
     pic_4(receipt dimension 1).txt: 33AAAAA0000A0Z0
  [pincode]
     pic_4(receipt dimension 1).txt: 636808
  [place-label]
     pic_4(receipt dimension 1).txt: City: [place]
  [ID-field]
     pic_4(receipt dimension 1).txt: (ID : [redacted])
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;
        23 of 23 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;I'm entering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall&lt;/strong&gt; — a genuinely useful, fully offline side-hustle tool with measured correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entire&lt;/strong&gt; — the agent session above shows how it was built and where it went wrong.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>I fed a global bird model foreign audio. It was confidently wrong. Then I fixed it.</title>
      <dc:creator>Melwin Siril Joel V</dc:creator>
      <pubDate>Sun, 11 Oct 2026 09:11:23 +0000</pubDate>
      <link>https://dev.to/cadets_atease/i-fed-a-global-bird-model-foreign-audio-it-was-confidently-wrong-then-i-fixed-it-3lgl</link>
      <guid>https://dev.to/cadets_atease/i-fed-a-global-bird-model-foreign-audio-it-was-confidently-wrong-then-i-fixed-it-3lgl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I started this project because I noticed something wrong with how bird identification works in my own town.&lt;/p&gt;

&lt;p&gt;BirdNET is a genuinely impressive open-source project. Point it at a recording and it will confidently tell you what you heard. I tried it on some audio recorded on my hostel campus in Chidambaram, Tamil Nadu, and it told me about &lt;strong&gt;Rose-ringed Parakeet, Common Grackle, and Dark-eyed Junco&lt;/strong&gt;. The grackle is a North American species. It has never been in India. It was never in my garden. The model was not confused - it was &lt;em&gt;sure&lt;/em&gt;, and wrong.&lt;/p&gt;

&lt;p&gt;That is the whole problem in one clip. A global classifier trained on global audio will happily tell you about birds that live four thousand kilometres away, because nothing in its architecture knows or cares where you are standing. There is no "not in my region" button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;chidambaram-birdnudge&lt;/strong&gt; is that button. It runs the same open weights on your own machine, then filters every prediction through a species list built from real observations near you. It costs nothing to run, needs no account, works with no signal, and never sends your recordings anywhere.&lt;/p&gt;

&lt;p&gt;A local Node.js pipeline that takes phone recordings, identifies birds with an open-weight audio model, filters the results to a regional species list, and tells you where to go next.&lt;/p&gt;

&lt;p&gt;The pipeline is five scripts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;script&lt;/th&gt;
&lt;th&gt;what it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;download-model.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fetches the BirdNET+ weights and label set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fetch-pack.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;builds the local species list from iNaturalist observations within a radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;convert.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;batch-converts &lt;code&gt;.m4a&lt;/code&gt; / &lt;code&gt;.mp3&lt;/code&gt; / &lt;code&gt;.ogg&lt;/code&gt; to the 32 kHz mono WAV the model wants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analyze.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;runs ONNX inference, writes a detection CSV per clip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summarize.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scores predictions against the pack, writes &lt;code&gt;docs/overview.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;case.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;generates the case study from the detection data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two design decisions are worth calling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model runs whole-file peak-normalised&lt;/strong&gt;, so a quiet outdoor clip and a loud indoor one are comparable. This matters more than it sounds - my outdoor clip was about 6 dB quieter than my indoor ones, and without normalisation the comparison would have been meaningless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;min-conf&lt;/code&gt; defaults to 0.05, not 0.25.&lt;/strong&gt; BirdNET's own web demo defaults much higher, which means it silently discards low-confidence-but-real detections. I lowered it so the local filter gets to see everything, then let the pack do the rejecting. The pack is a better filter than an arbitrary threshold, because it is specific to where you are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;https://github.com/Emanuelmsjv/chidambaram-birdnudge&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Real output from a real run, on the five clips described above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model    ...\assets\BirdNET+_V3.0-preview3.1_Global_11K_FP16_pruned.onnx
labels   11560 species
settings chunk=3s overlap=0s minConf=0.05

model load: 971ms
inside-2.wav  9.9s audio  0.4s compute  6 detections
    0.870  Yellow-billed Babbler  (Argya affinis)
    0.589  Rufous Treepie  (Dendrocitta vagabunda)
    0.068  Indian White-eye  (Zosterops palpebrosus)
inside.wav  17.9s audio  0.5s compute  15 detections
    0.812  Yellow-billed Babbler  (Argya affinis)
    0.415  Common Grackle  (Quiscalus quiscula)
    0.283  Oriental Magpie-Robin  (Copsychus saularis)
outside.wav  17.5s audio  0.5s compute  9 detections
    0.873  Rose-ringed Parakeet  (Psittacula krameri)
    0.731  Common Myna  (Acridotheres tristis)
    0.509  Red-vented Bulbul  (Pycnonotus cafer)

5 file(s), 47 detections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And what the filter actually does with that output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm run summarize
Clips: 6  detections: 146
In-pack (top-5): 15/27 = 55.6%
In-pack (all 53 distinct): 24 = 45.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;The whole pipeline is open source under CC BY-NC-SA 4.0. Everything below runs on your&lt;br&gt;
own machine with Node 18+, no Python, no GPU, and no API keys.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Emanuelmsjv" rel="noopener noreferrer"&gt;
        Emanuelmsjv
      &lt;/a&gt; / &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;
        chidambaram-birdnudge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Offline open-weight bird ID for Chidambaram, Tamil Nadu. BirdNET V3.0 via ONNX Runtime in pure Node - no Python, no GPU. Output is filtered against an iNaturalist-derived local species pack, and scored with a label-free measure of how plausible that output is for this location.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;chidambaram-birdnudge&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Point an open-weight bird sound model at a 10-second recording of somewhere in
Chidambaram, Tamil Nadu. Get back one sentence telling you which species
plausibly live near you, whether they are in season right now, what time of day
they are worth listening for, and a real place within reach to go and stand.&lt;/p&gt;
&lt;p&gt;The model already exists and is very good. It is also, on published evidence
close to useless in this part of India. That gap is the project.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The premise&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A 2026 global evaluation of BirdNET across 4,224 soundscapes, 67 sites and every
continent, annotated by local experts, reported PR AUC of:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;PR AUC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;North America / Oceania / Europe&lt;/td&gt;
&lt;td&gt;0.16 – 0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Central &amp;amp; South America&lt;/td&gt;
&lt;td&gt;0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Africa and Asia&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.03 – 0.04&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Chidambaram sits in that last band. Globally, the share of predictions that are
correct (precision) is around 0.57–0.71, so roughly a…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Repo: &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge" rel="noopener noreferrer"&gt;https://github.com/Emanuelmsjv/chidambaram-birdnudge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A one-command setup fetches the 68 MB model weights and builds the local species pack:&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/Emanuelmsjv/chidambaram-birdnudge
&lt;span class="nb"&gt;cd &lt;/span&gt;chidambaram-birdnudge
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run setup        &lt;span class="c"&gt;# downloads the 68 MB model, builds your species pack&lt;/span&gt;

npm run convert      &lt;span class="c"&gt;# phone .m4a -&amp;gt; 32 kHz mono WAV&lt;/span&gt;
npm run analyze clips &lt;span class="nt"&gt;--out&lt;/span&gt; data/detections
npm run summarize    &lt;span class="c"&gt;# writes docs/overview.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Emanuelmsjv/chidambaram-birdnudge
&lt;span class="nb"&gt;cd &lt;/span&gt;chidambaram-birdnudge
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run setup        &lt;span class="c"&gt;# downloads the 68 MB model, builds your species pack&lt;/span&gt;

npm run convert      &lt;span class="c"&gt;# phone .m4a -&amp;gt; 32 kHz mono WAV&lt;/span&gt;
npm run analyze clips &lt;span class="nt"&gt;--out&lt;/span&gt; data/detections
npm run summarize    &lt;span class="c"&gt;# writes docs/overview.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Requires Node 18+ and ffmpeg on PATH. No Python, no GPU, no API keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Licence:&lt;/strong&gt; CC BY-NC-SA 4.0. BirdNET weights are CC BY-NC-SA. iNaturalist observation data retains its own per-record licences, referenced in &lt;code&gt;data/pack.json&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;BirdNET+ V3.0-preview3.1 Global 11K FP16 (pruned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weights format&lt;/td&gt;
&lt;td&gt;ONNX, &lt;strong&gt;68.2 MB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onnxruntime-node&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Labels&lt;/td&gt;
&lt;td&gt;11,560 species, plain CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;CPU only. No GPU. No Python.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model is a TarsNet2 classifier - a frozen convolutional backbone with a linear classification head. That architecture is the reason this project is tractable offline: the expensive part is fixed, and the part you retarget is one dense layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed:&lt;/strong&gt; the five-clip case study below is &lt;strong&gt;83.8 seconds of audio, analysed in under 4 seconds&lt;/strong&gt; after a ~1 second model load. That is roughly 25x realtime on a laptop CPU with no accelerator.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;The challenge asks where an open approach did better than a closed one. I can answer that with a measurement rather than an opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same weights, same code, two recordings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;audio&lt;/th&gt;
&lt;th&gt;in-pack fraction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;upstream BirdNET sample, recorded in New York&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 / 20&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;my recordings, Chidambaram&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23 / 33&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nineteen of the twenty species the model proposed for the New York audio do not exist within 30 km of Chidambaram. The twentieth is House Sparrow at 0.051 - rank 20 of 20 by confidence, barely clearing my own threshold. Every confident prediction was a bird that does not live here:&lt;/p&gt;

&lt;p&gt;A hosted bird-ID service gives you a fixed species list, the one its provider chose. If that list does not contain your local birds, you cannot add them. &lt;strong&gt;With open weights I can change what the model is allowed to say.&lt;/strong&gt; That is the entire difference between 1/20 and 23/33, and it is not available behind an API.&lt;/p&gt;

&lt;p&gt;The other reasons the open pieces mattered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs with no signal.&lt;/strong&gt; After a one-time 68 MB download, &lt;code&gt;npm run analyze&lt;/code&gt; never touches the network. A bird call identifier is only useful where bird calls happen, which is usually nowhere with good connectivity. I did not get to actually test this on a trail with no bars - I tested it on a laptop, and the mobile app I originally used needed a working connection. I am not claiming more than I measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your recordings never leave your machine.&lt;/strong&gt; The case study below is audio recorded at a university hostel. Uploading that to a third-party inference endpoint hands someone a location-precise recording of where a person lives and when they were there. Local inference means the audio is read once by &lt;code&gt;decodeWav&lt;/code&gt; and never transmitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing.&lt;/strong&gt; No API key, no per-minute billing, no quota. A hosted audio-classification endpoint bills by the minute precisely when you most want to use it and least want to depend on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is replaceable.&lt;/strong&gt; The labels are a CSV and the regional list is JSON. Retargeting to another city is a data edit, not a code change. Nothing about the pipeline is specific to BirdNET except the inference call.&lt;/p&gt;
&lt;h3&gt;
  
  
  Honest limits of the open approach
&lt;/h3&gt;

&lt;p&gt;Being straightforward about where this does &lt;em&gt;not&lt;/em&gt; win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The weights are &lt;strong&gt;CC BY-NC-SA&lt;/strong&gt;, so this is non-commercial. That is fine for a personal tool and a real constraint if you wanted to build a product.&lt;/li&gt;
&lt;li&gt;Fine-tuning for a local species set is architecturally trivial here (frozen encoder, linear head, logistic regression on embeddings, trainable on CPU) but &lt;strong&gt;I did not do it.&lt;/strong&gt; With four labelled clips and no working route to bulk training audio, any claim I made about it would be unverified.&lt;/li&gt;
&lt;li&gt;BirdNET's Python pipeline has features my port does not - proper spectrogram caching, multi-label handling, and more careful segmenting. I ported the part that made my question answerable, not the whole project.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  I took it outside
&lt;/h2&gt;

&lt;p&gt;The brief offers "a bird call identifier that works on the trail with no signal" as an example. I took it literally.&lt;/p&gt;

&lt;p&gt;I heard a group of &lt;strong&gt;Yellow-billed Babbler&lt;/strong&gt; (&lt;em&gt;Argya affinis&lt;/em&gt;) on my hostel campus and recorded five clips on my phone - two indoors, one outdoors, and two while I was talking, to see what happened. Then I ran them through the pipeline.&lt;/p&gt;

&lt;p&gt;The species was already in my local pack at rank 18 of 130, with 19 iNaturalist observations in range - a locally well-established bird, not a rarity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;clip&lt;/th&gt;
&lt;th&gt;babblers audible?&lt;/th&gt;
&lt;th&gt;model's best score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inside-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.870&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inside&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.812&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;speech-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.961&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;speech&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outside&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of the four clips where I confirmed the bird was audible got a correct first-place identification.&lt;/p&gt;

&lt;p&gt;The two I find most interesting are the &lt;code&gt;speech&lt;/code&gt; pair. Both were recorded in the same room, minutes apart, with me talking. I listened back afterwards to check what was in the background. In one, babblers were chirping behind my voice; in the other, there were none. The model returned 0.961 for the one where they were there and &lt;strong&gt;nothing at all&lt;/strong&gt; for the one where they were not.&lt;/p&gt;

&lt;p&gt;That distinction matters, because the cheap explanation would have been that the model just fires on human voices and happened to point at a common local babbler. Two clips with identical foreground content and opposite answers rules that out.&lt;/p&gt;

&lt;p&gt;The one result I still cannot fully explain is &lt;code&gt;speech-2&lt;/code&gt;, where the babbler is buried under speech and still scored 0.961. I am reporting it because it happened, not because I understand it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it got wrong
&lt;/h2&gt;

&lt;p&gt;Every one of these is in the repo, not just here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It missed the bird entirely on the outdoor clip.&lt;/strong&gt; I was there, the babblers were there, and the model proposed parakeet and myna instead. That clip was also my quietest recording. I still do not know whether the model failed or the microphone failed. It is the first thing I would try to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence is not a probability.&lt;/strong&gt; BirdNET will hand you 0.9 and mean something much weaker. The New York control demonstrates this directly - the model was extremely confident about species that were not present at all. The number is a ranking signal, nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My species pack is biased, and I can show how.&lt;/strong&gt; It is built from iNaturalist, which is a &lt;em&gt;photography&lt;/em&gt; platform. I pulled 1,000 research-grade observations from around Chidambaram and checked the result: &lt;strong&gt;Indian White-eye appeared zero times.&lt;/strong&gt; It is one of the most common birds in India and it is, to my ear, constantly audible from my window. The pack over-represents birds people stop to photograph - Pied Stilt is the second most-recorded species in range - and under-represents small common passerines nobody bothers to upload.&lt;/p&gt;

&lt;p&gt;So "not in your local pack" is weaker evidence than it appears. It means &lt;em&gt;not established as local by this data source&lt;/em&gt;, not &lt;em&gt;absent&lt;/em&gt;. I would not have found this without checking the raw observations against my own pack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I had a real bug for most of this project.&lt;/strong&gt; The pack builder wrote entries as &lt;code&gt;{ rank: i + 1, ...s }&lt;/code&gt; - the spread lands after, so the taxonomic rank from iNaturalist overwrote my ordinal in all 130 entries. Every &lt;code&gt;rank&lt;/code&gt; field was the string &lt;code&gt;"species"&lt;/code&gt;. The ordering was still correct, which is exactly why I did not notice for weeks. Fixed in &lt;code&gt;a087f8f&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My recordings were not independent data.&lt;/strong&gt; I collected them, then evaluated the model on them. There is no held-out set, and with four positives any accuracy number would be meaningless. I have not claimed one anywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;Here is the session where the agent found the &lt;code&gt;rank&lt;/code&gt; bug, in full. It is twelve messages: you can watch it notice that &lt;code&gt;rank&lt;/code&gt; prints as &lt;code&gt;"species"&lt;/code&gt; instead of a number, work out that &lt;code&gt;{ rank: i + 1, ...s }&lt;/code&gt; lets iNaturalist's own &lt;code&gt;rank&lt;/code&gt; string land on top of the ordinal, and fix it. The interesting detail is that nothing was broken as far as anyone could tell - the pack was still sorted correctly the whole time. The bug only became visible because a species position was checked against a literal &lt;code&gt;18&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Debugging a clobbered rank field in fetch-pack.mjs (opencode)&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;what is observed is a group of Common Myna, where inside my hostel Golden Jubille hostels trees and outside of the hostel campus too. and im checking in the birdnet app ill update until then well completed debugging and furnishing and deploying too&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;&lt;strong&gt;Common Myna is a strong test case&lt;/strong&gt; - and I can tell you something useful before you run it. Let me verify.&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nWrite-Output \"=== Common Myna in the Chidambaram pack? ===\"\nnode -e \"\nconst p=require('./data/pack.json');\nconst hits=p.species.filter(s=&amp;gt;/Acridotheres tristis/i.test(s.scientific));\nhits.forEach(s=&amp;gt;console.log('  '+s.common+' ('+s.scientific+')  rank='+s.rank+'  observations='+s.observations));\nconsole.log('  pack size: '+p.species.length+' species');\nconst mug=s=&amp;gt;p.species.filter(x=&amp;gt;new RegExp(s,'i').test(x.common||'')).map(x=&amp;gt;x.common);\nconsole.log('');\nconsole.log('  other starlings/mynas in pack:');\n['starling','myna','sturnid'].forEach(q=&amp;gt;{const r=mug(q);if(r.length)console.log('    '+q+': '+r.join(', '));});\n\"\nWrite-Output \"\"\nWrite-Output \"=== does extract.mjs understand 'group' / 'flock'? ===\"\nSelect-String -Path scripts\\extract.mjs -Pattern \"group|flock|several|couple|pair\" | ForEach-Object { \"   \" + $_.Line.Trim() }","timeout":300000}&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;=== Common Myna in the Chidambaram pack? ===
  Common Myna (Acridotheres tristis)  rank=species  observations=23
  pack size: 130 species

  other starlings/mynas in pack:
    starling: Brahminy Starling, Indian Pied Starling
    myna: Common Myna

=== does extract.mjs understand 'group' / 'flock'? ===
Select-String : Cannot find path '[redacted:home-path]\hacktoberfest\chidambaram-birdnudge\scripts\extract.mjs' because it 
does not exist.
At line:16 char:1
+ Select-String -Path scripts\extract.mjs -Pattern "group|flock|several ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: ([redacted:home-path]\...pts\extract.mjs:String) [Select-String], ItemNotFound 
   Exception
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.SelectStringCommand
 
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;strong&gt;Common Myna confirmed in the pack&lt;/strong&gt; - 23 observations, and notably the pack contains Brahminy Starling and Indian Pied Starling but &lt;strong&gt;no Common Starling&lt;/strong&gt;. That matters. Let me confirm one suspicion:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nWrite-Output \"=== pack.json: is the 'rank' field a number or a taxonomic rank? ===\"\nnode -e \"\nconst p=require('./data/pack.json');\nconsole.log('  first 3 entries:');\np.species.slice(0,3).forEach(s=&amp;gt;console.log('    rank='+JSON.stringify(s.rank)+'  common='+s.common));\nconsole.log('  distinct rank values across all 130: ' + JSON.stringify([...new Set(p.species.map(s=&amp;gt;s.rank))]));\n\"\nWrite-Output \"\"\nWrite-Output \"=== how project 1 matches species (scientific only?) ===\"\nSelect-String -Path scripts\\nudge.mjs -Pattern \"binomial|scientific|common\" | Select-Object -First 6 | ForEach-Object { \"   \" + $_.Line.Trim() }\nWrite-Output \"\"\nWrite-Output \"=== README 'not built yet' section ===\"\n$idx = (Select-String -Path README.md -Pattern \"^## Not built yet\").LineNumber\n(Get-Content README.md)[($idx-1)..($idx+9)] | ForEach-Object { \"   $_\" }","timeout":300000}&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;=== pack.json: is the 'rank' field a number or a taxonomic rank? ===
  first 3 entries:
    rank="species"  common=House Crow
    rank="species"  common=Little Egret
    rank="species"  common=Black Drongo
  distinct rank values across all 130: ["species"]

=== how project 1 matches species (scientific only?) ===
   function binomial(name) {
   const cSci = header.indexOf("scientific_name");
   const cCom = header.indexOf("common_name");
   .map((f) =&amp;gt; ({ scientific: f[cSci] ?? "", common: f[cCom] ?? "", confidence: Number(f[cCon]) }))
   const key = binomial(r.scientific);
   if (!k.startsWith(prefix)) map.set(binomial(k), v);

=== README 'not built yet' section ===
   ## Not built yet
   
   Stated plainly so nobody assumes otherwise:
   
   - No microphone capture, so audio still has to be recorded separately
   - No local fine-tuning; the model is used as-is
   - No PWA packaging and no hosted demo
   - WAV only. No m4a or ogg decoding without ffmpeg on PATH
   - Seasonal notes are hand-curated for roughly the top 45 species in the pack
   - Place coordinates in `data/spots.json` are approximate and need verifying
   - Distances and travel times are not precomputed because they depend on where you
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Common Myna is in the pack (23 observations). But I need to be honest with you about the test case, because it matters for how the post reads.&lt;/p&gt;
&lt;h2 id="agent-session-751-3-common-myna-is-the-easiest-possible-test"&gt;Common Myna is the &lt;em&gt;easiest&lt;/em&gt; possible test&lt;/h2&gt;

&lt;p&gt;It's one of the loudest, most distinctive, most-recorded birds in India - Xeno-canto is saturated with it. If BirdNET names it correctly, that tells you almost nothing about regional weakness. It just confirms the model works on an easy target, with n=1.&lt;/p&gt;

&lt;p&gt;I marked it &lt;code&gt;ubiquitous&lt;/code&gt; in the fieldnote pack precisely because it's the &lt;em&gt;worst&lt;/em&gt; possible "go outside now" trigger - hearable from your desk all day. Good for detection testing, useless as a nudge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So don't build the case study around "did it get the myna right."&lt;/strong&gt; Build it around the &lt;strong&gt;tail&lt;/strong&gt;: even on a correct detection, BirdNET emits 11,560 classes and a long junk tail above 0.05. The story is &lt;em&gt;what else it said&lt;/em&gt; - the confident non-local predictions, and what fraction of its output was locally plausible. A correct top-1 with40 nonsense runners-up is a more interesting result than a clean single prediction, and it still supports the thesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your indoor/outdoor split is genuinely useful&lt;/strong&gt; - that gives acoustic contrast from the same species at the same location. Indoor hostel means ventilation noise and people; outdoor campus is open-air. That's a controlled comparison of exactly the variable I flagged as the risk.&lt;/p&gt;
&lt;h2 id="agent-session-751-3-two-real-bugs-found"&gt;Two real bugs found&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;rank&lt;/code&gt; is clobbered in all 130 pack entries&lt;/strong&gt; - &lt;code&gt;"species"&lt;/code&gt; instead of a number. I wrote &lt;code&gt;{ rank: i + 1, ...s }&lt;/code&gt; and the spread lands &lt;em&gt;after&lt;/em&gt;, so iNat's own taxonomic &lt;code&gt;rank&lt;/code&gt; field overwrites mine. Classic ordering bug, invisible because the array is still sorted correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;docs/overview.md&lt;/code&gt; is gitignored&lt;/strong&gt; - the repo publishes &lt;strong&gt;zero results&lt;/strong&gt;. Anyone cloning it sees a pipeline and a negative control, but no findings. Fieldnote got this right (&lt;code&gt;docs/eval.md&lt;/code&gt; is committed); project 1 never got the equivalent.&lt;/p&gt;
&lt;h1 id="agent-session-751-3-plan"&gt;Plan&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 - no audio needed, ~45 min&lt;/strong&gt;&lt;br&gt;
1. Fix the &lt;code&gt;rank&lt;/code&gt; spread-order bug&lt;br&gt;
2. Un-ignore &lt;code&gt;docs/overview.md&lt;/code&gt;, commit results as a tracked artefact&lt;br&gt;
3. Add the all-detections metric (higher count, lower variance - this is what makes a small n credible)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 - needs your audio, ~1 hr&lt;/strong&gt;&lt;br&gt;
4. Install ffmpeg (&lt;code&gt;Gyan.FFmpeg&lt;/code&gt;, winget)&lt;br&gt;
5. &lt;code&gt;scripts/convert.mjs&lt;/code&gt; - batch convert a folder, one command&lt;br&gt;
6. &lt;code&gt;scripts/case.mjs&lt;/code&gt; - produces &lt;code&gt;docs/case-report.md&lt;/code&gt;: your observation, BirdNET's full ranked output, top-1 match, rank of the observed species, in-pack fraction, and an explicit list of non-local predictions&lt;br&gt;
7. Run on both the indoor and outdoor myna clips&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 - ~1 hr&lt;/strong&gt;&lt;br&gt;
8. README updated with the honest framing&lt;br&gt;
9. Post drafted from &lt;code&gt;docs/case-report.md&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="agent-session-751-3-three-things-i-still-need"&gt;Three things I still need&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How many clips, and do you have both indoor and outdoor separately?&lt;/strong&gt; The indoor/outdoor contrast is the most valuable thing you have - please keep them as distinct files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is there speech or identifiable people in the indoor audio?&lt;/strong&gt; Hostel recordings usually do. If so we trim, or report text-only and don't publish the audio.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are you comfortable publishing the audio at all?&lt;/strong&gt; If you'd rather not name the hostel or have voices audible, say so and I'll design around it. Better to know now than after it's pushed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="agent-session-751-3-what-to-grab-from-the-birdnet-app"&gt;What to grab from the BirdNET app&lt;/h2&gt;

&lt;p&gt;Since you're checking it anyway, the useful comparison is &lt;strong&gt;app output vs our local pipeline on the same file.&lt;/strong&gt; Not ground truth - same family of model - but if they disagree, that's worth knowing (different model version, or server-side vs local inference). Worth noting if the app returns European species for your Indian audio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One warning:&lt;/strong&gt; if you checked the bird ID &lt;em&gt;using the BirdNET app&lt;/em&gt; before deciding it was Common Myna, that's not independent ground truth - it's the same model telling you what it told you. You said you saw them, so your own observation is the label. If the app is what convinced you, tell me and I'll frame it as agreement-between-models rather than a confirmed identification.&lt;/p&gt;

&lt;p&gt;Want me to start Phase 1 now? It's independent of the audio and gets the two bugs fixed before results depend on them.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;oh its not common myna it is yellow-billed babblerat 65.8% confidernce of Argy affinis species and well start the process budd&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;&lt;strong&gt;Yellow-billed Babbler&lt;/strong&gt; - much better test case than Common Myna, and a good find. Let me verify it against the pack before I start.&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nnode -e \"\nconst p=require('./data/pack.json');\nconst hits=p.species.filter(s=&amp;gt;/Argya affinis/i.test(s.scientific));\nif(hits.length){const s=hits[0];console.log('  IN PACK: '+s.common+' ('+s.scientific+')');console.log('  observations: '+s.observations+'   position in sorted pack: '+(p.species.findIndex(x=&amp;gt;x.scientific===s.scientific)+1));}\nelse console.log('  NOT IN PACK');\nconsole.log('');\nconsole.log('  all Argya babblers in pack:');\np.species.filter(s=&amp;gt;/Argya/i.test(s.scientific)).forEach(s=&amp;gt;console.log('    '+s.common+' ('+s.scientific+') obs='+s.observations));\n\"\nWrite-Output \"\"\nWrite-Output \"=== is it in seasonal.json (for the nudge layer)? ===\"\nSelect-String -Path data\\seasonal.json -Pattern \"Argya affinis\" | ForEach-Object { \"   \" + $_.Line.Trim() }","timeout":300000}&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;  IN PACK: Yellow-billed Babbler (Argya affinis)
  observations: 19   position in sorted pack: 18

  all Argya babblers in pack:
    Yellow-billed Babbler (Argya affinis) obs=19

=== is it in seasonal.json (for the nudge layer)? ===
   "Argya affinis": { "common": "Yellow-billed Babbler", "season": "year-round", "active": "day", "spotType": "open scrub / roadside", "cue": "Noisy chattering groups moving through scrub" },
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;strong&gt;Yellow-billed Babbler is in the pack&lt;/strong&gt; - position 18, 19 observations, and &lt;code&gt;seasonal.json&lt;/code&gt; already describes it as &lt;em&gt;"noisy chattering groups moving through scrub, day, open scrub/roadside."&lt;/em&gt; That matches what you saw almost exactly.&lt;/p&gt;

&lt;p&gt;One thing I'll hold onto for the report: the 65.8% came &lt;em&gt;from the BirdNET app&lt;/em&gt;. Your observation is the ground truth; the app is a second voice agreeing. That's fine - but I won't write it up as the model confirming an independent identification. Not belabouring it, just noting the framing.&lt;/p&gt;

&lt;p&gt;Starting Phase 1. First bug - the clobbered &lt;code&gt;rank&lt;/code&gt;:&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;{"filePath":"[redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\\scri...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;chidambaram-birdnudge&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;fetch-pack.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count: kept.length,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, rank: i + 1 })),"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count: kept.length,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    species: kept.map((s, i) =&amp;gt; ({ rank: i + 1, ...s })),"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Edit applied successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"filePath":"[redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\\scri...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;chidambaram-birdnudge&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;fetch-pack.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"const record = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        inatTaxonId: taxon.id ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        scientific: name,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        common: taxon.preferred_common_name ?? taxon.common_name ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        taxonomicRank: taxon.rank ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        observations: row.count ?? 0,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        photoUrl: taxon.default_photo?.medium_url ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        key: fullKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        binomial: binomialKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&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="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"const record = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        inatTaxonId: taxon.id ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        scientific: name,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        common: taxon.preferred_common_name ?? taxon.common_name ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        rank: taxon.rank ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        observations: row.count ?? 0,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        photoUrl: taxon.default_photo?.medium_url ?? null,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        key: fullKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        binomial: binomialKey(name),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;      };"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Edit applied successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nnode scripts\\fetch-pack.mjs 2&amp;gt;&amp;amp;1 | Select-Object -First 6\nWrite-Output \"\"\nWrite-Output \"=== rank field now numeric? ===\"\nnode -e \"\nconst p=require('./data/pack.json');\np.species.slice(0,4).forEach(s=&amp;gt;console.log('  rank='+s.rank+'  taxRank='+s.taxonomicRank+'  '+s.common));\nconsole.log('  distinct rank types: '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;typeof s.rank))]));\nconsole.log('  distinct rank values: '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;s.rank))].slice(0,6))+' ...');\nconst b=p.species.find(s=&amp;gt;/Argya affinis/i.test(s.scientific));\nconsole.log('  Yellow-billed Babbler rank = '+b.rank+'  (was broken before)');\n\"","timeout":300000}&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;Fetching Aves within 30 km of Chidambaram, Tamil Nadu, India ...
Hub reports 165 species in range.
Kept 130 with &amp;gt;= 2 observations.
Wrote [redacted:home-path]\hacktoberfest\chidambaram-birdnudge\data\pack.json
    1. Corvus splendens - House Crow
    2. Egretta garzetta - Little Egret

=== rank field now numeric? ===
  rank=1  taxRank=undefined  House Crow
  rank=2  taxRank=undefined  Little Egret
  rank=3  taxRank=undefined  Black Drongo
  rank=4  taxRank=undefined  Great Egret
  distinct rank types: ["number"]
  distinct rank values: [1,2,3,4,5,6] ...
  Yellow-billed Babbler rank = 18  (was broken before)
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;&lt;code&gt;rank&lt;/code&gt; is numeric now (Yellow-billed Babbler = 18). &lt;code&gt;taxonomicRank&lt;/code&gt; is undefined because my edit to the record object didn't take effect - checking:&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nSelect-String -Path scripts\\fetch-pack.mjs -Pattern \"rank\" | ForEach-Object { \"{0,4}: {1}\" -f $_.LineNumber, $_.Line.Trim() }"}&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;  77: taxonomicRank: taxon.rank ?? null,
 120: species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, rank: i + 1 })),
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"oldString":"species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, ran...&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"species: kept.map((s, i) =&amp;gt; ({ ...s, taxonomicRank: s.rank, rank: i + 1 })),"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[redacted:home-path]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;hacktoberfest&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;chidambaram-birdnudge&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;fetch-pack.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"species: kept.map((s, i) =&amp;gt; ({ ...s, rank: i + 1 })),"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&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"&gt;&lt;code&gt;Edit applied successfully.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;{"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hackt...&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;$ {"command":"$ErrorActionPreference='Continue'\ncd [redacted:home-path]\\hacktoberfest\\chidambaram-birdnudge\nnode scripts\\fetch-pack.mjs 2&amp;gt;&amp;amp;1 | Select-Object -First 3\nnode -e \"\nconst p=require('./data/pack.json');\nconsole.log('');\nconsole.log('  rank field    : '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;typeof s.rank))])+' values '+[...new Set(p.species.map(s=&amp;gt;s.rank))].slice(0,5));\nconsole.log('  taxRank field : '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;typeof s.taxonomicRank))])+' values '+JSON.stringify([...new Set(p.species.map(s=&amp;gt;s.taxonomicRank))].slice(0,4)));\n\"\nWrite-Output \"\"\nWrite-Output \"=== nudge.mjs uses hit.rank - check what it displays ===\"\nSelect-String -Path scripts\\nudge.mjs -Pattern \"localRank|hit\\.rank|rank\" | ForEach-Object { \"   \" + $_.Line.Trim() }","timeout":300000}&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;Fetching Aves within 30 km of Chidambaram, Tamil Nadu, India ...
Hub reports 165 species in range.
Kept 130 with &amp;gt;= 2 observations.

  rank field    : ["number"] values 1,2,3,4,5
  taxRank field : ["string"] values ["species"]

=== nudge.mjs uses hit.rank - check what it displays ===
&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;
        12 of 12 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;a href="https://dev.to/agent_sessions/debugging-a-clobbered-rank-field-in-fetch-packmjs-opencode-ot6g6e"&gt;View this session on DEV&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This project was built end to end with an AI coding agent (opencode, tracked with DevRelay). The interesting part is not that it worked, but the two places where it did not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;rank&lt;/code&gt; bug above survived several rounds because the agent optimised the thing I asked about and the symptom I noticed - ordering - happened to still be correct.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;ffmpeg&lt;/code&gt; download was corrupted twice in transit, producing a 66 MB "executable" that Windows rejected with &lt;em&gt;"not a valid application for this OS platform."&lt;/em&gt; The PE header was valid and the machine type was correct; the file was truncated. Diagnosing that took longer than installing ffmpeg should have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A third failure worth naming: I spent a deadline-night attempt adding a local Gemma 3 270M model to write the field brief in plain English. It ran, and every single output was invented - weather forecasts at 35 C, links to websites that do not exist, and not one mention of the bird it was supposed to describe. I deleted the feature rather than ship a birder being sent to check a website. The detection and the species filter are the project. The prose was decoration, and bad decoration is worse than none.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall&lt;/strong&gt; - this is what I am actually aiming for&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Entire&lt;/strong&gt; - the project was built end to end in an agent session, the session above is embedded, and the write-up is the account of where that help stopped being useful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I considered claiming DigitalOcean and Render and deliberately did not. Deploying a deliberately-offline tool to a cloud runtime to win a hosting prize would contradict the entire premise of the project. Same reasoning for Sentry Agent Tracing - this is deterministic DSP with an ONNX model, no LLM calls, no tokens, no spans to trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full write-up
&lt;/h2&gt;

&lt;p&gt;The complete case study, including the control methodology and every non-local species the model proposed, is in the repo: &lt;a href="https://github.com/Emanuelmsjv/chidambaram-birdnudge/blob/main/docs/case-report.md" rel="noopener noreferrer"&gt;docs/case-report.md&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>birds</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>The Bricks</title>
      <dc:creator>Melwin Siril Joel V</dc:creator>
      <pubDate>Thu, 13 Aug 2026 02:19:02 +0000</pubDate>
      <link>https://dev.to/cadets_atease/the-bricks-3h3e</link>
      <guid>https://dev.to/cadets_atease/the-bricks-3h3e</guid>
      <description>&lt;h2&gt;
  
  
  Meet the Builder: Melwin Siril Joel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Building open source, one mobile commit at a time.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I Am
&lt;/h2&gt;

&lt;p&gt;I'm Melwin Siril Joel, a second-year B.E. Computer Science Engineering student at Annamalai University, Chidambaram, Tamil Nadu. Beyond the classroom, I'm an NCC Army Wing Cadet and an active open source contributor — a combination that's taught me as much about discipline and problem-solving as any textbook has.&lt;/p&gt;

&lt;p&gt;What makes my journey a little different: I build almost entirely from my phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mobile-Only Developer
&lt;/h2&gt;

&lt;p&gt;Most tutorials assume you have a laptop. I didn't always have that luxury, so I turned constraint into a workflow. Using &lt;strong&gt;Termux&lt;/strong&gt; and &lt;strong&gt;GitHub Mobile&lt;/strong&gt;, I learned to navigate the full open source contribution pipeline — cloning repos, writing code, running tests, and submitting pull requests — all from an Android device. It's slower than a full IDE setup, but it forced me to understand every step instead of relying on one-click conveniences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source Contributions
&lt;/h2&gt;

&lt;p&gt;My open source journey took shape through real, merged code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MapLibre Martin&lt;/strong&gt; — Contributed and got PR #2583 merged, documenting a binary diff process for &lt;code&gt;.mbtiles&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;maplibre-agent-skills&lt;/strong&gt; — Opened and worked on PR #31 and PR #30.&lt;/li&gt;
&lt;li&gt;Participated in &lt;strong&gt;FOSShack 2026&lt;/strong&gt;, &lt;strong&gt;GSSoC 2026&lt;/strong&gt; (Open Source Track), and &lt;strong&gt;Nexus: Spring of Code&lt;/strong&gt; — three programs that pushed me to contribute consistently and learn how real-world open source communities operate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these wasn't just a line on a resume — it was a lesson in navigating unfamiliar codebases, communicating with maintainers, and shipping something that actually got used.&lt;/p&gt;

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

&lt;p&gt;I balance my technical work with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NCC Army Wing Cadet duties&lt;/strong&gt;, including completing the CATC camp — a reminder that discipline off-screen sharpens focus on-screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitive chess&lt;/strong&gt;, representing Annamalai University in the Chief Minister's Trophy 2026 (Tamil Nadu) in the College category.&lt;/li&gt;
&lt;li&gt;Early groundwork on &lt;strong&gt;campus community building&lt;/strong&gt; — exploring things like FOSS United Campus Clubs and GDG on Campus to help bring open source culture to my university.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Drives Me
&lt;/h2&gt;

&lt;p&gt;I'm drawn to open source because it rewards persistence over privilege. You don't need the best hardware or a head start — you need curiosity and the willingness to figure things out. That philosophy is what took me from learning C++ and Python fundamentals as a first-year student to shipping merged pull requests on real projects, all from a phone screen.&lt;/p&gt;

&lt;p&gt;I'm just getting started — and I'm building in public, one commit at a time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Melwin Siril Joel | B.E. CSE, Annamalai University | Open Source Contributor | NCC Cadet&lt;/em&gt;&lt;/p&gt;

</description>
      <category>learning</category>
      <category>devjournal</category>
    </item>
  </channel>
</rss>
