<?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: Ayan Khan</title>
    <description>The latest articles on DEV Community by Ayan Khan (@thunderkhan).</description>
    <link>https://dev.to/thunderkhan</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%2F4096640%2F883d8c76-3337-4af0-b6dc-158eba59dd86.jpg</url>
      <title>DEV Community: Ayan Khan</title>
      <link>https://dev.to/thunderkhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thunderkhan"/>
    <language>en</language>
    <item>
      <title>Meowfolio: I Built an AI Scrapbook for the Cats You Meet Outside 🐱🌿</title>
      <dc:creator>Ayan Khan</dc:creator>
      <pubDate>Sat, 10 Oct 2026 03:01:04 +0000</pubDate>
      <link>https://dev.to/thunderkhan/meowfolio-i-built-an-ai-scrapbook-for-the-cats-you-meet-outside-2430</link>
      <guid>https://dev.to/thunderkhan/meowfolio-i-built-an-ai-scrapbook-for-the-cats-you-meet-outside-2430</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;There's a difference between seeing a cat and &lt;em&gt;knowing&lt;/em&gt; a cat.&lt;/p&gt;

&lt;p&gt;The first time, it's just an orange cat sitting outside a shop. The next time, you recognize its face. Eventually, you start looking for it whenever you walk down that street.&lt;/p&gt;

&lt;p&gt;And then, somehow, this random neighborhood cat becomes part of your day.&lt;/p&gt;

&lt;p&gt;I liked that idea.&lt;/p&gt;

&lt;p&gt;For this week's &lt;strong&gt;Touch Grass&lt;/strong&gt; challenge, I wanted to build something around the little things we notice when we actually go outside. Not another productivity dashboard, fitness tracker, or AI chatbot.&lt;/p&gt;

&lt;p&gt;Something a little more personal.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Meowfolio&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meowfolio is a private, browser-based scrapbook for the cats you meet outside, powered by open-weight computer vision running locally in your browser.&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%2F1ofsa3bcj8fn0zd1bl6p.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%2F1ofsa3bcj8fn0zd1bl6p.png" alt="Meowfolio — Little cats, big memories" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is simple: every neighborhood cat deserves a little page in your memories.&lt;/p&gt;

&lt;p&gt;You take a photograph, let AI find the cat, give it a name, and save that encounter. The next time you meet that cat, you can add another sighting to its profile.&lt;/p&gt;

&lt;p&gt;Over time, these ordinary encounters become a personal scrapbook of the animals you've met.&lt;/p&gt;

&lt;p&gt;Here's what that looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Go outside and meet a cat.&lt;/strong&gt; Photograph it using your phone's camera or select an existing picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let AI find the cat.&lt;/strong&gt; An open-weight object-detection model locates cats in the photograph. If there are several, you choose which one to document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give your new friend a name.&lt;/strong&gt; Create a new profile or manually associate the sighting with a cat you've already saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the memory.&lt;/strong&gt; Save the original photograph, date, an optional note, and an optional location.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Come back someday.&lt;/strong&gt; Open the cat's profile and revisit every encounter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each cat gets its own scrapbook page, complete with a chronological history and photographs.&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%2F6q1k2gs62kfhhh6t7mjo.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%2F6q1k2gs62kfhhh6t7mjo.png" alt="Meowfolio's pink retro scrapbook displays cat photo cards labeled computer kitty, kitty, and Noir, with encounter counts and last-seen dates. The collection contains five saved cats." width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My Meowfolio collection. Every cat gets its own little corner of the internet, stored privately in my browser.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I also built a &lt;strong&gt;Story Studio&lt;/strong&gt; that turns saved photographs into 1080×1920 story cards. There are three visual themes, adjustable photo framing, and local PNG export.&lt;/p&gt;

&lt;p&gt;And because running AI models on a phone isn't always convenient, there's a &lt;strong&gt;Save Photo for Later&lt;/strong&gt; feature. You can capture the moment now and process the photograph when you have time.&lt;/p&gt;

&lt;p&gt;I wanted the interface to feel like something you might have discovered on the internet in 2003.&lt;/p&gt;

&lt;p&gt;So Meowfolio has pink pixel windows, chunky borders, old-school buttons, Polaroid-inspired photographs, and a little pixel-cat mascot.&lt;/p&gt;

&lt;p&gt;That aesthetic wasn't just decoration.&lt;/p&gt;

&lt;p&gt;A scrapbook should feel like a place where you keep things that matter to you, even if they seem insignificant to everyone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cat is the reason to go outside. The app is just where the memory lives.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🐱 &lt;a href="https://mymeowfolio.vercel.app/" rel="noopener noreferrer"&gt;Try Meowfolio live&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Meowfolio runs directly in your browser. There's no account to create, no application to install, and no API key to configure.&lt;/p&gt;

&lt;p&gt;To try it yourself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Meowfolio on your phone or desktop.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Camera&lt;/strong&gt; or &lt;strong&gt;Add Photo&lt;/strong&gt; and select a picture containing a cat.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Find the Cat&lt;/strong&gt;. On first use, the app asks permission before downloading the AI models.&lt;/li&gt;
&lt;li&gt;Let the local detector find the cat. If multiple cats are detected, choose the one you want.&lt;/li&gt;
&lt;li&gt;Name your cat and save its first encounter.&lt;/li&gt;
&lt;li&gt;Open the scrapbook and revisit the cat's profile.&lt;/li&gt;
&lt;/ol&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%2F6fixcpb0vn8nivsze4tu.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%2F6fixcpb0vn8nivsze4tu.png" alt="Two encounters, one familiar face. Meowfolio keeps each sighting as part of the cat's growing history." width="799" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reload the website, and your saved memories should still be there in the same browser.&lt;/p&gt;

&lt;p&gt;You can also save an unprocessed photograph to your local inbox, add another encounter to a saved cat, export a story card, or download a private backup of your collection.&lt;/p&gt;

&lt;p&gt;I tested the end-to-end application on my Android phone, and the test passed successfully. That was an especially satisfying milestone because Meowfolio is meant to be used while you're out exploring, not just demonstrated on a desktop.&lt;/p&gt;

&lt;p&gt;One limitation worth mentioning: &lt;strong&gt;the first model download requires internet access&lt;/strong&gt;. Afterward, the browser can reuse cached model assets, although browser caches are not guaranteed to persist forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Meowfolio is open source, and the complete implementation is available on GitHub.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ThunderKhan" rel="noopener noreferrer"&gt;
        ThunderKhan
      &lt;/a&gt; / &lt;a href="https://github.com/ThunderKhan/meowfolio" rel="noopener noreferrer"&gt;
        meowfolio
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A privacy-first, Y2K cat scrapbook for the cats you meet outside. Powered by open-weight AI running in your browser. Built for Hacktoberfest's Touch Grass challenge. 🐈
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/ThunderKhan/meowfolio/public/og-image.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%2FThunderKhan%2Fmeowfolio%2FHEAD%2Fpublic%2Fog-image.png" alt="Meowfolio — pink Y2K pixel-window scrapbook with cat photos and pixel-cat mascot" width="100%"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;p&gt;&lt;a href="https://github.com/ThunderKhan/meowfolio/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/ThunderKhan/meowfolio/actions/workflows/ci.yml/badge.svg" alt="Verify Meowfolio"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/e52eb5b0595c773b57497a069df9d35b810175d8d77c2cc86b7f9485edffd2e9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d504c2d2d322e302d666637376237"&gt;&lt;img src="https://camo.githubusercontent.com/e52eb5b0595c773b57497a069df9d35b810175d8d77c2cc86b7f9485edffd2e9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d504c2d2d322e302d666637376237" alt="License: MPL-2.0"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/fa76871d1389de20969c0e358caeb5483fe92603e2e33553abd970027ff59012/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f41492d6f70656e2d2d7765696768742532302532422532306c6f63616c2d366334636261"&gt;&lt;img src="https://camo.githubusercontent.com/fa76871d1389de20969c0e358caeb5483fe92603e2e33553abd970027ff59012/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f41492d6f70656e2d2d7765696768742532302532422532306c6f63616c2d366334636261" alt="AI"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d3ebf502e55f5333aef795838b8f705e06ededf7a7f9f53a1e44f45a12ebaefa/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f63617425323070686f746f732d62726f777365722532306c6f63616c2d643334623931"&gt;&lt;img src="https://camo.githubusercontent.com/d3ebf502e55f5333aef795838b8f705e06ededf7a7f9f53a1e44f45a12ebaefa/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f63617425323070686f746f732d62726f777365722532306c6f63616c2d643334623931" alt="Privacy"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/af63b0da990f62e38fed2b8b80ec09e74f4d0541a3c92c9b308dd92ff4b73b00/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d352e392d3331373863363f6c6f676f3d74797065736372697074266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/af63b0da990f62e38fed2b8b80ec09e74f4d0541a3c92c9b308dd92ff4b73b00/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d352e392d3331373863363f6c6f676f3d74797065736372697074266c6f676f436f6c6f723d7768697465" alt="TypeScript"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Little cats, big memories.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A private, pixel-pink scrapbook for the cats you meet outside. Powered by real open-weight computer vision that runs &lt;strong&gt;in your browser&lt;/strong&gt;, not on a photo-upload server.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mymeowfolio.vercel.app" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Try Meowfolio ↗&lt;/strong&gt;&lt;/a&gt; · &lt;a href="https://github.com/ThunderKhan/meowfolio#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; · &lt;a href="https://github.com/ThunderKhan/meowfolio#try-it-yourself" rel="noopener noreferrer"&gt;Try it yourself&lt;/a&gt; · &lt;a href="https://github.com/ThunderKhan/meowfolio#under-the-hood" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/ThunderKhan/meowfolio#testing" rel="noopener noreferrer"&gt;Testing&lt;/a&gt; · &lt;a href="https://github.com/ThunderKhan/meowfolio/docs/TOUCH_GRASS_SUBMISSION.md" rel="noopener noreferrer"&gt;Touch Grass challenge&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The street is full of recurring characters&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There's a cat you see near the corner shop. Another that always sleeps under the same tree. You photograph them, forget which one was which, and keep walking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Meowfolio gives those tiny encounters a place to live.&lt;/strong&gt; Take a photo on your walk, let local AI find the cat, name it, and build a personal history one sighting at a time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Collect sightings, not screen time.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Meowfolio was built for the &lt;strong&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05" rel="nofollow"&gt;Hacktoberfest Open-Source AI Challenge · Week 1: Touch Grass&lt;/a&gt;&lt;/strong&gt;. The idea is to make looking up from your phone more rewarding—not…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ThunderKhan/meowfolio" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Live application:&lt;/strong&gt; &lt;a href="https://mymeowfolio.vercel.app" rel="noopener noreferrer"&gt;https://mymeowfolio.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/ThunderKhan/meowfolio/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;v0.1.0 — Touch Grass Edition&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;License:&lt;/strong&gt; Mozilla Public License 2.0 (MPL-2.0)&lt;/p&gt;

&lt;p&gt;The repository includes the application source, local AI pipeline, browser storage layer, model evaluation results, automated tests, and technical documentation.&lt;/p&gt;

&lt;p&gt;You can clone it, inspect the implementation, run it locally, or build on it under the project's open-source license.&lt;/p&gt;

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

&lt;p&gt;Meowfolio has two main parts: the AI pipeline that understands a photograph and the scrapbook that remembers the encounter.&lt;/p&gt;

&lt;p&gt;The interesting part is that both run in the user's browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Real computer vision without an inference server
&lt;/h3&gt;

&lt;p&gt;I used two open-weight models:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/Xenova/yolos-tiny" rel="noopener noreferrer"&gt;YOLOS-tiny&lt;/a&gt;&lt;/strong&gt; handles object detection. It examines the image and identifies where the cats are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/Xenova/dinov2-small" rel="noopener noreferrer"&gt;DINOv2-small&lt;/a&gt;&lt;/strong&gt; generates visual embeddings from the selected cat crop. Meowfolio stores normalized 384-dimensional representations alongside encounters for research and future matching experiments.&lt;/p&gt;

&lt;p&gt;Both models run through &lt;strong&gt;Transformers.js&lt;/strong&gt;, using WebAssembly inside a dedicated Web Worker.&lt;/p&gt;

&lt;p&gt;The pipeline looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mury23mw054jxhx5dj1.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%2F4mury23mw054jxhx5dj1.png" alt="Pastel pixel-art flowchart inside a pink, retro computer window, surrounded by playful, sleeping, and reading cats. A photo enters YOLOS-tiny for cat detection, then DINOv2-small creates a visual embedding. A human chooses whether the sighting belongs to a new or saved cat, and the result is stored in IndexedDB before appearing in the scrapbook." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Meowfolio pipeline: open-weight AI handles detection and visual embeddings, while the human makes the final identity decision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Web Worker keeps expensive inference operations away from the main UI thread, so the application can still display progress and respond to the user.&lt;/p&gt;

&lt;p&gt;I pinned the model revisions and added browser-side caching to avoid unnecessary downloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. My first model choice didn't work
&lt;/h3&gt;

&lt;p&gt;I initially explored YOLOv10n for cat detection.&lt;/p&gt;

&lt;p&gt;On paper, it looked suitable.&lt;/p&gt;

&lt;p&gt;In practice, the Transformers.js version I was using didn't support that model architecture through the browser pipeline I needed.&lt;/p&gt;

&lt;p&gt;So I changed direction.&lt;/p&gt;

&lt;p&gt;I tested &lt;strong&gt;YOLOS-tiny&lt;/strong&gt; with real Chromium/WebAssembly execution and used it as the detector.&lt;/p&gt;

&lt;p&gt;It was a useful reminder that a model being impressive isn't enough.&lt;/p&gt;

&lt;p&gt;It also has to work in the environment where people will actually use it.&lt;/p&gt;

&lt;p&gt;For Meowfolio, that meant a browser on an ordinary device, not a GPU server.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The feature I deliberately didn't ship
&lt;/h3&gt;

&lt;p&gt;This was probably the most interesting part of building Meowfolio.&lt;/p&gt;

&lt;p&gt;My original vision included automatic recognition of cats you'd encountered before.&lt;/p&gt;

&lt;p&gt;Imagine taking a photograph and having the scrapbook suggest:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Hey, this might be the orange cat you met last Tuesday!”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To explore that, I built a matching evaluation using DINOv2 embeddings and a dataset of &lt;strong&gt;50 photographs of individual cats&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I separated development data from a held-out evaluation and compared different matching strategies.&lt;/p&gt;

&lt;p&gt;The selected centroid-based approach produced correct familiar-cat suggestions for 14 of 20 held-out repeat cases, counting four processing failures in that total.&lt;/p&gt;

&lt;p&gt;But there was a problem.&lt;/p&gt;

&lt;p&gt;In one test, I deliberately removed the photographed cat's real identity from the saved gallery.&lt;/p&gt;

&lt;p&gt;The model still suggested a different cat.&lt;/p&gt;

&lt;p&gt;It was a false positive.&lt;/p&gt;

&lt;p&gt;For a casual demo, it might have been tempting to ignore that mistake.&lt;/p&gt;

&lt;p&gt;But think about what it means for a scrapbook.&lt;/p&gt;

&lt;p&gt;If the application confidently merges two different cats into the same history, every later memory becomes a little less trustworthy.&lt;/p&gt;

&lt;p&gt;So I made a decision:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I disabled automatic familiar-cat suggestions in the public release.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The application still uses real local AI for detection and embedding extraction. Users can also manually associate new sightings with cats they've saved previously.&lt;/p&gt;

&lt;p&gt;But Meowfolio doesn't pretend to know something the model hasn't demonstrated reliably.&lt;/p&gt;

&lt;p&gt;I preserved the evaluation methodology, threshold, results, and failure case in the &lt;a href="https://github.com/ThunderKhan/meowfolio/blob/main/docs/evaluation-results/2026-10-08-cat-individuals.md" rel="noopener noreferrer"&gt;public evaluation report&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I think this is one of the most valuable aspects of working with open models.&lt;/p&gt;

&lt;p&gt;You can inspect the behavior, challenge your assumptions, and decide not to ship a capability when the evidence doesn't support it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keeping everything in the browser
&lt;/h3&gt;

&lt;p&gt;The frontend uses &lt;strong&gt;React, TypeScript, Vite, and Tailwind CSS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of building a conventional backend, I used &lt;strong&gt;IndexedDB&lt;/strong&gt; for the scrapbook.&lt;/p&gt;

&lt;p&gt;It stores the cat profiles, original photographs, encounter history, embeddings, notes, and optional location information.&lt;/p&gt;

&lt;p&gt;The save operations use database transactions to avoid partially saved encounters.&lt;/p&gt;

&lt;p&gt;There's no account system and no cloud-hosted database of people's cat photographs.&lt;/p&gt;

&lt;p&gt;Model files are downloaded from approved external hosts after user consent, but the actual photograph processing happens locally.&lt;/p&gt;

&lt;p&gt;I also built JSON backup and restore because browser storage can be cleared or evicted, and Meowfolio doesn't offer automatic cloud synchronization.&lt;/p&gt;

&lt;p&gt;The architecture is intentionally straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static website + local AI + local storage.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Making it usable beyond a demo
&lt;/h3&gt;

&lt;p&gt;A working model pipeline is only one part of a usable application.&lt;/p&gt;

&lt;p&gt;I had to deal with model initialization, download consent, slow mobile inference, interrupted scans, IndexedDB upgrades, preserving unsaved photographs, and keeping the interface responsive on smaller screens.&lt;/p&gt;

&lt;p&gt;One Android test uncovered a blocked IndexedDB upgrade that prevented a photograph from being saved.&lt;/p&gt;

&lt;p&gt;That led me to improve how database connections respond to version changes and how blocked storage initialization recovers.&lt;/p&gt;

&lt;p&gt;I also added lazy-loaded application sections, paginated cat galleries, deferred image decoding, and improved asset caching.&lt;/p&gt;

&lt;p&gt;For testing, I used &lt;strong&gt;Vitest&lt;/strong&gt;, &lt;strong&gt;Playwright&lt;/strong&gt;, real Chromium/WASM model runs, and &lt;strong&gt;Lighthouse&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In CI Lighthouse measurements, mobile performance improved from 78 to 100 after the performance pass. Those are synthetic lab scores, not measurements of real-world Android inference speed.&lt;/p&gt;

&lt;p&gt;And finally, I verified the application on my Android phone.&lt;/p&gt;

&lt;p&gt;It was satisfying to see the whole experience work on the kind of device it was actually designed for.&lt;/p&gt;

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

&lt;p&gt;I could have built Meowfolio using a hosted computer-vision API.&lt;/p&gt;

&lt;p&gt;The architecture would have been fairly straightforward: upload a photograph, wait for a server to analyze it, and receive a result.&lt;/p&gt;

&lt;p&gt;But that wasn't what I wanted.&lt;/p&gt;

&lt;p&gt;I didn't think a photograph of a neighborhood cat needed to travel to somebody else's inference server just so I could add it to my scrapbook.&lt;/p&gt;

&lt;p&gt;Using open-weight models made a different approach possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data stays closer to the person who created it
&lt;/h3&gt;

&lt;p&gt;The AI models come to the browser instead of the photograph being sent to a remote inference API.&lt;/p&gt;

&lt;p&gt;Cat photos, names, embeddings, notes, and optional coordinates remain in local browser storage.&lt;/p&gt;

&lt;p&gt;This doesn't eliminate every privacy consideration, and it doesn't make browser storage permanent.&lt;/p&gt;

&lt;p&gt;But it gives the application a much smaller data-sharing footprint than a conventional hosted inference service.&lt;/p&gt;

&lt;h3&gt;
  
  
  I can inspect and change the behavior
&lt;/h3&gt;

&lt;p&gt;Open models gave me the freedom to experiment with different implementations, examine their limitations, pin model revisions, and evaluate matching strategies.&lt;/p&gt;

&lt;p&gt;That mattered when the familiar-cat recognition experiment failed.&lt;/p&gt;

&lt;p&gt;I wasn't forced to accept an opaque confidence score as proof that two photographs showed the same animal.&lt;/p&gt;

&lt;p&gt;I could measure the failure, document it, and choose a more trustworthy interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  There is no per-photo inference bill
&lt;/h3&gt;

&lt;p&gt;The application doesn't need a paid model API every time someone photographs a cat.&lt;/p&gt;

&lt;p&gt;Once the assets are available, inference runs on the user's device using their own computing resources.&lt;/p&gt;

&lt;p&gt;That makes the core experience easier to keep publicly accessible as a small open-source project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other people can build on it
&lt;/h3&gt;

&lt;p&gt;The source code is available under MPL-2.0, with attribution for the model creators, conversions, and major runtime dependencies.&lt;/p&gt;

&lt;p&gt;Someone could improve the interface, evaluate a better detector, build a more rigorous open-set recognition experiment, or adapt the same approach to birds, plants, or other things people notice outdoors.&lt;/p&gt;

&lt;p&gt;The project doesn't have to end with my version of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open innovation made it possible to build something private, inspectable, and inexpensive to run. It also made it possible to admit where the AI wasn't good enough yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And for me, that's just as important.&lt;/p&gt;

&lt;h3&gt;
  
  
  A small reason to go outside
&lt;/h3&gt;

&lt;p&gt;There's something funny about building an AI project for a challenge called &lt;em&gt;Touch Grass&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Usually, the goal of a software project is to make people spend more time using it.&lt;/p&gt;

&lt;p&gt;Here, I wanted almost the opposite.&lt;/p&gt;

&lt;p&gt;Meowfolio doesn't count your steps. It doesn't maintain a daily streak. It doesn't send notifications telling you to go find another cat.&lt;/p&gt;

&lt;p&gt;It doesn't need to.&lt;/p&gt;

&lt;p&gt;Maybe there's a cat you always see near your house.&lt;/p&gt;

&lt;p&gt;Maybe there's one that appears every morning near your college.&lt;/p&gt;

&lt;p&gt;Maybe you've walked past the same animal dozens of times without ever thinking about it.&lt;/p&gt;

&lt;p&gt;Now you have a small excuse to notice.&lt;/p&gt;

&lt;p&gt;You can give that cat a name, remember when you met it, and slowly build a collection of tiny encounters.&lt;/p&gt;

&lt;p&gt;And when you're finished, you can put your phone away and keep walking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Little cats. Big memories.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🐱 &lt;strong&gt;&lt;a href="https://mymeowfolio.vercel.app/" rel="noopener noreferrer"&gt;Try Meowfolio&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;&lt;a href="https://github.com/ThunderKhan/meowfolio" rel="noopener noreferrer"&gt;Explore the source code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🌿 &lt;em&gt;Built for Hacktoberfest 2026 — Week 1: Touch Grass.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built C++ Test Impact Analysis With Zero Runtime Dependencies</title>
      <dc:creator>Ayan Khan</dc:creator>
      <pubDate>Sun, 30 Aug 2026 08:56:17 +0000</pubDate>
      <link>https://dev.to/thunderkhan/i-built-c-test-impact-analysis-with-zero-runtime-dependencies-4bo8</link>
      <guid>https://dev.to/thunderkhan/i-built-c-test-impact-analysis-with-zero-runtime-dependencies-4bo8</guid>
      <description>&lt;p&gt;I entered a zero-dependency hackathon thinking the hard part would be selecting tests.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;The hard part was deciding when I had enough evidence to safely &lt;em&gt;not&lt;/em&gt; run one.&lt;/p&gt;

&lt;p&gt;Most C++ projects have a simple answer when code changes: run the test suite. That is safe, but it can get expensive fast. Change one header in a large project and there is a good chance most tests have nothing to do with it.&lt;/p&gt;

&lt;p&gt;So I spent the Zero Dependency Hackathon building &lt;strong&gt;diff2test&lt;/strong&gt;, a C++20 CLI that tries to answer a narrower question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given these changed files, which CTest tests can I justify running?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The word &lt;em&gt;justify&lt;/em&gt; ended up mattering more than I expected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;diff2test&lt;/code&gt; reads changed paths and metadata that a CMake/CTest build has already produced. It reconstructs the relationship between files, translation units, targets, executables, and tests. If all the evidence checks out, it emits the affected subset.&lt;/p&gt;

&lt;p&gt;If something important is missing or suspicious, the optimization disappears.&lt;/p&gt;

&lt;p&gt;That decision shaped almost everything that followed.&lt;/p&gt;




&lt;h2&gt;
  
  
  I already had the graph. It was just scattered everywhere.
&lt;/h2&gt;

&lt;p&gt;I did not want to predict test impact from filenames.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;parser.cpp&lt;/code&gt; probably means &lt;code&gt;ParserTest&lt;/code&gt;. No directory heuristics. No fuzzy matching. No history model.&lt;/p&gt;

&lt;p&gt;A normal C++ build already knows much more useful information.&lt;/p&gt;

&lt;p&gt;GCC and Clang can emit Make-style &lt;code&gt;.d&lt;/code&gt; files containing the prerequisites of each compilation.&lt;/p&gt;

&lt;p&gt;CMake's File API can describe targets, their sources, their dependencies, and the artifacts they produce.&lt;/p&gt;

&lt;p&gt;CTest can export its registered tests and their commands as JSON.&lt;/p&gt;

&lt;p&gt;Put those pieces together and the path I wanted looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;changed path
    ↓
compiler .d file
    ↓
translation unit
    ↓
CMake target
    ↓
targets that depend on it
    ↓
executable artifact
    ↓
CTest test
&lt;/code&gt;&lt;/pre&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%2F7l2q1kk04rzs6stshf91.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%2F7l2q1kk04rzs6stshf91.png" alt="How diff2test maps changed files to affected tests" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That looked almost suspiciously convenient.&lt;/p&gt;

&lt;p&gt;Then I checked the hackathon rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  The organizer email changed the project
&lt;/h2&gt;

&lt;p&gt;My first design would have been much easier.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;diff2test&lt;/code&gt; could run &lt;code&gt;git diff&lt;/code&gt;, ask CMake for metadata, invoke CTest, maybe use a helper command to discover dependency files, and combine the results.&lt;/p&gt;

&lt;p&gt;There was one problem: this was the &lt;strong&gt;Zero Dependency Hackathon&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I emailed the organizers and asked specifically whether launching Git, CMake, or CTest from the program would count as depending on separately installed software.&lt;/p&gt;

&lt;p&gt;Their answer was yes.&lt;/p&gt;

&lt;p&gt;If my executable shelled out to &lt;code&gt;git&lt;/code&gt;, then Git was part of its runtime dependency story. Same for CMake and CTest.&lt;/p&gt;

&lt;p&gt;They did give me an important escape route: parsing files those tools had &lt;strong&gt;already generated&lt;/strong&gt; was allowed, provided I disclosed that boundary and handled missing metadata gracefully.&lt;/p&gt;

&lt;p&gt;So I changed the architecture.&lt;/p&gt;

&lt;p&gt;The program would not produce its own evidence. It would consume evidence.&lt;/p&gt;

&lt;p&gt;A workflow can still do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; HEAD~1 | ./build/diff2test analyze &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but &lt;code&gt;diff2test&lt;/code&gt; never launches Git. The shell does that. Git writes paths to stdout and &lt;code&gt;diff2test&lt;/code&gt; reads newline-delimited paths from stdin.&lt;/p&gt;

&lt;p&gt;The same input could just as easily come from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'include/alpha.hpp\n'&lt;/span&gt; | ./build/diff2test analyze &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./build/diff2test analyze &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--changed-files&lt;/span&gt; changed.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CMake, CTest, and the compiler follow the same boundary. They can generate metadata before analysis. The running &lt;code&gt;diff2test&lt;/code&gt; process only reads files and stdin.&lt;/p&gt;

&lt;p&gt;This distinction became important because calling the project "zero dependency" without explaining where the boundary sits would be misleading. The supported workflow absolutely uses CMake metadata. CMake just is not bundled, linked, or executed by the shipped program.&lt;/p&gt;




&lt;h2&gt;
  
  
  My first real fixture broke two assumptions quickly
&lt;/h2&gt;

&lt;p&gt;I built a tiny CMake/CTest project early instead of spending the whole hackathon implementing against imaginary metadata.&lt;/p&gt;

&lt;p&gt;Good decision.&lt;/p&gt;

&lt;p&gt;The first thing I had to correct was CMake target traversal.&lt;/p&gt;

&lt;p&gt;CMake naturally tells me that something like a test executable depends on a library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alpha_test
    ↓
alpha
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Impact analysis asks the opposite question.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;alpha&lt;/code&gt; was affected, I need to find everything &lt;em&gt;downstream&lt;/em&gt; that depends on it.&lt;/p&gt;

&lt;p&gt;So I built reverse adjacency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alpha
    ↓
alpha_test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and propagate impact outward from the target owning the changed translation unit.&lt;/p&gt;

&lt;p&gt;That part was straightforward once I saw the real data.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.d&lt;/code&gt; files were more annoying.&lt;/p&gt;

&lt;p&gt;My original plan was to recursively scan the build directory and collect anything ending in &lt;code&gt;.d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I found &lt;code&gt;link.d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Right extension. Wrong meaning.&lt;/p&gt;

&lt;p&gt;That was enough to kill recursive discovery.&lt;/p&gt;

&lt;p&gt;The final MVP takes an explicit dependency-file list through &lt;code&gt;--dep-list&lt;/code&gt;. That list is created outside the process, and &lt;code&gt;diff2test&lt;/code&gt; still validates every entry against the CMake targets and compiled sources it already knows about.&lt;/p&gt;

&lt;p&gt;It is slightly more manual.&lt;/p&gt;

&lt;p&gt;I trust it more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then I had to decide what "fallback" actually means
&lt;/h2&gt;

&lt;p&gt;At this point I had the basic graph working, and my mental model had two possible results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;affected subset
or
full suite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I started deleting inputs.&lt;/p&gt;

&lt;p&gt;Suppose the CTest catalogue is valid and contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AlphaTest
BetaTest
CoreTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but one required dependency file disappears.&lt;/p&gt;

&lt;p&gt;I can no longer prove that a narrow result is safe, but I still know exactly what the full test catalogue contains.&lt;/p&gt;

&lt;p&gt;So the correct result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AlphaTest
BetaTest
CoreTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FULL_SUITE_SELECTED
exit 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I deleted the CTest catalogue itself.&lt;/p&gt;

&lt;p&gt;Now there was a more basic problem.&lt;/p&gt;

&lt;p&gt;I no longer knew what "the full suite" meant.&lt;/p&gt;

&lt;p&gt;Printing three remembered test names would be fabricated output. An empty test list could be misinterpreted as "nothing needs testing."&lt;/p&gt;

&lt;p&gt;That produced a separate state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FULL_SUITE_REQUIRED
exit 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means the program cannot safely enumerate the suite and the caller should run its normal full-test procedure.&lt;/p&gt;

&lt;p&gt;The successful narrow case is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUBSET_SELECTED
exit 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the three central outcomes became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;complete supported evidence
    → SUBSET_SELECTED

trusted test catalogue, unsafe impact evidence
    → FULL_SUITE_SELECTED

test catalogue itself cannot be trusted
    → FULL_SUITE_REQUIRED
&lt;/code&gt;&lt;/pre&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%2Flad9ti8qeu25xt46vjpn.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%2Flad9ti8qeu25xt46vjpn.png" alt="diff2test conservative safety model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I made both fallback conditions non-zero on purpose. If this is used in CI, I want degraded analysis to be visible.&lt;/p&gt;

&lt;p&gt;That little experiment with deleting &lt;code&gt;ctest-info.json&lt;/code&gt; changed the safety model more than any graph algorithm did.&lt;/p&gt;




&lt;h2&gt;
  
  
  What narrowing looks like when the evidence is good
&lt;/h2&gt;

&lt;p&gt;The repository contains a controlled fixture with three tests.&lt;/p&gt;

&lt;p&gt;If I change a header used only by Alpha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'include/alpha.hpp\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | ./build/diff2test analyze fixture &lt;span class="nt"&gt;--format&lt;/span&gt; names
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AlphaTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a shared header used by Alpha and Beta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'include/features_shared.hpp\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | ./build/diff2test analyze fixture &lt;span class="nt"&gt;--format&lt;/span&gt; names
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AlphaTest
BetaTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CoreTest&lt;/code&gt; stays out.&lt;/p&gt;

&lt;p&gt;Remove required dependency evidence and all three known tests come back with exit &lt;code&gt;10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Remove the CTest catalogue and no test names are invented at all. The program exits &lt;code&gt;11&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The changed path can stay identical through all of those runs. What changes is how much of the evidence graph I am willing to trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero dependencies meant I owned every boring parser
&lt;/h2&gt;

&lt;p&gt;The runtime implementation is a single C++20 source file.&lt;/p&gt;

&lt;p&gt;That was one of the hackathon bonus constraints, but the more interesting consequence was that I could not quietly pull in the libraries I would normally use for the tedious parts.&lt;/p&gt;

&lt;p&gt;JSON was the obvious one.&lt;/p&gt;

&lt;p&gt;CMake File API and CTest both give me JSON, so I needed a parser.&lt;/p&gt;

&lt;p&gt;At first this sounds like objects, arrays, strings, numbers, and a recursive-descent function or two.&lt;/p&gt;

&lt;p&gt;Then malformed input enters the picture.&lt;/p&gt;

&lt;p&gt;The parser ended up handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;strict JSON number grammar&lt;/li&gt;
&lt;li&gt;UTF-8 validation&lt;/li&gt;
&lt;li&gt;string escapes&lt;/li&gt;
&lt;li&gt;Unicode escapes and surrogate pairs&lt;/li&gt;
&lt;li&gt;duplicate object keys&lt;/li&gt;
&lt;li&gt;positional error information&lt;/li&gt;
&lt;li&gt;nesting limits&lt;/li&gt;
&lt;li&gt;maximum input size&lt;/li&gt;
&lt;li&gt;maximum string size
I also wrote the Make-style dependency parser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real &lt;code&gt;.d&lt;/code&gt; files can contain line continuations, escaped spaces, escaped characters, comments, CRLF input, repeated prerequisites, and malformed rules. One stress test feeds the parser a rule containing &lt;strong&gt;10,000 prerequisites&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is probably the part of the hackathon that changed my view of dependencies the most.&lt;/p&gt;

&lt;p&gt;In a normal project I would use a mature JSON library. Happily.&lt;/p&gt;

&lt;p&gt;Writing my own here was useful because it exposed how much correctness work disappears behind a small include statement.&lt;/p&gt;

&lt;p&gt;The standard library gave me the pieces. It did not give me JSON, Make dependency syntax, or the trust policy I needed around either of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;std::filesystem&lt;/code&gt; did not solve path safety for me
&lt;/h2&gt;

&lt;p&gt;Path handling looked less interesting than parsing.&lt;/p&gt;

&lt;p&gt;It consumed plenty of time anyway.&lt;/p&gt;

&lt;p&gt;A changed file can be deleted, so I cannot assume every path exists on disk and blindly canonicalize it.&lt;/p&gt;

&lt;p&gt;I also needed to handle things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/project/foo
/project/foobar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which share a string prefix without one being contained in the other.&lt;/p&gt;

&lt;p&gt;There are also &lt;code&gt;..&lt;/code&gt; escapes, project-root boundaries, build-root boundaries, relative metadata paths, and artifact paths coming from CMake.&lt;/p&gt;

&lt;p&gt;I ended up using lexical normalization plus explicit containment checks instead of treating filesystem canonicalization as the answer to every path question.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;std::filesystem&lt;/code&gt; was excellent machinery.&lt;/p&gt;

&lt;p&gt;The policy was still mine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same source file can mean two different compilations
&lt;/h2&gt;

&lt;p&gt;Another issue showed up once I started thinking about completeness.&lt;/p&gt;

&lt;p&gt;Imagine &lt;code&gt;foo.cpp&lt;/code&gt; is compiled into two CMake targets.&lt;/p&gt;

&lt;p&gt;Those compilations may have different definitions or include paths.&lt;/p&gt;

&lt;p&gt;If I see one dependency file for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;foo.cpp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I cannot mark the source globally "covered."&lt;/p&gt;

&lt;p&gt;That dependency information belongs to one compilation in one target.&lt;/p&gt;

&lt;p&gt;So dependency completeness is tracked per:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(CMake target, compiled source)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than only by source path.&lt;/p&gt;

&lt;p&gt;It sounds like a small bookkeeping choice. Without it, one valid &lt;code&gt;.d&lt;/code&gt; file could accidentally make another compilation of the same file look covered.&lt;/p&gt;

&lt;p&gt;I would rather widen than make that assumption.&lt;/p&gt;




&lt;h2&gt;
  
  
  CTest executable matching also needed a stricter rule
&lt;/h2&gt;

&lt;p&gt;CTest tells me what command a registered test runs.&lt;/p&gt;

&lt;p&gt;CMake tells me which executable artifacts targets produce.&lt;/p&gt;

&lt;p&gt;The tempting shortcut is to compare executable basenames.&lt;/p&gt;

&lt;p&gt;I decided against that.&lt;/p&gt;

&lt;p&gt;Two directories can contain executables with the same name. Wrapper commands complicate the relationship further. Multi-configuration builds add another source of ambiguity.&lt;/p&gt;

&lt;p&gt;For the supported workflow, a CTest command has to map to exactly one normalized CMake executable artifact.&lt;/p&gt;

&lt;p&gt;If it does not, subset selection stops.&lt;/p&gt;

&lt;p&gt;That choice narrowed the projects I could support during the hackathon, but it kept the mapping explainable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stale evidence was worse than missing evidence
&lt;/h2&gt;

&lt;p&gt;Missing metadata is easy to reason about.&lt;/p&gt;

&lt;p&gt;You know it is gone.&lt;/p&gt;

&lt;p&gt;Stale metadata is more dangerous because it still looks valid.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;.d&lt;/code&gt; file may exist while describing an older compilation. A header might have changed afterward.&lt;/p&gt;

&lt;p&gt;For project-local prerequisites, &lt;code&gt;diff2test&lt;/code&gt; performs a timestamp check. If a prerequisite is newer than the dependency file that claims to describe it, narrow selection is disabled.&lt;/p&gt;

&lt;p&gt;If a timestamp that is required for the check cannot be read safely, the result widens too.&lt;/p&gt;

&lt;p&gt;I am deliberately careful about what this proves.&lt;/p&gt;

&lt;p&gt;A passing timestamp check does not establish that the metadata cryptographically matches the current source tree. It means I found &lt;strong&gt;no detectable staleness under that policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There are other inputs that I simply refuse to model narrowly.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;CMakeLists.txt&lt;/code&gt; or another &lt;code&gt;.cmake&lt;/code&gt; file changes, that can alter source membership, target relationships, definitions, generated files, or test registration. Predicting all of that would mean interpreting CMake itself.&lt;/p&gt;

&lt;p&gt;So build-configuration changes fall back.&lt;/p&gt;

&lt;p&gt;Unknown changed paths do too. A file outside the dependency graph might be irrelevant, or it might be a generated input, script, resource, or something else the current model cannot see.&lt;/p&gt;

&lt;p&gt;I do not classify "unknown" as "unaffected."&lt;/p&gt;




&lt;h2&gt;
  
  
  Explanations came almost for free
&lt;/h2&gt;

&lt;p&gt;Once I was storing the graph and predecessor relationships, it became possible to show why a test had been selected.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;changed path: include/alpha.hpp
dependency file: CMakeFiles/alpha.dir/src/alpha.cpp.o.d
translation unit: src/alpha.cpp
owning target: alpha
dependent target: alpha_test
registered test: AlphaTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That turned into &lt;code&gt;--explain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I like this more than I expected.&lt;/p&gt;

&lt;p&gt;If a CI optimization decides that hundreds of tests can be skipped, I want some way to inspect the chain that led to the tests it kept.&lt;/p&gt;

&lt;p&gt;It also made debugging much easier during the hackathon. A wrong final set only tells you that something went wrong. A wrong evidence chain often tells you where.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing a program whose job is to skip tests felt slightly recursive
&lt;/h2&gt;

&lt;p&gt;By the end, the repository had seven dependency-free C++ test executables covering the JSON parser, &lt;code&gt;.d&lt;/code&gt; parser, path handling, CTest metadata, CMake metadata, impact analysis, and additional hardening.&lt;/p&gt;

&lt;p&gt;One synthetic target graph deliberately contains a chain, a diamond, a cycle, and an unrelated branch at the same time.&lt;/p&gt;

&lt;p&gt;That caught the properties I cared about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transitive reverse traversal&lt;/li&gt;
&lt;li&gt;no duplicate propagation through a diamond&lt;/li&gt;
&lt;li&gt;cycle termination&lt;/li&gt;
&lt;li&gt;unrelated tests remaining unrelated
I also wanted real generated metadata in CI, not only JSON fixtures I had written myself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CI workflow creates a CMake File API query, configures and builds the controlled fixture, collects real compiler &lt;code&gt;.o.d&lt;/code&gt; files, exports real CTest JSON, and analyzes those artifacts with &lt;code&gt;diff2test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The narrow Alpha case, the Alpha/Beta shared-header case, missing dependency evidence, and missing CTest catalogue are all exercised there.&lt;/p&gt;

&lt;p&gt;Some failures were less interesting.&lt;/p&gt;

&lt;p&gt;At one point a helper function in a test collided with &lt;code&gt;std::quoted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I renamed it.&lt;/p&gt;

&lt;p&gt;That was the fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  I also tried to attack the zero-dependency claim itself
&lt;/h2&gt;

&lt;p&gt;"Zero runtime dependencies" is easy to put in a README.&lt;/p&gt;

&lt;p&gt;I wanted the repository to make the claim inspectable.&lt;/p&gt;

&lt;p&gt;CI scans the runtime source for process-spawning APIs.&lt;/p&gt;

&lt;p&gt;The Release executable is inspected with &lt;code&gt;ldd&lt;/code&gt;. On the Linux CI runner it showed the normal system/toolchain runtime entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linux-vdso.so.1
libstdc++.so.6
libgcc_s.so.1
libc.so.6
libm.so.6
/lib64/ld-linux-x86-64.so.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no CMake application library or third-party project library in that list.&lt;/p&gt;

&lt;p&gt;A separate development CI job runs the test executables under AddressSanitizer and UndefinedBehaviorSanitizer. Those are verification tools used while building and testing the project; they are not dependencies of the normal shipped executable.&lt;/p&gt;

&lt;p&gt;I also tested determinism.&lt;/p&gt;

&lt;p&gt;Reversing CMake references, reversing the explicit dependency list, changing CTest catalogue order, and duplicating changed paths should not change user-visible output.&lt;/p&gt;

&lt;p&gt;CI captures real CLI stdout and stderr, compares reordered-evidence runs byte-for-byte, and repeats the same real-fixture analysis 20 times.&lt;/p&gt;

&lt;p&gt;That may sound excessive for a hackathon project.&lt;/p&gt;

&lt;p&gt;The program decides what &lt;em&gt;not&lt;/em&gt; to test. I was comfortable being slightly paranoid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducible builds
&lt;/h2&gt;

&lt;p&gt;The hackathon had a reproducible-build bonus.&lt;/p&gt;

&lt;p&gt;I asked the organizers what they considered reproducible in this context. The target was two independent builds using the same environment and toolchain producing identical output, rather than pretending GCC, Clang, MSVC, Linux, and Windows should all produce the same bytes.&lt;/p&gt;

&lt;p&gt;CI performs two clean Release builds on the same runner.&lt;/p&gt;

&lt;p&gt;The binaries were byte-identical and both produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;162a6bbf52034f0c468ab2c7c82853a449590530768e9ed6ddd82f1b7aabc903
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the scope of the claim.&lt;/p&gt;

&lt;p&gt;Nothing broader.&lt;/p&gt;




&lt;h2&gt;
  
  
  What did this make unnecessary?
&lt;/h2&gt;

&lt;p&gt;The hackathon also had a "Package Killer" bonus: identify an installable package or tool whose role your standard-library implementation can remove.&lt;/p&gt;

&lt;p&gt;I compared &lt;code&gt;diff2test&lt;/code&gt; with &lt;strong&gt;RTS++ / Ekstazi++&lt;/strong&gt;, a C++ regression-test-selection system whose broader approach involves infrastructure including LLVM instrumentation and its own RTS components.&lt;/p&gt;

&lt;p&gt;I want to keep this claim narrow because the projects do not have identical feature sets.&lt;/p&gt;

&lt;p&gt;For the specific CMake/CTest workflow that &lt;code&gt;diff2test&lt;/code&gt; supports, I can perform useful conservative test-impact analysis without adding a dedicated RTS runtime stack.&lt;/p&gt;

&lt;p&gt;That works because I am reusing evidence that the existing compiler, CMake, and CTest workflow already generated.&lt;/p&gt;

&lt;p&gt;No compiler plugin.&lt;/p&gt;

&lt;p&gt;No runtime agent.&lt;/p&gt;

&lt;p&gt;No historical coverage database.&lt;/p&gt;

&lt;p&gt;No daemon.&lt;/p&gt;

&lt;p&gt;No network service.&lt;/p&gt;

&lt;p&gt;For a broader production RTS system, some of those techniques may be exactly what you want. For this constrained workflow, I wanted to see how far the existing metadata could take me.&lt;/p&gt;

&lt;p&gt;Quite far, as it turned out.&lt;/p&gt;




&lt;h2&gt;
  
  
  A small performance note
&lt;/h2&gt;

&lt;p&gt;I benchmarked the controlled fixture with 20 warmups and 200 measured full-process invocations.&lt;/p&gt;

&lt;p&gt;The result was approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;median: 2.106 ms
p95:    2.209 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixture is tiny, so I am not using that number as evidence that &lt;code&gt;diff2test&lt;/code&gt; has solved monorepo-scale performance.&lt;/p&gt;

&lt;p&gt;I did not have a verified 100,000-node or million-node benchmark during the hackathon, so I did not invent one.&lt;/p&gt;

&lt;p&gt;The useful observation is simply that analysis itself is lightweight once the metadata already exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would change next
&lt;/h2&gt;

&lt;p&gt;The most obvious rough edge is &lt;code&gt;--dep-list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Making the caller explicitly provide the dependency files was the safe choice after the &lt;code&gt;link.d&lt;/code&gt; discovery, but it makes setup more manual than I would ultimately like.&lt;/p&gt;

&lt;p&gt;I would not solve that by going back to recursive extension matching.&lt;/p&gt;

&lt;p&gt;I would add proper evidence adapters for additional build shapes and generators.&lt;/p&gt;

&lt;p&gt;Ninja should understand Ninja's dependency representation.&lt;/p&gt;

&lt;p&gt;MSVC support should understand MSVC's dependency/path semantics.&lt;/p&gt;

&lt;p&gt;Other CMake generators should get their own tested mapping rules.&lt;/p&gt;

&lt;p&gt;That is slower than adding heuristics, but it preserves the property I care about.&lt;/p&gt;

&lt;p&gt;I would also happily delete my JSON parser in a normal production version and use a mature library.&lt;/p&gt;

&lt;p&gt;The hackathon was useful precisely because I had to discover what that dependency normally buys me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it ended
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;diff2test&lt;/code&gt; finished the hackathon as one C++20 runtime source file with no third-party runtime code, no runtime subprocess execution, no network requirement, and a deliberately narrow CMake/CTest test-impact model.&lt;/p&gt;

&lt;p&gt;The repository verifies the implementation with native C++ tests, real generated metadata, sanitizer runs, deterministic-output checks, dependency inspection, and byte-identical same-runner Release builds.&lt;/p&gt;

&lt;p&gt;There are plenty of things it does not support yet: MSVC dependency formats, Ninja's dependency database, arbitrary CMake generator layouts, wrapper/interpreter-style CTest commands, generated custom-command relationships, coverage-guided selection, and historical test mappings.&lt;/p&gt;

&lt;p&gt;Those boundaries are written down because guessing around them would defeat the point.&lt;/p&gt;

&lt;p&gt;I started the weekend focused on the graph that selects affected tests.&lt;/p&gt;

&lt;p&gt;I finished it thinking much more about the checks surrounding that graph.&lt;/p&gt;

&lt;p&gt;A fast path is easy to write.&lt;/p&gt;

&lt;p&gt;The interesting engineering starts when you have to decide whether you are actually allowed to take it.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;diff2test&lt;/code&gt;, that became the lesson I care about most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;safe optimization is mostly precondition engineering.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/ThunderKhan/diff2test/releases/tag/v0.1.1" rel="noopener noreferrer"&gt;https://github.com/ThunderKhan/diff2test/releases/tag/v0.1.1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=TQp6_BOJHbw" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=TQp6_BOJHbw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built during the &lt;strong&gt;Zero Dependency Hackathon 2026&lt;/strong&gt; by Hackathon Raptors.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>opensource</category>
      <category>devtools</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
