<?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: CodersAcademy006</title>
    <description>The latest articles on DEV Community by CodersAcademy006 (@codersacademy006).</description>
    <link>https://dev.to/codersacademy006</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%2F1255300%2Fa1d1ad8f-1435-40e8-913e-a93aa5a15055.jpeg</url>
      <title>DEV Community: CodersAcademy006</title>
      <link>https://dev.to/codersacademy006</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codersacademy006"/>
    <language>en</language>
    <item>
      <title>Every repository is haunted</title>
      <dc:creator>CodersAcademy006</dc:creator>
      <pubDate>Sat, 31 Oct 2026 08:58:47 +0000</pubDate>
      <link>https://dev.to/codersacademy006/every-repository-is-haunted-h9h</link>
      <guid>https://dev.to/codersacademy006/every-repository-is-haunted-h9h</guid>
      <description>&lt;p&gt;There is a file in vLLM that imports a module which does not exist.&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="c1"&gt;# tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_attention_backend.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vllm.attention.backends.placeholder_attn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PlaceholderAttentionBackend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On upstream &lt;code&gt;main&lt;/code&gt; at &lt;code&gt;900b8b08e2&lt;/code&gt;, checked today:&lt;br&gt;
&lt;code&gt;vllm/attention/backends/placeholder_attn.py&lt;/code&gt; was deleted on 24 September 2025,&lt;br&gt;
in commit &lt;code&gt;969b4da3a6&lt;/code&gt;, titled "[V0 Deprecation] Remove placeholder attn (#25510)".&lt;br&gt;
&lt;code&gt;PlaceholderAttentionBackend&lt;/code&gt; is defined nowhere in the repository today. That&lt;br&gt;
line has been importing a module that does not exist for thirteen months.&lt;/p&gt;

&lt;p&gt;Being careful about what that does and does not mean, because I checked and my&lt;br&gt;
first guess was wrong. The file is not orphaned: &lt;code&gt;dummy_platform.py:32&lt;/code&gt; names&lt;br&gt;
&lt;code&gt;vllm_add_dummy_platform.dummy_attention_backend.DummyAttentionBackend&lt;/code&gt; as a&lt;br&gt;
string that vLLM resolves at runtime, and the plugin is pip-installed in a two&lt;br&gt;
GPU CI job. My first instinct was to say this must break that job. Then I looked:&lt;br&gt;
forty one commits have touched the paths that trigger it since the deletion, the&lt;br&gt;
most recent in September, and the line is still there. So either that code path&lt;br&gt;
is never reached or the job does not gate anything, and I cannot tell which&lt;br&gt;
without two L4s.&lt;/p&gt;

&lt;p&gt;What I can tell you, from reading alone, is that the reference is dead. That is&lt;br&gt;
the claim this tool makes, and it is the only claim it makes.&lt;/p&gt;

&lt;p&gt;I did not find it by reading vLLM. I found it by asking a repository who it&lt;br&gt;
still talks to, and listening for the answers that nobody is there to give.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ghost in the Machine
&lt;/h2&gt;

&lt;p&gt;The theme this week is spooky, or honouring the past. I went for both, because&lt;br&gt;
the thing I wanted to build only works if you take the haunting seriously.&lt;/p&gt;

&lt;p&gt;Every repository is built on top of its own dead. Files get deleted, and the&lt;br&gt;
deletion is clean in the only place anyone checks, which is the directory&lt;br&gt;
listing. What does not get cleaned is everything that was pointing at the file:&lt;br&gt;
the import that resolves to nothing, the CI script that still invokes a test&lt;br&gt;
that was removed, the docstring that says "see this other module" about a module&lt;br&gt;
that was buried two years ago. Those references survive. They say a name, and&lt;br&gt;
nothing answers.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;ouija&lt;/strong&gt; finds them, and &lt;strong&gt;ofrenda&lt;/strong&gt; builds them an altar.&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%2Fkw674kr2z10ll4yo9f4x.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%2Fkw674kr2z10ll4yo9f4x.jpg" alt="The ofrenda index, fourteen public repositories and their dead" width="799" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://codersacademy006.github.io/ouija/" rel="noopener noreferrer"&gt;https://codersacademy006.github.io/ouija/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What counts as a ghost
&lt;/h2&gt;

&lt;p&gt;The whole tool is one claim, and I wanted the claim to be the kind that can be&lt;br&gt;
proven wrong:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This exact file is gone. Nothing replaced it. Something still says its name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Getting from the idea to something that survives contact with a real repository&lt;br&gt;
was almost entirely a process of deleting my own false positives. Six of them,&lt;br&gt;
in the order they embarrassed me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was renamed.&lt;/strong&gt; &lt;code&gt;moved_module.py&lt;/code&gt; disappears from the directory, so git calls&lt;br&gt;
it a deletion. A file with that basename exists one folder over. Not dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It became a package.&lt;/strong&gt; &lt;code&gt;tracing.py&lt;/code&gt; is deleted and &lt;code&gt;tracing/__init__.py&lt;/code&gt; appears.&lt;br&gt;
Every &lt;code&gt;from vllm.tracing import ...&lt;/code&gt; still works perfectly. My first run reported&lt;br&gt;
this as a ghost with 29 live references, and the top five results in vLLM were&lt;br&gt;
all the same mistake wearing different names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The name is a substring.&lt;/strong&gt; My first version matched symbol names, so a deleted&lt;br&gt;
&lt;code&gt;ir/value.h&lt;/code&gt; matched the text &lt;code&gt;value.handle&lt;/code&gt; in a test file, and &lt;code&gt;size_t&lt;/code&gt; was&lt;br&gt;
confidently declared dead across the whole of Triton. A reference only counts if&lt;br&gt;
the characters either side of it are not more path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a bulk list.&lt;/strong&gt; TensorRT-LLM reported &lt;strong&gt;234 proven ghosts&lt;/strong&gt;, more than&lt;br&gt;
every other repository in the corpus put together. Four files produced 232 of&lt;br&gt;
them: &lt;code&gt;legacy-files.txt&lt;/code&gt;, &lt;code&gt;ruff-legacy.toml&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;, each enumerating the same long inventory of paths for&lt;br&gt;
tooling to skip. That inventory being full of files that no longer exist is one&lt;br&gt;
piece of stale config. Calling it 232 separate hauntings would be inflating a&lt;br&gt;
headline on purpose. A living file that names ten or more distinct dead files is&lt;br&gt;
a list of them, not a reference to any one of them, and its mentions stopped&lt;br&gt;
counting. That repository has 31.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a changelog.&lt;/strong&gt; This one was subtler. My first full corpus run reported&lt;br&gt;
&lt;strong&gt;77 ghosts in a single repository&lt;/strong&gt;, more than every other repository combined.&lt;br&gt;
All 77 came from &lt;code&gt;docs/daily-updates/2026-04-*.mdx&lt;/code&gt;, a dated log that lists the&lt;br&gt;
pull requests merged each day. A changelog naming a file it deleted is doing&lt;br&gt;
exactly its job. That is a memory, not a haunting. Once dated and archival&lt;br&gt;
documents stopped counting, that repository had four.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a redirect.&lt;/strong&gt; vLLM's &lt;code&gt;mkdocs.yaml&lt;/code&gt; has a line reading&lt;br&gt;
&lt;code&gt;training/routed_experts_replay.md: training/routed_experts.md&lt;/code&gt;. The file on the&lt;br&gt;
left is gone, which is the whole point: the line is the redirect that sends&lt;br&gt;
anyone following an old link to the page that replaced it. That is a deletion&lt;br&gt;
handled properly, and my tool was reading it as a deletion forgotten. A line&lt;br&gt;
that names both a dead file and a living one is pointing at the replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a module path that means something else.&lt;/strong&gt; The worst one, and the last&lt;br&gt;
to go. A python file's import path is a strong signal, so I was also matching&lt;br&gt;
the last two components of it. TensorRT-LLM deleted&lt;br&gt;
&lt;code&gt;.../modeling_v2/catalog/torch/empty.py&lt;/code&gt;, whose last two components are&lt;br&gt;
&lt;code&gt;torch.empty&lt;/code&gt;. That repository was reported as having &lt;strong&gt;twenty three&lt;/strong&gt; dangling&lt;br&gt;
references to it, every one of them a line that allocates a tensor. A module path&lt;br&gt;
now has to be the file's whole path from the repository root, and has to appear&lt;br&gt;
in an import or quoted in prose rather than called.&lt;/p&gt;

&lt;p&gt;I kept the evidence tiering rather than hiding the weak cases. If the surviving&lt;br&gt;
reference carries two or more path segments, or the file's full import path used&lt;br&gt;
as an import path, it can only mean this file, and it is reported as &lt;strong&gt;proven&lt;/strong&gt;.&lt;br&gt;
If only the bare filename survives it is &lt;strong&gt;suspected&lt;/strong&gt;, listed separately, and&lt;br&gt;
never folded into a headline number, because a bare &lt;code&gt;image_processor.py&lt;/code&gt; in a&lt;br&gt;
docstring is as likely to be talking about somebody else's file as about this&lt;br&gt;
repository's.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repository&lt;/th&gt;
&lt;th&gt;proven&lt;/th&gt;
&lt;th&gt;suspected&lt;/th&gt;
&lt;th&gt;files buried&lt;/th&gt;
&lt;th&gt;deletions read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TensorRT-LLM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;319&lt;/td&gt;
&lt;td&gt;696&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1093&lt;/td&gt;
&lt;td&gt;1799&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama.cpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;586&lt;/td&gt;
&lt;td&gt;773&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pytorch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;td&gt;578&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cutlass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1630&lt;/td&gt;
&lt;td&gt;1875&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opensre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;655&lt;/td&gt;
&lt;td&gt;1687&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TensorRT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;622&lt;/td&gt;
&lt;td&gt;924&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;triton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;584&lt;/td&gt;
&lt;td&gt;916&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;numba-cuda&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automodel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;458&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;graphify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TransformerEngine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flash-attention&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;365&lt;/td&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;29 distinct projects, 19 of them with any deletion history at all. 69 proven&lt;br&gt;
ghosts and 34 suspected, out of 6391 files buried for good across 10671&lt;br&gt;
deletions. 6 projects came back clean.&lt;/p&gt;

&lt;p&gt;The sweep ran against local clones. For any repository where my checkout carries&lt;br&gt;
my own branches, the numbers above come from a throwaway worktree at that&lt;br&gt;
project's upstream default branch instead, so every line quoted below is one&lt;br&gt;
anybody can go and read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://codersacademy006.github.io/ouija/" rel="noopener noreferrer"&gt;Browse the altar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three worth reading:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vLLM.&lt;/strong&gt; The dead import at the top of this post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TensorRT-LLM&lt;/strong&gt;, and a correction I had to make to myself. My clone of it sits&lt;br&gt;
on an older branch of my own, and there the tool found three example scripts&lt;br&gt;
opening with &lt;code&gt;from tensorrt_llm.builder import Builder&lt;/code&gt; against a deleted&lt;br&gt;
module. A good finding, except that upstream had already deleted those example&lt;br&gt;
scripts too. They were ghosts in my checkout, not in the project.&lt;/p&gt;

&lt;p&gt;What is still there on upstream &lt;code&gt;main&lt;/code&gt; at &lt;code&gt;6adeccc026&lt;/code&gt; is quieter and I like it&lt;br&gt;
more. Line 42 of &lt;code&gt;.github/CODEOWNERS&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;/tensorrt_llm/builder.py @NVIDIA/trt-llm-devs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule routing review of a file that does not exist. Two live files also carry&lt;br&gt;
the comment &lt;code&gt;# Modified from tensorrt_llm/builder.py _init_max_seq_len&lt;/code&gt;, which&lt;br&gt;
is now a pointer to nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triton&lt;/strong&gt;, on upstream &lt;code&gt;main&lt;/code&gt; at &lt;code&gt;0861b90f69&lt;/code&gt;. &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; still&lt;br&gt;
defines a hook that regenerates &lt;code&gt;.github/workflows/integration-tests.yml&lt;/code&gt; from&lt;br&gt;
an &lt;code&gt;.in&lt;/code&gt; file. Both the workflow and the &lt;code&gt;.in&lt;/code&gt; file were deleted in April 2025,&lt;br&gt;
in "[REPO] modularize worklfows (#6582)". The hook is still in the config, and&lt;br&gt;
it survives precisely because it is broken: its &lt;code&gt;files:&lt;/code&gt; pattern can never&lt;br&gt;
match, so it never runs, so it never fails.&lt;/p&gt;
&lt;h2&gt;
  
  
  The model writes prose, and nothing else
&lt;/h2&gt;

&lt;p&gt;Each ghost gets a two-sentence epitaph from &lt;code&gt;gemma3:4b&lt;/code&gt;, running locally through&lt;br&gt;
Ollama. Every path, date, commit hash, line number and count on the page is read&lt;br&gt;
from git by the detector. The model never sees a number.&lt;/p&gt;

&lt;p&gt;That was not the original design. It is the result of catching it lying.&lt;/p&gt;

&lt;p&gt;The first prompt included the burial date. &lt;code&gt;gemma3:4b&lt;/code&gt; took a file buried on the&lt;br&gt;
eighteenth of June and eulogised it as having vanished "on the sixth of June". It&lt;br&gt;
had not been asked to mention a date at all. The fix was not a better&lt;br&gt;
instruction. The fix was taking the date out of the prompt, so there was nothing&lt;br&gt;
left for it to get wrong. It now receives the commit subject with the digits&lt;br&gt;
stripped out, the kinds of files that still mention the dead one, and nothing&lt;br&gt;
else.&lt;/p&gt;

&lt;p&gt;Two guards survive on every epitaph: no digits, and no month or weekday names,&lt;br&gt;
since it is never given a date. Across the whole published site, 46 epitaphs&lt;br&gt;
were requested and &lt;strong&gt;one&lt;/strong&gt; tripped a guard. It is printed on the page, flagged&lt;br&gt;
in red, next to the epitaph that broke the rule. Regenerating it quietly until&lt;br&gt;
it behaved would have made the page prettier and the claim worthless.&lt;/p&gt;
&lt;h3&gt;
  
  
  A small model given one prompt repeats itself
&lt;/h3&gt;

&lt;p&gt;The first batch of epitaphs was unusable for a different reason. Eight ghosts,&lt;br&gt;
eight calls, and five of them opened with the words "It rests here". Measured&lt;br&gt;
properly, eight epitaphs produced &lt;strong&gt;3 distinct openings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fix is one line of prompt. There are eight opening instructions, and each&lt;br&gt;
ghost gets one chosen by hashing its file path, so a given file always receives&lt;br&gt;
the same instruction and the output stays reproducible. Same model, same&lt;br&gt;
temperature, same everything else:&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;distinct openings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;one prompt for every ghost&lt;/td&gt;
&lt;td&gt;3 of 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rotated opening instruction&lt;/td&gt;
&lt;td&gt;7 of 8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;bench_angles.py&lt;/code&gt; runs both arms back to back and writes every raw epitaph to&lt;br&gt;
&lt;code&gt;bench_angles.json&lt;/code&gt;, so the table above can be checked rather than believed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sweeping a whole laptop without losing the sweep
&lt;/h2&gt;

&lt;p&gt;Fifty clones on one laptop, twenty nine distinct projects, is a long job, and&lt;br&gt;
&lt;code&gt;ray&lt;/code&gt; alone (thirty one thousand commits, seven and a half thousand deleted&lt;br&gt;
files) takes a larger share of it than everything else put together. The first&lt;br&gt;
version was a shell loop, and I killed it twice to fix detection bugs. Both&lt;br&gt;
times I threw away everything it had already done, including a very expensive&lt;br&gt;
&lt;code&gt;ray&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the corpus run is a &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow. One repository&lt;br&gt;
is one activity. Temporal records each completed activity in the workflow&lt;br&gt;
history, so a worker that dies is replaced by one that replays the history and&lt;br&gt;
continues from where the last one stopped.&lt;/p&gt;

&lt;p&gt;That is a claim worth testing rather than asserting, so &lt;code&gt;durability_test.sh&lt;/code&gt;&lt;br&gt;
does it: it waits for the sweep to get going, sends the worker &lt;code&gt;SIGKILL&lt;/code&gt; with no&lt;br&gt;
chance to save anything, starts a fresh one, and checks the finished count only&lt;br&gt;
ever goes up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before kill:      8 repositories finished
SIGKILL sent to worker pid 11807 at 07:00:12
with no worker:   query unanswerable (), workflow state is on the server
new worker:       pid 12098
after restart:    10 repositories finished
RESULT: resumed at repository 9, the first 8 were not scanned again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the worker is dead, the progress query comes back&lt;br&gt;
&lt;code&gt;no poller seen for task queue recently, worker may be down&lt;/code&gt;. The work is not in&lt;br&gt;
the worker. The worker is just the thing holding the shovel.&lt;/p&gt;

&lt;p&gt;Building that found a real bug of my own. The activity declared a five minute&lt;br&gt;
heartbeat timeout and never sent a heartbeat, so any repository slower than five&lt;br&gt;
minutes would be abandoned and retried until the workflow failed. &lt;code&gt;ray&lt;/code&gt; is very&lt;br&gt;
much slower than five minutes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why open matters here
&lt;/h2&gt;

&lt;p&gt;This tool reads your git history. Not your public repositories, your real ones:&lt;br&gt;
the private monorepo, the client work, the thing you have not shipped yet. It&lt;br&gt;
walks the whole deletion record, pulls the last living version of every buried&lt;br&gt;
file, and greps the entire working tree.&lt;/p&gt;

&lt;p&gt;Sending all of that to a hosted API so a model can write a nice sentence about&lt;br&gt;
ghosts would be an absurd trade. The valuable half of this tool, the detection,&lt;br&gt;
has no model in it at all. It is &lt;code&gt;git&lt;/code&gt; and the Python standard library, nothing&lt;br&gt;
installed, nothing to import, no key. &lt;code&gt;--no-model&lt;/code&gt; gives you the complete&lt;br&gt;
findings with nothing loaded.&lt;/p&gt;

&lt;p&gt;The prose half is 3.3 GB on disk and takes about seven seconds per epitaph on an&lt;br&gt;
M-series laptop with no GPU. It runs on a plane. That is the whole point: the&lt;br&gt;
part that touches your source never leaves the machine, and the part that writes&lt;br&gt;
is small enough to come with you.&lt;/p&gt;

&lt;p&gt;That cuts both ways, and it caught me out. I built the site, went to push it,&lt;br&gt;
and my own tooling refused on the grounds that I was about to publish private&lt;br&gt;
data. It was right. The pages quote real paths and real lines, and the sweep had&lt;br&gt;
read repositories that are nobody's business but mine. Worse, a file listing&lt;br&gt;
every project folder on this laptop was already committed.&lt;/p&gt;

&lt;p&gt;So publishing has three rules now, each of which fails closed. Only repositories&lt;br&gt;
confirmed public. Never my own clones, because a clone of my own public&lt;br&gt;
repository can still hold a branch I have not pushed. And for a third-party&lt;br&gt;
repository where my checkout carries my branches, scan a throwaway worktree at&lt;br&gt;
upstream's default branch instead of my working copy. Fourteen repositories made&lt;br&gt;
it onto the site. Fifteen did not.&lt;/p&gt;

&lt;p&gt;A tool that reads everything has to be careful about what it says out loud, and&lt;br&gt;
that is far easier to get right when the model is on the same machine as the&lt;br&gt;
secrets rather than on the other end of a request.&lt;/p&gt;

&lt;p&gt;There is a quieter reason too. A tool about things that outlive their&lt;br&gt;
infrastructure should not itself depend on an endpoint that might be gone in&lt;br&gt;
three years. An ofrenda built on an API that gets deprecated is a joke with a&lt;br&gt;
punchline I did not want to be in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/CodersAcademy006/ouija
&lt;span class="nb"&gt;cd &lt;/span&gt;ouija

python3 ouija.py /path/to/your/repo              &lt;span class="c"&gt;# the findings, no model&lt;/span&gt;
python3 ouija.py /path/to/your/repo &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ghosts.json
python3 ofrenda.py ghosts.json &lt;span class="nt"&gt;-o&lt;/span&gt; ofrenda.html   &lt;span class="c"&gt;# epitaphs, needs ollama&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Python 3.9+, &lt;code&gt;git&lt;/code&gt;, and &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt; if you want the altar. No pip&lt;br&gt;
install. There is nothing to install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 test_ouija.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;builds a throwaway repository containing one real ghost and three things that&lt;br&gt;
only look like ghosts, and asserts it finds exactly the one.&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%2F60flatnpdfssqkfmu6se.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%2F60flatnpdfssqkfmu6se.jpg" alt="The vLLM ghosts, each with its epitaph and the lines that still name it" width="799" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;It finds dangling references to deleted &lt;strong&gt;files&lt;/strong&gt;. It does not find dangling&lt;br&gt;
references to deleted &lt;strong&gt;symbols&lt;/strong&gt; inside files that still exist, which is the&lt;br&gt;
larger and harder problem, and the one my first version failed at so badly that&lt;br&gt;
it declared &lt;code&gt;size_t&lt;/code&gt; dead.&lt;/p&gt;

&lt;p&gt;It will not tell you a ghost is harmful. The vLLM import is a real broken import.&lt;br&gt;
The Triton hook is a leftover that cannot fire. A docstring pointing at a&lt;br&gt;
deleted module is just stale. Those are three different severities and the tool&lt;br&gt;
reports all three the same way, because deciding which of them matters in your&lt;br&gt;
repository is not something a filename can tell you.&lt;/p&gt;

&lt;p&gt;And it only sees what git saw. A file that was never committed was never buried,&lt;br&gt;
and leaves no ghost.&lt;/p&gt;




&lt;p&gt;Every repository that has lived long enough is standing on files it does not&lt;br&gt;
remember deleting. Most of them went quietly. Go and find the ones that did not.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/CodersAcademy006/ouija" rel="noopener noreferrer"&gt;https://github.com/CodersAcademy006/ouija&lt;/a&gt;&lt;br&gt;
Licence: MIT&lt;/p&gt;

&lt;p&gt;Built for the Hacktoberfest Open-Source AI Challenge: Week 4, Ghost in the Machine.&lt;/p&gt;

&lt;p&gt;Entering &lt;strong&gt;Best Use of Gemma&lt;/strong&gt;, because &lt;code&gt;gemma3:4b&lt;/code&gt; writes every epitaph locally&lt;br&gt;
and nothing else writes them, and &lt;strong&gt;Best Use of Temporal&lt;/strong&gt;, because the corpus&lt;br&gt;
sweep is a durable workflow and the kill test above is the receipt.&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>GraphSloth: auditing 29 abandoned project folders so I don't have to</title>
      <dc:creator>CodersAcademy006</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:12:42 +0000</pubDate>
      <link>https://dev.to/codersacademy006/graphsloth-auditing-29-abandoned-project-folders-so-i-dont-have-to-3elo</link>
      <guid>https://dev.to/codersacademy006/graphsloth-auditing-29-abandoned-project-folders-so-i-dont-have-to-3elo</guid>
      <description>&lt;h1&gt;
  
  
  GraphSloth: auditing 29 abandoned project folders so I don't have to
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://codersacademy006.github.io/graphsloth/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt; · &lt;a href="https://github.com/CodersAcademy006/graphsloth" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;My &lt;code&gt;~/Desktop/Project&lt;/code&gt; folder has 29 project folders in it, about 95GB total. I had no idea which ones were safe to archive and which ones I'd regret deleting. Auditing that by hand is exactly the kind of task International Sloth Day exists for, so I built something to do it instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphSloth&lt;/strong&gt; scans a folder of projects and, for each one, pulls real facts: disk size, git commit age, and (for git repos) a real &lt;a href="https://github.com/Graphify-Labs/graphify" rel="noopener noreferrer"&gt;Graphify&lt;/a&gt; structural extraction — real tree-sitter AST parsing, zero LLM cost — giving node/edge counts and the most-referenced symbol. A local Gemma model then reads those facts and returns a verdict: ACTIVE, DORMANT, or ARCHIVE-CANDIDATE, with a one-sentence reason that has to cite the actual numbers, not invent anything.&lt;/p&gt;

&lt;p&gt;Nothing gets deleted automatically. The output is a static report, and an optional &lt;code&gt;--apply&lt;/code&gt; flag only moves archive candidates into a sibling &lt;code&gt;_Archive/&lt;/code&gt; folder, one at a time, asking before every move.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29974om3ykua3gp6ypc8.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%2F29974om3ykua3gp6ypc8.png" alt="GraphSloth report: 29 folders audited, 29.4G reclaimable, 9 flagged for manual review" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Real run against my actual Desktop: 29 folders audited, 10 flagged ARCHIVE-CANDIDATE (29.4G reclaimable if I trust them), 16 ACTIVE, 3 DORMANT. 9 of those were flagged with real deploy signals and automatically kept out of the archive pile, including my own live e-commerce store and a production exam-scoring backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caught Lying to Itself
&lt;/h2&gt;

&lt;p&gt;All three of these were found by running GraphSloth on my own live, production folders, not a test suite, and each one got fixed the same day I found it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A live store flagged safe to archive.&lt;/strong&gt; Srijan Saree, a real store with real orders, came back as ARCHIVE-CANDIDATE. The deploy-signal check only looked at a folder's top level; the store's actual code and its &lt;code&gt;.vercel&lt;/code&gt; config sat one folder down. Fixed by checking one level into subfolders when the top level isn't a git repo itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One busy repo almost spoke for 34.&lt;/strong&gt; OSS, 26.8G holding 34 separate git clones, was one busy clone away from being called ACTIVE as a whole. My first nested-repo fix picked whichever single clone was touched most recently and judged the entire 26.8G folder by it — only 10 of the 34 clones had actually moved in the last 30 days. Fixed by detecting a multi-repo container and reporting the real distribution instead of one point sample, with a hard cap: a container with stale repos among its recent ones is never called ACTIVE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A raw exception almost shipped as a verdict.&lt;/strong&gt; A single Ollama timeout, right after a restart, nearly produced a report with the literal text &lt;code&gt;[Gemma call failed after 3 attempts: timed out]&lt;/code&gt; sitting in a folder's "reason" field. Turned out the model emits a chain-of-thought before its answer, and a complex fact block pushed that past a 300-second timeout on CPU-only inference. Fixed by passing &lt;code&gt;think: false&lt;/code&gt; and capping &lt;code&gt;num_predict&lt;/code&gt; — the same call that used to hang indefinitely now returns in single-digit seconds — plus a check that catches and retries that shape of error before anything publishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It Actually Died
&lt;/h2&gt;

&lt;p&gt;Partway through the real run, the OS killed the process. Not a bug in my code — Gemma sits at about 8GB resident in RAM, and my laptop ran low on memory in the background while the audit was running. 27 of 29 folders were already done, but they only existed as text in a log file. I had to write a second script just to parse completed verdicts back out of stderr output.&lt;/p&gt;

&lt;p&gt;That's exactly the problem &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflows exist to solve, so I rebuilt the audit loop as one. &lt;code&gt;temporal_audit.py&lt;/code&gt; runs the same per-folder audit, but each folder's result is a durable activity instead of a line in a Python list. I proved it, not just claimed it: started a 5-folder run, let 2 finish, &lt;code&gt;kill -9&lt;/code&gt;'d the worker process mid-activity, confirmed with &lt;code&gt;temporal workflow describe&lt;/code&gt; that the workflow was still alive on the server with its 2 results intact, restarted the worker, and watched it pick up from folder 3 instead of starting over.&lt;/p&gt;

&lt;p&gt;(Temporal's workflow code has to be deterministic, so it forbids filesystem and subprocess access in the workflow definition itself. That meant splitting the workflow logic from the activity implementation into two files: &lt;code&gt;ta_workflow.py&lt;/code&gt; stays sandboxed and clean, &lt;code&gt;ta_activities.py&lt;/code&gt; does the real I/O and is only imported by the worker process.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug I Almost Shipped
&lt;/h2&gt;

&lt;p&gt;My first version scored folders mostly on git commit age. That's wrong for anything that's deployed and stable: a live backend I rarely need to touch would look just as "stale" as something I actually abandoned a year ago. Several of my own folders are live production services, and under the first version they were one unlucky LLM response away from getting flagged for archiving.&lt;/p&gt;

&lt;p&gt;So GraphSloth checks for cheap, local, no-network deploy signals — &lt;code&gt;vercel.json&lt;/code&gt;, &lt;code&gt;.vercel&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, &lt;code&gt;render.yaml&lt;/code&gt;, &lt;code&gt;fly.toml&lt;/code&gt;, a live URL in the README — before Gemma ever sees the folder, and the result is a hard rule in code, not something I'm hoping the model remembers: &lt;strong&gt;a folder with any deploy signal can never come back as ARCHIVE-CANDIDATE.&lt;/strong&gt; It's capped at DORMANT and shown with a visible warning instead. This isn't proof a service is actually live, so the flag means "check this yourself," never "it's definitely fine."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero cost.&lt;/strong&gt; Graphify's extraction is local tree-sitter parsing with no API key. Gemma runs locally through Ollama. Auditing dozens of gigabytes of my own code costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; Several of these folders are private client work. None of their contents leave my laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real evidence beats guessing.&lt;/strong&gt; Most "clean up your projects" advice is pure vibes based on file dates. Grounding the verdict in an actual structural graph of the code is the same idea that worked for &lt;a href="https://github.com/CodersAcademy006/onegraph" rel="noopener noreferrer"&gt;OneGraph&lt;/a&gt;, an MLH Hack Day project I built the week before this challenge: an LLM reasoning over real graph facts is more trustworthy than an LLM reasoning over nothing.&lt;/li&gt;
&lt;/ul&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/CodersAcademy006" rel="noopener noreferrer"&gt;
        CodersAcademy006
      &lt;/a&gt; / &lt;a href="https://github.com/CodersAcademy006/graphsloth" rel="noopener noreferrer"&gt;
        graphsloth
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Graphify + local Gemma 4 (+ Temporal) audit of abandoned project folders, Hacktoberfest Week 3
    &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;GraphSloth&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://codersacademy006.github.io/graphsloth/" rel="nofollow noopener noreferrer"&gt;Live demo →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A lazy-friendly audit of your abandoned project folders. Built for the Hacktoberfest Open-Source AI Challenge: Week 3 ("Slow and Steady").&lt;/p&gt;
&lt;p&gt;Most developers have a &lt;code&gt;~/Desktop/Project&lt;/code&gt; folder full of things they built, half-finished, and never looked at again. Deciding what's safe to archive is tedious enough that nobody does it. GraphSloth does it for you, overnight, and cites real evidence instead of guessing.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/CodersAcademy006/graphsloth/report_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%2FCodersAcademy006%2Fgraphsloth%2FHEAD%2Freport_screenshot.png" alt="GraphSloth report: 29 folders audited, 29.4G reclaimable, 9 flagged for manual review"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Real run against the author's own Desktop: &lt;strong&gt;29 folders audited, 10 flagged ARCHIVE-CANDIDATE (29.4G reclaimable), 16 ACTIVE, 3 DORMANT.&lt;/strong&gt; 9 of those were flagged with real deploy signals and kept out of the archive pile automatically, including a live e-commerce store and a production exam-scoring backend.&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;p&gt;For each top-level folder:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Disk size&lt;/strong&gt; — &lt;code&gt;du -sk&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git facts&lt;/strong&gt; — last commit age, branch, dirty state. Checked one level into subfolders too, for folders that &lt;em&gt;hold&lt;/em&gt; a git repo rather than being…&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/CodersAcademy006/graphsloth" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;Best Use of Gemma, Best Use of Temporal.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>OneGraph: grounding Gemma 4's code reviews in a real dependency graph instead of a guess</title>
      <dc:creator>CodersAcademy006</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:12:55 +0000</pubDate>
      <link>https://dev.to/codersacademy006/onegraph-grounding-gemma-4s-code-reviews-in-a-real-dependency-graph-instead-of-a-guess-3949</link>
      <guid>https://dev.to/codersacademy006/onegraph-grounding-gemma-4s-code-reviews-in-a-real-dependency-graph-instead-of-a-guess-3949</guid>
      <description>&lt;h1&gt;
  
  
  OneGraph: grounding Gemma 4's code reviews in a real dependency graph instead of a guess
&lt;/h1&gt;

&lt;p&gt;An LLM code review tool that reads a diff in isolation has a blind spot: it cannot see what else in the repository depends on the lines that changed. It can comment on the diff, but it cannot tell you the function you just touched is called from nineteen other places, three of them in a different service. That is not a hallucination in the usual sense, it is a coverage gap, and it is the gap &lt;a href="https://dev.to/stevengonsalvez/code-review-graph-stop-your-agent-reading-the-whole-repo-4272"&gt;code-review-graph&lt;/a&gt; and the knowledge-graph-over-vector-search approach in &lt;a href="https://dev.to/kenimo49/i-replaced-grep-based-code-review-with-a-knowledge-graph-mcp-here-are-3-bugs-vector-search-3djn"&gt;this post about replacing grep-based review&lt;/a&gt; both set out to close, each in their own way.&lt;/p&gt;

&lt;p&gt;OneGraph is my attempt at the same problem, built for Hacktoberfest Hack Day Pune x GDG on Campus VU. It pairs &lt;a href="https://github.com/Graphify-Labs/graphify" rel="noopener noreferrer"&gt;Graphify&lt;/a&gt;, a tree-sitter based dependency graph extractor, with Gemma 4, and wires the result directly into &lt;a href="https://github.com/qodo-ai/pr-agent" rel="noopener noreferrer"&gt;PR-Agent&lt;/a&gt; as a live &lt;code&gt;/graph_risk&lt;/code&gt; command. There's also a related post worth reading if you want the extraction side in more depth: &lt;a href="https://dev.to/mech_app_ai/graphify-ast-based-knowledge-graphs-for-agent-code-navigation-2d33"&gt;Graphify: AST-Based Knowledge Graphs for Agent Code Navigation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/CodersAcademy006/onegraph" rel="noopener noreferrer"&gt;https://github.com/CodersAcademy006/onegraph&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live demos:&lt;/strong&gt; &lt;a href="https://graphs-site-omega.vercel.app/" rel="noopener noreferrer"&gt;https://graphs-site-omega.vercel.app/&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live in a real PR:&lt;/strong&gt; &lt;a href="https://github.com/CodersAcademy006/onegraph-flask-demo/pull/1" rel="noopener noreferrer"&gt;https://github.com/CodersAcademy006/onegraph-flask-demo/pull/1&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%2Fpw8he4nnabolcp17wkgw.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%2Fpw8he4nnabolcp17wkgw.png" alt="OneGraph landing page, dark theme with an ambient node graph in the hero" width="800" height="777"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The split that makes this work
&lt;/h2&gt;

&lt;p&gt;Everything in OneGraph follows one rule: &lt;strong&gt;code owns the facts, Gemma owns the words.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Graphify extracts.&lt;/strong&gt; &lt;code&gt;graphify extract --code-only&lt;/code&gt; runs tree-sitter over the repo and builds every call, import and containment edge into a real graph. No LLM call, no API key, $0 to build, deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graphify labels.&lt;/strong&gt; &lt;code&gt;graphify label&lt;/code&gt; runs community detection (Leiden/Louvain) and names every cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 narrates.&lt;/strong&gt; It reads the graph traversal result, not the raw source, and writes the plain-English explanation. It never computes risk, it never counts dependents, it only describes what the graph already proved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR-Agent posts it.&lt;/strong&gt; A &lt;code&gt;/graph_risk&lt;/code&gt; comment on any pull request, with a risk level computed from a real blast-radius count and a link to the full interactive graph.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The risk level is arithmetic, not a model's opinion of its own severity. A self-rated "this feels risky" from an LLM is noise; a count of how many real call sites a changed function has is a fact.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it looks like on a real PR
&lt;/h2&gt;

&lt;p&gt;I ran this against a real demo PR, not a constructed example, and verified the result through the GitHub API before trusting it:&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%2Fo9qe557apj4op91ktuqf.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%2Fo9qe557apj4op91ktuqf.png" alt="The posted /graph_risk review on a real PR: Risk High, a dependents table, and the analysis paragraph" width="800" height="558"&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%2F6c5kjp2k6gw3iz6rgpqx.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%2F6c5kjp2k6gw3iz6rgpqx.png" alt="An inline comment anchored to the exact changed line, listing real downstream dependents" width="776" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The comment text comes from a small f-string template in &lt;code&gt;pr_graph_risk.py&lt;/code&gt;:&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="n"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Risk: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;**, full repo graph: **&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node_count&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; nodes / &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;edge_count&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; edges** &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;real_time_ms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms, $0 LLM cost to build)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Affects: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;affects&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gemma_analysis&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Graphify&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s own viewer: force layout, community clusters, click any node.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;graph_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything numeric in that string comes from &lt;code&gt;graph.json&lt;/code&gt;, computed before Gemma ever sees a prompt. Gemma only fills in &lt;code&gt;gemma_analysis&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring into an existing tool, not rebuilding it
&lt;/h2&gt;

&lt;p&gt;PR-Agent already has a tool registry. Adding &lt;code&gt;/graph_risk&lt;/code&gt; is two lines:&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="c1"&gt;# pr_agent/tools/pr_graph_risk.py registers itself here
&lt;/span&gt;&lt;span class="n"&gt;command2class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;graph_risk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PRGraphRisk&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;I did the same thing for &lt;a href="https://github.com/AsyncFuncAI/deepwiki-open" rel="noopener noreferrer"&gt;deepwiki-open&lt;/a&gt;: one environment variable, &lt;code&gt;ONEGRAPH_BACKEND=graphify&lt;/code&gt;, gates &lt;code&gt;research_chat()&lt;/code&gt;'s context source. Unset, the original embedding-RAG path runs untouched. Both diffs are in the repo under &lt;code&gt;integrations/&lt;/code&gt;, each under 40 lines against the upstream file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real scale, not a toy demo
&lt;/h2&gt;

&lt;p&gt;I ran Graphify against four public repos end to end, nothing trimmed, nothing simulated. Every number below is read directly from the extracted &lt;code&gt;graph.json&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Nodes&lt;/th&gt;
&lt;th&gt;Edges&lt;/th&gt;
&lt;th&gt;Communities&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://graphs-site-omega.vercel.app/graphs/showcase-flask.html" rel="noopener noreferrer"&gt;pallets/flask&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2,151&lt;/td&gt;
&lt;td&gt;3,569&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://graphs-site-omega.vercel.app/graphs/megatron-lm.html" rel="noopener noreferrer"&gt;NVIDIA/Megatron-LM&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;48,295&lt;/td&gt;
&lt;td&gt;133,164&lt;/td&gt;
&lt;td&gt;1,275&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://graphs-site-omega.vercel.app/graphs/graphify-self.html" rel="noopener noreferrer"&gt;Graphify-Labs/graphify&lt;/a&gt; (the sponsor tool, graphing itself)&lt;/td&gt;
&lt;td&gt;18,552&lt;/td&gt;
&lt;td&gt;42,018&lt;/td&gt;
&lt;td&gt;865&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://graphs-site-omega.vercel.app/graphs/supabase.html" rel="noopener noreferrer"&gt;supabase/supabase&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;65,361&lt;/td&gt;
&lt;td&gt;247,294&lt;/td&gt;
&lt;td&gt;1,400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All four are live, clickable, force-directed graphs, not screenshots. Supabase's 65k-node graph loads and stays interactive because Graphify's exporter aggregates above a node-limit into community clusters for rendering, while &lt;code&gt;graph_stats()&lt;/code&gt; still reports the true full count underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest finding from the A/B test
&lt;/h2&gt;

&lt;p&gt;I ran the same question against the project both with and without the graph as context, to see whether grounding actually changes anything. The tempting headline would be "zero hallucinated citations," but that is not what the data showed: the embedding-RAG baseline did not hallucinate either, both runs were 100% citation-accurate on the files they cited.&lt;/p&gt;

&lt;p&gt;The real difference was coverage. The graph-grounded answer found and cited more of the actually-relevant call sites, because it could traverse real edges instead of ranking chunks by embedding distance. This matches what the vector-search-replacement post above found independently: vector search returns what is semantically similar, a graph traversal returns what is actually connected. Those are not the same question, and for a dependency question, only one of them is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs to run
&lt;/h2&gt;

&lt;p&gt;Instead of estimating a token price, I measured one real Gemma 4 call end to end, served locally through Ollama (&lt;code&gt;gemma4:12b-it-q4_K_M&lt;/code&gt;), no API key, no cloud bill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wall time: 132.8s
prompt_eval_count (input tokens): 2,438
eval_count (output tokens): 1,658
context sent: 6,697 characters
cost: $0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the real number for one &lt;code&gt;/graph_risk&lt;/code&gt; run on a local machine, not a projection.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug the subprocess wrapper hid
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;core.py&lt;/code&gt; wraps the &lt;code&gt;graphify&lt;/code&gt; CLI with &lt;code&gt;subprocess.run(..., cwd=str(path))&lt;/code&gt;. Early on I passed a relative &lt;code&gt;Path&lt;/code&gt; into &lt;code&gt;export_graph_html()&lt;/code&gt; and got a bare &lt;code&gt;CalledProcessError&lt;/code&gt; with nothing useful in stderr. The cause: a relative path gets re-resolved against the changed &lt;code&gt;cwd&lt;/code&gt; inside the subprocess, so the graph-json argument silently points somewhere that does not exist. Running the underlying &lt;code&gt;graphify export html&lt;/code&gt; command directly, outside the wrapper, worked fine and gave it away. The fix is one &lt;code&gt;.resolve()&lt;/code&gt; call at every site that calls into the wrapper. It is a small bug, but it is the kind that looks like a Graphify problem until you actually trace where the path gets rewritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own repo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./onegraph &amp;lt;github-url&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;slug]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clones, extracts, labels, exports, deploys, and prints the live URL. For a judge who wants to run it on their own repository right there during review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source mattered for this build
&lt;/h2&gt;

&lt;p&gt;Graphify's extraction is $0 because it never calls an LLM to understand structure, tree-sitter already knows the grammar. Gemma running locally through Ollama means the narration step also costs nothing per call, which matters for a tool meant to run on every PR, not just a demo. And because PR-Agent and deepwiki-open are both open, the two-line and one-env-var integration points above are things anyone can verify by reading the actual diff, not something I am asking you to take on faith.&lt;/p&gt;

&lt;p&gt;Full README, architecture, and every screenshot above: &lt;a href="https://github.com/CodersAcademy006/onegraph" rel="noopener noreferrer"&gt;https://github.com/CodersAcademy006/onegraph&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>RaktSetu: blood for an emergency patient in minutes, without the phone tree</title>
      <dc:creator>CodersAcademy006</dc:creator>
      <pubDate>Sat, 17 Oct 2026 19:02:49 +0000</pubDate>
      <link>https://dev.to/codersacademy006/raktsetu-blood-for-an-emergency-patient-in-minutes-without-the-phone-tree-84h</link>
      <guid>https://dev.to/codersacademy006/raktsetu-blood-for-an-emergency-patient-in-minutes-without-the-phone-tree-84h</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 2026 Week 2 challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;When a patient in an Indian hospital needs blood urgently, someone at the blood bank desk picks up a phone. They call the bank down the road, then another, then start working through a list of donors who may or may not answer, may or may not be eligible, and may or may not be anywhere close. India has more than 28 lakh donors registered on the government's e-RaktKosh platform, and almost none of them are reachable this way at 2 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RaktSetu replaces that phone tree.&lt;/strong&gt; A nurse shares the patient's location and answers four questions: what is needed (whole blood, red cells, platelets or plasma), the blood group, how many units, and how urgent. Or they type it the way they would say it: &lt;em&gt;"O+ blood required, 2 units, Andheri, urgent accident case."&lt;/em&gt; From there RaktSetu:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reserves what nearby blood banks already have on the shelf&lt;/strong&gt;, nearest first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruits donors only for the shortfall&lt;/strong&gt;, in batches, nearest and eligible first, widening the search when nobody answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tells each donor exactly where to go&lt;/strong&gt;: hospital, a map link, and the number to call, plus a private page where they can say yes, no, or reply in their own words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stands everyone else down&lt;/strong&gt; the moment the request is covered.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Who pays:&lt;/strong&gt; hospitals and blood banks, as a monthly subscription. What they are buying is staff time during the worst ten minutes of someone's day. Donors and patients never pay anything; blood donation in India is voluntary by law, and the product is built around that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The spark:&lt;/strong&gt; a hackathon pitch called &lt;a href="https://www.youtube.com/watch?v=okJqVvkQTQ8" rel="noopener noreferrer"&gt;RaktKhoj&lt;/a&gt; (team Blood Boys, &lt;em&gt;Build What Moves India&lt;/em&gt;) laid out this exact problem, including the fact that 12% of Indian districts are more than 100 km from the nearest blood bank. RaktSetu is my own independent build on that problem statement, with its own name, code and architecture.&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%2Fi8a3xp9z8cysa9u3e7j7.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%2Fi8a3xp9z8cysa9u3e7j7.png" alt="The live request view: units reserved from a bank, a donor on the way with an ETA, and a reply that needs a call" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://answered-buyer-latin-underground.trycloudflare.com" rel="noopener noreferrer"&gt;https://answered-buyer-latin-underground.trycloudflare.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo runs from my laptop through a temporary tunnel. If it happens to be offline when you visit, the screenshots here and the repo show the full flow.&lt;/p&gt;

&lt;p&gt;To try it: fill in any hospital name and number, tap &lt;strong&gt;Use demo location (Andheri, Mumbai)&lt;/strong&gt;, answer the four cards, and press &lt;strong&gt;Dispatch&lt;/strong&gt;. Then scroll to the &lt;strong&gt;Donor simulator&lt;/strong&gt; and either answer for a donor or tap &lt;strong&gt;Open donor page&lt;/strong&gt; to see exactly what they would get on their phone. Try typing a reply like &lt;em&gt;"haan aa raha hoon, 20 min mein"&lt;/em&gt; or &lt;em&gt;"can I come after 6pm?"&lt;/em&gt; and watch where it lands on the hospital side. Each batch gets 60 seconds before the search moves on.&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%2F5whbyu17nyl0af6q29n6.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%2F5whbyu17nyl0af6q29n6.png" alt="The intake deck: one question at a time, tap to answer, swipe back to change" width="800" height="513"&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/CodersAcademy006" rel="noopener noreferrer"&gt;
        CodersAcademy006
      &lt;/a&gt; / &lt;a href="https://github.com/CodersAcademy006/raktsetu" rel="noopener noreferrer"&gt;
        raktsetu
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Emergency blood-donor dispatch console — Gemma + Temporal + MongoDB Atlas (Hacktoberfest 2026)
    &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;RaktSetu&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Emergency blood dispatch for hospital and blood-bank staff. Built for the
Hacktoberfest 2026 "Side Hustle" DEV Challenge.&lt;/p&gt;
&lt;p&gt;Staff share the patient's location and answer four questions (component
blood group, units, urgency), or describe the need in a sentence. RaktSetu
then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reserves units from nearby blood banks first&lt;/strong&gt; (within 10 km, exact
group before other compatible groups, each unit taken by an atomic
conditional decrement so two requests never claim the same one).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruits donors only for the shortfall&lt;/strong&gt;, through a durable Temporal
workflow: batches of eligible, compatible donors nearest first, every
donor at a radius before widening (5, 10, 25, 50 km), early wake-up when
a whole batch has answered, and a hard stop when nobody is left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tells donors exactly where to go&lt;/strong&gt;: hospital, map link, callback
number, and a private link to a donor page where they accept, decline
or reply in their own words.&lt;/li&gt;
&lt;li&gt;…&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/CodersAcademy006/raktsetu" 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;The design rule I held myself to: &lt;strong&gt;the open model does the language work, and every medical or safety decision stays in plain, testable code.&lt;/strong&gt; Each piece of open technology has a job the product would fall apart without.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma: three jobs, each with a guard
&lt;/h3&gt;

&lt;p&gt;Gemma 3 (4B, served locally through Ollama) handles everything that involves messy human language, and each of its outputs passes a deterministic check before anything acts on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reading requests typed in a hurry.&lt;/strong&gt; Gemma turns &lt;em&gt;"critical: B+ x3 units, near Powai"&lt;/em&gt; into structured fields. Then the code checks that the blood group, the unit count and the locality it returned all literally appear in what the nurse typed. Early on, a request for blood "in Delhi" could get quietly matched to the closest Mumbai locality in the list. Now it is refused with a message telling staff what to fix. A model that helpfully fills in a gap is the most dangerous kind of mistake in this domain, so the system sends the request back instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Writing to donors.&lt;/strong&gt; Gemma writes the warm, human opening of each outreach message. The facts that follow (what is needed, which hospital, the map link, the number to call) are assembled from the database record. Any draft containing a digit or a link is thrown away, so the model can never invent an address or a phone number that sends a donor to the wrong place.&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%2Fi858evfw3mli7knumug4.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%2Fi858evfw3mli7knumug4.png" alt="A donor's message: Gemma's opening line, then facts built from the record" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Understanding replies.&lt;/strong&gt; Donors answer however they answer. &lt;em&gt;"haan aa raha hoon, 20 min mein pahunch jaunga"&lt;/em&gt; becomes an accept with a 20 minute ETA. &lt;em&gt;"Sorry, I had fever last week"&lt;/em&gt; becomes a decline with a note for staff. During testing, Gemma read &lt;em&gt;"Can I come after 6pm instead?"&lt;/em&gt; as a yes. So now any negation or condition in a reply (a question mark, "after", "instead", "maybe", "kal") vetoes an accept and puts the reply under &lt;strong&gt;Needs a call&lt;/strong&gt;, with the donor's number one tap away. Marking a donor as coming when they are not is the expensive error; one extra phone call is cheap.&lt;/p&gt;

&lt;p&gt;If Gemma is slow or unreachable, the wording falls back to fixed text. Dispatch never waits on the model.&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%2F9ft87aqrhrrkf59phd5r.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%2F9ft87aqrhrrkf59phd5r.png" alt="The donor's page: what is needed, where to go, and three ways to answer" width="800" height="1167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Temporal: waiting on humans, durably
&lt;/h3&gt;

&lt;p&gt;The hard part of this product is time. You message ten people, then wait. Some answer in a minute, some never do, and the server should be able to crash in the middle without anyone getting messaged twice or forgotten.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EmergencyRequestWorkflow&lt;/code&gt; is a Temporal workflow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reserves bank units first and finishes immediately if the banks cover everything,&lt;/li&gt;
&lt;li&gt;sends a batch of the nearest eligible donors and waits on a durable timer,&lt;/li&gt;
&lt;li&gt;treats each donor's reply as a signal, so the moment the target is met it stops,&lt;/li&gt;
&lt;li&gt;wakes early when every donor in a batch has answered (for example, all declined) instead of sitting out the timer,&lt;/li&gt;
&lt;li&gt;works through &lt;strong&gt;every&lt;/strong&gt; eligible donor at a radius before widening (5, 10, 25, then 50 km),&lt;/li&gt;
&lt;li&gt;and when it ends, tells every donor still pending that they are no longer needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Activities are retry-safe: each message is written as an upsert keyed on request and donor, so a retried activity never messages the same person twice. This mattered in practice. The network I built this on drops connections to the database every so often, and I watched activities fail mid-request, retry with backoff, and carry on exactly where they left off, with the hospital view never noticing. The workflow logic is covered by tests that run the real workflow on Temporal's time-skipping test server, including the full escalation ladder and the all-decline early wake-up.&lt;/p&gt;

&lt;h3&gt;
  
  
  MongoDB Atlas: geography and medicine in one query
&lt;/h3&gt;

&lt;p&gt;Donors and blood banks live in Atlas with &lt;code&gt;2dsphere&lt;/code&gt; indexes. Finding the next batch is a single &lt;code&gt;$geoNear&lt;/code&gt; query that also applies the medical rules, so the batch limit counts only donors who can actually give:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eligibility&lt;/strong&gt;, from India's NBTC guidelines: age 18 to 65, at least 45 kg, and 90 days since the last donation for men or 120 days for women.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility by component.&lt;/strong&gt; Red cells follow the familiar donor table. Plasma runs the other way: AB is the universal plasma donor and Rh does not matter. Platelets follow the plasma rule and give Rh-negative patients Rh-negative donors only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bank stock is reserved with an atomic conditional decrement, so two hospitals asking at the same moment can never be promised the same bag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built to be trusted
&lt;/h3&gt;

&lt;p&gt;Staff endpoints sit behind a per-hospital access code (switched off for this public demo, since all data is synthetic). Each donor's link carries a random ID in the URL fragment, so it never shows up in server logs. Staff only see a donor's phone number after that donor says yes or asks for a call. The page runs under a strict Content Security Policy with no inline scripts or styles. 33 tests cover the medical rules, the model guards and the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is simulated
&lt;/h3&gt;

&lt;p&gt;I do not have access to e-RaktKosh, so donors and banks are synthetic: 460 donors and 13 banks across Mumbai, plus a cluster near Pune and Nashik with no banks at all, which is what the 12% of districts far from a bank look like. Messages are shown in the donor simulator and on the donor page instead of going out over WhatsApp. Sending is behind a single &lt;code&gt;NotificationProvider&lt;/code&gt; interface, so plugging in the WhatsApp Cloud API changes one file and nothing in matching or the workflow.&lt;/p&gt;

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

&lt;p&gt;The pitch that inspired this never made it past a demo. That is the usual fate of good public-interest ideas: building them for real needs a team, a budget, and an API bill that grows with every message.&lt;/p&gt;

&lt;p&gt;Open tools change that math. Gemma runs on the hospital's own machine, so a patient's blood group and location never leave the building, and reading a donor's reply costs nothing per message. That matters for a product whose busiest night, a highway pile-up or a dengue surge, is exactly when a per-request bill would spike. Temporal's open-source server gave me production-grade durable workflows on a laptop, the same engine large companies run, with no license standing between a solo builder and reliability. And because each of these is open, a state health department or an NGO can inspect exactly how a donor gets chosen, run it themselves, and change it. For a system that decides who gets asked to give blood, that transparency is part of the product.&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;: request parsing, outreach writing and donor reply understanding, each with a deterministic safety guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: the whole dispatch lifecycle is one durable workflow that retries failed steps, resumes after crashes, and never double-messages a donor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of MongoDB Atlas&lt;/strong&gt;: the data layer, with geospatial matching, eligibility in the query, and atomic bank-stock reservation.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Butterflies don't take weekends off: 40,000 Pune sightings and a forecast that fell apart when I removed one column</title>
      <dc:creator>CodersAcademy006</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:27:30 +0000</pubDate>
      <link>https://dev.to/codersacademy006/butterflies-dont-take-weekends-off-40000-pune-sightings-and-a-forecast-that-fell-apart-when-i-1g4</link>
      <guid>https://dev.to/codersacademy006/butterflies-dont-take-weekends-off-40000-pune-sightings-and-a-forecast-that-fell-apart-when-i-1g4</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;p&gt;On an average Sunday, Pune's naturalists log &lt;strong&gt;12.2&lt;/strong&gt; butterfly species. On an average Monday they log &lt;strong&gt;4.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I did not believe the butterflies had a schedule, so I built a forecaster, backtested it on a year it had never seen, and then took away the one feature I suspected. The forecast went from useful to useless.&lt;/p&gt;

&lt;p&gt;This post is the build, the number, and the afternoon a worker process got &lt;code&gt;kill -9&lt;/code&gt; in the middle of a 13-minute download and the pipeline finished anyway.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Fieldcast&lt;/strong&gt; answers the question that comes before "what is this bird?". It answers &lt;em&gt;which day should I go?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the next seven days near Pune it forecasts how many butterfly species and how many bird species local observers will log, drops the wet days, and prints the best one on a single page with the species most likely to turn up. You read the card, pocket the phone, and go.&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;strong&gt;Place&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pune, 30 km radius (18.52, 73.86)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40,186 research-grade iNaturalist observations (2019 to 4 Oct 2026) and daily Open-Meteo weather&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TabPFN v2, open weights, CPU only, no training loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemma 3 4B running locally through Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plumbing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporal workflow with retries, heartbeats and checkpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One printable card, light, dark and print layouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;365 held-out days against a seasonal average and gradient boosting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;strong&gt;Live card: &lt;a href="https://codersacademy006.github.io/fieldcast/card.html" rel="noopener noreferrer"&gt;codersacademy006.github.io/fieldcast/card.html&lt;/a&gt;&lt;/strong&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%2Fjltf961bi7nxz0hbymro.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%2Fjltf961bi7nxz0hbymro.png" alt="Fieldcast card on desktop, light theme" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a phone, which is where you will read it before you leave:&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%2Fet3igyd6hru16vc29t9c.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%2Fet3igyd6hru16vc29t9c.png" alt="Fieldcast card on a phone, dark theme" width="800" height="1965"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And on paper, so the phone can stay in your pocket:&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%2F8662e2ffk74g3dsnyw4g.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%2F8662e2ffk74g3dsnyw4g.png" alt="Fieldcast card, print layout" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The forecast it generated on 6 October 2026:&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%2Fjwb9zqr3lirqm67hwpfr.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%2Fjwb9zqr3lirqm67hwpfr.png" alt="Bar chart of forecast butterfly and bird species per day for 6 to 12 October, with the weekend highlighted" width="799" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It picks &lt;strong&gt;Saturday 10 October&lt;/strong&gt;: dry, 34.8 C, about 27 butterfly species and 20 bird species expected. The likeliest sightings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Species&lt;/th&gt;
&lt;th&gt;Model-estimated chance it is logged that day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lemon Migrant&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lemon Pansy&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common Mormon Swallowtail&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red Pierrot&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at that chart again. Five weekdays sit near 19, then the weekend jumps to 27. I will come back to that jump, because it turned out to be the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the forecast work?
&lt;/h2&gt;

&lt;p&gt;I trained on everything before 5 October 2025 (2,469 days) and tested on the next 365 days. Two baselines: a &lt;strong&gt;seasonal average&lt;/strong&gt; (the mean for that time of year) and &lt;strong&gt;gradient boosting&lt;/strong&gt; on the same features.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Rank correlation (Spearman)&lt;/th&gt;
&lt;th&gt;Error (species)&lt;/th&gt;
&lt;th&gt;Best-day lift&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Butterflies&lt;/td&gt;
&lt;td&gt;Seasonal average&lt;/td&gt;
&lt;td&gt;0.36&lt;/td&gt;
&lt;td&gt;6.03&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Butterflies&lt;/td&gt;
&lt;td&gt;Gradient boosting&lt;/td&gt;
&lt;td&gt;0.48&lt;/td&gt;
&lt;td&gt;5.34&lt;/td&gt;
&lt;td&gt;1.45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Butterflies&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.52&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Birds&lt;/td&gt;
&lt;td&gt;Seasonal average&lt;/td&gt;
&lt;td&gt;-0.04&lt;/td&gt;
&lt;td&gt;11.40&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Birds&lt;/td&gt;
&lt;td&gt;Gradient boosting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.27&lt;/td&gt;
&lt;td&gt;1.54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Birds&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.37&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.36&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.69&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Best-day lift:&lt;/em&gt; each week I took the day the model ranked highest and divided what was logged that day by the week's average day. 1.0 means the pick was no better than that week's average day. TabPFN's picks averaged 1.4 times the butterfly species and 1.7 times the bird species of a normal day.&lt;/p&gt;

&lt;p&gt;TabPFN wins on butterflies and ties gradient boosting on birds, with the lower error. On individual species it beats the seasonal average for 11 of 14 (ROC AUC), and loses on three: Lemon Migrant, Red Pierrot and Rose-ringed Parakeet, which scores 0.476, below a coin flip. AUC measures ranking only; I did not validate calibration, so read the percentages as model estimates, not calibrated probabilities. The full per-species table is in the repo.&lt;/p&gt;

&lt;p&gt;That is a decent result. It is also where most weekend projects stop, with a table and a pat on the back. I kept going because of the chart above.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ablation that ate my result
&lt;/h2&gt;

&lt;p&gt;Why would a model that knows the season and the monsoon predict a weekend jump of 8 species? Because it learned something I handed it. One of my features is a &lt;code&gt;weekend&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;So I ran the backtest twice, with and without it. Same data, same split, same model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Weekend flag&lt;/th&gt;
&lt;th&gt;Rank correlation&lt;/th&gt;
&lt;th&gt;Best-day lift&lt;/th&gt;
&lt;th&gt;Weeks the pick was a weekend day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Butterflies&lt;/td&gt;
&lt;td&gt;with&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;1.40&lt;/td&gt;
&lt;td&gt;52 of 52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Butterflies&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;without&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.33&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.95&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13 of 52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Birds&lt;/td&gt;
&lt;td&gt;with&lt;/td&gt;
&lt;td&gt;0.37&lt;/td&gt;
&lt;td&gt;1.69&lt;/td&gt;
&lt;td&gt;52 of 52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Birds&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;without&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.07&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.06&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21 of 52&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without the flag, the butterfly forecast drops to a lift of 0.95, which is slightly worse than picking a day at random. The bird forecast falls to a rank correlation below zero. With the flag, &lt;strong&gt;every single week's best day is a weekend day&lt;/strong&gt;, 52 out of 52.&lt;/p&gt;

&lt;p&gt;The forecast is mostly a forecast of people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same butterflies, three times the logging
&lt;/h2&gt;

&lt;p&gt;Here are all 2,834 days in the dataset, averaged by weekday. Each number is the count of distinct species logged that day.&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;Mon&lt;/th&gt;
&lt;th&gt;Tue&lt;/th&gt;
&lt;th&gt;Wed&lt;/th&gt;
&lt;th&gt;Thu&lt;/th&gt;
&lt;th&gt;Fri&lt;/th&gt;
&lt;th&gt;Sat&lt;/th&gt;
&lt;th&gt;Sun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Butterfly species&lt;/td&gt;
&lt;td&gt;3.98&lt;/td&gt;
&lt;td&gt;4.18&lt;/td&gt;
&lt;td&gt;4.29&lt;/td&gt;
&lt;td&gt;4.37&lt;/td&gt;
&lt;td&gt;4.40&lt;/td&gt;
&lt;td&gt;7.82&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.22&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bird species&lt;/td&gt;
&lt;td&gt;2.96&lt;/td&gt;
&lt;td&gt;3.09&lt;/td&gt;
&lt;td&gt;3.22&lt;/td&gt;
&lt;td&gt;3.39&lt;/td&gt;
&lt;td&gt;3.83&lt;/td&gt;
&lt;td&gt;5.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.13&lt;/strong&gt;&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%2F289gojguae9xykexyhbb.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%2F289gojguae9xykexyhbb.png" alt="Average butterfly and bird species logged per weekday in Pune, Monday 4.0 to Sunday 12.2" width="799" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Weekdays average 4.24 butterfly species and weekends 10.02, a factor of 2.4. For birds it is 3.30 against 6.32, a factor of 1.9. Now the number that explains it: observers log &lt;strong&gt;9.7 observations on a weekday and 25.3 on a weekend day&lt;/strong&gt;, a factor of 2.6. Divide species by observations and the two kinds of day come out close, about 0.44 species per observation on weekdays and 0.40 on weekends.&lt;/p&gt;

&lt;p&gt;Pune does not get more butterflies on Sunday. It gets more people holding phones. The model learned that correctly, and that is the problem with calling it a butterfly forecast.&lt;/p&gt;

&lt;p&gt;I kept the flag in the shipped model on purpose. If you are choosing a day for a trip, "a day when a lot will be logged" is a fair proxy for "a day when someone nearby is looking". But the card now says what it is, and I would not put it in front of anyone as a map of where the animals are.&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/CodersAcademy006" rel="noopener noreferrer"&gt;
        CodersAcademy006
      &lt;/a&gt; / &lt;a href="https://github.com/CodersAcademy006/fieldcast" rel="noopener noreferrer"&gt;
        fieldcast
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      TabPFN + local Gemma forecast of which day to go outside near Pune
    &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;Fieldcast&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Which day this week is best to go outside near Pune, and what will you probably see. TabPFN forecasts daily butterfly and bird species counts from weather and calendar. Local Gemma writes a one-page printable card.&lt;/p&gt;
&lt;p&gt;Finding that shaped the project: observers log 2.6 times more observations on weekends, so a forecast of logged species is largely a forecast of people. &lt;code&gt;ablate.py&lt;/code&gt; removes the weekend flag and shows how much of the skill disappears.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/CodersAcademy006/fieldcast/assets/weekday-effect.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%2FCodersAcademy006%2Ffieldcast%2FHEAD%2Fassets%2Fweekday-effect.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;python3.11 -m venv .venv &amp;amp;&amp;amp; .venv/bin/pip install -r requirements.txt
brew install ollama &amp;amp;&amp;amp; ollama serve &amp;amp; ollama pull gemma3:4b
.venv/bin/python fetch.py                                  # iNaturalist + Open-Meteo, about 20 min
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py backtest   # about 10 min, writes metrics.json
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py forecast   # about 9 min, writes forecast.json
.venv/bin/python card.py                                   # writes card.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Durable run (survives a killed worker, resumes the fetch from its page checkpoint), with the optional Sentry tracing of the Gemma step:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&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/CodersAcademy006/fieldcast" 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;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%2F1hyxu5rv8u55nvip2jt9.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%2F1hyxu5rv8u55nvip2jt9.png" alt="Pipeline: data, features, TabPFN, plain code, Gemma with a checker, card, all inside one Temporal workflow" width="798" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code owns the numbers, Gemma writes the words, Temporal keeps it all alive.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Features and model
&lt;/h3&gt;

&lt;p&gt;Day of year as sine and cosine, the weekend flag, a slow year trend, and rain and temperature windows over 7 and 30 days. The 30-day rain window is how the model learns the monsoon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is a tabular foundation model, so there is no training loop to tune. One regressor predicts species per day and one classifier per common species predicts "will it be logged today". On a MacBook Air M5 CPU the backtest takes about 10 minutes and the forecast about 6.&lt;/p&gt;

&lt;p&gt;Ordinary Python then ranks the seven days, drops the wet ones and builds a data structure that holds every number that will appear on the card.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temporal: the afternoon I killed the worker
&lt;/h3&gt;

&lt;p&gt;The fetch alone takes about 13 minutes, because iNaturalist pages are slow and polite. A laptop sleeping, a network drop or one bad page should cost a retry, not the whole run. So the pipeline is a &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow, and each step is an activity:&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="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_to_close_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;heartbeat_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;retry_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                     &lt;span class="n"&gt;backoff_coefficient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maximum_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch_obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GROUPS&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                     &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch_weather&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merge_obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_forecast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Butterflies, birds and weather download in parallel. Each long activity runs its blocking code in a thread and heartbeats every 10 seconds, so Temporal can tell a slow step from a dead one. The fetch also writes a checkpoint file after every page.&lt;/p&gt;

&lt;p&gt;Then I tested it properly. Mid-fetch, I ran &lt;code&gt;kill -9&lt;/code&gt; on the worker. Temporal got no goodbye. Two minutes later the heartbeat timeout fired, Temporal scheduled attempt 2, I started a fresh worker, and the fetch &lt;strong&gt;resumed from the last saved page&lt;/strong&gt; instead of starting over.&lt;/p&gt;

&lt;p&gt;Then it got more interesting, because the run failed for a real reason I had not planned. The forecast step died six times in a row with TabPFN's guard: &lt;em&gt;"Running on CPU with more than 1000 samples is not allowed by default."&lt;/em&gt; I had set the override as an environment variable in my shell, and the worker did not have it. I fixed it in code with one line in &lt;code&gt;model.py&lt;/code&gt;, restarted the worker, and the pending retry succeeded on attempt 7 without anyone re-running anything.&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%2Fb37qq23kf7r7aphwtm91.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%2Fb37qq23kf7r7aphwtm91.png" alt="Temporal timeline: fetch_obs twice with a retry after the kill, run_forecast after repeated failures, finishing in 24 minutes 42 seconds" width="799" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Total wall time for the whole run, kill, bug and fix included: &lt;strong&gt;24 minutes 42 seconds&lt;/strong&gt;, status Completed. I checked that the resumed run matches a clean one. The weather table is identical. The resumed data has 40,191 rows against 40,186, and the extra 5 are observations people uploaded in between. The 7-day forecast agrees within 0.4 species on every day.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gemma: let it talk, never let it count
&lt;/h3&gt;

&lt;p&gt;Gemma 3 4B writes three sentences on the card: why this day, what to look for and in what order, and a reminder to leave the phone alone. It gets the facts in its prompt. Every number on the card comes from code.&lt;/p&gt;

&lt;p&gt;A small model told to follow a format does not always follow it, so I measured. Over two batches of 30 cards, &lt;strong&gt;17 and 18 of them broke the three-sentence rule&lt;/strong&gt;, which is about three in five. Across the 60 cards, &lt;strong&gt;zero invented a number&lt;/strong&gt;. So the checker rejects any draft with the wrong length or any digit that is not in the facts, retries up to five times, and falls back to a plain template if all five fail. The model decides the wording. The code decides what counts as acceptable.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The backtest feeds the model the &lt;strong&gt;actual&lt;/strong&gt; weather for the test year, not archived forecasts, so real use will score lower.&lt;/li&gt;
&lt;li&gt;Gradient boosting is close behind TabPFN here. I will not claim a big win over it.&lt;/li&gt;
&lt;li&gt;Without the weekend flag, the forecast has little left. That is the finding, and it is also a limit.&lt;/li&gt;
&lt;li&gt;I have not yet carried the card outside and checked it against what I saw.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Run it
&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/CodersAcademy006" rel="noopener noreferrer"&gt;
        CodersAcademy006
      &lt;/a&gt; / &lt;a href="https://github.com/CodersAcademy006/fieldcast" rel="noopener noreferrer"&gt;
        fieldcast
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      TabPFN + local Gemma forecast of which day to go outside near Pune
    &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;Fieldcast&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Which day this week is best to go outside near Pune, and what will you probably see. TabPFN forecasts daily butterfly and bird species counts from weather and calendar. Local Gemma writes a one-page printable card.&lt;/p&gt;

&lt;p&gt;Finding that shaped the project: observers log 2.6 times more observations on weekends, so a forecast of logged species is largely a forecast of people. &lt;code&gt;ablate.py&lt;/code&gt; removes the weekend flag and shows how much of the skill disappears.&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/CodersAcademy006/fieldcast/assets/weekday-effect.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%2FCodersAcademy006%2Ffieldcast%2FHEAD%2Fassets%2Fweekday-effect.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;python3.11 -m venv .venv &amp;amp;&amp;amp; .venv/bin/pip install -r requirements.txt
brew install ollama &amp;amp;&amp;amp; ollama serve &amp;amp; ollama pull gemma3:4b
.venv/bin/python fetch.py                                  # iNaturalist + Open-Meteo, about 20 min
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py backtest   # about 10 min, writes metrics.json
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py forecast   # about 9 min, writes forecast.json
.venv/bin/python card.py                                   # writes card.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Durable run (survives a killed worker, resumes the fetch from its page checkpoint), with the optional Sentry tracing of the Gemma step:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&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/CodersAcademy006/fieldcast" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.11 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; .venv/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
brew &lt;span class="nb"&gt;install &lt;/span&gt;ollama &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama serve &amp;amp; ollama pull gemma3:4b
temporal server start-dev                      &lt;span class="c"&gt;# UI at localhost:8233&lt;/span&gt;
.venv/bin/python workflow.py worker            &lt;span class="c"&gt;# second terminal&lt;/span&gt;
.venv/bin/python workflow.py run               &lt;span class="c"&gt;# third terminal, writes card.html&lt;/span&gt;
.venv/bin/python ablate.py                     &lt;span class="c"&gt;# the weekend ablation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;LAT&lt;/code&gt; and &lt;code&gt;LNG&lt;/code&gt; in &lt;code&gt;fetch.py&lt;/code&gt; and &lt;code&gt;model.py&lt;/code&gt; to point it at your own city.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run the ablation you are afraid of.&lt;/strong&gt; One column decided whether this project was a result or an artifact. The tables looked good right up until I removed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A forecast of observations is a forecast of observers.&lt;/strong&gt; Citizen-science counts carry the habits of the people who log them. Rank correlation will not tell you that. A weekday table will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability is a design choice you make before the crash.&lt;/strong&gt; The heartbeat and the checkpoint cost about ten lines. The &lt;code&gt;kill -9&lt;/code&gt; cost me two minutes of waiting and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the language model describe and never decide.&lt;/strong&gt; Gemma is wrong about format three times in five and has never been wrong about a number, because it never makes one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replace actual weather with archived forecasts so the backtest matches real use.&lt;/li&gt;
&lt;li&gt;Model observer effort directly, so the forecast separates animals from people.&lt;/li&gt;
&lt;li&gt;Carry the card outside and compare it with what I actually see.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anyone can check the weights.&lt;/strong&gt; TabPFN's v2 checkpoints are public on Hugging Face and the README pins them, so anyone can rerun the backtest and the ablation and get my numbers. The newer v3.5 weights sit behind a licence gate, so I used the open ones and stayed reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Almost nothing leaves the machine.&lt;/strong&gt; The only network calls are two public APIs. Gemma runs locally, so the card writer needs no key and no account, and it sees a weather summary and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data is a commons.&lt;/strong&gt; Every row is a person who photographed a butterfly and let strangers identify it. A forecast built on that should be shareable the same way, so the repo has the fetch script, the model, the metrics and the ablation.&lt;/li&gt;
&lt;/ol&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 3 4B runs locally through Ollama and writes the card prose from code-supplied facts, behind a checker that rejects wrong length and invented numbers, measured over 60 runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; the whole forecast is TabPFN, a regressor for daily counts and a classifier per species, backtested on a held-out year against gradient boosting and a seasonal baseline, then ablated to show how much of its skill is observer behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal:&lt;/strong&gt; the pipeline is a Temporal workflow with parallel activities, heartbeats, retries and checkpointed resume, demonstrated by killing the worker mid-fetch and by recovering from a real configuration failure on retry.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>I Built a Local DuckDB Engine to Mathematically Settle Cricket Debates</title>
      <dc:creator>CodersAcademy006</dc:creator>
      <pubDate>Fri, 29 May 2026 12:02:39 +0000</pubDate>
      <link>https://dev.to/codersacademy006/i-built-a-local-duckdb-engine-to-mathematically-settle-cricket-debates-5b38</link>
      <guid>https://dev.to/codersacademy006/i-built-a-local-duckdb-engine-to-mathematically-settle-cricket-debates-5b38</guid>
      <description>&lt;p&gt;For years, sports commentators have repeated the same clichés about T20 cricket: &lt;em&gt;"You have to win the powerplay,"&lt;/em&gt; &lt;em&gt;"You need an anchor to win,"&lt;/em&gt; and &lt;em&gt;"Team X always chokes."&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;I wanted to know if any of that was actually true. &lt;/p&gt;

&lt;p&gt;The problem is that querying 15+ years of historical ball-by-ball telemetry (over 294,000 deliveries) via commercial sports APIs is painfully slow and brutally rate-limited. &lt;/p&gt;

&lt;p&gt;So, I built &lt;a href="https://github.com/CodersAcademy006/Midwicket" rel="noopener noreferrer"&gt;Midwicket&lt;/a&gt;—an open-source SDK that bypasses APIs entirely. It pulls raw open data into a local DuckDB and PyArrow engine, turning your laptop into a sub-millisecond sports data warehouse. &lt;/p&gt;

&lt;p&gt;To test the architecture, I wrote a logistic regression Win Probability model (AUC 0.87) trained on 1,239 IPL matches. Because the query engine is entirely local, I could calculate the exact &lt;strong&gt;Win Probability Added (WPA)&lt;/strong&gt; for every single ball ever bowled. &lt;/p&gt;

&lt;p&gt;When I ran the numbers, the data completely broke some of the sport's biggest myths.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The "Choke Index" is real, and it's ruthless.
&lt;/h2&gt;

&lt;p&gt;I queried the database for every match where a chasing team reached an &lt;strong&gt;80% Win Probability&lt;/strong&gt; at any point, but still managed to lose the game. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Franchise&lt;/th&gt;
&lt;th&gt;80%+ WP Matches&lt;/th&gt;
&lt;th&gt;Chokes&lt;/th&gt;
&lt;th&gt;Choke %&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Royal Challengers Bangalore (RCB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;66.7%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Notorious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mumbai Indians (MI)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chennai Super Kings (CSK)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kolkata Knight Riders (KKR)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Composed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Finding:&lt;/strong&gt; RCB has a staggering 66.7% choke rate from commanding positions. If they reach an 80% probability of winning, they are mathematically more likely to throw it away than finish the job. KKR, on the other hand, converts almost 78% of these positions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Anchor vs. The Finisher (Kohli vs Dhoni)
&lt;/h2&gt;

&lt;p&gt;Who is more valuable in a run chase: the guy who bats for 15 overs to build a foundation (Kohli), or the guy who comes in at the end (Dhoni)? &lt;/p&gt;

&lt;p&gt;The WPA math heavily favors the finisher. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Player&lt;/th&gt;
&lt;th&gt;Total Innings Analyzed&lt;/th&gt;
&lt;th&gt;Positive WPA %&lt;/th&gt;
&lt;th&gt;Average WPA per Innings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MS Dhoni&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+46.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Virat Kohli&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+25.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Finding:&lt;/strong&gt; Kohli is vastly consistent, but Dhoni mathematically contributes ~20% more total win probability per innings. The model reveals that run-chases are highly non-linear; the leverage in the final 3 overs is so massive that a finisher essentially holds the entire win probability of the team in their hands.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. "Winning the Powerplay" is a myth. Over 19 is the cliff.
&lt;/h2&gt;

&lt;p&gt;If you simulate a catastrophic 2-wicket collapse at any point in a chase, the Powerplay (Overs 1-6) is surprisingly forgiving—a collapse there drops your win probability by about 25%. &lt;/p&gt;

&lt;p&gt;However, a collapse in &lt;strong&gt;Over 19&lt;/strong&gt; drops it by &lt;strong&gt;60.9%&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;By the 19th over, the margin of error mathematically compresses to zero. It is the definitive breaking point of the sport.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Open Source Engine
&lt;/h3&gt;

&lt;p&gt;If you’re a sports analytics nerd, a data engineer, or just someone who wants to run SQL queries against 15 years of sports data without paying for an API, I’ve open-sourced the entire engine, the PyArrow schema, and the trained models. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the repo here:&lt;/strong&gt; &lt;a href="https://github.com/CodersAcademy006/Midwicket" rel="noopener noreferrer"&gt;Midwicket on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts on the architecture, the DuckDB implementation, or the data findings! What team should I run the choke index on next?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>data</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
