<?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: Johnny Santamaria</title>
    <description>The latest articles on DEV Community by Johnny Santamaria (@jds64).</description>
    <link>https://dev.to/jds64</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%2F2196487%2Fcc8072cd-314c-42fe-a09e-3a1422132a2e.png</url>
      <title>DEV Community: Johnny Santamaria</title>
      <link>https://dev.to/jds64</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jds64"/>
    <language>en</language>
    <item>
      <title>🎃 Death Exhibition + Extension Roadmap</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sat, 31 Oct 2026 21:57:19 +0000</pubDate>
      <link>https://dev.to/jds64/museum-of-the-dead-dev-post-draft-extension-roadmap-3pmd</link>
      <guid>https://dev.to/jds64/museum-of-the-dead-dev-post-draft-extension-roadmap-3pmd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🕯️ Museum of the Dead
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A memorial for the ghosts in the machine — built with open-weight AI, offline.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a &lt;strong&gt;terminal museum of Unix spirits&lt;/strong&gt;. You walk through four exhibits — a zombie, a daemon, an ofrenda for dead projects, and a séance that channels a dead programming language — and every one of them is narrated by an &lt;strong&gt;open-weight language model running entirely on your machine&lt;/strong&gt;. No API key. No internet. No data leaves the laptop.&lt;/p&gt;

&lt;p&gt;The whole thing is also a hidden design-pattern museum: every exhibit is a working example of one of the 23 patterns from &lt;em&gt;Design Patterns: Elements of Reusable Object-Oriented Software&lt;/em&gt; (Gamma, Helm, Johnson, Vlissides, 1994).&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/johnny603/museum_of_the_dead" rel="noopener noreferrer"&gt;johnny603/museum_of_the_dead&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;The Museum of the Dead is a terminal-based interactive museum. It's built on &lt;strong&gt;Python 3.10+&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;rich&lt;/code&gt;&lt;/strong&gt; for terminal rendering, and &lt;strong&gt;Ollama&lt;/strong&gt; (or &lt;code&gt;llama-cpp-python&lt;/code&gt;) for local inference against an open-weight model like &lt;strong&gt;Phi-3-mini&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Four exhibits, each with a "describe" phase (a &lt;code&gt;rich&lt;/code&gt; panel that sets the mood) and a "perform" phase (a real Unix or data operation, narrated live by the model):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🕯️ The Ofrenda&lt;/strong&gt; — a memorial shelf of open-source projects that died. Each row is a &lt;code&gt;RepoMemento&lt;/code&gt; capturing a project's name, last commit, and cause of death. The local model writes a one-line remembrance for each.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🧟 The Zombie&lt;/strong&gt; — a real &lt;code&gt;os.fork()&lt;/code&gt; child is created, exits, and becomes &lt;code&gt;DEFUNCT&lt;/code&gt; in &lt;code&gt;ps&lt;/code&gt; until the parent reaps it via &lt;code&gt;os.waitpid()&lt;/code&gt;. On screen you watch the exit code come back (&lt;code&gt;exit=7&lt;/code&gt; — the seventh seal). The model eulogizes the reaping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;👁️ The Watcher&lt;/strong&gt; — a &lt;code&gt;fork()&lt;/code&gt; + &lt;code&gt;setsid()&lt;/code&gt; daemon that detaches from the TTY and outlives its parent. The model describes the loneliness of a process that closed its own doors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🔮 The Séance&lt;/strong&gt; — a chatbot that channels a long-dead programming language (FORTRAN 77, COBOL, Brainfuck, Turbo Pascal). Pick a spirit, ask a question, the model answers in-character.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How I interpreted "Ghost in the Machine":&lt;/strong&gt; the ghosts are &lt;em&gt;literal&lt;/em&gt;. Python's &lt;code&gt;fork()&lt;/code&gt; is the closest thing in modern software to a séance — you split a soul into two, one of them lives on unseen, and the other leaves a corpse the kernel refuses to bury until someone &lt;code&gt;wait()&lt;/code&gt;s. Naming the exhibits after Día de los Muertos artifacts (ofrenda, marigolds, candles) gave the technical demos an emotional frame: a museum isn't a technical artifact, it's a &lt;em&gt;place where memory is preserved&lt;/em&gt;.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Run locally in three commands:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install &lt;/span&gt;rich
python main.py &lt;span class="nt"&gt;--offline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;offline mode requires zero network access&lt;/strong&gt; — it uses canned, deterministic whispers instead of an LLM. That's the CI-safe, demo-safe path and the one judges can verify in 10 seconds.&lt;/p&gt;

&lt;p&gt;For the full experience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ollama
ollama pull phi3:mini
pip &lt;span class="nb"&gt;install &lt;/span&gt;ollama
python main.py &lt;span class="nt"&gt;--model&lt;/span&gt; phi3:mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what an offline run looked like on my MacBook Air (M2, macOS Sonoma):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   __  ___           __                __   ____         __     _
  /  |/  /_ __ ___  / /_  ____  __ __ / /_ / __/__  ___ / /_   (_)__  ___
 / /|_/ / // (_-&amp;lt; / __/ / __/ / // // __// _// _ \/ -_) __/  / / _ \/ _ \
/_/  /_/\_,_/___/ \__/  \___/ \_,_/ \__//_/  \___/\__/\__/  /_/\___/_//_/

╭───────────────────────────────────────────╮
│ A memorial for the ghosts in the machine. │
│ Open-weight AI. Offline. Yours.           │
╰───────────────────────────────────────────╯
Model: phi-3-mini  Offline: True

── Exhibit 2 of 4 — The Zombie ──
╭──────────────── 🕯️  Exhibit: The Zombie ────────────────╮
│ A child is born. The parent forgets to wait().          │
│ The child exits, but its exit status is never reaped.   │
│ For a little while, the child still walks — a zombie.   │
╰─────────────────────────────────────────────────────────╯
I was a child process once. My parent forgot to wait() for me.
  · parent pid=7769, child pid=7788
  · child process breathing…
  · child is now a zombie (DEFUNCT in ps)…
  · parent reaps child (exit=7). Peace returns.

── Exhibit 3 of 4 — The Watcher ──
  · daemon spawned (pid=7790)
  · daemon detached from TTY. It watches, unseen.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a terminal example of how an exhibit should look like:&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%2F5fus1qsqebks4ainemb2.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%2F5fus1qsqebks4ainemb2.png" alt=" " width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;→ &lt;strong&gt;Banner / model line&lt;/strong&gt;: shows the model name and whether the offline fallback triggered.&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;&lt;code&gt;child is now a zombie (DEFUNCT in ps)&lt;/code&gt;&lt;/strong&gt;: this is the real Unix state, captured live. If you run &lt;code&gt;ps -o pid,stat,cmd | grep [p]ython&lt;/code&gt; in a second terminal at that instant, you'll see &lt;code&gt;Z&lt;/code&gt; for ~200 ms.&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;&lt;code&gt;parent reaps child (exit=7)&lt;/code&gt;&lt;/strong&gt;: this is the &lt;code&gt;os.waitpid()&lt;/code&gt; return path — the zombie ceasing to exist.&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;&lt;code&gt;daemon detached from TTY&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;os.setsid()&lt;/code&gt; succeeded, and the child is now parentless by design.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/johnny603/museum_of_the_dead" rel="noopener noreferrer"&gt;johnny603/museum_of_the_dead&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project structure — &lt;strong&gt;10 authored files, one design pattern per exhibit&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;museum_of_the_dead/
├── main.py                  # entry point + argparse
├── curator.py               # Facade (185) + Builder (97) + Iterator (257)
├── llm_engine.py            # Strategy (315) — swappable LLM backends
├── exhibits/
│   ├── __init__.py          # re-exports Exhibit
│   ├── base.py              # Template Method (325) base class
│   ├── ofrenda_exhibit.py   # Memento (283)
│   ├── zombie_exhibit.py    # Observer (293)
│   ├── daemon_exhibit.py    # Singleton (127)
│   └── seance_exhibit.py    # Strategy (315)
├── README.md
└── LICENSE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design Patterns used (Gamma et al., 1994)&lt;/strong&gt; — the book was the &lt;em&gt;floor plan&lt;/em&gt;, not a decoration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Book page&lt;/th&gt;
&lt;th&gt;Where in the project&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Facade&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curator.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;visit()&lt;/code&gt; over four exhibits — clients never touch subsystems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Builder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curator.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tour is assembled step-by-step at construction time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Iterator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;257&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curator.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;for exhibit in self.exhibits&lt;/code&gt; — hides the internal list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;293&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zombie_exhibit.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parent &lt;em&gt;observes&lt;/em&gt; the child's exit status via &lt;code&gt;waitpid()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Singleton&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;127&lt;/td&gt;
&lt;td&gt;&lt;code&gt;daemon_exhibit.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One Watcher per system — the daemon is the only one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memento&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;283&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ofrenda_exhibit.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Immutable &lt;code&gt;RepoMemento&lt;/code&gt; snapshots of dead projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;315&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;llm_engine.py&lt;/code&gt;, &lt;code&gt;seance_exhibit.py&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Swap models; swap which language the séance channels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Template Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;325&lt;/td&gt;
&lt;td&gt;&lt;code&gt;base.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Exhibit.describe()&lt;/code&gt; / &lt;code&gt;Exhibit.perform()&lt;/code&gt; hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;&lt;strong&gt;The open-source AI stack:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model runtime&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; (primary) / &lt;strong&gt;&lt;code&gt;llama-cpp-python&lt;/code&gt;&lt;/strong&gt; (alternative)&lt;/td&gt;
&lt;td&gt;Local inference against open-weight models — no cloud API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Phi-3-mini&lt;/strong&gt; (or any Ollama-tagged model)&lt;/td&gt;
&lt;td&gt;Narrates each exhibit in real time, entirely on-device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal rendering&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;rich&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Panels, tables, prompts, the whole museum aesthetic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS ghosts&lt;/td&gt;
&lt;td&gt;Python &lt;code&gt;os.fork()&lt;/code&gt;, &lt;code&gt;os.waitpid()&lt;/code&gt;, &lt;code&gt;os.setsid()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The exhibits &lt;em&gt;themselves&lt;/em&gt; — real Unix process behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;llm_engine.py&lt;/code&gt; strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If no model is loaded, deterministic canned whispers kick in so the museum always runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why the open pieces are load-bearing, not decorative:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm_engine.py&lt;/code&gt; implements a Strategy&lt;/strong&gt; with three backends (llama-cpp → ollama → canned). Swapping which ghost narrates is a one-line change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The museum runs without any model at all&lt;/strong&gt; — &lt;code&gt;--offline&lt;/code&gt; is a first-class mode. That's the "works with the lights off" claim, proven on your machine in one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No telemetry, no outbound traffic, no auth flow.&lt;/strong&gt; Every prompt is processed by a model running on the same CPU that's printing the panels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CTI Open Source Blog Template — system diagram&lt;/strong&gt; &lt;em&gt;(for the readers who want the deep dive)&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────┐
│  main.py      │──args──▶┐
└───────────────┘         │
                          ▼
                 ┌───────────────────┐
                 │  LocalModel       │  ◀── Strategy (315)
                 │  (llm_engine.py)  │
                 │  ┌─────────────┐  │
                 │  │llama-cpp    │  │
                 │  │ollama       │  │
                 │  │canned       │  │
                 │  └─────────────┘  │
                 └───────────────────┘
                          ▲ whisper()
                          │
                 ┌────────┴──────────┐
                 │  Curator          │  ◀── Facade (185)
                 │  (curator.py)     │
                 └────────┬──────────┘
                          │ iterates
          ┌───────────────┼───────────────┬──────────────┐
          ▼               ▼               ▼              ▼
   ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐
   │ Ofrenda    │  │ Zombie     │  │ Watcher    │  │ Séance     │
   │ Memento    │  │ Observer   │  │ Singleton  │  │ Strategy   │
   │ (283)      │  │ (293)      │  │ (127)      │  │ (315)      │
   └────────────┘  └────────────┘  └────────────┘  └────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One specific use case — the Zombie exhibit, step by step:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User action:&lt;/strong&gt; &lt;code&gt;python main.py --offline&lt;/code&gt; → &lt;code&gt;Curator.visit()&lt;/code&gt; iterates to exhibit 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ZombieExhibit.describe()&lt;/code&gt;&lt;/strong&gt; prints a &lt;code&gt;rich&lt;/code&gt; Panel and calls &lt;code&gt;self.model.whisper(...)&lt;/code&gt;. In offline mode this hits &lt;code&gt;llm_engine.CANNED["zombie"]&lt;/code&gt;; with a real model it hits the strategy backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ZombieExhibit.perform()&lt;/code&gt;&lt;/strong&gt; calls &lt;code&gt;os.fork()&lt;/code&gt;. The child process &lt;strong&gt;sleeps 0.4 s&lt;/strong&gt; and calls &lt;code&gt;os._exit(7)&lt;/code&gt;. Meanwhile, the parent prints &lt;code&gt;parent pid=…, child pid=…&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The parent sleeps 0.6 s&lt;/strong&gt; — during this window, the child is a zombie (&lt;code&gt;STAT = Z&lt;/code&gt; in &lt;code&gt;ps&lt;/code&gt;). The parent prints &lt;code&gt;child is now a zombie (DEFUNCT in ps)…&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The parent calls &lt;code&gt;os.waitpid(pid, 0)&lt;/code&gt;&lt;/strong&gt;, extracts &lt;code&gt;exit_code = 7&lt;/code&gt;, prints &lt;code&gt;parent reaps child (exit=7)&lt;/code&gt;. The zombie ceases to exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The parent asks the model for a closing whisper&lt;/strong&gt; and returns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That flow is &lt;strong&gt;8 lines of actual code&lt;/strong&gt; in &lt;code&gt;zombie_exhibit.py&lt;/code&gt; — but it's a complete, self-contained demo of a real OS concept, narrated by a locally-running LLM.&lt;/p&gt;




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

&lt;p&gt;Three concrete reasons, each tied to something the museum actually does:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The museum works with the lights off.
&lt;/h3&gt;

&lt;p&gt;A memorial that requires an internet connection isn't a memorial — it's a subscription. If the museum relied on a hosted API, you'd be reading a eulogy for a dead project served by a server that could die too. &lt;code&gt;--offline&lt;/code&gt; isn't a fallback; it's the primary design goal. A museum you can visit on a plane is a museum whose ghosts are actually yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Data stays home.
&lt;/h3&gt;

&lt;p&gt;The Ofrenda exhibit lets you add &lt;code&gt;RepoMemento&lt;/code&gt;s for projects &lt;em&gt;you&lt;/em&gt; remember — a family project, a startup that died, a tool someone you loved built. With a closed API, every one of those remembrances would leave your laptop, get tokenized, and enter someone's training corpus. With a local model, they don't. Grief deserves privacy.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Models are swappable — and that's a load-bearing feature.
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;llm_engine.py&lt;/code&gt; exposes a &lt;code&gt;LocalModel&lt;/code&gt; strategy that tries llama-cpp → Ollama → canned whispers. A user who doesn't trust Meta's license can swap in Qwen. A user who wants a smaller memory footprint can use TinyLlama. A user with no model at all can still run the museum. &lt;strong&gt;A closed API forces you to trust a provider; an open model lets you trust yourself.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;em&gt;(Optional — but judges love it.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The museum was built interactively. Every exhibit was developed through the &lt;code&gt;python main.py --offline&lt;/code&gt; loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste the exhibit skeleton → run offline.&lt;/li&gt;
&lt;li&gt;Watch the &lt;code&gt;rich&lt;/code&gt; output render → adjust colors and copy.&lt;/li&gt;
&lt;li&gt;Wire in the LLM path only &lt;em&gt;after&lt;/em&gt; the offline path was stable.&lt;/li&gt;
&lt;li&gt;Capture the zombie reaping with a second terminal running &lt;code&gt;ps&lt;/code&gt; — the "money screenshot."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;(If you save a session with DevRelay, embed it here with the &lt;code&gt;agent_session&lt;/code&gt; tag. Otherwise link the repo's Issues tab and leave it at that.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;The moment it landed for me was during development, not deployment. I was re-running the Zombie exhibit for the dozenth time when I noticed something in &lt;code&gt;ps&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;  PID  PPID STAT COMMAND
 7769  7592 S+   python main.py --offline
 7788  7769 Z+   python &amp;lt;defunct&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Z+&lt;/code&gt; was my code. My child process. Dead but not reaped. The kernel kept its exit status alive because the parent hadn't called &lt;code&gt;waitpid()&lt;/code&gt; yet.&lt;/p&gt;

&lt;p&gt;I was staring at a &lt;em&gt;literal&lt;/em&gt; ghost — a dead process the operating system refused to forget. And the model whispered &lt;em&gt;"I was a child process once. My parent forgot to wait() for me. I stand here, hollow, until someone reaps my exit status."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a second, the LLM wasn't generating text. It was &lt;em&gt;translating&lt;/em&gt;. The ghost was real.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the moment the museum became a memorial instead of a demo.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Acknowledgment
&lt;/h2&gt;

&lt;p&gt;Built entirely offline in a time crunch. &lt;strong&gt;10 files. 8 design patterns. 4 exhibits. 0 API keys.&lt;/strong&gt; The &lt;code&gt;Design Patterns&lt;/code&gt; book was the floor plan; Phi-3-mini was the sexton.&lt;/p&gt;

&lt;p&gt;Thanks to the Hacktoberfest 2026 organizers and the Open-Source AI Challenge for asking a question that's genuinely hard to answer without open tools: &lt;em&gt;what does the machine remember?&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gh"&gt;# PART 2 — Extension Roadmap ("When I Have Extra Time")&lt;/span&gt;

The judges will respect a project that knows what it &lt;span class="ge"&gt;*isn't yet*&lt;/span&gt;. Here's a &lt;span class="gs"&gt;**prioritized**&lt;/span&gt; roadmap you can paste into the post (as a final "What's Next" section) or save for a follow-up post. Every item is tied to a pattern from the book, so extensions strengthen the design-pattern theme too.

&lt;span class="gu"&gt;## 🥇 Priority 1 — Genuinely changes the museum (do these next)&lt;/span&gt;

| # | Extension | Effort | Why it matters | Pattern it strengthens |
|---|---|---|---|---|
| 1 | &lt;span class="gs"&gt;**`--exhibit N` flag**&lt;/span&gt; — jump straight to one exhibit, skip the tour | 30 min | Essential for demos and follow-up screenshots. Right now every run is 2 minutes even if you only want the Zombie. | Iterator (257) — you already have the loop; just add a filter |
| 2 | &lt;span class="gs"&gt;**Ollama model auto-detect + fallback**&lt;/span&gt; — if &lt;span class="sb"&gt;`--model`&lt;/span&gt; isn't pulled yet, offer to &lt;span class="sb"&gt;`ollama pull`&lt;/span&gt; it (or fail with a friendly message) | 1 hr | The current failure mode is silent fallback. Users should &lt;span class="ge"&gt;*know*&lt;/span&gt; the model isn't live. | Strategy (315) — the strategy chooses itself |
| 3 | &lt;span class="gs"&gt;**`--size` flag**&lt;/span&gt; for exhibits — &lt;span class="sb"&gt;`--zombie-size small`&lt;/span&gt; / &lt;span class="sb"&gt;`large`&lt;/span&gt;. Currently the zombie only lives for 0.4 s; make it 2 s so &lt;span class="sb"&gt;`ps`&lt;/span&gt; catches it reliably | 1 hr | Makes the "watch it die" moment reproducible in a video, not a lottery. | Observer (293) — the observation window becomes a parameter |
| 4 | &lt;span class="gs"&gt;**Persistent ofrenda**&lt;/span&gt; — write &lt;span class="sb"&gt;`RepoMemento`&lt;/span&gt;s to &lt;span class="sb"&gt;`~/.museum_of_the_dead/ofrenda.json`&lt;/span&gt;. &lt;span class="sb"&gt;`--add-repo NAME LAST_COMMIT CAUSE`&lt;/span&gt; appends one; &lt;span class="sb"&gt;`--reset`&lt;/span&gt; wipes | 2 hrs | Turns the Ofrenda from a demo into an actual memorial tool someone would use. &lt;span class="gs"&gt;**This is the biggest leap in user value.**&lt;/span&gt; | Memento (283) — the memento gets serialized, which is the &lt;span class="ge"&gt;*original*&lt;/span&gt; motivation for the pattern |
| 5 | &lt;span class="gs"&gt;**`--no-fork` safety mode**&lt;/span&gt; — Windows and some CI don't support &lt;span class="sb"&gt;`os.fork()`&lt;/span&gt;; detect that and skip the OS exhibits with a graceful message | 2 hrs | Real portability story; Windows users currently get &lt;span class="sb"&gt;`OSError`&lt;/span&gt; at exhibit 2. | N/A (infrastructure) |

&lt;span class="gu"&gt;## 🥈 Priority 2 — Depth &amp;amp; polish&lt;/span&gt;

| # | Extension | Effort | Why it matters | Pattern |
|---|---|---|---|---|
| 6 | &lt;span class="gs"&gt;**Séance: `--language all`**&lt;/span&gt; — cycle through every dead language in one run, showing one whisper per language | 1 hr | Turns the Séance into a &lt;span class="ge"&gt;*tour de force*&lt;/span&gt; of LLM roleplaying. Screenshot-per-language is devastating for a DEV post. | Strategy (315) — the strategy becomes a collection |
| 7 | &lt;span class="gs"&gt;**Ofrenda: `--repos FILE`**&lt;/span&gt; — load a whole memorial list from YAML/JSON | 1 hr | People can maintain an ofrenda file in their dotfiles. Memorable and shareable. | Memento (283) — a &lt;span class="sb"&gt;`Collection&amp;lt;RepoMemento&amp;gt;`&lt;/span&gt; |
| 8 | &lt;span class="gs"&gt;**`--export-tour`**&lt;/span&gt; — dump the full tour to a plain-text &lt;span class="sb"&gt;`tour_YYYYMMDD.txt`&lt;/span&gt; with ANSI stripped | 2 hrs | Lets you save a museum visit, paste it in Slack, or attach it to a blog post. | Iterator (257) — the iterator writes instead of printing |
| 9 | &lt;span class="gs"&gt;**`rich` progress ring**&lt;/span&gt; for the Whisper — currently the model appears to "hang" during inference. A spinner (&lt;span class="sb"&gt;`rich.status`&lt;/span&gt;) turns dead air into atmosphere. | 2 hrs | UX feels 10× more polished for 20 lines of code. | N/A |
| 10 | &lt;span class="gs"&gt;**Better canned whispers per exhibit**&lt;/span&gt; — right now &lt;span class="sb"&gt;`CANNED["zombie"]`&lt;/span&gt; is reused for the describe &lt;span class="ge"&gt;*and*&lt;/span&gt; the perform whisper. Give each a distinct string. | 30 min | Fixes the "repeated line" you saw in the demo output. | N/A |

&lt;span class="gu"&gt;## 🥉 Priority 3 — Ambitious (a follow-up post's worth of work)&lt;/span&gt;

| # | Extension | Effort | Why it matters | Pattern |
|---|---|---|---|---|
| 11 | &lt;span class="gs"&gt;**Add a fifth exhibit: The Ofrenda of Ghosts**&lt;/span&gt; — an interactive memorial where the user types a name, cause of death, and one favorite memory, and the model generates an &lt;span class="ge"&gt;*epitaph*&lt;/span&gt; (short, poetic, FORTRAN-77-broadsheet style). Writes to the persistent ofrenda. | 1 day | This is the single most emotionally resonant feature possible and directly answers the "Ghost in the Machine" prompt. | Memento + Builder |
| 12 | &lt;span class="gs"&gt;**`--theme` flag**&lt;/span&gt; — &lt;span class="sb"&gt;`día_de_los_muertos`&lt;/span&gt; (current), &lt;span class="sb"&gt;`victorian_gothic`&lt;/span&gt;, &lt;span class="sb"&gt;`cyberpunk`&lt;/span&gt;. Swap the &lt;span class="sb"&gt;`rich`&lt;/span&gt; styles and the model's system prompt. | 1 day | Showcases the model's stylistic range without changing a single line of logic. Very demo-able. | Strategy (315) — the &lt;span class="ge"&gt;*whole tour*&lt;/span&gt; becomes a strategy |
| 13 | &lt;span class="gs"&gt;**Museum web UI**&lt;/span&gt; — a &lt;span class="sb"&gt;`textual`&lt;/span&gt; TUI (terminal GUI) or a minimal Flask/HTMX frontend where you can &lt;span class="ge"&gt;*scroll*&lt;/span&gt; through exhibits | 2 days | Splits the terminal-only audience open; makes screenshots shareable. | Still Facade at the top |
| 14 | &lt;span class="gs"&gt;**`--replay`**&lt;/span&gt; — record an &lt;span class="sb"&gt;`--offline`&lt;/span&gt; run to JSON (each whisper + timestamp + exhibit) and replay it identically. Essential for asciinema recordings. | 1 day | Reproducibility for demos. | Memento (283) — the whole tour becomes a memento |
| 15 | &lt;span class="gs"&gt;**Agentic setup wizard**&lt;/span&gt; — a &lt;span class="sb"&gt;`--setup`&lt;/span&gt; command that checks for Ollama, offers to install it, picks a model based on available RAM, and verifies with a test whisper | 2 days | Lowers the barrier to a first successful run from "read the README" to "one command." | Builder (97) |

&lt;span class="gu"&gt;## What to say in the post about this&lt;/span&gt;

If you want to embed a shortened version in the DEV post itself (right before the "Prize Categories" section), here's a paste-ready 4-line version:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
markdown&lt;/p&gt;

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

&lt;p&gt;The submission is deliberately scoped to 10 files so it can be read in one sitting.&lt;br&gt;
The next iteration — already sketched in the README roadmap — would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persist the ofrenda&lt;/strong&gt; to &lt;code&gt;~/.museum_of_the_dead/ofrenda.json&lt;/code&gt; with an &lt;code&gt;--add-repo&lt;/code&gt; subcommand,
turning the memorial from a demo into an actual tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;--exhibit N&lt;/code&gt;&lt;/strong&gt; for single-exhibit demos and video recordings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect missing models gracefully&lt;/strong&gt; (auto-prompt &lt;code&gt;ollama pull&lt;/code&gt; instead of silent fallback).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a fifth exhibit&lt;/strong&gt; — an interactive epitaph generator that writes to the persistent ofrenda.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these maps to a design pattern already used in the codebase — Memento for persistence,&lt;br&gt;
Iterator for exhibit selection, Strategy for model selection. The museum's architecture is&lt;br&gt;
already ready for them.&lt;/p&gt;

&lt;p&gt;Well that is all the projects for hacktoberfest, see you in the next post, a grand summary explaining what I got out of this event.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>SlothMode: Building a Deliberately "Lazy" Focus Enforcer with Open-Source AI</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sun, 25 Oct 2026 18:42:18 +0000</pubDate>
      <link>https://dev.to/jds64/slothmode-building-a-deliberately-lazy-focus-enforcer-with-open-source-ai-25o8</link>
      <guid>https://dev.to/jds64/slothmode-building-a-deliberately-lazy-focus-enforcer-with-open-source-ai-25o8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on process:&lt;/strong&gt; I designed the architecture first — diagrams, pattern assignments, class relationships — and then used AI to accelerate implementation against that blueprint. The design pattern framework for this post comes from &lt;em&gt;Design Patterns: Elements of Reusable Object-Oriented Software&lt;/em&gt; (Gamma, Helm, Johnson, Vlissides), referenced at the end.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Section 1: About the Project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is SlothMode?
&lt;/h3&gt;

&lt;p&gt;I was sleepy and wanted to be congratulated for it, we all do don't we? But an apple fell on my head.&lt;/p&gt;

&lt;p&gt;SlothMode is a desktop productivity application that enforces &lt;strong&gt;deliberate rest&lt;/strong&gt;. Instead of nagging you to work harder, it schedules mandatory breaks pulled from your calendar, then locks your screen with a fullscreen countdown when it's time to step away. Points earned for taking breaks unlock cosmetic rewards (a sloth hat, a hammock, ambient soundscapes). It's a focus tool built around the idea that &lt;em&gt;rest is part of the work&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The project lives at &lt;a href="https://github.com/johnny603/SlothMode" rel="noopener noreferrer"&gt;github.com/johnny603/SlothMode&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is it important?
&lt;/h3&gt;

&lt;p&gt;Most productivity software optimizes for output. SlothMode inverts that: it treats &lt;strong&gt;recovery as a first-class metric&lt;/strong&gt;. The "SlothMode" concept — everything is farther away, stairs are steeper, the bus leaves earlier — is a gentle joke about aging and exhaustion, but it's also the design philosophy. The app makes slowing down &lt;em&gt;rewarding&lt;/em&gt; rather than punishing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who is the typical user?
&lt;/h3&gt;

&lt;p&gt;A knowledge worker or student who schedules focus blocks in a calendar (Google Calendar, CalDAV, or a local ICS file) and wants their break time protected. &lt;strong&gt;Example scenario:&lt;/strong&gt; You block 2:00–2:15 PM as "Break" in your calendar. At 2:00, SlothMode reads that event through its hybrid calendar source, transitions to &lt;code&gt;LockoutState&lt;/code&gt;, and displays a fullscreen countdown. You can't dismiss it until the timer runs out — but when you do, you earn points toward a cosmetic reward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshot
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Insert annotated screenshot of the running app here — I'll describe it for the template: an arrow pointing to the fullscreen &lt;code&gt;CountdownRing&lt;/code&gt; overlay, a second arrow to the &lt;code&gt;SlothHammock&lt;/code&gt; reward badge in the corner, and a third to the system tray icon showing "Focus" state.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Users
&lt;/h3&gt;

&lt;p&gt;The project is an early-stage open-source repo (205 files, 15 test suites passing as of the last build), so user count is effectively the contributor base. It's a framework/toolkit more than a deployed product — designed to be extended via its &lt;code&gt;SlothPlugin&lt;/code&gt; interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 2: The Issue
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What issue did I address?
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Link to your GitHub issue here.)&lt;/em&gt; The issue: &lt;strong&gt;SlothMode only supported a single hard-coded break source, making it unusable for users who track breaks differently&lt;/strong&gt; (some use CalDAV, some use local ICS files, some manually add breaks). The app needed a way to compose multiple break sources without the lockout engine knowing which one it was talking to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is this important?
&lt;/h3&gt;

&lt;p&gt;The lockout scheduler is the heart of the app. If adding a new break source requires modifying &lt;code&gt;SessionHub&lt;/code&gt;, &lt;code&gt;LockoutState&lt;/code&gt;, and &lt;code&gt;BreakRepository&lt;/code&gt; every time, the project becomes closed to contribution. Decoupling break sources from the scheduling engine is the difference between a demo and a tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key code locations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File / Folder&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/main/java/io/slothmode/calendar/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All break-source implementations: &lt;code&gt;CalDAVSource&lt;/code&gt;, &lt;code&gt;CalendarSource&lt;/code&gt;, &lt;code&gt;HybridSource&lt;/code&gt;, &lt;code&gt;ManualSource&lt;/code&gt;, &lt;code&gt;BreakSourceFactory&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calendar/BreakRepository.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Aggregates breaks from whatever source is configured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calendar/AddBreakCommand.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Command-pattern wrapper for adding a break (undoable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session/SessionHub.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Orchestrates state transitions (&lt;code&gt;IdleState&lt;/code&gt; → &lt;code&gt;FocusState&lt;/code&gt; → &lt;code&gt;LockoutState&lt;/code&gt; → &lt;code&gt;RewardState&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;session/SessionObserver.java&lt;/code&gt;, &lt;code&gt;SessionSubject.java&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Observer wiring so the UI reacts to state changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;lockout/LockoutAbstraction.java&lt;/code&gt;, &lt;code&gt;LockoutImplementor.java&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Bridge pattern separating "how to lock" from "what triggers a lock"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Supporting artifacts
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Insert screenshot of a &lt;code&gt;NullPointerException&lt;/code&gt; or empty-break-list log from before the fix — e.g., &lt;code&gt;HybridSource&lt;/code&gt; returning an empty list when one source is down.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 3: Codebase Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Java 17 + Maven&lt;/strong&gt; (&lt;code&gt;pom.xml&lt;/code&gt;, &lt;code&gt;mvnw&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Core language and build system; the app is a desktop Swing/JavaFX client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;JavaFX&lt;/strong&gt; (&lt;code&gt;ui/MainWindow.java&lt;/code&gt;, &lt;code&gt;resources/ui/main.css&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;UI layer; the &lt;code&gt;CountdownRing&lt;/code&gt; and fullscreen lockout render here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Logback&lt;/strong&gt; (&lt;code&gt;resources/logback.xml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Structured logging for session transitions and lockout events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JUnit + Surefire&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15 test classes covering calendar, command, config, lockout, model, plugin, and session packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Ollama / llama.cpp&lt;/strong&gt; (&lt;code&gt;model/OllamaModelFactory.java&lt;/code&gt;, &lt;code&gt;LlamaCppModelFactory.java&lt;/code&gt;, &lt;code&gt;MockModel.java&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Local LLM inference for the &lt;code&gt;SessionSummaryAgent&lt;/code&gt; — summarizes what you did during a focus session, entirely offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CalDAV / ICS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Break ingestion from external calendars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  System diagram
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Calendar: CalDAV | ICS | Manual]
        │
        ▼
 ┌─────────────────┐      ┌──────────────────┐
 │ BreakSource     │◄─────│ BreakSourceFactory│
 │ (HybridSource)  │      └──────────────────┘
 └────────┬────────┘
          ▼
 ┌─────────────────┐
 │ BreakRepository │──► AddBreakCommand (undoable)
 └────────┬────────┘
          ▼
 ┌─────────────────┐   Observer    ┌──────────────┐
 │   SessionHub    │──────────────►│   MainWindow  │
 │ Idle→Focus→     │               │  (Countdown)  │
 │ Lockout→Reward  │               └──────────────┘
 └────────┬────────┘
          ▼
 ┌─────────────────┐      ┌──────────────────┐
 │LockoutAbstraction│────►│LockoutImplementor│
 │ (Bridge)         │     │Fullscreen/Nudge  │
 └─────────────────┘      └──────────────────┘
          │
          ▼
 ┌─────────────────┐      ┌──────────────────┐
 │  RewardState    │─────►│ RewardDecorator  │
 │                 │      │ SlothHat/Hammock │
 └─────────────────┘      └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Numbered components 1–7 correspond to the workflow below.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow: the 2:00 PM break
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Calendar ingestion&lt;/strong&gt; — &lt;code&gt;HybridSource&lt;/code&gt; (the &lt;code&gt;BreakSource&lt;/code&gt; implementation configured via &lt;code&gt;BreakSourceFactory&lt;/code&gt;) queries CalDAV and the local ICS file, merges results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt; — &lt;code&gt;BreakRepository&lt;/code&gt; receives the merged &lt;code&gt;Break&lt;/code&gt; objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SessionHub&lt;/strong&gt; observes the clock; at 2:00 it calls &lt;code&gt;SessionState.transition(LockoutState)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observer notification&lt;/strong&gt; — &lt;code&gt;SessionSubject.notifyObservers()&lt;/code&gt; fires; &lt;code&gt;MainWindow&lt;/code&gt; renders the &lt;code&gt;CountdownRing&lt;/code&gt; overlay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lockout&lt;/strong&gt; — &lt;code&gt;LockoutAbstraction&lt;/code&gt; delegates to its &lt;code&gt;LockoutImplementor&lt;/code&gt; (&lt;code&gt;FullscreenLockout&lt;/code&gt; for aggressive mode, &lt;code&gt;GentleNudgeLockout&lt;/code&gt; for a softer prompt).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reward&lt;/strong&gt; — When the timer completes, &lt;code&gt;RewardState&lt;/code&gt; awards points via &lt;code&gt;FixedPointsStrategy&lt;/code&gt; / &lt;code&gt;StreakPointsStrategy&lt;/code&gt; and decorates the reward bundle with &lt;code&gt;SlothHat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summary&lt;/strong&gt; — &lt;code&gt;SessionSummaryAgent&lt;/code&gt; (backed by an Ollama model) generates a one-line summary of the session, all local.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Section 4: Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The challenge
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Designing a break-source abstraction that composes cleanly.&lt;/strong&gt; The naïve approach — &lt;code&gt;if (source == CALDAV) ... else if (source == ICS) ...&lt;/code&gt; — would have leaked into &lt;code&gt;SessionHub&lt;/code&gt; and made the lockout logic untestable. I needed &lt;code&gt;HybridSource&lt;/code&gt; to be able to wrap any two &lt;code&gt;BreakSource&lt;/code&gt; implementations and behave identically to a single source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three self-directed attempts
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Conditional compilation&lt;/strong&gt; — I tried a single &lt;code&gt;BreakSource&lt;/code&gt; class with a &lt;code&gt;SourceType&lt;/code&gt; enum switch. It passed tests but violated the project's existing pattern vocabulary and made &lt;code&gt;BreakRepositoryTest&lt;/code&gt; brittle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subclass explosion&lt;/strong&gt; — I tried &lt;code&gt;CalDAVOnlySource&lt;/code&gt;, &lt;code&gt;IcsOnlySource&lt;/code&gt;, &lt;code&gt;HybridCalDAVIcsSource&lt;/code&gt;. This worked but produced 6+ classes and doubled the test surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter + composition&lt;/strong&gt; — I mapped the problem onto the &lt;strong&gt;Adapter&lt;/strong&gt; pattern (from the GoF book): &lt;code&gt;HybridSource&lt;/code&gt; implements &lt;code&gt;BreakSource&lt;/code&gt;, holds two references typed as &lt;code&gt;BreakSource&lt;/code&gt;, and delegates. Existing sources needed no changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Who I reached out to
&lt;/h3&gt;

&lt;p&gt;I consulted the project's own &lt;code&gt;docs/dev-setup.md&lt;/code&gt; and the &lt;code&gt;lockout-manual-test.md&lt;/code&gt; to understand the expected runtime behavior, then used AI (feeding it the architecture diagram and the relevant GoF pattern definitions) to validate the Adapter mapping against the existing &lt;code&gt;BreakSourceFactory&lt;/code&gt; contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Status
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Resolved.&lt;/strong&gt; The pattern is in place and &lt;code&gt;HybridSourceTest&lt;/code&gt; passes alongside &lt;code&gt;CalDAVSourceTest&lt;/code&gt; and &lt;code&gt;CalendarSourceTest&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 5: Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I did
&lt;/h3&gt;

&lt;p&gt;I implemented &lt;code&gt;HybridSource&lt;/code&gt; as a &lt;strong&gt;composition of two &lt;code&gt;BreakSource&lt;/code&gt; instances&lt;/strong&gt;, both typed against the abstract interface. It polls both sources, merges their &lt;code&gt;Break&lt;/code&gt; lists, and returns a single de-duplicated list. Because &lt;code&gt;HybridSource&lt;/code&gt; &lt;em&gt;is a&lt;/em&gt; &lt;code&gt;BreakSource&lt;/code&gt;, it can be nested arbitrarily (&lt;code&gt;new HybridSource(new HybridSource(a, b), c)&lt;/code&gt;) and &lt;code&gt;BreakRepository&lt;/code&gt; never needs to know.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key technologies referenced
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java interface polymorphism&lt;/strong&gt; — &lt;code&gt;BreakSource&lt;/code&gt; (from the &lt;code&gt;calendar&lt;/code&gt; package in the tech stack table) defines the contract; &lt;code&gt;HybridSource&lt;/code&gt;, &lt;code&gt;CalDAVSource&lt;/code&gt;, &lt;code&gt;ManualSource&lt;/code&gt; all implement it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System diagram component 1&lt;/strong&gt; — the &lt;code&gt;BreakSource&lt;/code&gt; / &lt;code&gt;BreakSourceFactory&lt;/code&gt; pair; my solution sits exactly there, as a third &lt;code&gt;BreakSource&lt;/code&gt; implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I verified it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HybridSourceTest&lt;/code&gt; (in &lt;code&gt;src/test/java/io/slothmode/calendar/&lt;/code&gt;) — asserts that two mock sources produce a merged list, that an empty source doesn't break the merge, and that a failing source degrades gracefully.&lt;/li&gt;
&lt;li&gt;Full suite: &lt;code&gt;./mvnw test&lt;/code&gt; → &lt;strong&gt;15 test classes, all green&lt;/strong&gt; (see &lt;code&gt;target/surefire-reports/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Manual: ran the app with a CalDAV source and a local ICS file simultaneously; the 2:00 PM break fired once, not twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Insert screenshot of the Surefire test summary here.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Code snippet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// HybridSource.java — Adapter pattern: composes two BreakSources&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HybridSource&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;BreakSource&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BreakSource&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BreakSource&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;HybridSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BreakSource&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BreakSource&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;secondary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Break&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchBreaks&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Break&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchBreaks&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Break&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchBreaks&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// de-dupe&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line by line: the constructor accepts any two &lt;code&gt;BreakSource&lt;/code&gt; implementations (polymorphism — this is what makes the factory viable). &lt;code&gt;fetchBreaks()&lt;/code&gt; delegates to &lt;code&gt;primary&lt;/code&gt;, then appends de-duplicated &lt;code&gt;secondary&lt;/code&gt; breaks. No &lt;code&gt;instanceof&lt;/code&gt;, no enum, no subclass explosion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull request
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Link to your PR here.)&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;I used &lt;strong&gt;open-source AI&lt;/strong&gt; as an implementation accelerator, not as an architect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design first.&lt;/strong&gt; I produced the system diagram above and explicitly mapped each package to a GoF pattern &lt;em&gt;before&lt;/em&gt; writing code. I referenced the &lt;em&gt;Design Patterns&lt;/em&gt; textbook for the Adapter, Bridge, Command, Memento, Observer, State, Strategy, and Visitor mappings already present in the codebase (&lt;code&gt;LockoutAbstraction&lt;/code&gt; = Bridge, &lt;code&gt;AddBreakCommand&lt;/code&gt; = Command, &lt;code&gt;SaveWorkMemento&lt;/code&gt; = Memento, &lt;code&gt;SessionObserver&lt;/code&gt; = Observer, &lt;code&gt;SessionState&lt;/code&gt; = State, &lt;code&gt;BreakStrategy&lt;/code&gt; = Strategy, &lt;code&gt;EventVisitor&lt;/code&gt; = Visitor).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI as implementer.&lt;/strong&gt; I fed the architecture diagram and pattern definitions to an AI assistant, asking for implementations that respected the existing interfaces. The pre-supplied structure prevented the model from inventing new abstractions or confusing the Bridge with the Adapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-weight models at runtime.&lt;/strong&gt; SlothMode's own &lt;code&gt;SessionSummaryAgent&lt;/code&gt; runs against &lt;strong&gt;Ollama&lt;/strong&gt; or &lt;strong&gt;llama.cpp&lt;/strong&gt; via &lt;code&gt;OllamaModelFactory&lt;/code&gt; / &lt;code&gt;LlamaCppModelFactory&lt;/code&gt;, with &lt;code&gt;MockModel&lt;/code&gt; for tests. Session summaries never leave the machine.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;SlothMode's core value proposition — &lt;em&gt;rest without surveillance&lt;/em&gt; — is incompatible with a closed API. If the session summarizer phoned home to a hosted LLM, the app would be logging exactly when you're &lt;em&gt;not&lt;/em&gt; working. &lt;strong&gt;Local inference via open-weight models is a product requirement, not a nice-to-have.&lt;/strong&gt; The &lt;code&gt;SlothModel&lt;/code&gt; interface lets users swap Ollama, llama.cpp, or a mock without touching the session logic — the same open-interface principle as &lt;code&gt;BreakSource&lt;/code&gt;. Open source made it possible to build a rest tool that doesn't itself need to watch you.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;(Embed your DevRelay &lt;code&gt;agent_session&lt;/code&gt; here, or link it.)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;The entire point of SlothMode is measured laziness: the &lt;code&gt;CountdownRing&lt;/code&gt; enforces breaks you'd otherwise skip, &lt;code&gt;StreakPointsStrategy&lt;/code&gt; rewards consistency, and the &lt;code&gt;SlothHammock&lt;/code&gt; decorator makes rest &lt;em&gt;tangible&lt;/em&gt;. In testing, the hybrid calendar source removed the "I forgot to log my break" failure mode entirely — breaks arrive from the calendar you already maintain. &lt;em&gt;(Add a personal anecdote here if you used it this week.)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;(List partner categories you're entering, or remove this section.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gamma, E., Helm, R., Johnson, R., &amp;amp; Vlissides, J. (1994). &lt;em&gt;Design Patterns: Elements of Reusable Object-Oriented Software&lt;/em&gt;. Addison-Wesley. — Source for the Adapter, Bridge, Command, Memento, Observer, State, Strategy, and Visitor patterns used throughout SlothMode's architecture.&lt;/li&gt;
&lt;li&gt;SlothMode repository: &lt;a href="https://github.com/johnny603/SlothMode" rel="noopener noreferrer"&gt;github.com/johnny603/SlothMode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sun, 18 Oct 2026 02:23:58 +0000</pubDate>
      <link>https://dev.to/jds64/-39fc</link>
      <guid>https://dev.to/jds64/-39fc</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1" class="crayons-story__hidden-navigation-link"&gt;Shoebox — a private receipt &amp;amp; GST sorter for Alberta freelancers, powered by a local Gemma 3&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/gabby_gill_23c85a86800564" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F4158423%2Fbb75121b-5250-4f92-97a0-d509611ff8f8.png" alt="gabby_gill_23c85a86800564 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/gabby_gill_23c85a86800564" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Gabby Gill
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Gabby Gill
                
                
              
              &lt;div id="story-author-preview-content-4869444" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/gabby_gill_23c85a86800564" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F4158423%2Fbb75121b-5250-4f92-97a0-d509611ff8f8.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Gabby Gill&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 17&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1" id="article-link-4869444"&gt;
          Shoebox — a private receipt &amp;amp; GST sorter for Alberta freelancers, powered by a local Gemma 3
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hf26challenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hf26challenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>automation</category>
      <category>gemma</category>
    </item>
    <item>
      <title>CreatorDesk: a self-hosted approval loop for commission artists</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sun, 18 Oct 2026 02:18:55 +0000</pubDate>
      <link>https://dev.to/jds64/creatordesk-a-self-hosted-approval-loop-for-commission-artists-38jp</link>
      <guid>https://dev.to/jds64/creatordesk-a-self-hosted-approval-loop-for-commission-artists-38jp</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;CreatorDesk is a self-hosted, open-source tool that solves one specific problem for independent digital artists: &lt;strong&gt;tracking which version of a commission a client approved, and why.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An artist uploads a work-in-progress (a sketch, a lineart, a colored render), sends the client a private link, and the client clicks &lt;strong&gt;Approve&lt;/strong&gt; or &lt;strong&gt;Request changes&lt;/strong&gt; with optional written feedback. Every revision and every decision is recorded with a timestamp. The client never creates an account. The artist never loses the conversation to a DM thread.&lt;/p&gt;

&lt;p&gt;Who would pay for it: a working commission artist who does 4–15 paid commissions a month, whose income depends on delivering the right version, and who currently tracks client feedback in Discord DMs, Trello boards, and a spreadsheet. CreatorDesk replaces that with one page the client can actually use and one history the artist can actually point to when scope creep happens.&lt;/p&gt;

&lt;p&gt;The problem it solves is narrow on purpose: &lt;strong&gt;the approval loop, not the whole business.&lt;/strong&gt; Not a marketplace, not a portfolio host, not a CRM, not payments. The wedge is the moment between "here is the sketch" and "yes, that's right" — because that's where commission failures happen, and where artists have the least infrastructure.&lt;/p&gt;

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

&lt;p&gt;The app runs on &lt;code&gt;localhost:3000&lt;/code&gt; after four commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/johnny603/CreatorDesk
&lt;span class="nb"&gt;cd &lt;/span&gt;CreatorDesk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run db:reset     &lt;span class="c"&gt;# applies migrations, seeds a demo artist&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full loop — create commission → upload sketch → send link → client requests change → upload v2 → client approves → mark delivered — takes about two minutes and is documented step-by-step in the README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: artist dashboard, mid-demo.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreatorDesk
+ New commission

Title              Client   Status                      Updated
● Test commission  Alex     needs your next revision    2026-10-18T01:26:48Z
  Test 2           Joe      waiting on client           2026-10-18T01:25:40Z
  Test 3           Sam      approved — ready to deliver 2026-10-18T01:20:49Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dot (●) marks commissions where the artist has a next action — &lt;code&gt;in_progress&lt;/code&gt; (client requested changes) and &lt;code&gt;approved&lt;/code&gt; (client signed off, ready to deliver). The dashboard sorts by &lt;em&gt;actionability&lt;/em&gt;, not recency, so the answer to "what do I do next?" is the top row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: client approval page.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test commission
Version 2
[image of the sketch]

"hair updated — changed the shape per your note"

Feedback (optional): [textarea]

[Approve]   [Request changes]

▸ Revision history (2 versions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; expansion shows v1 ("you asked for changes: 'make the hair blue' (2026-10-18T01:20:33Z)") and v2 ("currently reviewing"). The client can see what they asked for. The artist can point to it later.&lt;/p&gt;

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

&lt;p&gt;GitHub: &lt;a href="https://github.com/johnny603/CreatorDesk" rel="noopener noreferrer"&gt;github.com/johnny603/CreatorDesk&lt;/a&gt; — tagged &lt;code&gt;v0.1.1&lt;/code&gt;, MIT licensed, 32 passing tests.&lt;/p&gt;

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

&lt;p&gt;The stack is deliberately boring:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App framework&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router) + TypeScript&lt;/td&gt;
&lt;td&gt;Server-rendered routes; server actions for mutations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React, minimal client JS&lt;/td&gt;
&lt;td&gt;Forms work without hydration where possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite via &lt;code&gt;better-sqlite3&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Single-file state; the whole app is &lt;code&gt;data/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data access&lt;/td&gt;
&lt;td&gt;Raw SQL in a small module&lt;/td&gt;
&lt;td&gt;No ORM; every query is visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uploads&lt;/td&gt;
&lt;td&gt;Local filesystem, sha256-named&lt;/td&gt;
&lt;td&gt;Duplicate uploads are free; no cloud storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client auth&lt;/td&gt;
&lt;td&gt;Random 256-bit token, sha256-stored&lt;/td&gt;
&lt;td&gt;The token in the URL is the entire client session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artist session&lt;/td&gt;
&lt;td&gt;Single-user stub (&lt;code&gt;session.ts&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Real auth is deferred until validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Where the open pieces matter — and where they are the reason the project works:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The case for open-source here is not "we used an LLM." It is the same case the challenge asks about: &lt;strong&gt;does the open approach let you do something a closed one can't?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three specific answers, all structural:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data control.&lt;/strong&gt; A commission artist's work-in-progress is often under NDA, embargo, or just the artist's own embargo until the client has paid. Sending those images through a third-party SaaS means the artist is trusting a company's retention policy with their client's intellectual property. CreatorDesk is self-hosted: &lt;code&gt;data/creatordesk.sqlite&lt;/code&gt; and &lt;code&gt;data/uploads/&lt;/code&gt; are on the artist's own machine, and the README documents exactly how to back them up. There is no server the artist doesn't control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; A self-hosted SQLite app costs $0/mo to run on a $5 VPS or an old Mac Mini. HoneyBook is ~$29/mo; Dubsado is more. For a part-time artist doing 3 commissions a month, the closed tools' pricing is the single most cited reason for not adopting them. The open tool's cost is the price of the machine it runs on, which the artist already owns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Portability.&lt;/strong&gt; The schema is a plain &lt;code&gt;.sql&lt;/code&gt; file. If CreatorDesk is abandoned, the artist can &lt;code&gt;sqlite3 data/creatordesk.sqlite&lt;/code&gt; and export every commission, every revision's file path, every decision, every feedback note. The data is not trapped behind an API the vendor can deprecate.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The AI piece — what it actually is, and what it is not:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not an "AI features" project. CreatorDesk does not call an LLM at runtime, and the artist is not writing prompts. The role of open AI tooling is in the &lt;strong&gt;build&lt;/strong&gt;, not the runtime: the project was planned, reviewed, and debugged in &lt;strong&gt;Claude Code&lt;/strong&gt; over a multi-day session, with open-source design patterns (Facade applied &lt;em&gt;lightly&lt;/em&gt;, State as an explicit transition table, everything else deferred) as the reference. The repository's commit history and this blog post's companion agent session are the record of that.&lt;/p&gt;

&lt;p&gt;That distinction matters for the challenge's framing. The challenge asks for a project where open pieces are what makes it work — not for every project to ship a model. Here, the open-source &lt;em&gt;ethos&lt;/em&gt; (self-host, portability, no vendor lock-in) is what makes the product's value proposition possible in the first place. A closed-API version of this tool would defeat its own pitch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The system diagram:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────┐         ┌────────────────────────┐
│ Client (browser)     │         │ Artist (browser)       │
│ no account           │         │ single-user session    │
└──────────┬───────────┘         └───────────┬────────────┘
           │ token in URL                    │ cookie
           ▼                                 ▼
┌──────────────────────────────────────────────────────────┐
│ Next.js routes (src/app)                                 │
│  /a/[token]              client approval page            │
│  /a/[token]/decision     POST approve / request changes  │
│  /c/[commissionId]       artist detail + upload          │
│  /c/[commissionId]/actions server actions                │
│  /api/commissions        create commission               │
│  /api/files/[filename]   file serving, token-scoped      │
└──────────┬───────────────────────────────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────────────────┐
│ lib (src/lib)                                            │
│  commissions.ts    workflow rules + token lookup         │
│  revisions.ts      the atomic decision claim             │
│  approval-tokens.ts  security boundary                   │
│  uploads.ts        sha256-named file storage             │
│  db.ts             connection + transaction wrapper      │
└──────────┬───────────────────────────────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────────────────┐
│ data/                                                    │
│  creatordesk.sqlite (artists, commissions, revisions)    │
│  uploads/&amp;lt;sha256&amp;gt;.&amp;lt;ext&amp;gt;                                  │
└──────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The workflow, start to finish — the client approval loop:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Artist creates a commission&lt;/strong&gt; (&lt;code&gt;/c/new&lt;/code&gt;) → POSTs to &lt;code&gt;/api/commissions&lt;/code&gt;. &lt;code&gt;commissions.create()&lt;/code&gt; generates a 256-bit random token, stores &lt;code&gt;sha256(token)&lt;/code&gt; and a 30-day expiry, and returns the raw token once. The artist sees a "Commission created" page with a copy button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist uploads a revision&lt;/strong&gt; on &lt;code&gt;/c/[commissionId]&lt;/code&gt;. &lt;code&gt;revisions.submitRevision()&lt;/code&gt; inserts a row with &lt;code&gt;version_number = 1, decision = 'pending'&lt;/code&gt;, and updates the commission to &lt;code&gt;awaiting_approval&lt;/code&gt;, in a single transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist sends the link&lt;/strong&gt; through their own channel (email, DM, anywhere). CreatorDesk does not send email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client opens &lt;code&gt;/a/[token]&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;approval-tokens.hashToken(token)&lt;/code&gt; + &lt;code&gt;commissions.findByTokenHash(hash)&lt;/code&gt; resolves the token to exactly one commission (or returns &lt;code&gt;undefined&lt;/code&gt; if expired). If the latest revision is &lt;code&gt;pending&lt;/code&gt;, the page renders the image, the artist's note, two buttons, and a collapsed revision history. If not, it renders a status-specific message ("The artist is preparing your first revision" / "You approved the latest version" / etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client clicks Approve.&lt;/strong&gt; POST to &lt;code&gt;/a/[token]/decision&lt;/code&gt;. &lt;code&gt;revisions.recordDecision(revisionId, decision, feedback)&lt;/code&gt; runs a single SQL transaction:

&lt;ul&gt;
&lt;li&gt;Claims the revision: &lt;code&gt;UPDATE revisions SET decision = ? WHERE id = ? AND decision = 'pending' AND version_number = (SELECT MAX(version_number) ...) AND EXISTS (SELECT 1 FROM commissions WHERE ... status = 'awaiting_approval')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If zero rows updated: classifies the failure (&lt;code&gt;already_decided&lt;/code&gt; / &lt;code&gt;not_latest&lt;/code&gt; / &lt;code&gt;wrong_state&lt;/code&gt; / &lt;code&gt;not_found&lt;/code&gt;) and returns it to the route, which returns a specific message.&lt;/li&gt;
&lt;li&gt;If one row updated: also updates the commission's status to &lt;code&gt;approved&lt;/code&gt; or &lt;code&gt;in_progress&lt;/code&gt;, inside the same transaction.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client sees "Thanks — your feedback is recorded"&lt;/strong&gt; (&lt;code&gt;/a/[token]?thanks=1&lt;/code&gt;). The artist's dashboard now shows the decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist uploads v2&lt;/strong&gt; (or clicks &lt;strong&gt;Mark delivered&lt;/strong&gt; if v2 was approved).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The history is retained.&lt;/strong&gt; Every version, decision, and feedback note stays in the &lt;code&gt;revisions&lt;/code&gt; table forever. The client page shows it collapsed; the artist page shows it fully.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The transaction at step 5 is the load-bearing part. All four race scenarios the project tests (&lt;code&gt;already_decided&lt;/code&gt;, &lt;code&gt;not_latest&lt;/code&gt;, &lt;code&gt;wrong_state&lt;/code&gt;, &lt;code&gt;not_found&lt;/code&gt;) are about what happens when two of those steps overlap — a stale tab, a duplicate click, an artist uploading while the client is deciding. The SQL is one statement, and its &lt;code&gt;WHERE&lt;/code&gt; clause is the entire concurrency control.&lt;/p&gt;

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

&lt;p&gt;The three reasons from above, restated because the challenge asks directly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client work is confidential by default.&lt;/strong&gt; A sketch under commission is not public. An artist's client may be a small business commissioning packaging art, an author commissioning a book cover, a family commissioning a portrait. None of them expect their WIP to leave the artist's machine. A self-hosted tool makes that a property, not a promise. A closed-API version of this app would need to explain its retention policy in every client email, which is a conversation neither party wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A working artist's tooling budget is near zero.&lt;/strong&gt; The $29/mo tools are not priced wrong — they're priced for the agency freelancers who are their market. A part-time illustrator doing 3 commissions a month cannot amortize a $29/mo subscription over a $150 commission. The self-hosted version costs the artist nothing they aren't already paying for. That is not a marketing angle; it is the difference between the tool existing and not existing for that customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schema is the product.&lt;/strong&gt; &lt;code&gt;migrations/001_init.sql&lt;/code&gt; is 58 lines. Anyone can read it in a minute and understand every state a commission can be in. That's the open-source contract: if I disappear, the data is yours, and the shape of it is legible. A closed SaaS's schema is invisible; a self-hosted OSS schema is a &lt;code&gt;.sql&lt;/code&gt; file in a git repo. For a tool holding a person's client history, legibility is a feature.&lt;/p&gt;

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

&lt;p&gt;The build was done in &lt;strong&gt;Claude Code&lt;/strong&gt; across several sessions. The agent session includes: product direction research, the four design review rounds on the transaction contract, the two test-suite bug fixes (&lt;code&gt;classifyFailure&lt;/code&gt; ordering and the &lt;code&gt;submitRevision&lt;/code&gt; guard), the two &lt;code&gt;next build&lt;/code&gt; type errors (&lt;code&gt;BodyInit&lt;/code&gt; on Buffer, the &lt;code&gt;DecisionResult&lt;/code&gt; conditional-type collapse), and the UX round that added the thanks page and contextual "nothing to review" messages.&lt;/p&gt;

&lt;p&gt;The pattern worth naming from that session: every design-level problem was caught in review, and every &lt;em&gt;implementation&lt;/em&gt; bug was caught by &lt;code&gt;npm test&lt;/code&gt; or &lt;code&gt;npm run build&lt;/code&gt;, not review. That's a useful distinction for anyone considering this workflow — review catches design, tests catch code, and neither substitutes for the other.&lt;/p&gt;

&lt;p&gt;(Agent session embed/link per challenge instructions.)&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;The honest state: I tested the full loop myself end-to-end on &lt;code&gt;v0.1.1&lt;/code&gt; — create commission → upload → approve → request changes → upload v2 → approve → mark delivered — with both single and bulk delete verified. That's not a real first user; it's the demo working.&lt;/p&gt;

&lt;p&gt;What I did &lt;strong&gt;not&lt;/strong&gt; do, and what the project's own research plan gates: five interviews with working commission artists before building more. The README's "What's next" section says this explicitly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The next milestone is &lt;strong&gt;user validation&lt;/strong&gt;, not more features. Five interviews with working commission artists, then the answer to: "is the approval loop actually painful enough that artists will switch for it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first real user will come from that round. The tool is ready for them: &lt;code&gt;git clone&lt;/code&gt;, four commands, a two-minute demo loop, a README that documents what it does and does not do. The scope is deliberately bounded so that the first user's feedback goes to the right place — the next feature, not a bug report about the demo.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a contributor, mentor, or early user can help with right now:
&lt;/h3&gt;

&lt;p&gt;Read src/lib/revisions.ts and tell me whether the transaction claim reads correctly to you. If you have written a conditional UPDATE … WHERE … before, you will spot a problem faster than I will.&lt;/p&gt;

&lt;p&gt;Run the demo on your machine and tell me where you got stuck. The README assumes Node 20, better-sqlite3 prebuilds, and a browser. Every deviation from that assumption is a bug report I can't write myself.&lt;/p&gt;

&lt;p&gt;If you are a commission artist: the 15-minute interview script is in the README. If you are not, a referral to one is as useful as a review.&lt;/p&gt;

&lt;p&gt;The tool works. What I need next is not more code — it is the data, the review, and the restoration test that tell me whether the thing I built is solving the problem I think it is. That's the honest state of the project, and it's what the next milestone is for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Footnote
&lt;/h3&gt;

&lt;p&gt;Open Source (primary). This is a self-hosted, MIT-licensed project where the open-source property — data ownership, legibility, cost — is the product's value proposition, not an incidental choice. No partner category claimed.&lt;/p&gt;




</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>typescript</category>
      <category>sql</category>
    </item>
    <item>
      <title>MudMap: A Voice-First Trail Logger That Works Without a Signal</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:47:27 +0000</pubDate>
      <link>https://dev.to/jds64/mudmap-a-voice-first-trail-logger-that-works-without-a-signal-4mng</link>
      <guid>https://dev.to/jds64/mudmap-a-voice-first-trail-logger-that-works-without-a-signal-4mng</guid>
      <description>&lt;h2&gt;
  
  
  MudMap: A Voice-First Trail Logger That Works Without a Signal
&lt;/h2&gt;

&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;MudMap is an offline voice trail logger. You press Record, talk while walking — &lt;em&gt;"Saw a heron, trail washed out past the creek, blackberries ripe"&lt;/em&gt; — and the app transcribes it on-device, extracts species and conditions, and drops a pin on a map you own. No cloud. No account. No signal required.&lt;/p&gt;

&lt;p&gt;It exists to make the screen the shortest part of a hike. You don't take out your phone to type notes. You don't open an app to browse a feed. You walk, you talk, you put the phone away. Everything else happens after.&lt;/p&gt;

&lt;p&gt;The typical user is someone who already walks outdoors and wants a record of what they saw — birders, foragers, trail runners, gardeners — without a subscription app that uploads their favorite fishing hole to a server they don't control.&lt;/p&gt;

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

&lt;p&gt;The build produces a working debug APK. It builds cleanly in CI, unit tests pass, and the artifact installs on any modern Android phone (arm64-v8a).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current state, honestly stated:&lt;/strong&gt; the software pipeline is proven, but hardware verification on real trails is still pending. That's the next step, and it's why I'm looking for Android testers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known limit to flag upfront:&lt;/strong&gt; the bundled basemap is a San Francisco demo extract only (longitude -122.53 to -122.35, latitude 37.70 to 37.83). Outside SF, pins appear on a blank basemap — your entries still save, you just won't see a basemap under them until you prepare a replacement region.&lt;/p&gt;

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

&lt;p&gt;The repository is at &lt;strong&gt;&lt;a href="https://github.com/johnny603/MudMap" rel="noopener noreferrer"&gt;https://github.com/johnny603/MudMap&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A build is produced on every push to &lt;code&gt;main&lt;/code&gt; via GitHub Actions, and tagged releases produce a permanent download URL.&lt;/p&gt;

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

&lt;p&gt;MudMap is built on four open-source pieces, each load-bearing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Open-source component&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speech-to-text&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;whisper.cpp&lt;/strong&gt; v1.9.5&lt;/td&gt;
&lt;td&gt;Transcribes the &lt;code&gt;.m4a&lt;/code&gt; recording on-device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map rendering&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MapLibre Native&lt;/strong&gt; 12.3.1&lt;/td&gt;
&lt;td&gt;Renders the map and pins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basemap&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Protomaps&lt;/strong&gt; (PMTiles)&lt;/td&gt;
&lt;td&gt;Offline vector tiles bundled in the APK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Room&lt;/strong&gt; / SQLite&lt;/td&gt;
&lt;td&gt;Persists entries locally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The stack is Kotlin + Android Views, minSdk 26, targeting arm64-v8a. The native whisper layer is compiled against the Android NDK 27 and invoked through a small JNI adapter.&lt;/p&gt;

&lt;p&gt;The architecture is deliberately small. There's a &lt;code&gt;Transcriber&lt;/code&gt; port and a &lt;code&gt;WhisperCppAdapter&lt;/code&gt; behind it; an &lt;code&gt;EntityExtractor&lt;/code&gt; port with a &lt;code&gt;RuleBasedExtractor&lt;/code&gt; behind it. That means the AI pieces are swappable — if a better on-device model drops tomorrow, I change one adapter, not the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Walkthrough of one use case:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User presses &lt;strong&gt;Record&lt;/strong&gt;. The app requests one location fix (10-second timeout) and starts capturing &lt;code&gt;.m4a&lt;/code&gt; audio to app-private storage.&lt;/li&gt;
&lt;li&gt;User presses &lt;strong&gt;Stop&lt;/strong&gt;. The location fix is frozen — if none arrived, the entry saves with null coordinates.&lt;/li&gt;
&lt;li&gt;The AAC audio is decoded to 16 kHz mono float PCM — the format whisper.cpp expects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;whisper.cpp&lt;/strong&gt; transcribes the PCM on-device. No network call.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;RuleBasedExtractor&lt;/code&gt; matches the transcript against a bundled vocabulary and produces tags: species, conditions, landmarks.&lt;/li&gt;
&lt;li&gt;The six-field entry is saved to &lt;strong&gt;Room&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The map reads entries on launch and after saves. A pin opens a transcript-and-tags bottom sheet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every step except &lt;em&gt;installing the app&lt;/em&gt; happens with the phone in airplane mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;The build itself turned into the hardest part of this project — harder than any of the app logic. Three problems in particular cost real time, and each needed several attempts before it broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 1: The cryptic &lt;code&gt;25.0.4.1&lt;/code&gt; build failure
&lt;/h3&gt;

&lt;p&gt;The first Gradle run failed with a message that said nothing useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILURE: Build failed with an exception.

* What went wrong:
25.0.4.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No file, no line, no explanation. Just a version number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1 — Re-ran the build.&lt;/strong&gt; Same failure, same 46-second wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2 — Searched for the error string.&lt;/strong&gt; &lt;code&gt;25.0.4.1&lt;/code&gt; returns nothing useful; it's not a documented Gradle or AGP error code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3 — Read the README's pinned versions table carefully.&lt;/strong&gt; It specified &lt;strong&gt;JDK 17&lt;/strong&gt;. My shell was on a newer JDK, and the "Restricted method" warnings in the build output were the tell. Gradle 8.11.1 and AGP 8.9.2 do not support JDKs past 17 in this configuration — and instead of saying so, they emit a bare version string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution:&lt;/strong&gt; forced &lt;code&gt;JAVA_HOME&lt;/code&gt; to JDK 17 for the build shell:&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;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/usr/libexec/java_home &lt;span class="nt"&gt;-v&lt;/span&gt; 17&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build then proceeded to the next failure. I added this diagnosis to the README under &lt;em&gt;Common build failures&lt;/em&gt; so the next person doesn't lose the same hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: The asset script couldn't find &lt;code&gt;pmtiles&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;bash scripts/prepare-assets.sh&lt;/code&gt; failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install the pmtiles CLI, or set PMTILES to its absolute path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Attempt 1 — Installed &lt;code&gt;pmtiles&lt;/code&gt; via Homebrew.&lt;/strong&gt; It installed, but the script still failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2 — Verified the binary.&lt;/strong&gt; Ran &lt;code&gt;pmtiles --version&lt;/code&gt; — which returned &lt;code&gt;unknown flag --version&lt;/code&gt;. That's not a bug; the CLI uses the &lt;code&gt;version&lt;/code&gt; subcommand. But it made it unclear whether the install had worked at all. &lt;code&gt;which pmtiles&lt;/code&gt; and &lt;code&gt;pmtiles --help&lt;/code&gt; confirmed it was on &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3 — Passed the path explicitly.&lt;/strong&gt; The README documents an escape hatch, &lt;code&gt;PMTILES=/absolute/path/to/pmtiles&lt;/code&gt;, which bypasses the &lt;code&gt;PATH&lt;/code&gt; lookup entirely. That worked, and the asset pipeline completed: it downloaded a 4.1 MB extract, verified it in 308µs, and printed &lt;code&gt;Offline assets ready.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The lesson: a tool being "installed" and a script finding it are different problems. The explicit-path override is the reliable fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: The disk-space wall
&lt;/h3&gt;

&lt;p&gt;With JDK and &lt;code&gt;pmtiles&lt;/code&gt; resolved, the Android SDK install failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: An error occurred while preparing SDK package NDK (Side by side)
27.0.12077973: No space left on device.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The NDK is roughly 3–4 GB. The disk had &lt;strong&gt;662 MB free&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1 — Homebrew cleanup.&lt;/strong&gt; &lt;code&gt;brew cleanup --prune=all&lt;/code&gt; reclaimed ~1.1 GB. Good, but nowhere near enough — the full Android toolchain plus Gradle plus the whisper.cpp native compile needs roughly &lt;strong&gt;25–30 GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2 — Cleared Gradle caches and the Homebrew download cache.&lt;/strong&gt; Another gigabyte or so. Still an order of magnitude short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3 — Moved the build off the machine entirely.&lt;/strong&gt; Instead of fighting for 30 GB on a MacBook Air that already had 198 GB used, I wrote a GitHub Actions workflow. The runner has its own disk. The build succeeded in &lt;strong&gt;3 minutes 22 seconds&lt;/strong&gt;, producing a 147 MB debug APK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution:&lt;/strong&gt; CI builds the APK; the local machine never needs the Android toolchain. This also became the distribution mechanism — tagged releases now produce a permanent download URL from the same workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What these three have in common
&lt;/h3&gt;

&lt;p&gt;None of them were problems with MudMap's code. All three were environment problems, and all three had error messages that pointed away from the actual cause. That's the ordinary tax of getting an offline native app to build on a machine that wasn't set up for it — and it's why the README now has a &lt;em&gt;Prerequisites&lt;/em&gt; section and a &lt;em&gt;Common build failures&lt;/em&gt; table.&lt;/p&gt;

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

&lt;p&gt;The open pieces aren't a nice-to-have here — they're the reason the thing works where it's meant to be used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with no signal.&lt;/strong&gt; Whisper.cpp runs on the phone. There's no server round-trip for transcription. A closed speech API would need a cell tower, and the trail has none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your data stays on your phone.&lt;/strong&gt; Internet permission is removed entirely from the manifest. Cloud backup and device transfer are excluded. Your trail notes are yours. A closed equivalent would store your location history on someone else's disk, where it becomes part of a data set you don't own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The map is a file, not a subscription.&lt;/strong&gt; Protomaps ships offline tiles as a &lt;code&gt;.pmtiles&lt;/code&gt; archive bundled in the APK. MapLibre reads it locally. No tile-server bill, no API key, no per-request quota. A closed map SDK would meter every pan and zoom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can swap the model.&lt;/strong&gt; The &lt;code&gt;Transcriber&lt;/code&gt; and &lt;code&gt;EntityExtractor&lt;/code&gt; ports mean the AI backend is replaceable. If a smaller, faster, better model drops — or if you want to fine-tune one on your own observations — you change one adapter. No rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; There is no server component. There is no monthly bill. The APK does everything.&lt;/p&gt;

&lt;p&gt;The "open" part is what makes the whole thing possible &lt;em&gt;offline, on a trail, away from infrastructure&lt;/em&gt;. That's the point of the project.&lt;/p&gt;

&lt;p&gt;Hacktoberfest 2026 is a month-long celebration of open source, and this year it's specifically about building with open-weight models and open-source AI — which is exactly what MudMap is. The event runs all October, online and through 300+ in-person Fests, and anyone can take part regardless of where they are in their open-source journey. For MudMap, my immediate plan is to cut a v1.0.0 release with a permanent download link, then recruit Android testers to verify the software on real trails — microphone capture, GPS acquisition, transcription latency, and battery use. After that, the next slice of work is the deferred list: sync, delete, undo, and LLM-based entity extraction via Ollama. What I've learned building this is that the open pieces aren't decoration — they're the reason the app works where it's meant to be used, and Hacktoberfest is the right place to share that with people who care about the same thing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>CscGrader and generateRubrics: Open-Source Tools for Faster CS Grading</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sun, 04 Oct 2026 23:26:53 +0000</pubDate>
      <link>https://dev.to/jds64/cscgrader-and-generaterubrics-open-source-tools-for-faster-cs-grading-3n9k</link>
      <guid>https://dev.to/jds64/cscgrader-and-generaterubrics-open-source-tools-for-faster-cs-grading-3n9k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built a two-part toolkit to solve the biggest time-sink in teaching large CS courses: grading massive, complex student projects. This isn't about auto-grading, which often fails for creative or multi-part assignments. Instead, it’s about automating the tedious, mechanical work so instructors can focus on providing meaningful feedback.&lt;/p&gt;

&lt;p&gt;I built this for my friend, a CS professor who was spending entire weekends manually compiling, running, and debugging hundreds of student submissions for a single assignment.&lt;/p&gt;

&lt;p&gt;The problem is simple: when you have 150 students submitting multi-file Java projects, just getting each one to compile and run with the correct test input is a logistical nightmare. It’s slow, error-prone, and mentally draining.&lt;/p&gt;

&lt;p&gt;My solution is a two-part open-source toolkit:&lt;/p&gt;

&lt;p&gt;CscGrader: A language-agnostic tool that automates the "collection of evidence" phase. It detects student projects, compiles them, runs them with predefined inputs, and captures all the output (stdout, stderr, exit codes, timing).&lt;/p&gt;

&lt;p&gt;generateRubrics: A simple utility that takes a single master rubric file (Excel, PDF, etc.) and generates a personalized, ready-to-grade copy for every student on the roster.&lt;/p&gt;

&lt;p&gt;Together, these tools transform a weekend of grunt work into a streamlined, semi-automated workflow, freeing up my friend to do what they do best: teaching.&lt;/p&gt;

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

&lt;p&gt;The tools are designed for a command-line workflow. Here’s how they would be used together for a single assignment:&lt;/p&gt;

&lt;p&gt;Step 1: Generate all the grading rubrics for the class.&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  studentNames.txt contains a list of all students in the class
&lt;/h1&gt;

&lt;p&gt;python3 generateRubrics.py -inputFile studentNames.txt -fileToCopy "Assignment-03-Rubric.xlsx" --assignment 03&lt;br&gt;
This instantly creates a Rubrics/ folder with perfectly named files like JaneDoe-Assignment-03-Rubric.xlsx.&lt;/p&gt;

&lt;p&gt;Step 2: Process all student submissions to collect evidence.&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  The submissions folder contains one subfolder per student
&lt;/h1&gt;

&lt;h1&gt;
  
  
  The assignment profile tells CscGrader what to look for and how to run it
&lt;/h1&gt;

&lt;p&gt;cscgrader batch ./submissions/ --assignment ./csc215-assignment-03.json --input test-input.txt&lt;br&gt;
This processes every submission, compiles the Java code, runs it with the same test input, and saves a structured JSON file with all the results for each student.&lt;/p&gt;

&lt;p&gt;Step 3: Grade.&lt;/p&gt;

&lt;p&gt;My friend now opens the personalized rubric for a student in one window and the corresponding JSON evidence file in another. All the "did it compile? did it run? what was the output?" questions are already answered. They can now focus on code quality, design, and feedback.&lt;/p&gt;

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

&lt;p&gt;Here are the two repositories:&lt;/p&gt;

&lt;p&gt;CscGrader: &lt;a href="https://github.com/johnny603/CscGrader" rel="noopener noreferrer"&gt;https://github.com/johnny603/CscGrader&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;generateRubrics: &lt;a href="https://github.com/johnny603/generateRubrics" rel="noopener noreferrer"&gt;https://github.com/johnny603/generateRubrics&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This project is a prime example of using open-source tools for practical, targeted automation.&lt;/p&gt;

&lt;p&gt;Core Logic: Both tools are written in Python, relying only on the standard library. This was a deliberate choice to make them as portable and easy to run as possible for instructors who may not have admin rights or want to deal with virtual environments. The generateRubrics tool treats files as binary data, so it can copy .xlsx rubrics without needing libraries like openpyxl.&lt;/p&gt;

&lt;p&gt;AI-Assisted Development: I used an open-weight model as a coding partner throughout the process. It was instrumental in designing the modular architecture of CscGrader, specifically the idea of using "assignment profiles" (JSON files) to make the tool reusable across different courses and languages.&lt;/p&gt;

&lt;p&gt;Agent Harness: I used an open-source agent harness to automate the testing of CscGrader. The agent would create dummy Java projects with common errors (e.g., syntax errors, missing dependencies, ClassNotFoundException), run CscGrader on them, and verify that the tool correctly identified the failure category. This saved me hours of manual testing and made the tool far more robust.&lt;/p&gt;

&lt;p&gt;The architecture of CscGrader is built around a modular pipeline: Detection → Compilation → Execution → Evidence. This is all orchestrated by a central runner, which uses language-specific "adapters" to handle the unique aspects of each programming language. The agent harness was key to building and validating this complex workflow.&lt;/p&gt;

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

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

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

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Using AI as a Learning Tool: Lessons From the Classroom, STEM, and Software Engineering</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:01:57 +0000</pubDate>
      <link>https://dev.to/jds64/using-ai-as-a-learning-tool-lessons-from-the-classroom-stem-and-software-engineering-4jgf</link>
      <guid>https://dev.to/jds64/using-ai-as-a-learning-tool-lessons-from-the-classroom-stem-and-software-engineering-4jgf</guid>
      <description>&lt;p&gt;"Hello, I just wanted to check in..."&lt;/p&gt;

&lt;p&gt;I did not start thinking seriously about AI from one role. I started seeing it differently as I moved between being a computer science student, teaching STEM, working as an LA/DL, doing educational research, and working at a robotics startup. Each experience showed me a different part of the same problem: &lt;strong&gt;how can we use technology to make people better at learning and solving problems without taking the problem-solving away from them?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As an Instructional Student Assistant at San Francisco State University, I work with students on programming logic and software engineering while developing Python automation for academic workflows. As an E-GAISE research assistant, I have analyzed submissions from 34 student teams and contributed to research involving AI-integrated grading systems. Before that, I worked as an Engineering Operator at a robotics startup, where I built Python tools for data ingestion, labeling, and validation.&lt;/p&gt;

&lt;p&gt;These experiences led me to my main thesis: &lt;strong&gt;AI is most valuable in STEM education when it provides context-aware assistance, creates opportunities for practice and feedback, and keeps humans responsible for reasoning and verification.&lt;/strong&gt; A student can create a diagram before asking AI for design feedback. An instructor can provide course objectives and common mistakes before using AI to create a Kahoot, Blooket, or debugging game. A developer can provide an existing codebase before asking AI to make a change.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Needs Context
&lt;/h2&gt;

&lt;p&gt;One of the most useful ways I see AI being used in a classroom is by giving it context rather than starting with an empty prompt.&lt;/p&gt;

&lt;p&gt;For example, imagine that a student is working on a software design assignment. Instead of immediately asking an AI, "How should I build this?", the student could first create a class diagram, flowchart, wireframe, pseudocode outline, or system design.&lt;/p&gt;

&lt;p&gt;The student could then give that design to the AI and ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this design satisfy the requirements?&lt;/li&gt;
&lt;li&gt;What relationships am I missing?&lt;/li&gt;
&lt;li&gt;Is there unnecessary complexity?&lt;/li&gt;
&lt;li&gt;What edge cases should I consider?&lt;/li&gt;
&lt;li&gt;What could I improve?&lt;/li&gt;
&lt;li&gt;What tradeoffs am I making?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a different learning experience.&lt;/p&gt;

&lt;p&gt;The AI now has context about what the student is thinking, and the student has already performed part of the intellectual work. The AI becomes something that can critique and question the student's thinking instead of generating the entire solution.&lt;/p&gt;

&lt;p&gt;I think this is especially important in computer science because design is part of the learning process. If a student creates a diagram, writes pseudocode, or explains an algorithm before using AI, there is something concrete to evaluate.&lt;/p&gt;

&lt;p&gt;The same principle applies to professional software development. A developer can provide an existing codebase, architecture diagram, API specification, test case, or error log before asking AI for help. The more useful question is not simply, "Can AI write this?" but "What context does AI need to understand what I am trying to accomplish?"&lt;/p&gt;

&lt;h2&gt;
  
  
  AI and Gamified Learning
&lt;/h2&gt;

&lt;p&gt;Another classroom application I have become interested in is using AI to create short, bite-sized learning activities.&lt;/p&gt;

&lt;p&gt;During my LA/DL sessions, I use activities similar to Kahoot and Blooket to review programming concepts and assignments. I have found that these activities can make a 50-minute session more interactive because students are not simply listening to a lecture.&lt;/p&gt;

&lt;p&gt;AI could help instructors create variations of these activities.&lt;/p&gt;

&lt;p&gt;For example, I could provide AI with the assignment objectives, course concepts, common student mistakes, and examples of the code being taught. AI could then help generate questions for a Kahoot, Blooket, or another classroom game.&lt;/p&gt;

&lt;p&gt;The instructor would still need to review the material before using it, but AI could make it much faster to create several versions of an activity.&lt;/p&gt;

&lt;p&gt;The activity could also go beyond a traditional multiple-choice quiz. A programming class could have a &lt;strong&gt;Code Detective&lt;/strong&gt; game where students receive clues about a programming bug and identify the problematic line. Another activity could ask students to predict what a program will output before running it. Students could work in teams to determine which algorithm would be most appropriate for a particular problem.&lt;/p&gt;

&lt;p&gt;The goal is not to use AI simply because it is available. The goal is to use it to create more opportunities for students to practice.&lt;/p&gt;

&lt;p&gt;This connects to my previous experience teaching STEM, where I used project-based activities involving coding, game design, and robotics. I learned that students can understand technical concepts differently depending on how the material is presented. A short game, demonstration, or hands-on activity can sometimes create a better entry point than another explanation.&lt;/p&gt;

&lt;p&gt;AI gives educators another way to experiment with those activities.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as a Learning Assistant
&lt;/h2&gt;

&lt;p&gt;AI can also be useful when I am the student.&lt;/p&gt;

&lt;p&gt;When I encounter a difficult programming problem, I do not necessarily want an AI system to give me the finished solution. Doing that can remove one of the most important parts of computer science education: learning how to reason through a problem.&lt;/p&gt;

&lt;p&gt;Instead, I can ask questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What concept am I misunderstanding?&lt;/li&gt;
&lt;li&gt;What edge case am I missing?&lt;/li&gt;
&lt;li&gt;Can you give me a hint without giving me the solution?&lt;/li&gt;
&lt;li&gt;Why does my approach fail?&lt;/li&gt;
&lt;li&gt;What should I test next?&lt;/li&gt;
&lt;li&gt;Can you explain this error differently?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I can also give AI my own work first.&lt;/p&gt;

&lt;p&gt;For example, I might write pseudocode, create a class diagram, implement a first version, or explain my reasoning before asking for feedback. This gives the AI something concrete to evaluate.&lt;/p&gt;

&lt;p&gt;It also forces me to confront my own understanding before asking for help.&lt;/p&gt;

&lt;p&gt;That is an important distinction between &lt;strong&gt;using AI to learn&lt;/strong&gt; and &lt;strong&gt;using AI to avoid learning&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What My LA/DL Experience Taught Me
&lt;/h2&gt;

&lt;p&gt;My experience as an LA/DL has made this distinction even clearer.&lt;/p&gt;

&lt;p&gt;A student does not always need more information. Sometimes they need the information presented differently. Sometimes they need someone to ask a question that helps them identify their own mistake.&lt;/p&gt;

&lt;p&gt;For example, if a student is confused about tracing code, I might change the activity and have them diagram the execution of the program. If several students make the same mistake, I can turn that mistake into a group debugging activity.&lt;/p&gt;

&lt;p&gt;I have also learned that engagement matters. A 50-minute session is relatively short, so I try to structure the time around an icebreaker, a game or review activity, assignment content, demonstrations, and a workshop where students can ask questions and begin working.&lt;/p&gt;

&lt;p&gt;AI could help me create more variations of these activities, but it does not know my students in the same way that I do.&lt;/p&gt;

&lt;p&gt;I still have to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should students learn?&lt;/li&gt;
&lt;li&gt;What is appropriate for their current level?&lt;/li&gt;
&lt;li&gt;What misconceptions are they experiencing?&lt;/li&gt;
&lt;li&gt;How much content can realistically fit into 50 minutes?&lt;/li&gt;
&lt;li&gt;Which activity will encourage participation?&lt;/li&gt;
&lt;li&gt;Is the material appropriate for this particular class?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the human role remains important. AI can assist with preparation, but the instructor still has to understand the students and make the educational decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI in Educational Research
&lt;/h2&gt;

&lt;p&gt;My research experience has given me another perspective on AI in education.&lt;/p&gt;

&lt;p&gt;At San Francisco State University, I analyzed submissions from 34 student teams across two semesters using Python and Pandas. The work involved extracting chat logs and reflective writing and analyzing performance across multiple rubric dimensions. I also contributed to research involving AI-integrated grading systems and educational technology.&lt;/p&gt;

&lt;p&gt;I have also worked on an E-GAISE agentic grading pipeline that uses Python, LangGraph, Pandas, and Pydantic to evaluate submissions against a rubric. The system uses evidence and validation mechanisms rather than simply accepting an AI-generated evaluation as correct.&lt;/p&gt;

&lt;p&gt;This experience has made me more interested in &lt;strong&gt;how AI is integrated into a workflow&lt;/strong&gt;, rather than simply whether AI is being used.&lt;/p&gt;

&lt;p&gt;For example, an AI-assisted grading system needs a clear rubric, evidence from the student's work, and a way to evaluate whether its conclusions are consistent. The important question is not whether AI can produce a grade, but whether the overall process produces useful and defensible feedback.&lt;/p&gt;

&lt;p&gt;That perspective connects closely to my experience in software engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human-in-the-Loop: What I Learned From Robotics
&lt;/h2&gt;

&lt;p&gt;My experience at a robotics startup helped me understand why this distinction matters.&lt;/p&gt;

&lt;p&gt;My work involved data ingestion, labeling, validation, and internal tooling. The technical problem was not simply getting software to run. The larger problem was making sure the data moving through the workflow could actually be trusted.&lt;/p&gt;

&lt;p&gt;I learned that automation is valuable because it can reduce repetitive work and make systems faster. But automation can also make mistakes faster.&lt;/p&gt;

&lt;p&gt;That lesson carries directly into AI.&lt;/p&gt;

&lt;p&gt;An AI system can generate ten explanations in seconds, but if those explanations are incorrect, it has simply produced ten incorrect explanations faster. The solution is not necessarily to avoid automation. Instead, the workflow needs enough context, testing, and human judgment to determine whether the result is useful.&lt;/p&gt;

&lt;p&gt;This gives me a model for how I want to use AI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context → AI assistance → Human review → Feedback → Iteration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same workflow can apply to a classroom activity, an AI-generated explanation, an automated grading system, or a software development task.&lt;/p&gt;

&lt;p&gt;The human does not have to perform every step manually. Instead, AI can handle parts of the process while the human remains responsible for understanding the goal and deciding whether the result is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Risk of Overusing AI
&lt;/h2&gt;

&lt;p&gt;The biggest concern I have with AI in education is that convenience can become a substitute for learning.&lt;/p&gt;

&lt;p&gt;If a student asks an AI to solve every programming assignment, they may complete the assignment without developing the ability to solve a similar problem independently.&lt;/p&gt;

&lt;p&gt;There is a major difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Give me the answer."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Here is my design. Can you help me find weaknesses in it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second approach preserves the student's role in the learning process.&lt;/p&gt;

&lt;p&gt;The student has made decisions, created an artifact, and thought about the problem. AI becomes a source of feedback instead of a replacement for reasoning.&lt;/p&gt;

&lt;p&gt;This is why I think educators should focus less on simply asking whether students are using AI and more on teaching students how to use it responsibly.&lt;/p&gt;

&lt;p&gt;Students should know how to question AI output, test generated code, verify information, recognize incorrect assumptions, and explain their own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Has Taught Me About Learning
&lt;/h2&gt;

&lt;p&gt;My experiences have changed my definition of effective AI use.&lt;/p&gt;

&lt;p&gt;I used to think about AI primarily as something that could make tasks faster. Now I think about it more as something that can create additional opportunities for experimentation.&lt;/p&gt;

&lt;p&gt;As a student, I can use AI to challenge my reasoning.&lt;/p&gt;

&lt;p&gt;As an educator, I can use it to brainstorm activities, create practice problems, and develop different explanations.&lt;/p&gt;

&lt;p&gt;As a researcher, I can investigate how AI changes educational workflows and how those systems can be evaluated.&lt;/p&gt;

&lt;p&gt;As a software developer, I can use AI to investigate problems while still being responsible for the resulting system.&lt;/p&gt;

&lt;p&gt;In all of these situations, the most valuable workflow is not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human → AI → Answer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is closer to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human → Context/Design → AI → Feedback → Human Decision → Iteration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI will continue to change STEM education and software engineering, but I do not think the future should be about replacing the human parts of learning.&lt;/p&gt;

&lt;p&gt;My experiences in education and software engineering have taught me that context, iteration, feedback, and human judgment matter.&lt;/p&gt;

&lt;p&gt;In the classroom, that could mean creating a design before asking AI for feedback. It could mean using AI to turn common programming mistakes into a short debugging game. It could mean generating a Kahoot or Blooket from course material and then adapting it to the needs of the class.&lt;/p&gt;

&lt;p&gt;For a student, it could mean creating pseudocode before asking AI for implementation feedback. For a developer, it could mean giving AI enough information about an existing system before asking it to make a change. For an educational researcher, it could mean designing an AI-assisted grading workflow around evidence and clear evaluation criteria.&lt;/p&gt;

&lt;p&gt;These approaches have something in common: &lt;strong&gt;AI is helping with the process, but humans remain responsible for the thinking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the lesson I am carrying forward from my work as a student, educator, researcher, and engineer.&lt;/p&gt;

&lt;p&gt;AI should not make us think less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It should give us better ways to think, practice, test, create, and learn.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>career</category>
    </item>
    <item>
      <title>Lessons from a Robotics Startup: What I Learned About Data Pipelines</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sun, 09 Aug 2026 00:01:46 +0000</pubDate>
      <link>https://dev.to/jds64/lessons-from-a-robotics-startup-what-i-learned-about-data-pipelines-138j</link>
      <guid>https://dev.to/jds64/lessons-from-a-robotics-startup-what-i-learned-about-data-pipelines-138j</guid>
      <description>&lt;p&gt;"Smile because it happened" — Dr. Seuss&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Earlier this year, I took on a short-term trial role with an early-stage robotics startup. The premise was straightforward: help with data collection, annotation, and evaluation workflows—essentially the backbone of any modern robotics or embodied-AI system.&lt;br&gt;
The trial didn't work out long-term. I was let go after about two months — a decision that, honestly, came down in part to my bandwidth as a student. Balancing a full course load with a startup trial was harder than I anticipated.&lt;br&gt;
But that's not the story I want to tell. What I do want to share are the technical lessons I took away — lessons about building robust data pipelines, about the gap between theory and practice, and about what I'd do differently next time.&lt;/p&gt;

&lt;p&gt;These aren't company secrets. They're about the general engineering challenges that anyone working with robotics data pipelines will encounter — challenges I'd read about in papers but hadn't truly internalized until I was standing in front of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Data Pipeline Shape Is Universal—But the Details Aren't
&lt;/h2&gt;

&lt;p&gt;If you've spent any time in ML or robotics, you've seen this described:&lt;br&gt;
Data Collection → Annotation → Evaluation&lt;br&gt;
It's a standard three-stage pipeline. Industry vendors describe it explicitly in their robotics content. Academic projects model this structure. It's the field's shared vocabulary. Companies such as Scale AI and Toloka use similar industry workflows involving data collection, annotation, and evaluation.&lt;br&gt;
What isn't shared are the specifics: the sensor setup, the calibration procedures, the annotation rubric, and the evaluation metrics. Those are where a company's IP lives. The pipeline shape? That's just the map. And the map is public.&lt;/p&gt;

&lt;p&gt;What I'd do differently: Simulate before you collect. Data collection is expensive — in time, hardware wear, and cognitive load on operators. Before running a full session, run a feasibility study with a small batch. Verify your sync and capture scripts. I assumed this was standard practice. It wasn't. The cost of that assumption showed up as rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Annotation Granularity: More Isn't Always Better
&lt;/h2&gt;

&lt;p&gt;There's a natural temptation when designing an annotation schema: capture everything. Every possible label, every edge case, every attribute you might want later. This is a trap.&lt;/p&gt;

&lt;p&gt;Over-granular annotation creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher operator cognitive load → slower throughput, more errors&lt;/li&gt;
&lt;li&gt;Greater inconsistency → more labels means more disagreement&lt;/li&gt;
&lt;li&gt;Fragile evaluation → you're evaluating against things you might not need&lt;/li&gt;
&lt;li&gt;The better approach? Start with the minimal viable schema that answers your core research question. Add complexity only when the data tells you it's necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A concrete heuristic: If you're designing labels for a demonstration-collection task, ask yourself: "Will I actually use this attribute to decide if the demonstration is successful?" If the answer isn't an immediate yes, drop it. Retrofitting granularity is easier than cleaning up inconsistent over-labeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Automation Still Needs Oversight—Especially in Evaluation
&lt;/h2&gt;

&lt;p&gt;Automation is seductive. The idea that you can "just set up an automated evaluation pipeline and let it run" is appealing, especially in a startup where resources are tight.&lt;br&gt;
I learned the hard way that automated evaluation is not a replacement for human judgment. It's a filter.&lt;/p&gt;

&lt;p&gt;Automated checks catch obvious failures (missing data, format errors).&lt;br&gt;
They don't catch subtle issues (context-dependent errors, edge cases).&lt;br&gt;
They create a false sense of confidence without regular spot-checks.&lt;br&gt;
The workflow that actually works: Run your automated eval, spot-check a sample of what passes, find patterns in the failures, add those to your automated checks, and repeat. It's the "human-in-the-loop" pattern. Easy to forget when you're moving fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Tell My Past Self
&lt;/h2&gt;

&lt;p&gt;If I could go back to Day 1 of that trial, here is exactly what I'd say:&lt;br&gt;
Ask for explicit success criteria upfront. "Good work" means different things to different people in different contexts. Don't assume alignment. Get it in writing, or at least in a clear verbal agreement.&lt;/p&gt;

&lt;p&gt;Simulate before you scale. A small batch of test data can save days of rework. Verify your entire pipeline end-to-end with a single example before you collect a hundred.&lt;br&gt;
Design for iteration, not perfection. Your first annotation schema will be wrong. That's fine. Make it easy to change. Don't fall in love with your first draft.&lt;/p&gt;

&lt;p&gt;Trust automation as a filter, not a replacement. It will never catch everything. Spot-check everything that matters, especially early on.&lt;br&gt;
After the fact, write yourself a retrospective — from memory, not from any retained materials. Capture what worked and what didn't while it's fresh. You'll thank yourself later when you're interviewing or planning your next project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Getting let go from a trial role is never fun. It stings, and there's always the temptation to replay the "why."&lt;/p&gt;

&lt;p&gt;But the technical lessons? Those are mine. They're transferable. They're not tied to any particular company's proprietary stack.&lt;br&gt;
A startup trial is an accelerated learning environment — whether it works out or not. The key is to separate what you learned about yourself and your craft from what you learned about that specific company's internal operations. The former is valuable, portable, and belongs to you.&lt;br&gt;
The latter is their business.&lt;/p&gt;

&lt;p&gt;Now, over to you:&lt;br&gt;
What's one technical or operational lesson from a short-term role that completely changed how you approach your work today? I'd genuinely love to hear your stories in the comments — drop one below. 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>robotics</category>
      <category>productivity</category>
    </item>
    <item>
      <title>From Kilowatt-Hours to Carbon Counts: Shaping Accurate Energy Reporting with OED</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Wed, 10 Jun 2026 00:03:13 +0000</pubDate>
      <link>https://dev.to/jds64/from-kilowatt-hours-to-carbon-counts-shaping-accurate-energy-reporting-with-oed-4f99</link>
      <guid>https://dev.to/jds64/from-kilowatt-hours-to-carbon-counts-shaping-accurate-energy-reporting-with-oed-4f99</guid>
      <description>&lt;p&gt;"garbage in, garbage out" - Some quote I heard before&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every building tells a story through its energy usage.&lt;/p&gt;

&lt;p&gt;A classroom left lit overnight, an HVAC system running longer than expected, or a sudden spike in electricity consumption across campus, these patterns often go unnoticed without the right tools to visualize them. That’s where the Open Energy Dashboard (OED) comes in.&lt;/p&gt;

&lt;p&gt;OED is an open-source platform that helps organizations transform raw utility data into meaningful insights through charts, analytics, and real-time monitoring. &lt;/p&gt;

&lt;p&gt;Schools, universities, sustainability teams, and facility managers use the dashboard to better understand how energy and resources are consumed across their buildings. Instead of sorting through spreadsheets or complicated meter data, users can quickly identify inefficiencies, compare trends, and make informed decisions that reduce both operational costs and environmental impact.&lt;/p&gt;

&lt;p&gt;The development build is similar to the production build. In order to run development, you must install Docker which is a container for images, like screenshots of a build you can run on your particular operating system. Here is the app running using ‘docker compose up’, and to quit the browser I used ‘docker compose down’.&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.amazonaws.com%2Fuploads%2Farticles%2Febho3xddkbe4qvo4o869.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.amazonaws.com%2Fuploads%2Farticles%2Febho3xddkbe4qvo4o869.png" alt="OED dev build" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Issue
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/OpenEnergyDashboard/OED/issues/962" rel="noopener noreferrer"&gt;issue link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this contribution, I worked on LR18, an integration test focused on validating energy conversion behavior in the readings API. The test ensures that 15-minute interval energy readings can be correctly converted into kg CO₂ values while maintaining proper daily aggregation over large time ranges. While the change may seem small from the outside, it introduced interesting challenges involving fixture setup, unit conversions, expected CSV outputs, and backend testing consistency across the codebase.&lt;/p&gt;

&lt;p&gt;The following files and resources were the most important locations in the codebase for understanding and implementing the LR18 integration test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;src/server/test/web/readingsLineMeterRangeQuantity.js This was the primary file where the LR18 test case was implemented. The file contains integration tests for line chart range (min/max) quantity meter readings across different aggregation ranges and unit behaviors. I used the surrounding LR1–LR7 tests to understand the expected structure, helper utilities, API request format, and CSV validation workflow. LR18 was added here because it specifically validates range aggregation behavior combined with unit conversion into kg CO₂.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/OpenEnergyDashboard/DesignDocs/blob/main/testing/testing.md" rel="noopener noreferrer"&gt;testing.md Documentation link&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design document was important for understanding the intended testing requirements and naming conventions used throughout the project. The document outlines the required test scenarios for readings APIs, including line range (min/max) tests, aggregation expectations, and expected behavior across different unit conversions and time ranges. It helped confirm that LR18 belonged in the line range quantity test suite and clarified how expected CSV outputs should be structured.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;readings_ri_15_days_75.csv This readings dataset file contains the raw 15-minute interval test readings used for validating aggregation and conversion behavior. The LR18 test loads this dataset into the database during setup to simulate realistic meter readings over a long time range. These readings are later aggregated into daily points by the API.&lt;/li&gt;
&lt;li&gt;expected_line_range_ri_15_mu_kWh_gu_kgCO2_st_-inf_et_inf.csv This expected CSV file represents the correct API response output for LR18 after daily aggregation and unit conversion into kg CO₂. The test compares the API response against this file using helper assertions to verify timestamps, minimum/maximum values, and aggregation correctness.&lt;/li&gt;
&lt;li&gt;src/server/util/readingsUtils.js This utility file contains important helper functions and shared testing utilities used throughout the readings integration tests. Functions such as:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;prepareTest()&lt;/li&gt;
&lt;li&gt;parseExpectedCsv()&lt;/li&gt;
&lt;li&gt;expectRangeToEqualExpected()&lt;/li&gt;
&lt;li&gt;getUnitId() were essential for seeding test fixtures, loading expected CSV data, resolving unit IDs dynamically, and validating API responses against expected outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;src/server/test/web/readingsLineMeterQuantity.js This file was used as a reference implementation because it already contained an existing L18 conversion-based test. I compared its fixture setup, conversion seeding, and naming conventions against LR18 to ensure consistency between quantity tests and range quantity tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/OpenEnergyDashboard/OED/blob/development/package.json" rel="noopener noreferrer"&gt;package.json&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The package.json file was useful for identifying available npm scripts and test commands used to run the OED test suite. This helped with running targeted test cases and validating the LR18 implementation locally during debugging.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;npm run testData This command was referenced in the testing documentation and used to load sample data into the system for validating the readings API behavior during local testing and debugging.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Product/Codebase
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;p&gt;Redux Toolkit (RTK): Manages the application's state, ensuring that data remains consistent and synchronized across all parts of the dashboard. Handles things like user selections, fetched meter data, UI state, etc.&lt;/p&gt;

&lt;p&gt;Plotly.js: Generates the interactive graphs and charts that visualize energy and resource consumption.&lt;/p&gt;

&lt;p&gt;React: The core frontend library used to build the user interface. It renders components for dashboards, selectors, and controls.&lt;/p&gt;

&lt;p&gt;Node.js: Runs the backend server, handling data API requests and managing business logic and authentication.&lt;/p&gt;

&lt;p&gt;PostgreSQL: Stores all historical energy/resource data, meter definitions, user configurations, etc. Acts as the main database.&lt;/p&gt;

&lt;p&gt;Express.js: Web server running on Node.js, exposes RESTful API endpoints for the frontend to request and update data.&lt;/p&gt;

&lt;p&gt;TypeScript: Provides type safety and improved developer experience for both frontend and backend codebases.&lt;/p&gt;

&lt;p&gt;Docker: Used to containerize and deploy the application and its dependencies for consistent environments (optional in some deployments).&lt;/p&gt;

&lt;h3&gt;
  
  
  Syatem diagram
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fcewo42w92moe7ilvyrvg.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.amazonaws.com%2Fuploads%2Farticles%2Fcewo42w92moe7ilvyrvg.png" alt="System diagram of OED" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use case
&lt;/h3&gt;

&lt;p&gt;“A user wants to see the CO₂ emissions for electricity usage over a selected date range.”&lt;/p&gt;

&lt;p&gt;Step-by-step walkthrough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User selects a building and a date range in the dashboard's UI (React) and clicks "View CO₂ Emissions."&lt;/li&gt;
&lt;li&gt;The React UI dispatches an action to Redux Toolkit to update the state with the new selection.&lt;/li&gt;
&lt;li&gt;Redux triggers an asynchronous operation (a thunk or saga) to fetch the relevant readings and emission data from the backend.&lt;/li&gt;
&lt;li&gt;The browser makes a REST API request to the Node.js/Express API, specifying the building and date range.&lt;/li&gt;
&lt;li&gt;The backend API receives the request. It queries the PostgreSQL database for all energy readings within the range and uses internal logic (data services) to convert those readings into kg CO₂, applying the project’s conversion formulas (the logic tested by the LR18 integration test).&lt;/li&gt;
&lt;li&gt;The backend returns a JSON object with time-stamped CO₂ emissions data to the frontend.&lt;/li&gt;
&lt;li&gt;The Redux store receives the data and the UI updates accordingly.&lt;/li&gt;
&lt;li&gt;Plotly.js renders an interactive chart with the emissions data, letting the user explore and analyze the results visually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Inconsistent Unit Conversion Setup in LR18
&lt;/h3&gt;

&lt;p&gt;One of the main technical challenges I faced was diagnosing why the LR18 integration test did not match the expected API behavior for converting readings into kg CO₂. Originally, the test was still requesting the kWh graphic unit, even though project requirements and the expected CSV output specified kg CO₂.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Directed Problem-Solving Attempts
&lt;/h3&gt;

&lt;p&gt;Compared with Existing Tests: I reviewed LR1–LR7 to look for patterns in how units were handled and where API requests might be constructed differently.&lt;br&gt;
Codebase Search: I searched for kgCO2 and related conversion logic in both backend tests and the conversion fixture seeding code to find established best practices.&lt;br&gt;
Implementation Cross-Reference: I closely examined the relevant section in readingsLineMeterQuantity.js, focusing on how conversion fixtures were prepared and used, to spot differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Help
&lt;/h3&gt;

&lt;p&gt;I leveraged GitHub Copilot to reason through discrepancies, generating code suggestions and checking my test’s construction against unit-handling best practices.&lt;br&gt;
I carefully read the project’s testing.md guidelines to ensure my approach to data seeding and test assertions matched the intended design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Status
&lt;/h3&gt;

&lt;p&gt;Resolved. The LR18 test setup was corrected: the test now requests the API with the correct kg CO₂ graphic unit and seeds the conversion fixtures to align with expectations. The test passes and provides accurate verification for future code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: CSV Naming and Expected Output Consistency
&lt;/h3&gt;

&lt;p&gt;I also struggled with inconsistent naming conventions for expected CSV files (kg_of_CO2 vs. kgCO2) when comparing actual output to reference data files. This led to confusion in test assertions and in locating previous examples for regression testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Directed Problem-Solving Attempts
&lt;/h3&gt;

&lt;p&gt;Repository Search: I searched the repo for both naming styles to see which was most commonly used.&lt;br&gt;
Pattern Review: I surveyed surrounding tests (both quantity and range-based) to verify which format appeared in assertion logic and file loading calls.&lt;br&gt;
Assertion Validation: I checked the test utilities (such as readingsUtils.js) to see if there were helper functions that auto-resolved or validated expected outputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Help
&lt;/h3&gt;

&lt;p&gt;I referenced conversion-based tests in readingsLineMeterQuantity.js to ensure consistency in both naming and output structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Status
&lt;/h3&gt;

&lt;p&gt;Resolved. I updated the LR18 test’s CSV file expectations and assertions to use the kgCO2 convention, eliminating confusion across the codebase and achieving a passing assertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: Maintaining Consistent Branch Workflow (Using git cherry-pick)
&lt;/h3&gt;

&lt;p&gt;A key workflow challenge our team encountered was maintaining a consistent branch structure during development. There were instances when new commits were accidentally made to the wrong branch, which could cause confusion, code conflicts, or integration issues if left unaddressed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Directed Problem-Solving Attempts
&lt;/h3&gt;

&lt;p&gt;Manual Merging: Attempted to merge the changes or re-apply the edits manually onto the correct branch, but this was time-consuming and could introduce mistakes.&lt;br&gt;
Branch Reset &amp;amp; Rebase: Explored using git reset and git rebase to move commits, but these approaches risked rewriting history or affecting unrelated changes.&lt;br&gt;
Git Documentation Review: Consulted the official Git docs and several online tutorials to look for a safer, more targeted solution for moving just the desired commit(s).&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Help
&lt;/h3&gt;

&lt;p&gt;Once I identified git cherry-pick as a possible approach, I confirmed the workflow with Copilot and checked with project teammates (if applicable) to ensure this was in line with team conventions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Status
&lt;/h3&gt;

&lt;p&gt;Resolved. I successfully used git cherry-pick to copy the intended commit(s) to the correct branch without affecting unrelated work. This helped maintain branch hygiene and enabled a clean pull request history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;To address the issue of ensuring accurate conversion of energy usage data to kilograms of CO₂ (LR18), I developed an integration test that simulates a user requesting CO₂ emissions data over a specific date range. I modeled my implementation outline using the structure and methodology established in existing test cases LR1–LR7, which demonstrated good practices for preparing test data, invoking the API endpoint, and checking results.&lt;/p&gt;

&lt;p&gt;In my solution, I set up sample meter readings in the database, triggered the backend logic to request the calculated CO₂ values, and verified that the backend (Node.js/Express) returned the correct results according to the expected conversion rules. This test helps ensure the data displayed in the dashboard (via Redux Toolkit and Plotly.js in the frontend) will always be calculated correctly, catching any regressions as the codebase evolves.&lt;/p&gt;

&lt;p&gt;I relied heavily on Redux Toolkit to manage the application state in the frontend, this ensures that, once the backend API returns the correct CO₂ data, the entire dashboard updates consistently and all visualizations generated by Plotly.js reflect accurate numbers.&lt;/p&gt;

&lt;p&gt;On the backend, I focused on the API logic and data services component (see System Diagram: Node.js/Express and Data Services) that handles the data transformation and communicates directly with the PostgreSQL database. My test verifies that conversions at this backend layer, before the data even reaches Redux or Plotly, are correct.&lt;/p&gt;

&lt;p&gt;I verified the solution using automated test runners via Mocha. After implementing the LR18 test, I ran the full test suite and confirmed that all tests, including my new one, passed. This proves that both existing and new requirements for data conversion are met.&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.amazonaws.com%2Fuploads%2Farticles%2Fq7olgj1ukzqpkpgc7ptt.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.amazonaws.com%2Fuploads%2Farticles%2Fq7olgj1ukzqpkpgc7ptt.png" alt="Testing solution with Mocha" width="799" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See the full solution &lt;a href="https://github.com/OpenEnergyDashboard/OED/pull/1627" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>react</category>
      <category>typescript</category>
      <category>docker</category>
    </item>
    <item>
      <title>Keeping Your Secrets in Sync: Environment Lifecycle Management That Actually Works</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sat, 14 Feb 2026 01:25:14 +0000</pubDate>
      <link>https://dev.to/jds64/keeping-your-secrets-in-sync-environment-lifecycle-management-that-actually-works-24ga</link>
      <guid>https://dev.to/jds64/keeping-your-secrets-in-sync-environment-lifecycle-management-that-actually-works-24ga</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Keyshade is an open-source real-time secret and configuration management platform that helps developers securely store, sync, and rotate sensitive data like API keys, passwords, and environment variables across multiple environments. It uses end-to-end encryption and live updates to eliminate insecure .env file sharing, manual dashboard updates, and secret sprawl. Designed for teams and solo developers alike, Keyshade improves collaboration, auditability, and security by providing version history, access controls, and seamless integrations with modern deployment platforms.&lt;/p&gt;

&lt;p&gt;Try it out on &lt;a href="https://keyshade.io/" rel="noopener noreferrer"&gt;https://keyshade.io/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Silent Failures in Cloud Integrations (&lt;a href="https://github.com/keyshade-xyz/keyshade/issues/1254" rel="noopener noreferrer"&gt;#1254&lt;/a&gt;)
&lt;/h2&gt;

&lt;p&gt;The problem was that the integrations with Vercel or AWS Lambda would only respond to updates in secrets and variables. It would completely ignore situations where the user had deleted the environment or renamed the environment. This meant that when a user deleted an environment in Keyshade, the secrets and variables would still be available in Vercel or AWS Lambda but would be orphaned. This presented a problem from a security point of view. Also, when the environment was renamed, the integrations would be out of sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Bug Mattered to Production Teams
&lt;/h2&gt;

&lt;p&gt;This problem was significant because of several factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security Risk: When a user deleted an environment using Keyshade (like an environment in staging mode), the secrets became active in Vercel and AWS Lambda. This meant the secrets were exposed on another platform. That is, credentials became potential attack vectors.&lt;/li&gt;
&lt;li&gt;Data Integrity: The disconnect between Keyshade’s environment state and the external platforms led to a situation where users couldn’t be assured of data integrity. It defeats the purpose of having a reliable tool like Keyshade.&lt;/li&gt;
&lt;li&gt;User Experience: It will be the responsibility of the consumers of Keyshade to manually log in to the Vercel or AWS Lambda interface to clean up after environment changes, thereby defeating the purpose of the integration in the first place. Such an interface might discourage the use of Keyshade.&lt;/li&gt;
&lt;li&gt;Production Readiness: The integrations must be prepared to handle the full lifecycle of environments, not only the secrets within static environments, for Keyshade to be considered viable in production, especially by teams with high rates of environment changes (e.g., CI/CD workflows creating ephemeral environments). It was marked as "priority: high" and "difficulty: 4." This was my hardest PR that took around two months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Under the Hood: Key Code Changes
&lt;/h2&gt;

&lt;p&gt;keyshade/apps/api/src/integrations/plugins/vercel.integration.ts&lt;br&gt;
Purposes: Integrates the Vercel platform - responsible for syncing Keyshade secrets/variables to Vercel projects as environment variables.&lt;br&gt;
Key sections to edit:&lt;br&gt;
getPermittedEvents(). Added ENVIRONMENT_UPDATED and ENVIRONMENT_DELETED to the set of events the integration subscribes to.&lt;br&gt;
added case handlers to switch of emitEvent() for routing environment lifecycle events.&lt;br&gt;
New functions added: handleEnvironmentDeleted(): This cleans up project-level environment variables scoped to the deleted environment and breaks the integration relationship handleEnvironmentUpdated(): Renames custom Vercel environments if available (pro plan users) &amp;amp; update integration metadata&lt;/p&gt;

&lt;p&gt;apps/api/src/integration/plugins/aws-lambda.integration.ts&lt;br&gt;
Purpose: Implements the AWS Lambda integration, syncing Keyshade secrets to Lambda function environment variables&lt;br&gt;
Key sections modified:&lt;br&gt;
getPermittedEvents(): Added the two environment events&lt;br&gt;
emitEvent() switch statement: Added routing for environment events&lt;br&gt;
New functions added:&lt;br&gt;
handleEnvironmentDeleted(): Removes environment-scoped keys from Lambda function configuration and disconnects the environment relationship&lt;br&gt;
handleEnvironmentUpdated(): No-op handler (Lambda has no environment rename concept) that logs an audit event&lt;/p&gt;

&lt;p&gt;apps/api/src/common/util.ts&lt;br&gt;
Purpose: Added retryWithBackoff() helper function to handle transient failures when calling external APIs (Vercel/AWS)&lt;br&gt;
Why important: Cloud APIs can fail temporarily; this ensures cleanup operations are resilient and retry with exponential backoff&lt;/p&gt;

&lt;p&gt;apps/api/src/integration/reconciler.ts&lt;br&gt;
Purpose: Lightweight reconciliation system that processes failed cleanup operations stored in integration.metadata.pendingCleanup&lt;br&gt;
Why important: When retries fail after multiple attempts, the reconciler can later replay these operations, ensuring eventual consistency even if external APIs are temporarily unavailable&lt;/p&gt;

&lt;p&gt;apps/api/src/event/event.types.ts&lt;br&gt;
Purpose: Defines TypeScript types for event metadata&lt;br&gt;
Relevant types:&lt;br&gt;
EnvironmentUpdatedEventMetadata: Contains name and description fields for environment updates&lt;br&gt;
EnvironmentDeletedEventMetadata: Contains the environment name for deletion events&lt;/p&gt;

&lt;p&gt;Test files:&lt;br&gt;
apps/api/src/integration/plugins/vercel.integration.spec.ts: tests covering environment lifecycle handlers&lt;br&gt;
apps/api/src/integration/plugins/aws-lambda.integration.spec.ts: tests covering Lambda-specific handlers&lt;br&gt;
apps/api/src/integration/reconciler.spec.ts: testing the reconciler's pending cleanup processing&lt;/p&gt;

&lt;p&gt;Supporting materials/artifacts&lt;br&gt;
Before the fix:&lt;br&gt;
When a user deleted an environment in Keyshade, the integration would:&lt;/p&gt;

&lt;p&gt;Not receive or process the ENVIRONMENT_DELETED event&lt;br&gt;
Leave all environment-scoped secrets active on Vercel/AWS Lambda&lt;br&gt;
Not update the integration metadata to reflect the environment was gone&lt;br&gt;
Create orphaned credentials that users would have to manually clean up&lt;/p&gt;

&lt;p&gt;Error behavior:&lt;br&gt;
While there wasn't a specific error message, the silent failure was the problem. Users would delete an environment expecting it to be cleaned up everywhere, but the secrets would persist on external platforms with no warning or indication that cleanup failed.&lt;br&gt;
After the fix:&lt;br&gt;
The integration now:&lt;br&gt;
Subscribes to ENVIRONMENT_UPDATED and ENVIRONMENT_DELETED events&lt;br&gt;
Automatically removes environment variables from Vercel projects when environments are deleted&lt;br&gt;
Removes environment-scoped keys from AWS Lambda function configurations&lt;br&gt;
Updates integration metadata to stay in sync with environment name changes&lt;br&gt;
Uses retry logic with exponential backoff for resilience against transient API failures&lt;br&gt;
Persists failed operations in pendingCleanup for later reconciliation&lt;br&gt;
Logs audit events for compliance and debugging&lt;/p&gt;

&lt;p&gt;Key Implementation Detail:&lt;br&gt;
For Vercel's ENVIRONMENT_UPDATED handler, there's a special consideration: Vercel only allows renaming custom environments on Pro plans, not the standard "Preview", "Production", or "Development" environments. The code includes logic to attempt the rename and gracefully handle cases where it's not supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;While preparing my pull request, I ran into several real-world challenges that highlight what contributing to an active open-source project actually looks like behind the scenes. The first issue was merge conflicts between my branch (attempt-1254) and the target develop branch. In a fast-moving repository with many contributors, this is almost inevitable. I resolved the conflicts by pulling the latest upstream changes and manually reviewing each conflicting section to understand both sides. My goal was to preserve other contributors’ work while ensuring my environment lifecycle handler implementation remained intact. After merging, I tested everything locally to confirm nothing regressed, then committed with a clear explanation of the resolution. It was a reminder that merge conflicts aren’t just mechanical fixes — they’re moments where you have to understand the intent of multiple developers and reconcile them thoughtfully.&lt;br&gt;
The next challenge surfaced during code review. A maintainer questioned my use of any type casts in TypeScript, particularly when accessing integration.id and working with the pendingCleanup metadata array. They pointed out that this could introduce long-term technical debt and suggested using @ts-expect-error instead. That feedback was valuable. While the any casts were originally a temporary measure to unblock a critical bug fix, the reviewer highlighted how @ts-expect-error is self-validating: TypeScript warns you if the suppressed error disappears later, which prevents stale workarounds from lingering in the codebase. I acknowledged the concern, added TODO comments recommending a future typed refactor, and committed to either replacing any with @ts-expect-error where appropriate or creating a follow-up issue to properly type the metadata structures. This exchange was a great example of how open-source review culture improves not just correctness, but long-term maintainability.&lt;br&gt;
The biggest blocker came from the CI/CD pipeline. My PR was failing two checks: a Snyk security scan and an API validation build. The frustrating part was that I couldn’t reproduce the failures locally. All my local tests passed — dependencies installed cleanly, the build succeeded, and every unit test ran successfully. But Snyk repeatedly failed in CI with errors related to workspace handling and package manager compatibility. The CLI didn’t fully support the project’s pnpm workspace setup in the same way the CI environment did, and I couldn’t access the GitHub Actions logs directly from my development environment. I tried multiple workarounds, including running Snyk through pnpm and downloading the standalone CLI, but nothing mirrored the CI behavior. Even rerunning the workflows with empty commits didn’t resolve the issue.&lt;br&gt;
At that point, I documented everything I had tried in a detailed PR comment and asked for help. A maintainer with admin access reviewed the CI logs and identified the root cause: a vulnerability in a transitive dependency that required updating the root package.json and regenerating the lockfile. They also explained that the project’s Snyk integration uses a specialized workspace configuration that isn’t trivial to replicate locally. This led to a broader discussion about improving CI transparency for external contributors, including the possibility of automatically surfacing relevant log snippets in PR comments. The experience reinforced an important lesson: as an external contributor, you won’t always have full visibility into infrastructure, and clear documentation of your debugging efforts is essential for maintainers to step in effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution (&lt;a href="https://github.com/keyshade-xyz/keyshade/pull/1255" rel="noopener noreferrer"&gt;PR #1255&lt;/a&gt;)
&lt;/h2&gt;

&lt;p&gt;After working through these issues collaboratively, the pull request was successfully merged. The final solution included environment lifecycle event handlers for both Vercel and AWS Lambda, retry logic with exponential backoff, a reconciliation system for eventual consistency, and comprehensive test coverage. All CI checks passed, and review feedback was fully addressed. More importantly, the contribution closed a high-priority bug that had been blocking production adoption of cloud integrations. It was a strong reminder that persistence, careful communication, and respect for the review process are just as important as writing the code itself. Open-source success isn’t just technical, it’s collaborative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Did I Learn?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to read official Typescript documentation to break down confusing code&lt;/li&gt;
&lt;li&gt;How to ask beneficial questions to maintainers&lt;/li&gt;
&lt;li&gt;How to navigate CI/CD failures as an external contributor&lt;/li&gt;
&lt;li&gt;Understanding Event-Driven API Architecture&lt;/li&gt;
&lt;li&gt;Snyk improves security in development&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>ci</category>
      <category>github</category>
    </item>
    <item>
      <title>Keyshade Debugging: Mastering Workspace Role Tests and API Repair</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Sat, 08 Nov 2025 01:06:38 +0000</pubDate>
      <link>https://dev.to/jds64/keyshade-debugging-mastering-workspace-role-tests-and-api-repair-1ok6</link>
      <guid>https://dev.to/jds64/keyshade-debugging-mastering-workspace-role-tests-and-api-repair-1ok6</guid>
      <description>&lt;p&gt;"A good tester prevents problems; a great tester finds them." &lt;strong&gt;Keith Klain, Director of Quality Engineering at KPMG UK&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Four disabled tests. One cryptic comment: 'flaky.' When I dove into Keyshade's workspace role tests, I discovered the failures weren't random at all—they revealed a fundamental misunderstanding of how the API validates permissions.&lt;/p&gt;

&lt;p&gt;Keyshade is a modern solution for secure, centralized credential and configuration management, designed for developers, DevOps engineers, and open-source communities who need to manage secrets without the headache.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Issue (&lt;a href="https://github.com/keyshade-xyz/keyshade/issues/1122" rel="noopener noreferrer"&gt;#1122&lt;/a&gt;)
&lt;/h2&gt;

&lt;p&gt;I decided to continue my open source journey on Keyshade since my blog post going over how I solved &lt;a href="https://www.linkedin.com/pulse/fixing-cli-error-handling-deep-dive-keyshades-bug-johnny-santamaria-au5yc" rel="noopener noreferrer"&gt;issue 998&lt;/a&gt;, which was that the Keyshade command line interface’s “keyshade run” command fails to give a proper error that users can understand. Now I decided to explore Keyshade’s API since I have explored how the CLI functions in its codebase.&lt;/p&gt;

&lt;p&gt;I found an issue where included a bug in workspace role tests, which meant the problem stemmed from how workspace roles were being set up and asserted in tests, some test fixtures and assertions depended on implicit ordering and shared state, which made the tests fragile and allowed timing or state-dependent flakes to slip into CI (Continuous Integration). By making the setup deterministic and isolating role state for each test (explicitly creating the roles and their relationships, avoiding shared mutable fixtures, and tightening assertions to check the actual role assignments rather than relying on incidental data), the tests now reliably exercise the API behavior they were intended to cover.&lt;/p&gt;

&lt;p&gt;The result is a more robust test suite that accurately guards against regressions in role handling for workspaces, reduces CI noise from flaky failures, and gives us greater confidence in future changes to authorization and workspace logic. Here are some key locations in the codebase we found out to be useful in solving the issue:&lt;/p&gt;

&lt;p&gt;The apps/api/src/workspace-role/ folder includes the workspace-role feature module — contains controller, service, DTOs, and possibly unit/integration tests. Focus on business logic, dependency wiring, and any in-module role constants.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Inspect the service methods (assignRole, removeRole, listRoles) for edge-case logic (no-op on duplicate role, default role applied on creation).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check module exports and DI config: ensure the controller gets the expected implementation of the role service (mock vs real) in tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example check: verify if role constants are defined here (e.g., 'admin', 'member', 'viewer') and whether they match values used in DB/entities or frontend.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The apps/api/src/workspace-role/workspace-role-controller.ts file includes the controller / HTTP handlers for workspace-role endpoints — review request DTOs/validation, how the controller calls services, status codes, and permission checks performed at the controller layer.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Example request mapping: POST /workspaces/:id/roles -&amp;gt; controller.assignRole(req.params.id, req.body). Check expected body shape (userId, role).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example validation: if controller uses decorators or a schema, show the required fields (e.g., { userId: string, role: 'admin'|'member' }).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example permission check: controller may call guard like ensureWorkspaceAdmin(currentUser, workspaceId) — confirm where that check happens and what error it throws (403 vs 401).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The apps/api/src/workspace-role/workspace-role/workspace-role.e2e.spec.ts file has end-to-end tests for workspace-role flows — tests that spin up the app (or a test server), seed DB data or fixtures, run real HTTP requests, and assert full-stack behavior and responses.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Test setup/teardown: show DB seeding (create users, create workspace) and cleanup steps to avoid cross-test leakage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example test case: an e2e test that POSTs role assignment and then GETs membership list to assert the new role is present; include expected status codes and response shapes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flakiness checks: look for timeouts, async waits, or hard-coded delays that may make these tests flaky (and thus failing intermittently).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;General Diagram Walkthrough of the Issue&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fdyjrp522srr83v1oolb7.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.amazonaws.com%2Fuploads%2Farticles%2Fdyjrp522srr83v1oolb7.png" alt="General Diagram Walkthrough of the Issue" width="799" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Problem
&lt;/h2&gt;

&lt;p&gt;Here are steps regarding how I reproduced the problem before assessing my plan, implementation, and solution&lt;/p&gt;

&lt;p&gt;Scenario 1 — should not be able to update the workspace role with environments that do not belong to the project Steps to reproduce the behaviour:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create (or use) a workspace W.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Project A in W and capture PROJECT_A_ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Project B in W and capture PROJECT_B_ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Environment A in Project A (ENV_A_ID) and Environment B in Project B (ENV_B_ID).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a workspace role R for Project A that includes only ENV_A_ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure your CLI to target workspace W and authenticate with a token that can update roles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the CLI (or send an API request) to update role R to set environmentIds = [ENV_A_ID, ENV_B_ID].&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Observe the response/error shown by the CLI.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected behaviour&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The API should validate that ENV_B_ID does not belong to Project A and return a 4xx validation error (e.g., 400 or 422) with a clear, formatted message such as: "environment ENV_B_ID does not belong to project PROJECT_A_ID"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The CLI should print that formatted message to the user (no opaque stack trace or generic 500).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The update must be rejected and the role R must remain unchanged (still only ENV_A_ID).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scenario 2 — should be able to add environment access for projects to the role with READ_WORKSPACE, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT authorities Steps to reproduce the behaviour:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create (or use) a workspace W.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Project P in W and ensure it has at least one environment (ENV_P_1, …).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a workspace role R (it may initially have no project-level environment access).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Obtain an auth token that has the authorities: READ_WORKSPACE, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure your CLI to use workspace W and that token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the CLI (or send an API request) to add project-level environment access for Project P to role R (the endpoint that grants project environment access).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Observe the response/error shown by the CLI and then GET the role R to inspect its environment access.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected behaviour&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The API should accept the request and return 200/201 with a clear success body describing the new project-level environment access (or list of environmentIds that were added).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The CLI should print a readable success message and show the role now has access to Project P environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the token lacks any required authority, the API should return 403 Forbidden with a clear formatted error and the CLI should print that message.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scenario 3 — should be able to add environment access for projects to the role (permission variation) Steps to reproduce the behaviour:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create (or use) a workspace W, a Project X with environments, and a role R.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prepare two auth tokens:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* AUTH\_ADMIN: token with UPDATE\_WORKSPACE\_ROLE (and related read authorities).

* AUTH\_RESTRICTED: token without UPDATE\_WORKSPACE\_ROLE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Configure your CLI to use workspace W.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With AUTH_ADMIN, run the CLI command (or API request) to add Project X environment access to role R. Verify success.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With AUTH_RESTRICTED, attempt the same CLI command (or API request) to add Project X environment access to role R. Observe the response/error shown by the CLI.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected behaviour&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;With AUTH_ADMIN: API returns 200/201 and the role R gains access to Project X environments; CLI prints a readable success confirmation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With AUTH_RESTRICTED: API returns 403 Forbidden with a clear formatted message like "missing authority: UPDATE_WORKSPACE_ROLE" and the CLI prints that message; role R must not be modified.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scenario 4 — should not be able to create workspace role where environments do not belong to the project Steps to reproduce the behaviour:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create (or use) a workspace W.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Project A and Project B in W.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Environment B in Project B (ENV_B_ID).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure your CLI to target workspace W with a token that can create roles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the CLI (or POST API) to create a workspace role with:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* projectId = PROJECT\_A\_ID

* environmentIds = \[ENV\_B\_ID\]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Observe the response/error shown by the CLI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected behaviour&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The API must reject the create request with a 4xx validation error (e.g., 400 or 422) and a clear message such as: "environment ENV_B_ID does not belong to project PROJECT_A_ID"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The CLI should print that formatted error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No role should be created.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keyshade’s Codebase&lt;/p&gt;

&lt;p&gt;Here is Keyshade's codebase and API related to this issue, and how would I use the code to diagnose and fix the failing workspace-role tests, displaying the importance of code organization.&lt;/p&gt;

&lt;p&gt;High-level overview (relevant subsystems)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Schema / client shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;packages/schema/src/workspace-role/index.ts defines the WorkspaceRole payload shape (projects + environments arrays, authorities, etc.). That tells you the expected request/response shape for workspace role create/update operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API service layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;apps/api/src/workspace-role/workspace-role.service.ts contains the logic that parses project -&amp;gt; environment inputs and applies DB operations. The key function is parseProjectEnvironmentsDBOperation which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verifies that an environment slug exists and belongs to the provided project&lt;/li&gt;
&lt;li&gt;throws NotFoundException if an environment is not part of the project&lt;/li&gt;
&lt;li&gt;calls authorizationService.authorizeUserAccessToEnvironment to ensure the acting user has READ_ENVIRONMENT on that environment&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Authorization checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apps/api/src/auth/service/authorization.service.ts and apps/api/src/auth/service/authority-checker.service.ts implement the authority checks and higher-level authorization plumbing. authorizationService exposes methods such as authorizeUserAccessToEnvironment and authorizeUserAccessToSecret; these both call into the authority checker and also verify workspace membership.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tests &amp;amp; client usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;packages/api-client/tests/workspace-role.spec.ts contains tests that exercise the workspace-role APIs from a client perspective (create workspace, create project, create environments, then create/update roles).&lt;/li&gt;
&lt;li&gt;apps/platform and controller-instance reference the WorkspaceRoleController from the generated API client; you can use the API client or the CLI to reproduce e2e scenarios.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the tests were failing (what the issue description lists)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The failing/disabled tests are focused on cases where environments provided for a workspace role do not belong to the project, and cases that require multiple authorities (READ_WORKSPACE, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The service code enforces two separate checks:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Ownership check: the environment must belong to the provided project — otherwise parseProjectEnvironmentsDBOperation throws NotFoundException with a message like "Environment X is not part of project Y".

2. Authorization check: after confirming the environment belongs to the project, the service calls authorizationService.authorizeUserAccessToEnvironment which requires the user to have READ\_ENVIRONMENT on that environment (and also checks workspace access).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If tests tried to use an environment that belongs to a different project, the service will intentionally return 404/NotFound before attempting to grant access; tests must assert that behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If tests expected that adding environment access requires specific authorities, the test setup must give the test-user those authorities (or use a workspace-admin) before calling the endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How to use the codebase to reproduce, diagnose, and fix the tests&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce the failing case manually (fastest way to see precise error):&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* Use packages/api-client/tests/workspace-role.spec.ts as a reference for how tests set up the workspace/project/environment/role chain.

* From an integration/e2e test or an API client, create:

    * workspace A

    * project P1 inside workspace A

    * project P2 inside workspace A

    * environment E1 in P1 and environment E2 in P2

* Then attempt to create/update a workspace role that includes project P1 but lists environment E2 (an environment that belongs to P2).

* Observe the response: workspace-role.service.ts is expected to detect the mismatch and return a 404 Not Found with message "Environment ... is not part of project ...".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Check the authority path:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* If you provide a correct project-&amp;amp;gt;environment mapping, but the requestor lacks READ\_ENVIRONMENT (or other expected authorities like UPDATE\_WORKSPACE\_ROLE), authorizationService.authorizeUserAccessToEnvironment will throw Unauthorized/Forbidden. Ensure test user has the required authorities when the test expects success.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Fix the tests (common issues &amp;amp; fixes):&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* If a test was disabled because it used an environment from a different project but expected a different error (or success), restore the test and:

    * Either adjust the test to expect NotFound (because parseProjectEnvironmentsDBOperation intentionally rejects cross-project environments).

    * Or adjust test fixtures so the environment is created in the same project as the project entry in the request.

* For authority-related tests:

    * Make sure the test user has the necessary authorities before calling the API. Grant a role to the test user with READ\_WORKSPACE, UPDATE\_WORKSPACE\_ROLE, READ\_PROJECT, READ\_ENVIRONMENT when the test expects role creation/update to succeed.

    * If a test is verifying denial paths (should not be able to create/update), ensure the user has fewer/missing authorities and assert the API returns Unauthorized/Forbidden appropriately.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Where to change code if the behavior is incorrect:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* If you decide the server should validate authority first (instead of ownership first), modify parseProjectEnvironmentsDBOperation ordering; but be careful: the current behavior (check membership/ownership first, then authorization) is consistent with tests that expect NotFound for cross-project envs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;If messages or status codes are inconsistent with tests, adjust the thrown exceptions in parseProjectEnvironmentsDBOperation (NotFoundException) or in authorizationService (Unauthorized/Forbidden) to align with the test expectations — but prefer changing tests to reflect correct domain behavior unless an API bug is proven.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Useful files to inspect or instrument:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* packages/schema/src/workspace-role/index.ts (payload schema)

* apps/api/src/workspace-role/workspace-role.service.ts (parseProjectEnvironmentsDBOperation and create/update logic)

* apps/api/src/auth/service/authorization.service.ts (authorizeUserAccessToEnvironment)

* apps/api/src/auth/service/authority-checker.service.ts (lower-level authority checks)

* packages/api-client/tests/workspace-role.spec.ts (test setup and expected assertions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Concrete actionable checklist to resolve the issue in the repository&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Re-enable the disabled tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For each disabled test, verify which of the two server behaviors it is validating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ownership validation (environment belongs to project) → expect 404 Not Found. Ensure test builds a request with a mis-matched environment and asserts a 404 and the message text if desired.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authorization validation (user needs the listed authorities) → ensure test grants the required authorities to the requester when expecting success; when expecting failure, ensure the user lacks at least one required authority and assert Unauthorized/Forbidden.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the tests locally (or in CI):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workspace-role e2e test file: packages/api-client/tests/workspace-role.spec.ts is a good starting point.&lt;/li&gt;
&lt;li&gt;Add logging in workspace-role.service.ts parseProjectEnvironmentsDBOperation if you need to trace why an environment was rejected.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the test failure shows unexpected status codes or messages, adjust the service's thrown exceptions to match the documented API or update the tests to assert the documented behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you change server code, add unit tests in apps/api for parseProjectEnvironmentsDBOperation to cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;environment not in project → NotFound&lt;/li&gt;
&lt;li&gt;environment in project but user lacks READ_ENVIRONMENT → Unauthorized/Forbidden&lt;/li&gt;
&lt;li&gt;environment in project and user has authority → success path produces correct DB ops&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Short example of the expected behavior (mapping to the tests listed in the issue)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;"should not be able to update the workspace role with environments that do not belong to the project":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call update role with project P1 + env from P2 → server should return 404 Not Found (environment not part of project).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;"should be able to add environment access for projects to the role with READ_WORKSPACE, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT authorities":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure requestor has those authorities (assign a role in test fixture) → call create/update and assert success and that the role contains the project-&amp;gt;environment mapping in response.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;"should be able to add environment access for projects to the role":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same as above but likely using normal granted authorities; ensure fixtures grant appropriate permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;"should not be able to create workspace role where environments do not belong to the project":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create role payload with mismatched environment → expect 404 Not Found.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where to look in code to make changes or confirm behavior&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;parseProjectEnvironmentsDBOperation in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apps/api/src/workspace-role/workspace-role.service.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;authorization plumbing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apps/api/src/auth/service/authorization.service.ts&lt;/li&gt;
&lt;li&gt;apps/api/src/auth/service/authority-checker.service.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;request/response schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;packages/schema/src/workspace-role/index.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;packages/api-client/tests/workspace-role.spec.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Case
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Goal: Create/update a workspace role that grants access to specific projects and, within those projects, specific environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Client payload shape (what the API expects)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. name, description, authorities (array), projectEnvironments: \[{ projectSlug, environmentSlugs: \[slug\] }\]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Server behavior (what the code enforces)&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. For each projectEnvironments entry, resolve projectSlug -&amp;amp;gt; projectId (getProjectSlugToIdMap).

2. For each environmentSlug in environmentSlugs:

    * Verify that environmentSlug exists AND belongs to that project (prisma.environment.findFirst where slug + projectId). If not, throw NotFoundException (404) with a body "Environment not found" / "Environment X is not part of project Y".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Authorize the acting user has READ_ENVIRONMENT for that environment (authorizationService.authorizeUserAccessToEnvironment). This also verifies workspace-level access and permitted authorities (via authorityCheckerService).&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Finally, upsert projectWorkspaceRoleAssociation rows connecting roleId&amp;lt;&amp;gt;projectId and connect the environment slugs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why the workspace-role tests were failing&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The failing/disabled tests covered two distinct types of validation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ownership validation: environment must belong to the project in the request. If not, server should reject with 404 (Not Found).&lt;/li&gt;
&lt;li&gt;Authorization validation: even if environment belongs to project, the acting user must have proper authorities (READ_ENVIRONMENT, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_WORKSPACE_ROLE) to add environment access to a role.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Typical causes for failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests that assert 404 were previously commented out or used incorrect fixtures (e.g., environment not created, or created but in same project), so the server behavior didn't match test expectations.&lt;/li&gt;
&lt;li&gt;Tests that assert successful updates weren't ensuring the test actor had the required authorities (so server returned Unauthorized/401/403 instead of 200).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;My PR re-enabled those tests and fixed fixtures by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating environments in the correct projects (dev, stage).&lt;/li&gt;
&lt;li&gt;Explicitly setting the membership role authorities (e.g., update the workspace member role to include READ_ENVIRONMENT and UPDATE_WORKSPACE_ROLE) when the test expected updates to succeed.&lt;/li&gt;
&lt;li&gt;Using consistent test header 'x-e2e-user-email' (alice.email) for requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Walkthrough of the two failing test categories and what to change&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Environment doesn't belong to the project" (ownership)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Server side: parseProjectEnvironmentsDBOperation checks project membership:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs prisma.environment.findFirst({ where: { slug: environmentSlug, projectId } })&lt;/li&gt;
&lt;li&gt;If not found, it throws: throw new NotFoundException(constructErrorBody('Environment not found', Environment ${environmentSlug} is not part of project ${pe.projectSlug}))&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tests expecting 404 must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a workspace and two projects in that workspace.&lt;/li&gt;
&lt;li&gt;Ensure the environment used in the request was created in THE OTHER project, or not created at all (but prefer creating in other project to avoid false positives).&lt;/li&gt;
&lt;li&gt;Make a POST/PUT with projectSlug set to projectA and environmentSlugs containing an environment slug that actually belongs to projectB.&lt;/li&gt;
&lt;li&gt;Assert statusCode === 404.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;"Adding environment access requires specific authorities" (authorization)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Server side: after ownership check, parseProjectEnvironmentsDBOperation calls: await this.authorizationService.authorizeUserAccessToEnvironment({ user, slug: environmentSlug, authorities: [Authority.READ_ENVIRONMENT] })&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;That code path will call authorityCheckerService.checkAuthorityOverEnvironment which checks the acting user's permitted authorities for that environment (it can be from project-level or workspace-level roles).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tests expecting success must ensure the acting user has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;READ_WORKSPACE (if required by the flow), UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT — in practice, the tests can mutate an existing workspace role for the test user to include these authorities and also connect appropriate project/environment associations so the user has project-scoped authority.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exact test changes — enable one disabled test and adjust fixture&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Below is a concrete enabled test snippet for packages/api-client/tests/workspace-role.spec.ts that re-enables the “should not be able to create workspace role where environments do not belong to the project” scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This patch enables the test and makes the fixture explicit: create two projects, create an environment in projectB, then attempt to create a workspace role that references projectA + the environment slug from projectB. The test asserts 404.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;packages/api-client/tests/workspace-role.spec.ts&lt;/p&gt;

&lt;p&gt;// (This is the test snippet to add/enable within the existing test suite)&lt;/p&gt;

&lt;p&gt;it('should not be able to create workspace role where environments do not belong to the project', async () =&amp;gt; {&lt;/p&gt;

&lt;p&gt;// Precondition: two projects exist in the workspace&lt;/p&gt;

&lt;p&gt;// projects[0] -&amp;gt; projectA, projects[1] -&amp;gt; projectB (test fixture established earlier in the file)&lt;/p&gt;

&lt;p&gt;// Create an environment in projectB that should NOT be valid for projectA&lt;/p&gt;

&lt;p&gt;await prisma.environment.create({&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Notes about placement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put this enabled test near the other workspace-role create tests after the workspace + project fixtures are created and available.&lt;/li&gt;
&lt;li&gt;Ensure projects variable references are the ones created during beforeAll/beforeEach steps in that test file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A directly analogous enabled e2e test (what PR 1156 did)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PR 1156 enabled very similar tests in apps/api/src/workspace-role/workspace-role.e2e.spec.ts — it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created dev/stage environments in the proper projects,&lt;/li&gt;
&lt;li&gt;Updated the Member workspace role to add project associations and required authorities for the actor,&lt;/li&gt;
&lt;li&gt;Re-enabled the previously commented tests and asserted status codes 200 or 404 as appropriate.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small server-side unit test to lock in parseProjectEnvironmentsDBOperation behavior&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I recommend adding a unit test that calls parseProjectEnvironmentsDBOperation (or exercises the public create/update API) with a project-environment mismatch and asserts a NotFoundException is thrown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example test file (unit) to confirm rejection for cross-project environment:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;apps/api/src/workspace-role/workspace-role.service.spec.ts&lt;/p&gt;

&lt;p&gt;import { Test, TestingModule } from '@nestjs/testing'&lt;/p&gt;

&lt;p&gt;import { WorkspaceRoleService } from './workspace-role.service'&lt;/p&gt;

&lt;p&gt;import { AuthorizationService } from '@/auth/service/authorization.service'&lt;/p&gt;

&lt;p&gt;import { PrismaService } from '@/prisma/prisma.service'&lt;/p&gt;

&lt;p&gt;import { NotFoundException } from '@nestjs/common'&lt;/p&gt;

&lt;p&gt;Proposed server code changes — minimal / optional&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The current server code in parseProjectEnvironmentsDBOperation is already correct about ownership-first validation (it checks slug + projectId before calling authorizeUserAccessToEnvironment). The PR you provided does not require a behavior change on the server — tests should reflect the server semantics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you did want to change server behavior, the main two options are:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Keep current semantics (ownership check first, then authorization). Pros: deterministic 404 for cross-project envs; avoids running authorization queries for environment slugs that don't belong to the provided project. Cons: reveals that the environment slug exists in a different project (server replies 404 with message referencing project mismatch).

2. Run authorization first (i.e., check the caller's permissions to read the environment before checking project membership). Pros: could collapse some error channels and avoid disclosing cross-project existence if the caller lacks permission. Cons: often you still need to fetch the environment by slug (so you still learn about its existence) — and the authorization check typically requires fetching the environment anyway to know project/workspace id. It also changes the status codes returned for the cross-project case (may return 401 instead of 404), which is a breaking API semantics change for clients that currently rely on 404 in that case.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Recommendation: do NOT change the ordering of checks unless there's a clear security/privacy reason. The current behavior (404 on ownership mismatch) matches the tests in PR 1156 and is explicit. Prefer to fix test fixtures and grant/revoke authorities in test setup rather than altering server semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the PR's approach (what you already did in PR 1156) is correct&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You re-enabled tests and fixed fixtures instead of changing server semantics. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created missing environments in the appropriate projects before calling the API.&lt;/li&gt;
&lt;li&gt;Updated the test user/role authorities where tests expected success.&lt;/li&gt;
&lt;li&gt;Asserted the correct status codes: 404 for ownership mismatch and 200 for successful updates when authorities exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is minimal and non-breaking.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trade-offs if you change server behavior (ownership vs authorization ordering)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Changing the order to authorize first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;May return 401/403 instead of 404 for the cross-project environment case — tests and clients must be updated.&lt;/li&gt;
&lt;li&gt;Could obscure whether an environment exists in another project (slight security benefit).&lt;/li&gt;
&lt;li&gt;Could introduce additional database lookups if you need to check membership as well; may not yield significant performance gain.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Keeping ownership-first (current):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear, deterministic 404 response for mismatched environment ownership.&lt;/li&gt;
&lt;li&gt;Tests remain simple: assert 404 for the cross-project case.&lt;/li&gt;
&lt;li&gt;Slight information disclosure (you show that environment exists but in a different project), which may be acceptable in an internal system like Keyshade.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Summary of exact artifacts to change (concrete)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add/enable the test in packages/api-client/tests/workspace-role.spec.ts as above (or in apps/api/src/workspace-role/workspace-role.e2e.spec.ts — PR 1156 already did the same).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a small unit test in apps/api/src/workspace-role/workspace-role.service.spec.ts to lock in behavior of parseProjectEnvironmentsDBOperation for cross-project envs (example above).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensure any tests that expect successful updates explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create the environments in the projects they want to reference&lt;/li&gt;
&lt;li&gt;grant the test actor role(s) the required authorities (UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT) before calling create/update&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;I reviewed PR #1156 and walked the tests and service code. Below I list concrete challenges we encountered while making this PR and practical mitigations. I narrate what I checked and what I'd recommend next.&lt;/p&gt;

&lt;p&gt;What I did (quick recap)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Inspected the workspace-role service logic (parseProjectEnvironmentsDBOperation) and the authorization flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reviewed the test files that were re-enabled/modified in the PR (workspace-role e2e/spec files).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Confirmed the PR fixes were basically test-fixture and authority-setup issues (creating environments in right projects, granting the test user required authorities), not major server-behavior changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verified the PR merged successfully but noted a set of practical challenges that often show up in this work.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Challenges encountered (with examples and mitigations)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test fixture brittleness and DB state coupling&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: E2E tests depend on precise workspace/project/environment setup and roles. Small differences in fixture creation order or leftover DB rows cause flakiness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: A test assumed "production" env exists in projectA but CI used a different test run that already had production in projectB -&amp;gt; unexpected 200 instead of 404.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always create and assert fixtures in test setup (do not assume names).&lt;/li&gt;
&lt;li&gt;Use randomized slugs (e.g., slug-&amp;lt;timestamp&amp;gt;-&amp;lt;random&amp;gt;) to avoid collisions across parallel CI jobs.&lt;/li&gt;
&lt;li&gt;Add teardown/cleanup that removes created rows or run tests inside isolated databases/transactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Authority/permission complexity in tests&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Tests that check success paths require several authorities (READ_WORKSPACE, UPDATE_WORKSPACE_ROLE, READ_PROJECT, READ_ENVIRONMENT). If the test doesn't explicitly grant them, the server returns Unauthorized and the test fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: Test enabling environment access but not updating the Member role to include UPDATE_WORKSPACE_ROLE.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicitly set up the actor’s roles/authorities per test case.&lt;/li&gt;
&lt;li&gt;Add helper functions in tests for "grantMemberAuthoritiesForEnvironmentAccess" to make fixtures clear.&lt;/li&gt;
&lt;li&gt;Keep negative/positive authority tests separate and deterministic.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Merge conflicts (simple but annoying)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Enabling tests and modifying the same test files or fixtures on different branches led to conflicts (e.g., two branches each uncommented different blocks of tests).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: Two branches changed apps/api/src/workspace-role/workspace-role.e2e.spec.ts: one enabled a test, another refactored a beforeEach. Merge resulted in conflict markers around large commented blocks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rebase frequently on the develop branch to reduce long-lived divergences.&lt;/li&gt;
&lt;li&gt;Split large test-enabling changes into smaller PRs (enable + fix fixtures + authority wiring).&lt;/li&gt;
&lt;li&gt;When resolving conflicts, run the full test suite locally before pushing the merge commit.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Creating temporary test files or helper tests "to be removed later" causing issues&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Developers often create throwaway tests / temporary files to debug behavior and forget to remove them. These can be picked up by CI and cause nondeterministic runs or duplicate test names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: Added apps/api/src/workspace-role/tmp-debug.spec.ts to iterate quickly, later removed locally but remained in another branch → CI fails or duplicate DB setup occurs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid committing throwaway test files. If necessary, keep them in local-only branches or protect with a naming convention excluded from CI.&lt;/li&gt;
&lt;li&gt;Add CI test discovery rules to ignore test files matching tmp-*.spec.ts, if you must have temporary tests.&lt;/li&gt;
&lt;li&gt;Review diffs carefully before commit and run git status to ensure no debug/test files are included.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Test duplication across packages (api-client vs apps/api)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Similar workspace-role tests existed in both packages/api-client/tests and apps/api e2e test files; enabling one copy and forgetting to update the other caused inconsistent behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: One test in packages/api-client used the API client and different headers than the e2e server-side test; runtime differences led to mismatched assertions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consolidate canonical e2e tests in one place (prefer apps/api e2e that hits the server directly), keep api-client tests focused on client behavior.&lt;/li&gt;
&lt;li&gt;If duplication is unavoidable, keep test expectations aligned and use shared helpers/mocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Race conditions in setup/cleanup (parallel tests)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Creating projects/environments concurrently across parallelized test jobs (or even within same job) created timing-based failures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make tests idempotent and isolate data per test.&lt;/li&gt;
&lt;li&gt;Use unique slugs per test run.&lt;/li&gt;
&lt;li&gt;Consider serializing tests that share DB resources or use test DB instances per worker.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Error/status-code semantics and client expectations&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Tests rely on specific status codes (404 vs 401). If server-side code ordering changes (ownership check vs authorization check) the codes change and many tests break.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: If you move authorization first, a cross-project environment case might return 401 instead of 404.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep server semantics stable; if a change is required, update all tests and document the change.&lt;/li&gt;
&lt;li&gt;Add unit tests that assert the contract (e.g., cross-project environment -&amp;gt; NotFound).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;CI flakiness from environment variables and headers&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: Some tests rely on headers like 'x-e2e-user-email'. If CI environment variable email differs or parallel runs reuse the same email, tests can mutate shared state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a test-specific email per run (e.g., alice+&amp;lt;run-id&amp;gt;@keyshade.xyz).&lt;/li&gt;
&lt;li&gt;Make test harness read and propagate headers from the same fixture source.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Commit noise and history (multiple tiny commits while fixing test)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: WIP commits to enable tests, debug, revert, add files, remove files created a messy branch and bigger diffs, increasing review overhead and merge conflicts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Squash local fixup commits before opening PR or use interactive rebase to tidy commits.&lt;/li&gt;
&lt;li&gt;Keep PRs small and focused: one PR for enabling tests and fixtures, another for refactors.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Accidental API contract changes vs test fixes&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Problem: When tests fail, there’s a temptation to change server code to make tests pass instead of fixing tests. That can mask real bugs or break clients.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer fixing tests/fixtures if the server behavior matches documented contract.&lt;/li&gt;
&lt;li&gt;If server needs change, write unit tests to cover new contract and get reviewer approval.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete, practical steps I recommend next&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Add a small checklist to PR templates for test-enabling PRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm unique slugs in fixtures&lt;/li&gt;
&lt;li&gt;Confirm actor authorities are set explicitly&lt;/li&gt;
&lt;li&gt;Run full test suite locally (or run the specific e2e group) before pushing&lt;/li&gt;
&lt;li&gt;Clean up any temporary test files&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a helper in tests for deterministic unique slugs and user emails to avoid collisions in CI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consider adding a lint/CI guard that fails the build when "tmp-*.spec.ts" or "debug" test files are found in the commit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you expect future merges to touch the same big e2e files, coordinate branches or split PRs (e.g., create "enable tests" PR and a follow-up "refactor fixtures" PR).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Here is a summarized plan of my solution:&lt;/p&gt;

&lt;p&gt;Lock the contract with unit tests by adding unit tests that assert parseProjectEnvironmentsDBOperation rejects cross-project environment slugs with NotFound. This prevents accidental server reordering from changing semantics. Make tests deterministic by ensuring every test explicitly creates its fixtures (workspace, project, environment) and set unique slugs per run, which will explicitly grant the required authorities to the test actor in success-path tests.&lt;/p&gt;

&lt;p&gt;Prevent accidental temporary test files by adding the pre-commit hook above and a CI check to refuse pushes that include tmp/debug test files. Keep good merge strategies and PR hygiene by keeping PRs small and focused (enable tests separately from refactors). Make sure to rebase frequently and squash debug commits and add a PR/TODO checklist that requires: unique slugs, explicit authority setup, no temporary files, and local e2e run before requesting review. Consolidate test suites by deciding canonical location for workspace-role e2e tests (prefer apps/api e2e) and remove duplicates, or centralize test helpers. Here is my final PR solving the issue over a month:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/keyshade-xyz/keyshade/pull/1156" rel="noopener noreferrer"&gt;PR #1156&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In conclusion, Keyshade’s clean, well-structured codebase and deterministic tests make development faster, safer, and more collaborative. Clear intent, consistent conventions, and reliable CI build trust between maintainers and contributors—enabling focused changes, quicker reviews, and confident releases.&lt;/p&gt;

</description>
      <category>api</category>
      <category>devbugsmash</category>
      <category>typescript</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Fixing CLI Error Handling: A Deep Dive into Keyshade's WebSocket Communication Bug</title>
      <dc:creator>Johnny Santamaria</dc:creator>
      <pubDate>Tue, 12 Aug 2025 15:00:00 +0000</pubDate>
      <link>https://dev.to/jds64/fixing-cli-error-handling-a-deep-dive-into-keyshades-websocket-communication-bug-489m</link>
      <guid>https://dev.to/jds64/fixing-cli-error-handling-a-deep-dive-into-keyshades-websocket-communication-bug-489m</guid>
      <description>&lt;p&gt;“If you build software, every error message is marketing” - &lt;strong&gt;Jason Fried, &lt;a href="https://www.goodreads.com/work/quotes/6928276" rel="noopener noreferrer"&gt;Rework&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In today’s fast-paced software development landscape, managing credentials and sensitive configurations securely is more critical—and more challenging—than ever. Dispersed secrets, manual processes, and unplanned storage not only slow teams down but also increase the risk of breaches and operational errors. &lt;strong&gt;Keyshade&lt;/strong&gt; addresses these challenges head-on with a centralized, automated approach to credential and configuration governance. Purpose-built for engineering teams, DevOps practitioners, startups, and open-source maintainers. Keyshade streamlines secret management while enhancing security, collaboration, and efficiency.&lt;/p&gt;

&lt;p&gt;Keyshade is an advanced platform for centralized credential and configuration governance, designed to safeguard sensitive data, eliminate human error, and streamline operational complexity. By automating secret management, it fortifies security posture, amplifies cross-team collaboration, and liberates developers from the inefficiencies of fragmented secret storage. Its primary constituency—engineering teams, DevOps practitioners, early-stage startups, and open-source maintainers—adopts Keyshade to mitigate secret proliferation, minimize security liabilities, and embed robust secret orchestration seamlessly into their development pipelines. Users gravitate toward Keyshade for its ability to render secret management secure, intuitive, and collaborative, without imposing friction upon established workflows or toolchains. Below is a flow of control diagram discussing how users would typically use the product:&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.amazonaws.com%2Fuploads%2Farticles%2Fyn04j9jpb4md0z634g4o.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.amazonaws.com%2Fuploads%2Farticles%2Fyn04j9jpb4md0z634g4o.png" alt=" " width="800" height="858"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user can send commands via keyshades terminal or on keyshades web app
&lt;/li&gt;
&lt;li&gt;On the web, the user can sign up, and initialize projects within workspaces

&lt;ul&gt;
&lt;li&gt;Users can do the same in the &lt;a href="https://docs.keyshade.xyz/getting-started/setting-up-your-profile" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Once the a project is made a http request is made to the API websocket service
&lt;/li&gt;
&lt;li&gt;The websocket service then retrieves that data to put in the Keyshade’s database to save the data for future use&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keyshade is currently in alpha and not live so there really are not any users as of posting this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issue
&lt;/h2&gt;

&lt;p&gt;I partnered with a member of the Computing Talent Initiative with the task of solving &lt;a href="https://github.com/keyshade-xyz/keyshade/issues/998" rel="noopener noreferrer"&gt;issue 998&lt;/a&gt; in Keyshade for a Summer Open Source Experience sponsored by . The main problem we noticed was that the Keyshade command line interface’s “keyshade run” command fails to give a proper error that users can understand. We would want to extract an error message and output it to the user. The issue is a bug that seems urgent to solve right now for users to understand errors they might make when working with Keyshade’s CLI, which is the main feature of Keyshade. Here are some key locations in the codebase we found out to be useful in solving the issue:&lt;/p&gt;

&lt;h3&gt;
  
  
  run.command.ts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Handles CLI logic for running commands, including WebSocket client interactions
&lt;/li&gt;
&lt;li&gt;Within a CLI folder&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  change-notifier.socket.ts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Contains the handleRegister method, which processes the register-client-app event
&lt;/li&gt;
&lt;li&gt;Handles authentication, authorization, and client registration logic
&lt;/li&gt;
&lt;li&gt;Emits the client-registered response to the client and logs errors
&lt;/li&gt;
&lt;li&gt;Within an API folder&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  run.types.d.ts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;ClientRegisteredResponse describes the shape of the response sent by the server after registration (success, message)
&lt;/li&gt;
&lt;li&gt;Ensures type safety for client-side code handling server responses
&lt;/li&gt;
&lt;li&gt;Within a CLI folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are steps regarding how we reproduced the problem before finding the important locations in the codebase&lt;/p&gt;

&lt;p&gt;Steps to reproduce the behaviour:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a project on keyshade
&lt;/li&gt;
&lt;li&gt;Configure your CLI to tap into this project
&lt;/li&gt;
&lt;li&gt;Delete the project
&lt;/li&gt;
&lt;li&gt;Use keyshade run &amp;lt;&amp;gt; command
&lt;/li&gt;
&lt;li&gt;See the error&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Expected behaviour
&lt;/h2&gt;

&lt;p&gt;We would want the CLI to display what actually went wrong. The API should send the formatted message to the CLI. The CLI should print the error.&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.amazonaws.com%2Fuploads%2Farticles%2Fis4b0st0wzgdaml8xn3a.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.amazonaws.com%2Fuploads%2Farticles%2Fis4b0st0wzgdaml8xn3a.png" alt="Reproduced issue" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyshade’s Codebase
&lt;/h2&gt;

&lt;p&gt;Before delving into the problem itself, and after pinpointing the critical components, we took a step back to develop a comprehensive understanding of the codebase’s architecture. Keyshade’s backend is architected with &lt;strong&gt;NestJS&lt;/strong&gt; (a progressive Node.js framework) and &lt;strong&gt;TypeScript&lt;/strong&gt;, leveraging &lt;strong&gt;Prisma&lt;/strong&gt; as its database ORM, interfacing with a &lt;strong&gt;PostgreSQL&lt;/strong&gt; relational database. It also integrates &lt;strong&gt;Redis&lt;/strong&gt; for high-performance caching and publish/subscribe messaging, as well as &lt;strong&gt;Socket.IO&lt;/strong&gt; for real-time, bidirectional communication between its command line interface and application programming interface.&lt;/p&gt;

&lt;p&gt;On the frontend, the stack comprises &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, &lt;strong&gt;JavaScript&lt;/strong&gt;, and &lt;strong&gt;Next.js&lt;/strong&gt;, presumably to power a performant and SEO-friendly web interface. Development workflows are streamlined through &lt;strong&gt;Nx&lt;/strong&gt; (for monorepo orchestration), &lt;strong&gt;ESLint&lt;/strong&gt; and &lt;strong&gt;Prettier&lt;/strong&gt; (for code quality and style enforcement), alongside the TypeScript compiler for rigorous static type checking.&lt;/p&gt;

&lt;p&gt;For security, Keyshade employs &lt;strong&gt;Elliptic Curve Cryptography&lt;/strong&gt; for robust public-key encryption, &lt;strong&gt;JWT&lt;/strong&gt; for token-based authentication, and API key authentication for service-to-service communication. The infrastructure is containerized via &lt;strong&gt;Docker&lt;/strong&gt; and designed with cloud deployment in mind. A high-level system diagram encapsulates the overall architecture of the codebase as follows:&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.amazonaws.com%2Fuploads%2Farticles%2Fnx6hcxpex283fibc2l77.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.amazonaws.com%2Fuploads%2Farticles%2Fnx6hcxpex283fibc2l77.png" alt="Keyshade system diagram" width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a use case, using the components in the diagram to give you a better understanding of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Case: Developer Fetching Live Configuration Updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Developer Initiates CLI Command
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keyshade run \--workspace my-company \--project web-app \--environment production

What happens: The developer runs the CLI command to start their application with live configuration updates from Keyshade.

The CLI documentation can be found [here](https://docs.keyshade.xyz/getting-started/installing-the-cli)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 2: CLI Processes Command (run.command.ts)
&lt;/h3&gt;

&lt;p&gt;The run.command.ts file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses the command arguments (workspace, project, environment)
&lt;/li&gt;
&lt;li&gt;Reads the local configuration file to get API credentials
&lt;/li&gt;
&lt;li&gt;Validates the required parameters using types from run.types.d.ts
&lt;/li&gt;
&lt;li&gt;Prepares to establish WebSocket connection for live updates&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 3: WebSocket Connection Establishment
&lt;/h3&gt;

&lt;p&gt;The CLI creates a websocket connection:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;const socket = io('ws://api.keyshade.xyz/change-notifier')&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What happens: The client connects to the NestJS backend's WebSocket gateway (change-notifier.socket.ts).&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Client Registration Request
&lt;/h3&gt;

&lt;p&gt;The CLI emits the registration event:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;socket.emit('register-client-app', {  workspaceSlug: 'my-company',  projectSlug: 'web-app',   environmentSlug: 'production'});&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What happens: This triggers the handleRegister method in &lt;a href="http://change-notifier.socket.ts" rel="noopener noreferrer"&gt;change-notifier.socket.ts&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5a: Authentication
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const userContext \= await this.extractAndValidateUser(client)  
Extracts API key from WebSocket headers (x-keyshade-token)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Queries PostgreSQL database via Prisma to validate the API key
&lt;/li&gt;
&lt;li&gt;Verifies user account is active&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5b: Authorization checks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;await this.authorizationService.authorizeUserAccessToWorkspace({...})await this.authorizationService.authorizeUserAccessToProject({...})await this.authorizationService.authorizeUserAccessToEnvironment({...})&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Checks if user has READ_WORKSPACE, READ_VARIABLE, READ_SECRET permissions&lt;br&gt;&lt;br&gt;
Validates access to the specific project&lt;br&gt;&lt;br&gt;
Confirms environment access rights&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5c: Client Registration
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await this.addClientToEnvironment(client, environment.id)  
Creates entry in PostgreSQL mapping socket ID to environment ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Adds client to Redis set for real-time notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 6: Success Response
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What happens: Server sends confirmation back to CLI using the ClientRegisteredResponse interface from run.types.d.ts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 7: Configuration Fetching
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;After successful registration:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CLI fetches initial configuration values (secrets, variables)
&lt;/li&gt;
&lt;li&gt;Application starts with these environment variables
&lt;/li&gt;
&lt;li&gt;CLI continues listening for real-time updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 8: Real-Time Updates (Ongoing)
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Another developer updates a secret value through the web interface.  

await this.redisSubscriber.subscribe(CHANGE\_NOTIFIER\_RSC, this.notifyConfigurationUpdate.bind(this))  
The web interface publishes change to Redis channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;All API instances receive the notification&lt;/p&gt;

&lt;p&gt;this.server.to(clientId).emit('configuration-updated', data)&lt;br&gt;&lt;br&gt;
Server identifies all clients registered for that environment&lt;br&gt;&lt;br&gt;
Sends update notification to active CLI instances&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLI receives the configuration-updated event&lt;br&gt;&lt;br&gt;
Updates local environment variables&lt;br&gt;&lt;br&gt;
Application continues running with new values&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;One specific technical challenge I encountered while attempting to solve issue 998 was figuring out why my changes in change-notifier.socket.ts were not being reflected when I tested them through the CLI. Despite updating the logic inside the Socket.IO handlers, the CLI behavior remained unchanged, and it was unclear whether the API changes were even being registered. Here is an attempt to solve this challenge and other challenges I encountered.&lt;/p&gt;

&lt;p&gt;So I tried debugging the issue after reproducing the issue and found the debug information for keyshade issue 998&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;@johnny603 ➜ /workspaces/keyshade (teamAttempt/#998) $ DEBUG=* keyshade run "echo 'test'"INFO: Checking API key validity...INFO: API key is valid!INFO: Connecting to socket...socket.io-client:url parse wss://api.keyshade.xyz/change-notifier +0mssocket.io-client new io instance for wss://api.keyshade.xyz/change-notifier +0mssocket.io-client:manager readyState closed +0mssocket.io-client:manager opening wss://api.keyshade.xyz/change-notifier +0msengine.io-client:socket creating transport "websocket" +0msengine.io-client:socket options: {"path":"/socket.io/","agent":false,"withCredentials":false,"upgrade":true,"timestampParam":"t","rememberUpgrade":false,"addTrailingSlash":true,"rejectUnauthorized":true,"perMessageDeflate":{"threshold":1024},"transportOptions":{},"closeOnBeforeunload":false,"autoConnect":false,"extraHeaders":{"x-keyshade-token":"ks_a3adff2a9db8bf3c42fc817880ac4125983b83c171f0d04d"},"transports":[null],"hostname":"api.keyshade.xyz","secure":true,"port":"443","query":{"EIO":4,"transport":"websocket"},"socket":{"binaryType":"nodebuffer","writeBuffer":[],"_prevBufferLen":0,"_pingInterval":-1,"_pingTimeout":-1,"_maxPayload":-1,"_pingTimeoutTime":null,"secure":true,"hostname":"api.keyshade.xyz","port":"443","transports":["websocket"],"_transportsByName":{},"opts":{"path":"/socket.io/","agent":false,"withCredentials":false,"upgrade":true,"timestampParam":"t","rememberUpgrade":false,"addTrailingSlash":true,"rejectUnauthorized":true,"perMessageDeflate":{"threshold":1024},"transportOptions":{},"closeOnBeforeunload":false,"autoConnect":false,"extraHeaders":{"x-keyshade-token":"ks_a3adff2a9db8bf3c42fc817880ac4125983b83c171f0d04d"},"transports":[null],"hostname":"api.keyshade.xyz","secure":true,"port":"443"},"readyState":"opening"}} +0msengine.io-client:socket setting transport websocket +6mssocket.io-client:manager connect attempt will timeout after 20000 +8msengine.io-client:socket socket receive: type "open", data "{"sid":"qxJTaYpvOE4a6kIaAAB8","upgrades":[],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}" +756msengine.io-client:socket socket open +0mssocket.io-client:manager open +755mssocket.io-client:manager cleanup +0mssocket.io-client:socket transport is open - connecting +0mssocket.io-client:manager writing packet {"type":0,"nsp":"/change-notifier"} +1mssocket.io-parser encoding packet {"type":0,"nsp":"/change-notifier"} +0mssocket.io-parser encoded {"type":0,"nsp":"/change-notifier"} as 0/change-notifier, +0msengine.io-client:socket flushing 1 packets in socket +1msengine.io-client:socket starting upgrade probes +1msengine.io-client:socket socket receive: type "message", data "0/change-notifier,{"sid":"OpbhAQ0UYcfklhBoAAB9"}" +249mssocket.io-parser decoded 0/change-notifier,{"sid":"OpbhAQ0UYcfklhBoAAB9"} as {"type":0,"nsp":"/change-notifier","data":{"sid":"OpbhAQ0UYcfklhBoAAB9"}} +251mssocket.io-client:socket socket connected with id OpbhAQ0UYcfklhBoAAB9 +251mssocket.io-client:manager writing packet {"type":2,"data":["register-client-app",{}],"options":{"compress":true},"nsp":"/change-notifier"}+251mssocket.io-parser encoding packet {"type":2,"data":["register-client-app",{}],"options":{"compress":true},"nsp":"/change-notifier"} +0mssocket.io-parser encoded {"type":2,"data":["register-client-app",{}],"options":{"compress":true},"nsp":"/change-notifier"} as 2/change-notifier,["register-client-app",{}] +0msengine.io-client:socket flushing 1 packets in socket +1mssocket.io-client:socket draining queue +1msengine.io-client:socket socket receive: type "message", data "2/change-notifier,["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}]" +261mssocket.io-parser decoded 2/change-notifier,["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}] as {"type":2,"nsp":"/change-notifier","data":["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}]} +262mssocket.io-client:socket emitting event ["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}] +261msERROR: Error registering to API: [object Object]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I came to the conclusion that the client is sending an empty object {} in the registration message, but the server expects a ChangeNotifierRegistration object with workspace, project, and environment slugs. This results in undefined values being passed to the authorization check, causing the “Workspace undefined not found” error. The code correctly creates the registrationData object with workspaceSlug, projectSlug, and environmentSlug&lt;br&gt;&lt;br&gt;
But the debug output shows it’s sending an empty object {}&lt;/p&gt;

&lt;p&gt;This suggests that the data parameter being passed to connectToSocket has undefined values for workspace, project, and environment. Our team reached out to the issue maintainer radjip-b with a question regarding our process of the issue so far.&lt;/p&gt;

&lt;p&gt;This small technical challenge was solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;The solution for Keyshade’s issue 998 took five and a half weeks to complete during the eight-week internship. In the remaining time, we addressed additional related issues, including issue &lt;a href="https://github.com/keyshade-xyz/keyshade/issues/989" rel="noopener noreferrer"&gt;989&lt;/a&gt; and &lt;a href="https://github.com/keyshade-xyz/keyshade/issues/988" rel="noopener noreferrer"&gt;988&lt;/a&gt;, which involved adding Slack and Discord integration documentation, as well as contributing to &lt;a href="https://github.com/keyshade-xyz/keyshade/pull/1093" rel="noopener noreferrer"&gt;PR 1093&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Changes Introduced by PR #1093:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Slack Integration Documentation&lt;/strong&gt;: A brand-new &lt;strong&gt;“How It Works”&lt;/strong&gt; modal was added to the Keyshade platform’s UI, providing a visual, step-by-step guide—including screenshots—on how to set up Slack integration. This includes selecting Slack in the Integrations tab, entering credentials (signing secret, Bot token, channel ID), and testing with sample secrets and variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modal Improvements &amp;amp; Developer Feedback&lt;/strong&gt;: The modal component underwent adjustments—such as replacing a bare &amp;lt;DialogClose asChild /&amp;gt; (which caused rendering issues) with a proper close button, and revising instructions for better clarity (“Bot Token from Slack”). Additionally, an unused function (_handleClose) was cleaned up and textual instructions were streamlined.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Discord Integration Documentation&lt;/strong&gt;: The scope of the PR was expanded to include documentation for Discord integration as well—hence the updated title to “feat: added slack and discord integration docs.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Internal Routing and Formatting Refactor&lt;/strong&gt;: Responding to reviewer feedback, the team refactored the integration documentation routing logic—removing the DOCS_SLUG_MAP, simplifying routing, standardizing layout for project creation and event triggers, and cleaning up the formatting and positioning of images.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lint Fixes&lt;/strong&gt;: Minor linting errors (such as extraneous braces in index.tsx) were corrected to ensure code consistency and avoid build issues.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Summary Table&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;What Changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs &amp;amp; UI&lt;/td&gt;
&lt;td&gt;“How It Works” modal with guide and screenshots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component Cleanup&lt;/td&gt;
&lt;td&gt;Dialog close functionality fixed, redundant code removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discord Docs&lt;/td&gt;
&lt;td&gt;Documentation for Discord integration added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack Docs&lt;/td&gt;
&lt;td&gt;Documentation for Slack integration added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation Routing&lt;/td&gt;
&lt;td&gt;Routing logic simplified, layout standardized, visuals aligned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Quality&lt;/td&gt;
&lt;td&gt;Linting issues resolved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Solution Description For Issue #998
&lt;/h3&gt;

&lt;p&gt;Our team resolved a bug where the socket notifier and run command error handling would display [object Object] instead of a meaningful error message. The root cause was improper formatting of the error object before logging it to the user. The fix ensures that error messages are now properly stringified or formatted, providing clearer and more helpful feedback during CLI failures. To verify the fix, you can run the keyshade run command with an invalid configuration or project setup that triggers a connection error. This update significantly improves the developer experience by making error outputs more understandable.&lt;/p&gt;

&lt;p&gt;We determined that only three files required direct changes to address the issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NestJS WebSocket Gateway&lt;/strong&gt; – The server-side component uses NestJS’s @WebSocketGateway decorator with Socket.IO to handle real-time communication between the CLI and API server. The handleRegister method in change-notifier.socket.ts processes client registration requests and can return different error message formats depending on the failure type (authentication, authorization, or database errors).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI Command Handler&lt;/strong&gt; – The run.command.ts file contains the client-side logic that connects to the WebSocket server and handles the registration response. This component already included robust error parsing logic capable of handling both string and object formats, but the TypeScript interface was preventing proper type checking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Type Checker&lt;/strong&gt; – The run.types.d.ts file was updated to allow both string and object types, since the server can send either format.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the maintainer approved the fix, we made additional updates to the codebase by modifying two more files: changelog.md to document the changes, and package.json to bump the semantic version to &lt;strong&gt;3.2.3&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is in the file changes
&lt;/h3&gt;

&lt;p&gt;These were changes found using git’s diff command&lt;/p&gt;

&lt;p&gt;What has been changed can also be found &lt;a href="https://github.com/keyshade-xyz/keyshade/pull/1087/files" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changelog.md:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🐛 Bug Fixes* **api, cli:** ambiguous error messages on keyshade run failure ([#1087](&lt;a href="https://github.com/keyshade-xyz/keyshade/issues/1087)" rel="noopener noreferrer"&gt;https://github.com/keyshade-xyz/keyshade/issues/1087)&lt;/a&gt;) ([206dda7](&lt;a href="https://github.com/keyshade-xyz/keyshade/commit/206dda702b3f3f0cabeb73e4f74dd2ccfc962631)" rel="noopener noreferrer"&gt;https://github.com/keyshade-xyz/keyshade/commit/206dda702b3f3f0cabeb73e4f74dd2ccfc962631)&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;apps/cli/package.json: "version": "3.2.2-stage.2",&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;apps/cli/src/types/command/run.types.d.ts:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;message: string | object // Allow both string and object types since server can send either&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apps/cli/src/commands/run.commands.ts:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accurate WebSocket URL Parsing:&lt;br&gt;
The CLI’s socket connection logic now uses the full host from the base URL (via the URL class), ensuring reliable and correct WebSocket connections especially for complex URLs. &lt;/p&gt;

&lt;p&gt;Registration Timeout Added:A 30-second timeout was introduced for socket registration. If the server does not respond within this window, users receive a clear error message and the process exits gracefully. This prevents hanging connections and improves user feedback. &lt;/p&gt;

&lt;p&gt;Enhanced Error Message Extraction:The registration response error handling is now more robust: It intelligently extracts meaningful error messages whether the server responds with a string, an object, or a nested structure. Attempts to parse JSON error details for more informative output. Falls back to stringifying unknown error types, ensuring users always get useful feedback. All error messages are now logged in a consistent, user-friendly format. &lt;/p&gt;

&lt;p&gt;Minor Cleanup:Removed a redundant blank line after the socket connection initiation for improved code readability.   &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apps/api/src/socket/change-notifier.socker.ts: &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;1. Improved Error Handling &amp;amp; Feedback The registration process now provides much more detailed error messages to clients. Instead of a generic error or raw message, errors are parsed, logged, and emitted in user-friendly formats. &lt;/p&gt;

&lt;p&gt;2. Custom Authentication Logic for Socket Connections The previous decorator-based guards (@UseGuards(AuthGuard, ApiKeyGuard) and @RequiredApiKeyAuthorities) have been replaced by custom methods. New methods (extractAndValidateUser and convertToAuthenticatedUser) manually handle authentication and authority checks, mimicking guard behavior but allowing for more granular error handling and feedback. &lt;/p&gt;

&lt;p&gt;3. API Key Authority Checks The code now checks if the API key has the required authorities (READ_WORKSPACE, READ_PROJECT, and READ_ENVIRONMENT) directly within the socket handler. If the user has ADMIN authority, individual checks are bypassed. &lt;/p&gt;

&lt;p&gt;4. Enhanced Logging Connection logs now include the presence or absence of authentication headers. Errors during registration are logged with more context, including error types and constructors. &lt;/p&gt;

&lt;p&gt;5. Dependency Injection Updates The SlugGenerator service is now injected into the class, preparing for potential future use or slug-related operations. &lt;/p&gt;

&lt;p&gt;6. Cleaned-Up Imports Removed unused decorators and guards, reflecting the shift to manual authentication and authority verification. &lt;/p&gt;

&lt;p&gt;7. User Context Construction The code now constructs a detailed user context object for socket connections, including IP address, workspace, and API key authorities, improving downstream authorization and personalization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the solution
&lt;/h3&gt;

&lt;p&gt;While testing Keyshade, I encountered an issue that was easy to reproduce: create a project in Keyshade, configure your CLI to connect to it, delete the project, and then run the keyshade run &amp;lt;&amp;gt; command. This sequence would trigger an unclear error message, leaving the cause ambiguous. The expected behavior was for the CLI to clearly display the actual problem, with the API sending a properly formatted error message that the CLI would then print for the user. After applying the solution, the CLI behaved as intended, providing a clear and informative error message, making the debugging process much smoother.&lt;/p&gt;

&lt;p&gt;Another testing approach involved building custom WebSocket test clients designed to send invalid authentication data, deliberately triggering error conditions to assess system resilience. We closely monitored server-side logs to ensure errors were correctly detected and handled, while also verifying that the client could parse various error message formats. This end-to-end validation—from CLI input, through WebSocket communication and API processing, to the final CLI response—confirmed the integrity and reliability of the entire flow. This comprehensive process not only confirmed the integrity and reliability of the entire system but also successfully closed issue #998 with our &lt;a href="https://github.com/keyshade-xyz/keyshade/pull/1087" rel="noopener noreferrer"&gt;PR #1087&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We resolved a complex WebSocket bug by fixing authentication handling, type safety, and error messaging across the backend and CLI. They used systematic debugging, targeted fixes to three core components, and custom test clients to replace vague errors with clear feedback, improve authentication timeouts, and update integration docs—showcasing technical excellence, thorough testing, and a user-focused, collaborative approach.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devbugsmash</category>
      <category>security</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
