<?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: Niccolò Mascaro</title>
    <description>The latest articles on DEV Community by Niccolò Mascaro (@mascarock).</description>
    <link>https://dev.to/mascarock</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%2F4160118%2Fe5b68ae7-aa7b-478f-b926-6d06856b2aa4.jpg</url>
      <title>DEV Community: Niccolò Mascaro</title>
      <link>https://dev.to/mascarock</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mascarock"/>
    <language>en</language>
    <item>
      <title>I built Salt Wharf, a callboard where the later posted sheet wins</title>
      <dc:creator>Niccolò Mascaro</dc:creator>
      <pubDate>Sun, 04 Oct 2026 18:03:58 +0000</pubDate>
      <link>https://dev.to/mascarock/i-built-salt-wharf-a-callboard-where-the-later-posted-sheet-wins-4e5g</link>
      <guid>https://dev.to/mascarock/i-built-salt-wharf-a-callboard-where-the-later-posted-sheet-wins-4e5g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built Salt Wharf as a stage-door callboard for a fictional Valletta theatre company in tech week on &lt;em&gt;The Last Luzzu&lt;/em&gt;. Eight people, ten scenes. The public page is meant to feel like the sheet taped beside the stage door. It answers one question for tonight: who is actually called.&lt;/p&gt;

&lt;p&gt;The story is 4 October 2026. Two posted call sheets exist for that night. The earlier one still names Mara Camilleri as Rosa. The later one supersedes it and posts Lina Borg because Camilleri is off. The public door shows Borg and says why. The earlier sheet stays in the book, marked replaced.&lt;/p&gt;

&lt;p&gt;I did not want to overwrite history just to get the correct door. Both sheets stay as documents. The later posted sheet points at the earlier one with &lt;code&gt;supersedes&lt;/code&gt;. The door prints the posted sheet for that date that no other posted sheet supersedes.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://youtu.be/5juZLfRocXo" rel="noopener noreferrer"&gt;https://youtu.be/5juZLfRocXo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The public door is live, in fixture mode, with no login: &lt;a href="https://saltwharf.vibefy.net/" rel="noopener noreferrer"&gt;https://saltwharf.vibefy.net/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Walkthrough, with Lina Borg on the door and the earlier Mara Camilleri sheet marked replaced: &lt;a href="https://github.com/mascarock/salt-wharf/blob/main/docs/proof/salt-wharf-walkthrough.mp4" rel="noopener noreferrer"&gt;https://github.com/mascarock/salt-wharf/blob/main/docs/proof/salt-wharf-walkthrough.mp4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That page is not reading dataset &lt;code&gt;production&lt;/code&gt; on project &lt;code&gt;ebwymj6z&lt;/code&gt;. With no project id set, the build serves the committed seed. One visitor's backstage moves stay in that visitor's cookie.&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%2Fctnom25aw1oau6us3bgg.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%2Fctnom25aw1oau6us3bgg.png" alt="Public door: Lina Borg is Rosa" width="800" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How to judge it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://saltwharf.vibefy.net/" rel="noopener noreferrer"&gt;https://saltwharf.vibefy.net/&lt;/a&gt;. Lina Borg is Rosa. The slip says a later sheet replaced Mara Camilleri. Camilleri is not called. The book keeps the earlier sheet, marked replaced.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;/backstage&lt;/code&gt; and sign in as &lt;code&gt;elena&lt;/code&gt; / &lt;code&gt;callboard&lt;/code&gt;. This is a fake contest gate. It does not protect Sanity, GitHub, or any real system.&lt;/li&gt;
&lt;li&gt;Cover finder, role Rosa, night 4 October. Borg can cover. Vella fails range. Galea fails a concurrent scene. Camilleri fails the date. Micallef fails because they are already covering. Azzopardi fails a skill.&lt;/li&gt;
&lt;li&gt;Advance the 5 October draft. Each move writes a &lt;code&gt;workflowTransition&lt;/code&gt;. Posting it does not change 4 October, because supersedes is per date. The moves live in your browser only. Put the book back restores the committed story.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run verify&lt;/code&gt; asserts the door and the cover checks against the seed. The log is &lt;code&gt;docs/proof/verify.txt&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrmt481aeiqle55bpb8v.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%2Fsrmt481aeiqle55bpb8v.png" alt="Backstage sheet stack" width="800" height="1316"&gt;&lt;/a&gt;&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/mascarock/salt-wharf
&lt;span class="nb"&gt;cd &lt;/span&gt;salt-wharf
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://127.0.0.1:43174" rel="noopener noreferrer"&gt;http://127.0.0.1:43174&lt;/a&gt;. No environment variables are required.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/mascarock/salt-wharf" rel="noopener noreferrer"&gt;https://github.com/mascarock/salt-wharf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Longer notes than this post: &lt;code&gt;BUILD.md&lt;/code&gt; in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;I built this in Cursor as a Next.js app with Sanity behind it. I am not attaching a prompt transcript. The decisions that took the longest are the ones below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One role, one scene.&lt;/strong&gt; I kept a role as a character track with one scene, not "Rosa in every scene she plays." Splitting Rosa into Kitchen-Rosa, Storm-Rosa, and Finale-Rosa would make the call sheet honest about the book and ruin the cover story: Borg would already hold three tracks and fail "already covering two." &lt;code&gt;runsConcurrentWith&lt;/code&gt; is stored on the scene, in both directions, and the function also walks incoming edges, because a one-way link is easy to forget in Studio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage is computed.&lt;/strong&gt; There is no &lt;code&gt;cover&lt;/code&gt; document type. A later sheet may annotate an item with &lt;code&gt;note: "cover"&lt;/code&gt;. That is a house note after a decision, not the source of truth. The check is five predicates: range contains the role's required range, skills cover the role's skills, not cast or called in a concurrent scene, not unavailable that night, not already covering two other roles. Pawlu Galea fails Rosa only because he is cast as the Clerk in the harbour office, which runs with the kitchen. He has the range and the skills. A stored understudy list would have hidden that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow is documents, not the Workflows product.&lt;/strong&gt; A &lt;code&gt;workflowTransition&lt;/code&gt; records from, to, who, when, and why. Legal pairs are &lt;code&gt;draft → stageManagerReview → posted → struck&lt;/code&gt;, plus &lt;code&gt;posted → draft&lt;/code&gt; only with a note. Returning a posted sheet to draft takes it out of the posted set, so it stops hiding the sheet it superseded. I did not add a second mechanism for that. I also did not take a dependency on Sanity Workflows, the product. This is workflow-as-data next to the content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No App SDK app.&lt;/strong&gt; I read the current quickstart. A Dashboard app needs &lt;code&gt;sanity deploy&lt;/code&gt; and Dashboard auth. There is no App SDK deploy on this project, and I did not wrap unused SDK hooks to pretend there is one. What does run after an import is the TypeScript schema, a custom Studio structure (call sheets by status, the company graph, the transition log), and a Callboard tool that resolves the same standing sheet as the public door. In fixture mode, &lt;code&gt;/studio&lt;/code&gt; stays parked instead of booting Studio against a missing env.&lt;/p&gt;

&lt;p&gt;Skills stayed closed string ids, shared by people and roles, because a skill document would have made the seed harder to import. Vocal range is a pair of pitches, not the word "soprano." Containment is the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Project ID: &lt;code&gt;ebwymj6z&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Organization: &lt;code&gt;o1r4ucepz&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dataset: &lt;code&gt;production&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Seed the fixture and the import use: &lt;code&gt;sanity/seed.ndjson&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Schema: &lt;code&gt;sanity/schema&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live door does not write to that dataset. Live writes need a local write token, and that token is not in the repo.&lt;/p&gt;

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

&lt;p&gt;I did not attach an agent transcript. There isn't one in the repo. The rerunnable proof is the live door, the five steps above, and &lt;code&gt;npm run verify&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a harbour desk that refuses the old book</title>
      <dc:creator>Niccolò Mascaro</dc:creator>
      <pubDate>Sun, 04 Oct 2026 18:03:14 +0000</pubDate>
      <link>https://dev.to/mascarock/i-built-a-harbour-desk-that-refuses-the-old-book-396l</link>
      <guid>https://dev.to/mascarock/i-built-a-harbour-desk-that-refuses-the-old-book-396l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built Sounding, a tiny day-boat harbour desk, because I wanted the failure mode to feel concrete. Not "the chatbot hallucinated", but "a skipper searched the harbour name and got the old pocket note."&lt;/p&gt;

&lt;p&gt;The data is invented and is not for navigation. Eight central Mediterranean harbours, 140 seed documents. The desk walks relationships. A keyword hit on the harbour name is the wrong answer on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a keyword search returns
&lt;/h2&gt;

&lt;p&gt;Three cases, from the committed judge log (&lt;code&gt;docs/proof/judge.txt&lt;/code&gt;). All three verdicts are no-go. A harbour-name search opens the 2019 pocket note and stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marsamxett.&lt;/strong&gt; Friday after 18:00, 1.7 m draft, gregale expected. Keyword search for "Marsamxett" opens ALM-2019-MARSA-PONTOON and repeats the 2019 sheet: inner pontoons 2.1 m, visitors welcome. The walk does not stop there. HN-2024-17 supersedes that note and records 1.4 m on the inner fingers after winter silt. HN-2025-03 cites that notice and reserves Outer North and Outer South for the ferry on Fridays after 18:00. The gregale rule says those same two berths are the only sheltered visitor berths at 20 knots or more. The recommendation is no safe visitor berth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Syracuse.&lt;/strong&gt; Inner Basin East, 2.8 m draft. Keyword search opens ALM-2019-SYR-BASIN, the 2019 3.5 m note. The walk opens HN-2025-11, which cites silt survey SS-2025-02 (2.2 m) and supersedes the book. The wall is closed to drafts over 2.0 m. The survey matters only because the notice cites it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pozzallo.&lt;/strong&gt; Commercial quay, 2.9 m draft. Keyword search opens the 2019 commercial note. Standing depth is 2.8 m from HN-2024-44, which supersedes that note. HN-2025-08 is later and claims 3.2 m from an informal lead-line, and it does not supersede HN-2024-44, so 3.2 m is not standing. Latest document does not win.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://youtu.be/yILsuOLGDaw" rel="noopener noreferrer"&gt;https://youtu.be/yILsuOLGDaw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The desk is live, in fixture mode, with no login: &lt;a href="https://sounding.vibefy.net/" rel="noopener noreferrer"&gt;https://sounding.vibefy.net/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That page is the same fixture the judge runs locally. It does not call knowledge base &lt;code&gt;kbl6C1tJBXli&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Walkthrough, with the 2019 2.1 m note and HN-2024-17 at 1.4 m both on screen: &lt;a href="https://github.com/mascarock/sounding/blob/main/docs/proof/sounding-walkthrough.mp4" rel="noopener noreferrer"&gt;https://github.com/mascarock/sounding/blob/main/docs/proof/sounding-walkthrough.mp4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Judges can also run it locally with no token.&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/mascarock/sounding
&lt;span class="nb"&gt;cd &lt;/span&gt;sounding
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://127.0.0.1:43173" rel="noopener noreferrer"&gt;http://127.0.0.1:43173&lt;/a&gt;. Leave the env file empty. Then &lt;code&gt;npm run judge&lt;/code&gt;. The log above is that command.&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%2Fwkf9qd4hoheokzioegda.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%2Fwkf9qd4hoheokzioegda.png" alt="Sounding desk: no safe visitor berth at Marsamxett" width="800" height="1444"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;The Sanity project is &lt;code&gt;59vrectd&lt;/code&gt;, dataset &lt;code&gt;production&lt;/code&gt;, organization &lt;code&gt;o1r4ucepz&lt;/code&gt;, knowledge base &lt;code&gt;kbl6C1tJBXli&lt;/code&gt;. That knowledge base is the project's. The published proof does not call it.&lt;/p&gt;

&lt;p&gt;What a judge actually runs is fixture mode. It reads the committed corpus in &lt;code&gt;sanity/seed-data.ts&lt;/code&gt; through four tools: &lt;code&gt;initial_context&lt;/code&gt;, &lt;code&gt;schema_explorer&lt;/code&gt;, &lt;code&gt;groq_query&lt;/code&gt;, and &lt;code&gt;array_field_reader&lt;/code&gt;. No read token and no model key are in the repo. The screenshots and &lt;code&gt;npm run judge&lt;/code&gt; are that path. A local read token can point the same walker at project &lt;code&gt;59vrectd&lt;/code&gt;. I am not claiming the knowledge base resolved Marsamxett.&lt;/p&gt;

&lt;p&gt;The desk can also read a live wind from Open-Meteo and match it to a weather rule. The published judge states the wind in the question, so that log is not a weather-API transcript. The gregale rule fires from the wind in the question, not from "whichever document is newest."&lt;/p&gt;

&lt;p&gt;The deterministic walker is enough to judge. If a local model key is present, a model can call the same tools. The useful part is not the model. It is that the answer has to show the walk, and the old book loses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Project ID: &lt;code&gt;59vrectd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Organization: &lt;code&gt;o1r4ucepz&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dataset: &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;Knowledge base: &lt;code&gt;kbl6C1tJBXli&lt;/code&gt; (named on the project; the published judge does not call it)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I did not attach an agent transcript. There isn't one in the repo. The proof a judge can rerun is &lt;code&gt;npm run judge&lt;/code&gt; and the two screenshots above.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>A private budget chat for Paola, checked by local Gemma</title>
      <dc:creator>Niccolò Mascaro</dc:creator>
      <pubDate>Sun, 04 Oct 2026 11:46:06 +0000</pubDate>
      <link>https://dev.to/mascarock/a-private-budget-chat-for-paola-checked-by-local-gemma-iec</link>
      <guid>https://dev.to/mascarock/a-private-budget-chat-for-paola-checked-by-local-gemma-iec</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Local Computer is a small Mac-only budget companion for Paola, a marketer who needs to check spend against an approved budget without turning the budget into another cloud integration.&lt;/p&gt;

&lt;p&gt;The workflow is deliberately ordinary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load an approved CSV.&lt;/li&gt;
&lt;li&gt;Record spend locally.&lt;/li&gt;
&lt;li&gt;Ask one of five assistants what changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important constraint is the personal one. Paola's approved numbers should not be copied into a SaaS dashboard, a remote chatbot, or a shared analytics workspace just to answer: "Are we over budget anywhere?"&lt;/p&gt;

&lt;p&gt;So the app keeps the budget and spend log on the Mac. The demo uses clearly labeled &lt;strong&gt;SAMPLE DATA&lt;/strong&gt;, not Paola's real budget.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://youtu.be/epbTQIZQOek" rel="noopener noreferrer"&gt;https://youtu.be/epbTQIZQOek&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The walkthrough is in the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Video: &lt;code&gt;docs/Local-Computer-color-walkthrough.mp4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Screenshots: &lt;code&gt;docs/screenshots&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Judge path: &lt;code&gt;JUDGE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gemma proof: &lt;code&gt;docs/proof/README.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no hosted demo. That is part of the point: this version is designed to run locally, with local storage and local Ollama.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Local Computer has five budget assistants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ledger&lt;/strong&gt; reads the approved CSV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clerk&lt;/strong&gt; logs a spend against an existing approved item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watcher&lt;/strong&gt; names lines where &lt;code&gt;spent &amp;gt; approved&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remainder&lt;/strong&gt; shows approved, spent, and remaining totals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Note&lt;/strong&gt; asks local Gemma for one short sentence, then checks that sentence before showing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample walkthrough has one over-budget line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SAMPLE DATA: the Q4 trade fair is over budget by 200.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The computed sample totals are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;approved total: 16700
spent total: 6900
remaining total: 9800
over-budget line: Events / Q4 trade fair, approved 4500, spent 4700, over 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Use of Gemma
&lt;/h2&gt;

&lt;p&gt;Gemma is not used as a decorative chat layer. It sits at the exact place where a human needs wording: turning a computed budget result into one sentence Paola can read quickly.&lt;/p&gt;

&lt;p&gt;The model is local &lt;strong&gt;Gemma &lt;code&gt;gemma3:1b&lt;/code&gt;&lt;/strong&gt; through Ollama at &lt;code&gt;127.0.0.1:11434&lt;/code&gt;. The prompt gives it the tool result and asks for one short English sentence. The model is not trusted with the budget math.&lt;/p&gt;

&lt;p&gt;The sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Code computes the budget result.&lt;/li&gt;
&lt;li&gt;Code writes the exact facts Gemma may narrate.&lt;/li&gt;
&lt;li&gt;Gemma writes one sentence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;withholdNarration&lt;/code&gt; checks the sentence.&lt;/li&gt;
&lt;li&gt;The app shows the sentence only if it matches the computed facts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step matters because local models can still drift. In this app, a drifted sentence is not "mostly fine"; it is withheld and the computed result is shown instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof I added before submission
&lt;/h2&gt;

&lt;p&gt;I added a small proof harness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run proof:note-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/proof/note-guard-proof.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proof checks three candidate Gemma sentences against the same guard used by the app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Guard result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SAMPLE DATA: the Q4 trade fair is over budget by 200.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;accepted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SAMPLE DATA: Google Ads is over budget by 200.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;withheld: &lt;code&gt;not_over_budget&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SAMPLE DATA: the Q4 trade fair is over budget by 300.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;withheld: &lt;code&gt;invented_figure&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the core failure mode I wanted to handle: the sentence may sound plausible, but if the item or number is wrong, Paola should not see it as model narration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I showed Paola, and what I would ask next
&lt;/h2&gt;

&lt;p&gt;I did not put Paola's real budget in this repo, and I am not going to invent a quote from her.&lt;/p&gt;

&lt;p&gt;What I can honestly show her is the sample flow: one approved marketing budget, one local spend log, Watcher finding the Q4 trade fair over by 200, and Note producing a guarded sentence from local Gemma.&lt;/p&gt;

&lt;p&gt;The next question I would ask her is not "Do you trust the model?" It is more practical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the app refuses a sentence and shows the computed result instead, is that interruption clear enough for you to keep working?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a budget tool, that trust boundary is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;The app stores budgets and spend logs in local browser storage. The desktop shell blocks external navigation and remote renderer requests. The model call goes only to local Ollama. There is no cloud database, account sync, remote analytics, external font request, or hosted backend.&lt;/p&gt;

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

&lt;p&gt;I would add a compact review panel for withheld sentences: candidate sentence, reason, and computed facts. I would also test more real-world CSV names, because budget line names can be messy and the guard is intentionally conservative about names that read like totals, periods, or multiple items.&lt;/p&gt;

&lt;p&gt;The shape I want to keep is simple: let Gemma write like a person, but let code decide whether the sentence is safe enough to show.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
