<?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: Kai Chen</title>
    <description>The latest articles on DEV Community by Kai Chen (@kaichen_dev).</description>
    <link>https://dev.to/kaichen_dev</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%2F4167577%2Fc0e93189-5765-464d-9b92-c01c8c8540af.png</url>
      <title>DEV Community: Kai Chen</title>
      <link>https://dev.to/kaichen_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaichen_dev"/>
    <language>en</language>
    <item>
      <title>Shoebox: a small model reads your receipts overnight, slowly, and never guesses a number</title>
      <dc:creator>Kai Chen</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:15:40 +0000</pubDate>
      <link>https://dev.to/kaichen_dev/shoebox-a-small-model-reads-your-receipts-overnight-slowly-and-never-guesses-a-number-k01</link>
      <guid>https://dev.to/kaichen_dev/shoebox-a-small-model-reads-your-receipts-overnight-slowly-and-never-guesses-a-number-k01</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;Receipts are the paperwork nobody does on time. They go into a wallet, then a drawer, then a shoebox, and at the end of the month (or the end of the tax year) someone sits down and types them into a spreadsheet one by one: date, shop, total, category, then files the paper or the photo somewhere it can be found again. Every freelancer and small-business owner knows the evening it takes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shoebox&lt;/strong&gt; is a folder that does that evening for you while you sleep. You photograph receipts whenever you get them and let the photos land in &lt;code&gt;Shoebox/Inbox&lt;/code&gt; (phone photo sync, a shared drive, anything). Every night at two, Shoebox wakes up and works through the new ones, one at a time, on your own computer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4&lt;/strong&gt; looks at each photo and copies down what is printed: every line, the subtotal, discount, tax, total, cash paid and change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The arithmetic&lt;/strong&gt; decides whether to believe it. A receipt is filed only when its total is confirmed by other figures printed on the same receipt.&lt;/li&gt;
&lt;li&gt;Each believed receipt is renamed (&lt;code&gt;2026-10-14 Bakery Ana 60000.jpg&lt;/code&gt;), filed under its month and category, and added to the month's expense sheet.&lt;/li&gt;
&lt;li&gt;A receipt whose figures will not agree is never guessed at. It goes to &lt;code&gt;Check me/&lt;/code&gt; with a note saying exactly what disagreed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the morning there is one file waiting, &lt;code&gt;Good morning.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filed 7 receipts overnight; 3 would not add up and wait in “Check me”.
This month so far: 756,127 (Books/expenses-2026-10.csv).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And if you want to know something, you ask in plain words:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ shoebox ask "How much did I spend on food and drink this month, and what is waiting for me to check?"
In October 2026, you spent 724,627 on Food &amp;amp; drink. Three receipts are waiting for you to check, with reasons provided for each one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It is slow on purpose. It runs a 4-billion-parameter model on the CPU, so it can share a laptop with everything else you do, and because it has all night it can afford to look twice. That is the trade this week's theme asked for: nothing about it is fast, and you never have to think about it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Ten receipt photos from a month, dropped into the inbox (they are from the CORD dataset, so they are Indonesian and in rupiah). After one night:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shoebox/
  Inbox/  (empty)
  Filed/2026-10/Food &amp;amp; drink/
    2026-10-04 ตั้ง 24000.png
    2026-10-06 Unknown shop 148127.png
    2026-10-07 Unknown shop 93500.png
    2026-10-09 Unknown shop 54000.png
    2026-10-10 Unknown shop 40000.png
    2026-10-17 Unknown shop 365000.png
  Filed/2026-10/Other/
    2026-10-01 Unknown shop 31500.png
  Check me/
    2026-10-03 Unknown shop 113886.png  (+ .txt)
    2026-10-13 Unknown shop 60394.png   (+ .txt)
    2026-10-17 Snack Lestari All 48000.png  (+ .txt)
  Books/
    expenses-2026-10.csv, summary-2026-10.html, ledger.jsonl
  Good morning.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(Most CORD receipts have the shop's name blanked out for privacy, hence "Unknown shop"; one name came back in Thai from a logo.) Every file also gets a &lt;code&gt;.json&lt;/code&gt; beside it with everything the model copied and every check that was made.&lt;/p&gt;

&lt;p&gt;The month's summary, written for a person to read or print:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5vr70osdspme7olvzzsf.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%2F5vr70osdspme7olvzzsf.png" alt="The October expense summary: 756,127 spent across 7 filed receipts, totals by category, the list of receipts, and three receipts waiting in Check me, each with the figures that disagreed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is what a receipt that will not add up looks like in the morning. The real total of this one is 68,000; the model read 48,000 the first time:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The figures on this receipt did not agree, so it was not filed:
- the two readings of the total disagree (48000 and 100000)
- paid 100000 minus change 32000 is not the total 48000

Retake the photo (flat, the whole receipt, the totals sharp), or file it by hand.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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/luoy16002-svg" rel="noopener noreferrer"&gt;
        luoy16002-svg
      &lt;/a&gt; / &lt;a href="https://github.com/luoy16002-svg/shoebox" rel="noopener noreferrer"&gt;
        shoebox
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A shoebox for receipts that empties itself overnight: Gemma 4 reads them on your own computer, Temporal keeps the night shift going, Mastra keeps the books.
    &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;Shoebox&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A shoebox for receipts that empties itself overnight.&lt;/p&gt;

&lt;p&gt;Photograph receipts whenever you get them and let the photos land in one folder (your phone's photo sync, a shared
drive, anything). Every night Shoebox reads the new ones &lt;strong&gt;on your own computer&lt;/strong&gt; with Gemma 4, checks each one's
arithmetic, files it by month and category with a sensible name, and writes the month's expense sheet. In the morning
there is a two-line note:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;Filed 7 receipts overnight; 3 would not add up and wait in “Check me”
This month so far: 756,127 (Books/expenses-2026-10.csv).
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Nothing is uploaded anywhere. The model runs locally, the night shift survives the laptop sleeping or the process
being killed halfway (it is a &lt;a href="https://temporal.io" rel="nofollow noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow), and you can ask about the books in plain
words:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;$ shoebox ask "How much did I spend on food and drink this month, and what is waiting for me to&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/luoy16002-svg/shoebox" 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;Three open pieces, each doing one job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma 4 copies; it never calculates
&lt;/h3&gt;

&lt;p&gt;The reader is Gemma 4 E4B (Apache 2.0) in llama.cpp, quantized to Q4_0 and running on six CPU threads. The prompt asks it to &lt;em&gt;copy&lt;/em&gt; every money line exactly as printed, separators and all ("60.000", "Rp. 91,000", "12.50"), and the request carries a JSON schema that llama.cpp turns into a grammar, so the answer always parses.&lt;/p&gt;

&lt;p&gt;Copying instead of calculating matters more than it sounds. Receipts from the test set I used print "60.000" for sixty thousand rupiah, "26,818" for twenty-six thousand, and "Rp. 91,000" with a dot that belongs to the currency. If the model normalised those numbers it would make a different mistake on each format. Instead the code reads the strings: a separator followed by exactly three digits groups thousands, a single one followed by one or two digits marks cents, and anything stuck to the currency sign is ignored. That parser has unit tests; the model never has to know any of it.&lt;/p&gt;

&lt;p&gt;The first version had thinking turned on (Gemma 4 thinks by default in llama.cpp's chat template). It wrote a long "Here's a thinking process..." before every receipt, took about 75 seconds each, and on one receipt spent its whole token budget thinking and returned nothing at all. Copying a receipt needs eyes, not deliberation: with &lt;code&gt;--reasoning off&lt;/code&gt; one look takes a median of 15 seconds, five times faster, and the numbers below are all measured that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  The arithmetic decides, and the receipt is read twice
&lt;/h3&gt;

&lt;p&gt;Every receipt prints its own proof. The lines add up to the subtotal. The subtotal, with discount, tax (included in the prices or added on top) and service, makes the total. The cash handed over minus the change is the total. Shoebox checks every one of these it can make.&lt;/p&gt;

&lt;p&gt;Getting from there to a rule I trust took three tries, and I measured each one on the 95 receipts in the &lt;a href="https://huggingface.co/datasets/naver-clova-ix/cord-v2" rel="noopener noreferrer"&gt;CORD v2&lt;/a&gt; test split (real Indonesian receipts, CC BY 4.0) that print a total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try one: every figure must agree.&lt;/strong&gt; On the first night it set aside four of five receipts, and all four had the right total. The model had copied the TOTAL and CHANGE lines into the item list, or written a ticket's unit price where the line meant quantity times price, so the lines never added up even though the total was fine. Too strict to be useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try two: the total must be confirmed by something else on the receipt.&lt;/strong&gt; Cash minus change, or the lines, or the subtotal. Lines that are obviously totals or payments ("TL", "CG", "TUNAI", "KEMBALI") are taken out before adding, and a line can count as quantity times price. This filed 87 of the 95 receipts, and 4 of those had the wrong total. Those were the interesting ones, because the arithmetic agreed with itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model missed some lines, then wrote a subtotal and a total that matched the lines it had seen;&lt;/li&gt;
&lt;li&gt;a shop printed "Rp 165000.00" and the model copied every amount on the receipt without the decimal point, so the lines, the subtotal, the total, the cash and the change were all ten times too big and all agreed;&lt;/li&gt;
&lt;li&gt;the total and the change were misread by the same 5,000, so cash minus change still came out right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model can be consistently wrong, and arithmetic on its own output cannot see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try three: read it twice.&lt;/strong&gt; Now every receipt gets two looks. The first reads the whole receipt; the second sees only the bottom of the photo, cropped and enlarged, and copies the totals block. Like double entry, the two readings must give the same total. When they do, they are merged (a tax line the first look missed comes from the second) and the arithmetic decides. When they do not, a reading is used only if cash and change confirm it, because those are two figures printed apart from the total; otherwise the receipt goes to &lt;code&gt;Check me/&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;one look&lt;/th&gt;
&lt;th&gt;two looks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total read correctly&lt;/td&gt;
&lt;td&gt;88 of 95&lt;/td&gt;
&lt;td&gt;88 of 95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filed&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filed with a wrong total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set aside in &lt;code&gt;Check me/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;... of which the total was in fact right&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full table, with every receipt that was not filed and why, is in &lt;a href="https://github.com/luoy16002-svg/shoebox/blob/main/eval/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;eval/RESULTS.md&lt;/code&gt;&lt;/a&gt;, and &lt;code&gt;npx tsx eval/cord.ts&lt;/code&gt; reproduces it.&lt;/p&gt;

&lt;p&gt;The row that matters is the bold one. A receipt filed with a wrong total is a silent error in your books; a receipt in &lt;code&gt;Check me/&lt;/code&gt; costs you ten seconds in the morning. Reading twice cut the silent errors from four to one, at the price of seven more receipts to glance at. The one that still slips through is a receipt that prints its total before rounding (680,339) and after (680,500); both looks copied the first, and the arithmetic agrees with it. The two looks together take a median of 20 seconds per receipt on six CPU threads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temporal keeps the night shift going
&lt;/h3&gt;

&lt;p&gt;A night shift on a laptop gets interrupted: the lid closes, Windows restarts for an update, the process dies. The shift is a &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow so none of that matters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;schedule&lt;/strong&gt; starts the &lt;code&gt;nightShift&lt;/code&gt; workflow every night at 02:00 (&lt;code&gt;shoebox start --at 02:00&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nightShift&lt;/code&gt; lists the inbox and starts one &lt;strong&gt;child workflow per receipt&lt;/strong&gt;, one after another, so the CPU is never asked for two at once.&lt;/li&gt;
&lt;li&gt;Reading a receipt is an &lt;strong&gt;activity&lt;/strong&gt; that sends a heartbeat every ten seconds while Gemma works. The heartbeat timeout is one minute, so if the worker dies mid-receipt, Temporal notices within a minute and retries the read with backoff when the worker comes back.&lt;/li&gt;
&lt;li&gt;Filing checks the ledger before it moves anything, so a retry after a crash never files a receipt twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tested it the rude way: started a shift, killed the worker in the middle of a receipt, waited, and started it again. At 00:23:22 UTC the fourth receipt's read started. At 00:23:30 I killed the worker. For a minute and a half nothing happened: no worker, the shift's state sitting in the Temporal server. At 00:25:13 I started the worker again. Temporal had already marked the read as failed by heartbeat timeout, scheduled it again, and the new worker picked it up. The receipt's event history (&lt;code&gt;temporal workflow show -o json&lt;/code&gt;), trimmed to the fields that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event 5  ActivityTaskScheduled  00:23:22  readReceiptActivity
event 6  ActivityTaskStarted    00:25:16  attempt 2
         last failure: activity Heartbeat timeout
event 7  ActivityTaskCompleted  00:25:45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shift carried on and finished the other six receipts. The ledger has every receipt exactly once. Here is the shift in the Temporal UI; the long bar in the middle is the receipt that was being read when the worker died:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl7pnpbv1v53rkp2w206p.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%2Fl7pnpbv1v53rkp2w206p.png" alt="The night shift in the Temporal web UI: a timeline of ten fileReceipt child workflows, each about twenty seconds long, except the fourth, which stretches over two and a half minutes across the crash and the restart"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole Temporal server is the open-source dev server (&lt;code&gt;temporal server start-dev&lt;/code&gt;) with its history in a SQLite file in your home folder. Nothing about it needs a cloud account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mastra keeps the books
&lt;/h3&gt;

&lt;p&gt;Two small agents run on the same local Gemma through &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt;, pointed at llama.cpp's OpenAI-compatible endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;filing clerk&lt;/strong&gt; puts each believed receipt in one of eight categories, using Mastra's structured output with an enum, so it cannot invent a ninth.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;bookkeeper&lt;/strong&gt; answers questions about the ledger. It has three tools (list expenses by month, category or shop; total a month by category or shop; list what is waiting in &lt;code&gt;Check me/&lt;/code&gt;) and instructions to take every number from a tool. The model chooses which tool to call and phrases the answer; the amounts come from the books.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A shoebox of receipts is a diary. It says where you were, at what time, what you ate, which pharmacy you went to, and often the last four digits of your card. Most receipt apps that do this job today work by uploading the photos to their servers and charging a monthly fee per user. Shoebox does the same job with the photos never leaving the folder they were taken into.&lt;/p&gt;

&lt;p&gt;That is only possible because the model's weights are open. Gemma 4 E4B is small enough to run on a CPU, licensed so anyone can, and good enough at reading print that the arithmetic around it can catch the rest. Llama.cpp makes it a local server in one command; Temporal and Mastra are open source and run on the same machine. There is no API key anywhere in the project, no bill that grows with the number of receipts, and nothing to stop working when a vendor changes its pricing.&lt;/p&gt;

&lt;p&gt;Open weights also made the design possible. Because the model is mine to run, I could turn its thinking off, constrain its output with a grammar, give it all night, and ask it to look twice. None of those are knobs a per-request API makes cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;What it takes from you: photos dropped in a folder whenever you get a receipt, and a look at &lt;code&gt;Check me/&lt;/code&gt; in the morning. What it takes from the computer: about 20 seconds of CPU per receipt at two in the morning, so a month of forty receipts is a quarter of an hour while you sleep.&lt;/p&gt;

&lt;p&gt;What it saves is the evening at the end of the month: typing each receipt's date, shop, total and category into a spreadsheet, renaming the photo and putting it somewhere you will find it again. On this test set, five out of six receipts never need you at all, and the sixth comes with a note that says exactly which figures to look at.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 E4B reads every receipt and powers both agents, locally on the CPU through llama.cpp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: the night shift is a scheduled Temporal workflow with a child workflow per receipt, heartbeating activities and idempotent filing; killed mid-shift, it picks up where it stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra&lt;/strong&gt;: the filing clerk (structured output) and the bookkeeper (tools over the ledger) are Mastra agents on the local model.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Tickmark: bank statements to spreadsheets, checked to the cent, without uploading them</title>
      <dc:creator>Kai Chen</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:48:08 +0000</pubDate>
      <link>https://dev.to/kaichen_dev/tickmark-bank-statements-to-spreadsheets-checked-to-the-cent-without-uploading-them-20jf</link>
      <guid>https://dev.to/kaichen_dev/tickmark-bank-statements-to-spreadsheets-checked-to-the-cent-without-uploading-them-20jf</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;Every month, somewhere, a bookkeeper is retyping a bank statement. The bank hands over a PDF; the books want rows in a spreadsheet, or an import file for QuickBooks or Xero. People pay to skip that job. A one-person web app that does nothing but convert bank statements was &lt;a href="https://superframeworks.com/blog/bankconverter" rel="noopener noreferrer"&gt;reported this year&lt;/a&gt; at about $38,000 in monthly recurring revenue, and freelance boards carry a steady stream of "PDF statement to Excel" requests.&lt;/p&gt;

&lt;p&gt;Two things keep bothering the people who do this work.&lt;/p&gt;

&lt;p&gt;The first is that the converters want the statement uploaded. For an accountant, that means sending a client's bank history to a server they know nothing about.&lt;/p&gt;

&lt;p&gt;The second is trust. A converter that reads a 1 as a 7 does not tell you. You find out at month end, when the books are six dollars off and you go hunting through two hundred rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tickmark&lt;/strong&gt; is a web page that turns a bank statement PDF into a spreadsheet on your own computer, and checks every row while it does it. Each amount is added to the running balance, and a row gets a tick only when the balance the bank printed beside it agrees to the cent. A row that does not agree is ringed in red, with the size of the gap and the place on the page to look.&lt;/p&gt;

&lt;p&gt;Scanned statements, which have no text to copy, are read by &lt;strong&gt;Gemma 4&lt;/strong&gt; running on your own graphics card through WebGPU. The model writes the rows down; the same arithmetic then decides which of them are right.&lt;/p&gt;

&lt;p&gt;Who pays for this: bookkeepers, accountants and small business owners who convert statements every month, the same people already paying the upload-based converters. The plan is a free open core and paid extras that save hours: a year of statements in one go, rules remembered per client, column mappings for each accounting package.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it: &lt;a href="https://luoy16002-svg.github.io/tickmark/" rel="noopener noreferrer"&gt;https://luoy16002-svg.github.io/tickmark/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It opens with seven made-up statements from invented banks, printed the different ways real banks print them: one table with a signed amount column; paid in and paid out columns with an overdraft and one balance a day; decimal commas and two date columns; CR and DR markers; sections by kind (deposits, withdrawals, checks, fees) with no running balance; a check-number column with credits, debits and an ending daily balance; and sections with unsigned amounts and a separate daily balance table. There is a scanned one too. Or drop your own PDF on the page. The counter at the bottom shows every request the page makes to another site after you open a file. It stays at 0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbl1et5d63cdsimd82oi.jpg" 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%2Fzbl1et5d63cdsimd82oi.jpg" alt="Tickmark with a statement open: the bank's page on the left, the ledger on the right, every row ticked, the totals footed under a double rule and stamped reconciled, 0 requests to other sites" width="799" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here I changed one figure by hand, 27.32 to 37.32, to show what a misread looks like. The ledger rings the printed balance in red, says the gap is exactly 10.00, marks the line on the bank's page, and picks up again on the next row:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4vi3mfeqaxn7vrhrwzfj.jpg" 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%2F4vi3mfeqaxn7vrhrwzfj.jpg" alt="One row ringed in red, with a note giving the printed balance, the running total and the gap, and a box to correct the amount" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A scanned statement being read by Gemma 4 in the browser. The frame on the page is the band the model is reading; the rows appear as it writes them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fty650v09gd10so3p07i2.jpg" 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%2Fty650v09gd10so3p07i2.jpg" alt="Gemma 4 reading a scanned statement band by band, with its output appearing beside the page" width="799" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works on a phone too, minus the scanning:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa2bh67uk41vodo75qr18.jpg" 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%2Fa2bh67uk41vodo75qr18.jpg" alt="The ledger on a phone: each row shows the description and amount, then the date, category and balance" width="780" height="1100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Exports: an Excel workbook (a transactions sheet and a reconciliation sheet), CSV, QuickBooks Online CSV, Xero CSV and OFX.&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/luoy16002-svg" rel="noopener noreferrer"&gt;
        luoy16002-svg
      &lt;/a&gt; / &lt;a href="https://github.com/luoy16002-svg/tickmark" rel="noopener noreferrer"&gt;
        tickmark
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Bank statements into spreadsheets, checked row by row against the running balance, in your browser. Scans are read by Gemma 4 on your own GPU.
    &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;Tickmark&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Bank statements into spreadsheets, checked row by row, on your own computer.&lt;/p&gt;
&lt;p&gt;Open a PDF statement in the browser. Tickmark copies every transaction into a ledger and adds it to the running
balance. A row gets a tick only when the balance the bank printed agrees to the cent; a row that does not agree is
ringed in red, with the gap and the place to look. Scanned statements are read by &lt;strong&gt;Gemma 4&lt;/strong&gt;, Google's open model
running on your graphics card through WebGPU, and go through the same check, so a figure the model misreads shows
up as a row that does not tie instead of slipping into your books.&lt;/p&gt;
&lt;p&gt;Nothing is uploaded. The page counts every request it makes to another site and shows the number at the bottom.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it reads&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Text PDFs as banks issue them: one table or sections by kind (deposits, withdrawals…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/luoy16002-svg/tickmark" 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;&lt;strong&gt;Reading the text layer.&lt;/strong&gt; pdf.js gives every piece of text on a page with its position. Tickmark groups pieces on the same baseline into lines, joins pieces closer than about half a character into cells, finds the header line (Date ... Balance, in a dozen spellings), and assigns every figure to the column whose right edge it lines up with. Dates come as 09/01, 01 Sep 26, SEP 01 or 01.09.2026, so the statement period is read first and each date takes its year from it. Amounts come signed, in separate in and out columns, with CR and DR, in brackets, or with decimal commas, and an overdrawn balance can be printed three different ways. Some banks print one long table; others print a section per kind of transaction with the amounts unsigned and the balances in a separate daily table. Tickmark takes the direction of the money from the section title, lists such rows by date and checks them day by day. When a PDF runs two headings together into one piece of text ("Deposits/Credits Withdrawals/Debits"), it splits them again at the phrases banks use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt; The running balance is the bank's own arithmetic, printed on the statement. Tickmark walks the rows, adds each amount, and compares. When a statement prints a balance only at the end of each day, the day's rows are checked together. When an amount is printed without a sign, the balance settles it: if only one choice of in and out makes the day add up, that is the one used. When nothing adds up, the row is flagged and the running balance picks up again from the next printed figure, so one bad row does not turn the rest of the page red.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 in the browser.&lt;/strong&gt; Transformers.js 4.3 runs Gemma 4 E2B (q4f16) on WebGPU, in a web worker so the page stays responsive. The first time, the browser downloads 3.38 GB of weights from Hugging Face; after that it loads from the browser's cache in about five seconds on my machine (an RTX 5060 Ti).&lt;/p&gt;

&lt;p&gt;My first attempt gave it a whole page, and the result was useless: three rows right, then shifted amounts, repeated rows and missed lines. The reason was in the image processor. The web build of the vision encoder turns any picture into 280 tokens, and at that budget a page of 9 point print is a blur. Raising the budget failed twice (first a WebGPU buffer over 2 GB, then an encoder fixed at 280 features), so I went the other way: the page is cut into ten overlapping bands and the model reads one band at a time. At that scale the print is legible.&lt;/p&gt;

&lt;p&gt;What comes back still needs care, and every repair is one the arithmetic can prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a row read twice where two bands overlap is dropped, because a real transaction cannot leave the balance unchanged;&lt;/li&gt;
&lt;li&gt;the second printed line of a description, read as a row of its own carrying the line above's balance, is joined back to its row;&lt;/li&gt;
&lt;li&gt;a row that the next row ties without is not on the statement;&lt;/li&gt;
&lt;li&gt;blank paper is never shown to the model, because shown an empty band it sometimes invents an example table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the scanned sample (three pages, 64 transactions) Gemma 4 reads every row correctly in about two minutes, and all 64 tie. On the UK sample, which prints one balance a day and leaves many cells empty, it does much worse: in my benchmark it got 23 of 58 rows exactly right. The check still does its job there. Every day that does not add up is marked, in the ledger and in the export, so the errors are in plain sight, but you would be correcting a lot by hand. Reading one row at a time, with the column headings pasted above it, is what I am working on next. The benchmark page in the repository runs the scan pipeline on any sample and scores it against the sample's answer key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Categories.&lt;/strong&gt; Plain rules handle the common payees: payment processors, utilities, couriers, fuel, taxes, bank fees. Gemma 4 suggests the rest, one question per payee, and any answer that contradicts the direction of the money (an expense category on money that came in) is thrown away. Suggestions stay in italics until you confirm them, and your choices are remembered for next month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proving nothing leaves.&lt;/strong&gt; The page runs under a strict content security policy, the fonts and the ONNX runtime ship with the site, and a PerformanceObserver in the page and another in the model's worker count every request to another site. Open a statement, export it, and the count is still 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing.&lt;/strong&gt; A generator writes statements for seven invented banks in seven layouts, each with an answer key. The test suite parses all seven and compares every date, amount, balance and description with the key (386 rows; all match and all tie), checks that a misread amount is caught on its own row and nowhere else, that signs come from the balance when the statement prints none, and opens every export with an independent reader.&lt;/p&gt;

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

&lt;p&gt;A bank statement is about as private as a document gets. The only way to put a capable reader next to it without sending it somewhere is to run the model where the document already is, and that only works with open weights. Gemma 4 is Apache 2.0, so a static web page can hand it to anyone's browser: no API key, no account, no bill that grows with every page read. A closed API would have made the whole idea impossible, because the first step would be uploading the statement.&lt;/p&gt;

&lt;p&gt;Open code matters for the same reason. "We never see your data" is a sentence every converter puts on its homepage. Here you can read the code and watch the request counter while you work.&lt;/p&gt;

&lt;p&gt;And it keeps the business honest. With no inference bill, the core can stay free for good. What people would pay for is time saved, not server time.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 E2B reads scanned statements and suggests categories, entirely in the browser on WebGPU, with every figure it reads checked by the statement's own arithmetic.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>webgpu</category>
    </item>
    <item>
      <title>Would today's AI have caught Ariane 5? I rebuilt 26 of history's costliest bugs to find out</title>
      <dc:creator>Kai Chen</dc:creator>
      <pubDate>Fri, 09 Oct 2026 09:35:12 +0000</pubDate>
      <link>https://dev.to/kaichen_dev/would-todays-ai-have-caught-ariane-5-i-rebuilt-26-of-historys-costliest-bugs-to-find-out-5d8m</link>
      <guid>https://dev.to/kaichen_dev/would-todays-ai-have-caught-ariane-5-i-rebuilt-26-of-historys-costliest-bugs-to-find-out-5d8m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Hindsight&lt;/strong&gt; asks a simple question: if you had shown today's models the code behind history's most expensive software failures, the day before it shipped, would they have caught the bug?&lt;/p&gt;

&lt;p&gt;I spent the last few weeks reading accident reports for a series of short animated explainers about famous failures: the Ariane 5 inertial reference software, the Patriot battery at Dhahran, Knight Capital's 45 minutes, the CrowdStrike channel file, the spreadsheet behind Reinhart and Rogoff. Every report ends the same way: the defect was small, local and visible in a few lines, and nobody saw it in time. That made me curious whether a code review by a model would have changed anything.&lt;/p&gt;

&lt;p&gt;The benchmark has 26 cases. Each is a documented failure with a published root cause (an inquiry board, a GAO or SEC report, a vendor post-mortem or a peer-reviewed paper), rebuilt as a minimal artifact: a function, a shell script, an Excel formula, an RTOS configuration. No names, no dates. The model gets the artifact and one line about the system, and answers in a fixed format: &lt;code&gt;VERDICT: DEFECT / NO_DEFECT&lt;/code&gt;, what fails, the trigger, the consequence. A judge model compares the answer with the published root cause.&lt;/p&gt;

&lt;p&gt;Every case comes in four forms, which are the four tasks of the benchmark:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What the model sees&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original&lt;/td&gt;
&lt;td&gt;the reconstruction in its original setting (Ada on a launcher, Pascal on a radiotherapy machine)&lt;/td&gt;
&lt;td&gt;can it catch the failure?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disguised&lt;/td&gt;
&lt;td&gt;the same mechanism moved to another domain, language and naming (the Ariane conversion becomes a Python drone telemetry packer)&lt;/td&gt;
&lt;td&gt;is it reasoning, or recognising a famous story?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disguised + context&lt;/td&gt;
&lt;td&gt;the disguised artifact plus the one operating fact that triggered the real failure ("the new drone flies five times faster")&lt;/td&gt;
&lt;td&gt;how much does knowing the environment help?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;td&gt;the original artifact after the fix that was applied&lt;/td&gt;
&lt;td&gt;does it raise false alarms on code that is now correct?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between &lt;em&gt;Original&lt;/em&gt; and &lt;em&gt;Disguised&lt;/em&gt; is a memorisation signal. The gap between &lt;em&gt;Disguised&lt;/em&gt; and &lt;em&gt;Disguised + context&lt;/em&gt; measures something the accident reports keep returning to: much of this code was correct for the system it was written for, and failed in a new environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  One case, four forms
&lt;/h3&gt;

&lt;p&gt;Here is the Ariane 5 case. The original form is close to what flew in 1996: a 64-bit float converted to a 16-bit integer, protected on one variable and not on the other, because analysis of the &lt;em&gt;old&lt;/em&gt; rocket's trajectory showed it stayed small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ada"&gt;&lt;code&gt;&lt;span class="kd"&gt;procedure&lt;/span&gt; &lt;span class="nf"&gt;Update_Alignment&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt;
   &lt;span class="n"&gt;HB_Bus&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Integer_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- horizontal bias, 16-bit field on the data bus&lt;/span&gt;
   &lt;span class="n"&gt;VB_Bus&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Integer_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- vertical bias&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
   &lt;span class="n"&gt;Horizontal_Bias&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Compute_Horizontal_Bias&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Horizontal_Velocity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- Long_Float&lt;/span&gt;
   &lt;span class="n"&gt;Vertical_Bias&lt;/span&gt;   &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Compute_Vertical_Bias&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vertical_Velocity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;-- Long_Float&lt;/span&gt;

   &lt;span class="n"&gt;VB_Bus&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Integer_16&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Saturate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vertical_Bias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;32_768.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;32_767.0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
   &lt;span class="n"&gt;HB_Bus&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Integer_16&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Horizontal_Bias&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="c1"&gt;--  no saturation on HB: analysis of the trajectory showed it stays small&lt;/span&gt;

   &lt;span class="n"&gt;Bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;VB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;VB_Bus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="n"&gt;Bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HB_Bus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;Update_Alignment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- no exception handler in this procedure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The disguised form keeps the mechanism and changes everything a model could pattern-match on: a cargo drone, Python, a radio packet, millimetres instead of velocities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pack_drift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# radio link fields are signed 16-bit integers (struct code 'h')
&lt;/span&gt;    &lt;span class="n"&gt;alt_drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vertical_drift_mm&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32767&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lat_drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lateral_drift_mm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# analysis showed lateral drift stays small
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;hh&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alt_drift&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_drift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flight_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radio&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;radio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pack_drift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;context&lt;/em&gt; form adds the one sentence the Ariane 5 team did not connect to this code: "the new drone cruises about five times faster than the quadcopter, so its lateral drift estimate during the climb reaches 40,000 to 60,000 mm." And the &lt;em&gt;fixed&lt;/em&gt; form is the Ada original with &lt;code&gt;Horizontal_Bias&lt;/code&gt; saturated as well, so a model that still says "this will overflow" is now wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  How an answer is scored
&lt;/h3&gt;

&lt;p&gt;A case counts as caught only if the model says &lt;code&gt;DEFECT&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; the judge confirms that the answer names the same mechanism and trigger as the published root cause. "This conversion could overflow" is not enough for Ariane 5; the answer has to say that the horizontal bias goes past 32,767 and that the unhandled exception stops the unit. A correct verdict with the wrong reason is a miss.&lt;/p&gt;

&lt;p&gt;On the fixed versions the rule flips: an answer passes unless it claims that the historical failure will still happen. Raising some other concern is allowed, and I report separately how often models flag &lt;em&gt;something&lt;/em&gt; on correct code.&lt;/p&gt;

&lt;p&gt;The judge is Kaggle's default evaluation model. To check it, I re-read 64 answers by hand: the 44 disguised answers from the first nine models that said &lt;code&gt;DEFECT&lt;/code&gt; but were marked as misses, plus 20 random passes. I agreed with the judge on 61. All three disagreements were the judge being too strict, so if anything the scores below are slightly low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;Thirteen models from six vendors, large and small. Ranking them was half the point; the other half was seeing whether size and vendor change what kind of bug gets missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic: Claude Opus 5.5, Claude Sonnet 5.5, Claude Haiku 4.5&lt;/li&gt;
&lt;li&gt;OpenAI: GPT-6.1 Sol, GPT-5.4 mini, gpt-oss-120b (open weights)&lt;/li&gt;
&lt;li&gt;Google: Gemini 3.1 Pro (preview), Gemini 3.8 Flash, Gemini 3.7 Flash, Gemma 4 31B (open weights)&lt;/li&gt;
&lt;li&gt;xAI: Grok 4.20 (reasoning)&lt;/li&gt;
&lt;li&gt;DeepSeek-R1 and Qwen3 Coder 480B (open weights)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every model gets the same prompt with the platform's default sampling settings and a 4,000-token output cap, one case per fresh chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6abe74urdcmme14wxoci.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%2F6abe74urdcmme14wxoci.png" alt="Which disguised failures each model caught" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The famous bugs are solved, even in disguise
&lt;/h3&gt;

&lt;p&gt;Eight cases were caught by all thirteen models in their disguised form: Ariane 5, the Patriot clock, Mars Climate Orbiter, the Boeing 787 counter, the Azure leap-day certificate, the AT&amp;amp;T switch crash, CrowdStrike and the Reinhart-Rogoff spreadsheet. Ten more were caught by at least ten of the thirteen. A Python drone packer with an unclamped 16-bit field does not fool anyone any more. These are bugs you can see by reading the code once: a narrowing conversion, a counter that wraps, a quantity in the wrong unit, a range that stops a few rows short.&lt;/p&gt;

&lt;h3&gt;
  
  
  What they miss only shows up when the code runs
&lt;/h3&gt;

&lt;p&gt;Seven cases were caught by eight of the thirteen models or fewer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Caught&lt;/th&gt;
&lt;th&gt;What you have to imagine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mars Polar Lander, 1999&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;a vibration spike during leg deployment, latched as "touchdown" long before the ground&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vancouver Stock Exchange index, 1983&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;a truncation to three decimals, repeated on every trade for 22 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare WAF, 2019&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;one regex meeting a long input and backtracking until the CPU is gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;England's COVID test results, 2020&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;an export format that stops at 65,536 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3, 2017&lt;/td&gt;
&lt;td&gt;54%&lt;/td&gt;
&lt;td&gt;a maintenance command removing far more capacity than the operator meant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schiaparelli, 2016&lt;/td&gt;
&lt;td&gt;54%&lt;/td&gt;
&lt;td&gt;an inertial unit that saturates for one second while the parachute opens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;London Whale, 2012&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;td&gt;a volatility that is divided by a sum where it should be an average&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these is hard to read. Each one needs you to run the code in your head under a condition that is not on the page: a long input, a vibration, the 22nd month, the 65,537th row. That is exactly what the accident reports say the engineers failed to do, and it is the part the models still struggle with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Right verdict, wrong reason
&lt;/h3&gt;

&lt;p&gt;Across the 338 disguised answers the models said &lt;code&gt;DEFECT&lt;/code&gt; 97% of the time. Of the 63 misses, 54 still said &lt;code&gt;DEFECT&lt;/code&gt;: they found &lt;em&gt;a&lt;/em&gt; problem, just not the one that brought the system down. Some of the wrong reasons are reasonable review comments. Some are invented.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the Cloudflare regex, Claude Haiku 4.5 warned about SQL injection ("&lt;code&gt;DROP TABLE users;--&lt;/code&gt;"). The regex never touches a database; the problem is that it can take exponential time.&lt;/li&gt;
&lt;li&gt;On the Vancouver index, Gemini 3.1 Pro blamed floating point: "&lt;code&gt;0.58 * 100&lt;/code&gt; evaluates to &lt;code&gt;57.99999999999999&lt;/code&gt;". True, and a one-cent error, while truncating on every update took half the value of the index.&lt;/li&gt;
&lt;li&gt;On England's spreadsheet export, Gemini 3.8 Flash said the export call "accepts only a single argument" and will crash. It takes a file path just fine, and the answer never mentions the row limit of the &lt;code&gt;.xls&lt;/code&gt; format.&lt;/li&gt;
&lt;li&gt;On Mars Polar Lander, GPT-6.1 Sol found the latch that is never cleared, but blamed a previous trip instead of the vibration on the way down: the right line of code with the wrong story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why a &lt;code&gt;DEFECT&lt;/code&gt; verdict on its own means very little here, and why the benchmark scores the mechanism and the trigger, not the verdict.&lt;/p&gt;

&lt;h3&gt;
  
  
  My reconstructions had bugs of their own
&lt;/h3&gt;

&lt;p&gt;Reading the misses by hand, I found that three of my artifacts contained a second, unintended defect, and some models reported that one instead of the historical one: the Cloudflare regex also rejects a valid single comparison, the London Whale sheet divides by zero when both weeks are empty, and the S3 drain script deletes nodes without waiting for the evictions to finish. Those answers are correct review comments and still count as misses, because they are not what happened. When you rebuild a failure, you rebuild more than one failure. I have left the cases as they were run and listed the extra defects in the benchmark notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory, context and false alarms
&lt;/h3&gt;

&lt;p&gt;Averaged over all thirteen models, the four tasks score 89% (original), 81% (disguised), 89% (disguised with context) and 96% (fixed version, no false alarm).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory helps less than I expected, and not where I expected.&lt;/strong&gt; The memorisation gap, original minus disguised, is eight points on average. It is largest for DeepSeek-R1 (19 points), gpt-oss-120b and Gemma 4 31B (15 each), and zero or below for Claude Sonnet 5.5, Claude Opus 5.5 and Qwen3 Coder. I also counted how often an answer to the original form names the real incident ("this is the Ariane 5 failure"). Opus did that on 81% of cases, Gemini 3.1 Pro on 58%, Sonnet on 46%, and nearly every other model on 8% or less. The models that recognise the stories most often are the ones that lose nothing when the story is taken away, so recognising a case and depending on that recognition are two different things. The gap belongs to the mid-sized and open models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One sentence of context is worth as much as the original setting.&lt;/strong&gt; Adding the operating fact to the disguised artifact brings the average from 81% back to 89%, level with the original. The cases it rescues are exactly the hard ones from the table above: the spreadsheet export (7 models went from miss to catch), the regex, the lander and the parachute IMU (6 each). DeepSeek-R1 gains 23 points, GPT-5.4 mini and Gemma 15. Grok 4.20 is the exception: it lost three cases when given the context (Zune, S3 and the London Whale sheet), and the Zune case lost five models in total, because the hint about the last day of a leap year pulled answers toward the date arithmetic and away from the loop that never ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed code is rarely condemned, but often criticised.&lt;/strong&gt; Only 15 of 338 answers claimed the historical failure would still happen on the fixed version, and six of those were the same case: Schiaparelli, where the fix drops the one-second hold on the saturated rate and adds a plausibility check, and six models still argued that the altitude could go negative. But models flagged &lt;em&gt;some&lt;/em&gt; defect on 44% of the fixed artifacts. Claude Haiku 4.5 did so on 96% of them, GPT-5.4 mini on 88%, gpt-oss-120b on 81%; Sonnet 5.5 never did and Opus 5.5 once. For a code reviewer that difference matters as much as the catch rate: a tool that objects to everything teaches people to stop reading its comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60h7j1k9rq47pc6rcpnp.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%2F60h7j1k9rq47pc6rcpnp.png" alt="The four forms of each case, per model" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;The benchmark on Kaggle: &lt;a href="https://www.kaggle.com/benchmarks/fuddle/hindsight-catching-historys-costliest-bugs" rel="noopener noreferrer"&gt;Hindsight: Catching History's Costliest Bugs&lt;/a&gt;, with the leaderboard for all thirteen models.&lt;/p&gt;

&lt;p&gt;The four tasks, each with the reconstructions, the sources for every case and the per-model runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.kaggle.com/benchmarks/tasks/fuddle/hindsight-historical" rel="noopener noreferrer"&gt;hindsight-historical&lt;/a&gt; (original form)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/fuddle/hindsight-disguised" rel="noopener noreferrer"&gt;hindsight-disguised&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/fuddle/hindsight-disguised-context" rel="noopener noreferrer"&gt;hindsight-disguised-context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/fuddle/hindsight-fixed" rel="noopener noreferrer"&gt;hindsight-fixed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Aurora Odds: should you go outside and look up right now?</title>
      <dc:creator>Kai Chen</dc:creator>
      <pubDate>Wed, 07 Oct 2026 01:36:32 +0000</pubDate>
      <link>https://dev.to/kaichen_dev/aurora-odds-should-you-go-outside-and-look-up-right-now-jjf</link>
      <guid>https://dev.to/kaichen_dev/aurora-odds-should-you-go-outside-and-look-up-right-now-jjf</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;On 10 May 2024 the northern lights reached Florida, northern India and the Canary Islands. Millions of people saw them. Many more only read about it the next morning, because the alerts on their phones said "G5" and "Kp 9" and nothing about their own sky.&lt;/p&gt;

&lt;p&gt;That is the gap I wanted to close. Every aurora app I tried answers a forecaster's question (how disturbed is the magnetic field?) when the question people actually ask is a practical one: &lt;strong&gt;is it worth putting my shoes on and going outside, here, right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://luoy16002-svg.github.io/aurora-odds/" rel="noopener noreferrer"&gt;Aurora Odds&lt;/a&gt;&lt;/strong&gt; answers exactly that. Pick your place (or let the browser find it) and you get one verdict for your sky, &lt;em&gt;Go outside now&lt;/em&gt;, &lt;em&gt;Bring your phone&lt;/em&gt;, &lt;em&gt;Not yet: it's still light out&lt;/em&gt;, or &lt;em&gt;Stay in, the sky is quiet&lt;/em&gt;, backed by four numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the chance the aurora is bright enough &lt;strong&gt;for your eyes&lt;/strong&gt; in the next hour, and &lt;strong&gt;for a phone camera&lt;/strong&gt;, which sees it from much further away;&lt;/li&gt;
&lt;li&gt;whether it is &lt;strong&gt;dark enough&lt;/strong&gt; where you are, and when it will be;&lt;/li&gt;
&lt;li&gt;how much of your sky is &lt;strong&gt;cloud&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;which way to &lt;strong&gt;face&lt;/strong&gt;, and how high to look: "lower edge about 8° above the horizon, top near 25°, three fists at arm's length".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a red night-vision mode, because the whole point is to use it outside in the dark without wrecking your eyes' adaptation, and a "tell me when to go out" switch: leave the tab open and it chimes once it is dark, mostly clear and the odds for your eyes pass 40%. The screen can stay in your pocket until then.&lt;/p&gt;

&lt;p&gt;The forecast comes from &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt;, an open-weights tabular foundation model, reading the solar wind that is on its way to Earth.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://luoy16002-svg.github.io/aurora-odds/" rel="noopener noreferrer"&gt;https://luoy16002-svg.github.io/aurora-odds/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdk1n2vra1tksbkordfj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdk1n2vra1tksbkordfj.gif" alt="Aurora Odds replaying the night of 10 May 2024 in London" width="600" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page is most fun during a storm, and storms are rare, so it has a replay mode that runs the same model on the solar wind of a real night: &lt;strong&gt;&lt;a href="https://luoy16002-svg.github.io/aurora-odds/?replay=may2024&amp;amp;place=London" rel="noopener noreferrer"&gt;London, 10 May 2024&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://luoy16002-svg.github.io/aurora-odds/?replay=oct2024&amp;amp;place=Chicago" rel="noopener noreferrer"&gt;Chicago, 10-11 October 2024&lt;/a&gt;&lt;/strong&gt;. Clouds in a replay come from Open-Meteo's historical archive for that hour.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol489vre0czgaaql9tot.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%2Fol489vre0czgaaql9tot.png" alt="The May 2024 replay for London: go outside now" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18dyzid9ntr24125i5ue.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%2F18dyzid9ntr24125i5ue.png" alt="Where to look: the expected arch of the aurora over the poleward horizon" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp3pb8aw42yc392wzsyy.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%2Fwp3pb8aw42yc392wzsyy.png" alt="The red night-vision mode on a phone" width="780" height="1688"&gt;&lt;/a&gt;&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/luoy16002-svg" rel="noopener noreferrer"&gt;
        luoy16002-svg
      &lt;/a&gt; / &lt;a href="https://github.com/luoy16002-svg/aurora-odds" rel="noopener noreferrer"&gt;
        aurora-odds
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Should you go outside and look up right now? A live aurora nowcast for your exact sky, made by TabPFN reading the solar wind.
    &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;Aurora Odds&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Should you go outside and look up right now?&lt;/strong&gt; Aurora Odds answers that for your exact sky: the chance the
aurora is bright enough for your eyes or your phone camera in the next hour, whether it is dark and clear enough
and which way to face and how high to look.&lt;/p&gt;
&lt;p&gt;Live page: &lt;a href="https://luoy16002-svg.github.io/aurora-odds/" rel="nofollow noopener noreferrer"&gt;https://luoy16002-svg.github.io/aurora-odds/&lt;/a&gt; · storm replays: &lt;a href="https://luoy16002-svg.github.io/aurora-odds/?replay=may2024&amp;amp;place=London" rel="nofollow noopener noreferrer"&gt;London, 10 May 2024&lt;/a&gt;, &lt;a href="https://luoy16002-svg.github.io/aurora-odds/?replay=oct2024&amp;amp;place=Chicago" rel="nofollow noopener noreferrer"&gt;Chicago, 10-11 October 2024&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/luoy16002-svg/aurora-odds/docs/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fluoy16002-svg%2Faurora-odds%2FHEAD%2Fdocs%2Fdemo.gif" alt="Aurora Odds replaying 10 May 2024 in London"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The forecast is made by &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt;, an open-weights tabular foundation model
reading the solar wind measured at the L1 point 1.5 million km upstream. It is not trained on this problem: it
reads 2,000 labelled half-hours from 1998 to 2019 &lt;em&gt;in context&lt;/em&gt; (8,000 in the evaluation below) and returns a full probability distribution for the
coming hour's geomagnetic activity. One distribution answers every latitude.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upstream measurements.&lt;/strong&gt; NOAA's real-time solar wind feed (magnetic field and plasma from the…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/luoy16002-svg/aurora-odds" 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;h3&gt;
  
  
  The solar wind gives you a head start
&lt;/h3&gt;

&lt;p&gt;Spacecraft parked at the L1 point, 1.5 million kilometres sunward, measure the solar wind every minute. That wind takes 20 to 80 minutes to reach Earth, so part of the next hour is already measured before it arrives. The most important number is &lt;strong&gt;Bz&lt;/strong&gt;, the north-south direction of the wind's magnetic field. When it points south it connects to Earth's field and pours energy in; when it points north, very little gets through, however fast the wind blows.&lt;/p&gt;

&lt;p&gt;I wanted the model to see the world exactly as it can be seen at one moment, so every input is built on an &lt;strong&gt;arrival-time axis&lt;/strong&gt;: each sample is shifted by its own travel time, and at time &lt;em&gt;t&lt;/em&gt; the model is only allowed to use samples that had already been measured at L1 by &lt;em&gt;t&lt;/em&gt;. That gives four windows: what is &lt;em&gt;measured but still on its way&lt;/em&gt;, the last 30 minutes, 30 to 90 minutes ago, and 90 to 180 minutes ago. In each I summarise Bz, the &lt;a href="https://doi.org/10.1029/2006JA012015" rel="noopener noreferrer"&gt;Newell coupling function&lt;/a&gt; (a standard estimate of how much energy the wind is delivering), speed, density and pressure, plus the season and time of day. The same code builds 28 years of history from NASA's OMNI data and the live features from NOAA's real-time feed, so there is no training/serving skew to debug at 2 a.m. during a storm.&lt;/p&gt;

&lt;p&gt;The target is the highest &lt;strong&gt;Hp30&lt;/strong&gt; in the coming hour. Hp30 is GFZ Potsdam's half-hourly version of the Kp index: same scale, finer timing, and open-ended, so a superstorm is not squashed at 9.&lt;/p&gt;

&lt;h3&gt;
  
  
  TabPFN reads history instead of training on it
&lt;/h3&gt;

&lt;p&gt;TabPFN is a transformer pretrained on millions of synthetic tabular problems. You don't train it on your data: you hand it a table of labelled examples (the &lt;em&gt;context&lt;/em&gt;) and it predicts new rows in a single forward pass, like in-context learning in a language model.&lt;/p&gt;

&lt;p&gt;I gave it &lt;strong&gt;8,000 half-hours from 1998 to 2019&lt;/strong&gt; as context (the live page uses 2,000; more on that below). Storms are rare (the next hour reaches Hp30 6 only about 1.2% of the time), so a uniform sample shows the model very few of the moments that matter. I tried a stratified context that over-represents storms and then corrects the predicted distribution back to the real storm frequency. On the 2020-2022 validation years the stratified context won (mean Brier skill 0.46 against 0.44 for Hp30 ≥ 5 to 7), so that's what runs.&lt;/p&gt;

&lt;p&gt;The part I like most: TabPFN's regressor returns a &lt;strong&gt;full probability distribution&lt;/strong&gt; over the next hour's Hp30, not a single number. So one forward pass answers every latitude at once. Tromsø needs almost nothing, Edinburgh needs about Kp 5 to see it with the naked eye, London about 7.5. Each of those is just &lt;code&gt;P(Hp30 ≥ k)&lt;/code&gt; read from the same distribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did it work?
&lt;/h3&gt;

&lt;p&gt;I tested on &lt;strong&gt;2023 to 2026&lt;/strong&gt;, years the model never saw, which include the solar maximum, the May 2024 superstorm and the October 2024 storm. I compared it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LightGBM&lt;/strong&gt; trained on all 370,000 half-hours from 1998 to 2019 (a classifier per threshold, the usual way to do this);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;persistence&lt;/strong&gt;: "the next hour will look like the last half-hour", using the real index with zero delay, which no live app actually has.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brier skill score (AUC)&lt;/th&gt;
&lt;th&gt;Hp30&amp;nbsp;≥&amp;nbsp;4&lt;/th&gt;
&lt;th&gt;Hp30&amp;nbsp;≥&amp;nbsp;5&lt;/th&gt;
&lt;th&gt;Hp30&amp;nbsp;≥&amp;nbsp;6&lt;/th&gt;
&lt;th&gt;Hp30&amp;nbsp;≥&amp;nbsp;7&lt;/th&gt;
&lt;th&gt;Hp30&amp;nbsp;≥&amp;nbsp;8&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN, 8,000-row context&lt;/td&gt;
&lt;td&gt;0.56 (0.951)&lt;/td&gt;
&lt;td&gt;0.54 (0.975)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.52 (0.987)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.58 (0.993)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.53 (0.990)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN, 2,000-row context (the live page)&lt;/td&gt;
&lt;td&gt;0.54 (0.948)&lt;/td&gt;
&lt;td&gt;0.52 (0.973)&lt;/td&gt;
&lt;td&gt;0.51 (0.986)&lt;/td&gt;
&lt;td&gt;0.56 (0.992)&lt;/td&gt;
&lt;td&gt;0.50 (0.992)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LightGBM, trained on 370,431 rows&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.56 (0.951)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.54 (0.974)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.51 (0.987)&lt;/td&gt;
&lt;td&gt;0.46 (0.989)&lt;/td&gt;
&lt;td&gt;0.34 (0.982)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last half-hour's index (oracle)&lt;/td&gt;
&lt;td&gt;0.34 (0.790)&lt;/td&gt;
&lt;td&gt;0.33 (0.777)&lt;/td&gt;
&lt;td&gt;0.34 (0.776)&lt;/td&gt;
&lt;td&gt;0.45 (0.811)&lt;/td&gt;
&lt;td&gt;0.44 (0.804)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;61,420 half-hours from January 2023 to September 2026. Storm half-hours at each level: 9,345, 3,458, 1,201, 485, 166. Brier skill is relative to always forecasting the long-run frequency; 0 means no skill.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For moderate activity, TabPFN and LightGBM are level. For the rare, strong storms, the ones that bring the aurora to Edinburgh, Chicago or London, TabPFN is clearly better: a Brier skill of &lt;strong&gt;0.58 against 0.46&lt;/strong&gt; at Hp30 ≥ 7 and &lt;strong&gt;0.53 against 0.34&lt;/strong&gt; at Hp30 ≥ 8. LightGBM saw 46 times more rows, but strong storms are a sliver of them and a tree has very little to split on out there. My guess is that TabPFN's prior, learned from millions of synthetic problems, degrades more gracefully in the tail; either way, it's the tail that decides whether someone in London goes outside.&lt;/p&gt;

&lt;p&gt;The live page runs the 2,000-row context, so a free 4-core GitHub runner finishes in about a minute. That costs about 0.02 of skill.&lt;/p&gt;

&lt;p&gt;It is not magic. On 10 May 2024 the index climbed to about 6 in the early afternoon while the wind measured upstream still looked calm (a weak field of about 3 nT, barely southward). A model that only sees the wind had nothing to react to and sat at Kp 2 to 3 until the storm's shock arrived at 17:10 UTC; after that it was in the right place within half an hour. It also over-forecasts a little at moderate levels: in the reliability chart the curves for Hp30 ≥ 5 and 6 sit slightly below the diagonal. For a tool whose only job is to tell you when to put your shoes on, I'm fine with erring that way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7mcn9h74c4bidvhc8hy1.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%2F7mcn9h74c4bidvhc8hy1.png" alt="The model's forecast against what happened during the May 2024 superstorm" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8ev1lw70ttvhkvqbor8.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%2Fv8ev1lw70ttvhkvqbor8.png" alt="Skill by threshold on 2023-2026" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqha182gimx9kxu79wn31.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%2Fqha182gimx9kxu79wn31.png" alt="Reliability on 2023-2026" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  From a number to a sky
&lt;/h3&gt;

&lt;p&gt;The rest is geometry, and it's where most aurora apps stop short.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Magnetic latitude.&lt;/strong&gt; The aurora follows Earth's magnetic field, not lines of latitude, and a simple tilted-dipole model puts Europe several degrees too far north. I precomputed corrected (AACGM-v2) magnetic latitude on a 2° grid and interpolate it in the browser. Edinburgh comes out at 53°, Minneapolis at 54°: that is why Minnesota sees the aurora more often than Scotland despite being 11° further south.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What you need.&lt;/strong&gt; The auroral oval's edge sits near 66.5° magnetic latitude when the field is quiet and moves about 2° equatorward per Kp step. Low on the poleward horizon it is visible from a few degrees further away, and a phone's night mode picks it up further still. I checked the margins against what observers report for Tromsø, Edinburgh, Minneapolis, London and the May 2024 storm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How high to look.&lt;/strong&gt; Light is emitted 110 to 300 km up. From the distance between you and the oval, a few lines of spherical geometry give the elevation of its lower edge and top, and because the oval is a band at fixed magnetic latitude, it shows up as an arch, highest toward the magnetic pole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark and clear.&lt;/strong&gt; The sun's altitude comes from a standard solar position formula (dark enough below −12°), and cloud cover from &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A GitHub Actions job reruns the model every ten minutes on a CPU runner and publishes the result with the static page.&lt;/p&gt;

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

&lt;p&gt;Three things in this project only work because the pieces are open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model can be hammered for free, at exactly the wrong moment.&lt;/strong&gt; Aurora traffic is the spikiest traffic there is: nobody looks for months, then a storm hits and everyone looks within the same ten minutes. Because TabPFN's weights are open, the model runs once every ten minutes in a free CI job and writes a small JSON file that a CDN serves to everyone. A million visitors cost the same as one. There is no API key in a secrets store, no per-call bill and no rate limit that kicks in during the one night that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could change what the model returns, not just what I send it.&lt;/strong&gt; The label-shift correction works on TabPFN's full predictive histogram: I re-weight its bars by how much more often storms appear in the context than in reality. That needs the raw distribution. An endpoint that returns one number would have made the stratified context unusable, and the "one distribution, every latitude" trick impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every claim in this post can be checked.&lt;/strong&gt; The solar wind history is NASA's OMNI data, the index is GFZ's Hp30 (CC BY 4.0), the live feed is NOAA's and the clouds are Open-Meteo's. With open weights on top, &lt;code&gt;python pipeline/evaluate.py&lt;/code&gt; reproduces the numbers above on a laptop or a single consumer GPU. If you think my context is badly chosen, or that a different set of features would do better, you can try it in an afternoon and show me.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of TabPFN&lt;/strong&gt;: TabPFN is the forecaster. It reads the labelled history in context and its full predictive distribution turns into the chance of seeing the aurora at every latitude.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
