<?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: Hriday Vig</title>
    <description>The latest articles on DEV Community by Hriday Vig (@vighriday).</description>
    <link>https://dev.to/vighriday</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%2F3938816%2F12b7188d-172b-4306-80f7-3df6ac764729.png</url>
      <title>DEV Community: Hriday Vig</title>
      <link>https://dev.to/vighriday</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vighriday"/>
    <language>en</language>
    <item>
      <title>Your dog can learn to fear a recording of your voice</title>
      <dc:creator>Hriday Vig</dc:creator>
      <pubDate>Sun, 16 Aug 2026 16:58:35 +0000</pubDate>
      <link>https://dev.to/vighriday/your-dog-can-learn-to-fear-a-recording-of-your-voice-98d</link>
      <guid>https://dev.to/vighriday/your-dog-can-learn-to-fear-a-recording-of-your-voice-98d</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I don't own a dog.&lt;/p&gt;

&lt;p&gt;But &lt;a href="https://www.sciencedirect.com/science/article/pii/S1558787816300569" rel="noopener noreferrer"&gt;roughly one dog in six&lt;/a&gt; gets genuinely distressed when it's left home alone, and while reading up for this challenge I found something I couldn't stop thinking about.&lt;/p&gt;

&lt;p&gt;In 2021, a Finnish app called Digital Dogsitter was &lt;a href="https://www.sciencedirect.com/science/article/pii/S0168159121002471" rel="noopener noreferrer"&gt;put through a proper trial&lt;/a&gt;. It listened for the dog crying, and when it heard it, played back a recording of the owner's voice. Across 40 dogs, total vocalisation dropped by &lt;strong&gt;95.7%&lt;/strong&gt; in two weeks.&lt;/p&gt;

&lt;p&gt;So the mechanism isn't a hunch. It's published.&lt;/p&gt;

&lt;p&gt;But the same literature carries a warning, and it's the reason this post is called what it's called. One clip, looped identically, can stop being comfort and become a &lt;em&gt;cue&lt;/em&gt;: the sound that means you're gone. Hear it enough times and the dog starts to dread it. Something that predicts abandonment doesn't calm anyone down.&lt;/p&gt;

&lt;p&gt;And in 2021 there was no way around that, because you could only ever play back what you'd already recorded.&lt;/p&gt;

&lt;p&gt;In 2026 there is. So I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay&lt;/strong&gt; listens for your dog, waits for it to go quiet, then speaks to it in your voice. Different words every time, generated fresh, never a loop.&lt;/p&gt;

&lt;p&gt;Then when you get home, it tells you what happened while you were out.&lt;/p&gt;

&lt;p&gt;Most dog tech points one way, helping humans understand dogs. Stay points the other way: helping dogs hear their humans.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://stay-swart.vercel.app" rel="noopener noreferrer"&gt;https://stay-swart.vercel.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;No key, no microphone:&lt;/strong&gt; &lt;a href="https://stay-swart.vercel.app/demo" rel="noopener noreferrer"&gt;https://stay-swart.vercel.app/demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That second link is the one to click. Demo mode runs a real recording of a distressed dog through the real detector and answers it with real audio. No signup, no API key, no recording your voice. It takes about ten seconds.&lt;/p&gt;

&lt;p&gt;Watch for the gap. &lt;strong&gt;Stay doesn't answer while the dog is barking.&lt;/strong&gt; It waits for the barking to stop and speaks into the quiet. That's the most important decision in the whole thing, and it's the next section.&lt;/p&gt;

&lt;p&gt;When the clip ends, press &lt;strong&gt;"Read this session back to me"&lt;/strong&gt;. That's Gemini writing the closing summary, running on my key, so you get the full thing without an account.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(One number changes in demo mode and it's stated on the page: the cooldown drops from 90 seconds to 20, because the clip is only 40 seconds long and at the real setting the loop can complete just once. Every other rule is untouched, especially the wait for quiet.)&lt;/em&gt;&lt;/p&gt;


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

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/vighriday/stay-for-dogs" rel="noopener noreferrer"&gt;https://github.com/vighriday/stay-for-dogs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The detector's decision, in the audio worklet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loudEnough&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;deaf&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thresholdDb&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minBandRatio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voiced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;loudEnough&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;periodicity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNoise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;loudEnough&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;voiced&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minPeriodicity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule that decides &lt;em&gt;when&lt;/em&gt; to speak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPSET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Answer the quiet, not the barking.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quietRun&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quiet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;speak&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// But never ignore a dog that cannot settle on its own.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noiseRun&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;speak&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ceiling&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the constraints on every generated line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- 4 to 12 words. Never longer.
- Never use any word from the banned list, in any form.
- No questions. A question makes a dog expect something to happen.
- Never reference leaving, returning at a specific time, doors, or going out.
- Never use exclamation marks. Excitement is the opposite of what is needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line comes back through server-side validation that rejects anything breaking those rules and regenerates. I don't trust the model to follow its own instructions.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  The bit I'm proudest of: it answers the quiet, not the noise
&lt;/h3&gt;

&lt;p&gt;If the voice arrives the instant the dog barks, you've built a machine that rewards barking. The dog learns that barking summons its human. You'd be training the exact behaviour you're trying to reduce.&lt;/p&gt;

&lt;p&gt;So Stay marks the dog as upset, keeps listening, and only speaks after &lt;strong&gt;2.5 seconds of quiet&lt;/strong&gt;. It answers the calm.&lt;/p&gt;

&lt;p&gt;With one exception. If the dog never settles, Stay speaks anyway after 20 seconds, because an inconsolable dog shouldn't be ignored for failing to hit a threshold. The timeline records which rule fired, so you can tell &lt;em&gt;"your dog settled and was answered"&lt;/em&gt; from &lt;em&gt;"your dog never settled and was answered anyway."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's about fifteen lines of code, and it's the difference between a comfort device and a bark trainer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four things I got wrong
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The first detector never fired at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd written the obvious thing: a sound counts if it's loud, sits in the 300 to 2500 Hz band where barks live, and holds for 400ms. I ran it against 40 seconds of a barking dog and got two log entries. Session started, session ended.&lt;/p&gt;

&lt;p&gt;So I instrumented it instead of guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;frames 497 · loud 37 · in-band 174 · both 37
longest unbroken noisy run: 6 frames    rule required: 10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Barking is &lt;em&gt;impulsive&lt;/em&gt;. Bursts of 150 to 250ms with gaps between them. It never produces 400ms of continuous sound, so my rule was never going to fire on a real dog. I'd designed for a noise dogs don't make.&lt;/p&gt;

&lt;p&gt;The fix was to count separate &lt;strong&gt;onsets&lt;/strong&gt; in a sliding window: three onsets in 1.5 seconds catches barking. Whining and howling are the opposite problem, quiet but continuous, so an unbroken 1.2 second stretch counts too. Two different noises, two routes in. Dogs went to 5/5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then it fired on everything.&lt;/strong&gt; Four of seven household clips triggered it. Every decent door slam.&lt;/p&gt;

&lt;p&gt;A door slam is loud, sits in the same band as a bark, and is made of several separated transients. On loudness, frequency and repetition it genuinely &lt;em&gt;is&lt;/em&gt; the same thing as barking, so no threshold separates them. I tried a refractory gap between onsets and got nowhere.&lt;/p&gt;

&lt;p&gt;What separates them is something else: &lt;strong&gt;a bark is voiced.&lt;/strong&gt; It has a pitch, the waveform repeats. A door slam is broadband with no periodicity at all. So I added an autocorrelation test across the lags a dog's fundamental sits in, and swept the threshold:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;threshold&lt;/th&gt;
&lt;th&gt;dogs&lt;/th&gt;
&lt;th&gt;false positives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;5/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;td&gt;1/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;1/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.75&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;0/7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;0.75 and 0.80 both came back clean, so I took 0.75 as the middle of the plateau rather than its edge. No model, no download, nothing leaves the device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then I was wrong a third time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That 5/5 was measured on the same twelve clips I'd tuned against. So I went and found twenty-two more from a different source: whimpering, crying, yelping, plus household sounds the first set had none of, like conversation and a washing machine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measured on&lt;/th&gt;
&lt;th&gt;Tuned-on (12)&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Held-out (22)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dogs detected&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positives&lt;/td&gt;
&lt;td&gt;0/7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4/13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The perfect score was overfitting.&lt;/strong&gt; On audio it had never met: 56% detection, 31% false alarms.&lt;/p&gt;

&lt;p&gt;I could have shipped the first table. It was true, it was measured, and nobody would have checked. The second one describes what happens to somebody who isn't me, so that's the one on the site.&lt;/p&gt;

&lt;p&gt;One of those false alarms is people talking, which I should have seen coming. &lt;strong&gt;Human speech is the most voiced sound there is&lt;/strong&gt; and it sits right inside 300 to 2500 Hz. So a television left on will trigger Stay.&lt;/p&gt;

&lt;p&gt;The obvious fix works and I rejected it. Raising the pitch floor from 140 Hz to 300 Hz (adult speech is 85 to 255 Hz, dogs sit above) removes the false alarm, but it costs a &lt;strong&gt;whining&lt;/strong&gt; clip. Whining is the single most characteristic sound of separation distress. This app exists for dogs that whine when they're alone.&lt;/p&gt;

&lt;p&gt;Missing a distressed dog is the product failing at its job. Speaking when it shouldn't is annoying, and capped at once per 90 seconds anyway. So 140 Hz ships, and the worse-looking table ships with it.&lt;/p&gt;

&lt;p&gt;That's where signal processing runs out. It can tell you what &lt;em&gt;shape&lt;/em&gt; a sound is, never what made it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fourth one I found by writing a test, not by running it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a few hours left I added a test suite, not for coverage, but because the rules sitting between a language model and an animal should be checkable rather than just written down.&lt;/p&gt;

&lt;p&gt;The prompt had always said: &lt;em&gt;never use any word from the banned list, in any form, including inside other words.&lt;/em&gt; The validator behind it matched &lt;code&gt;\bwalk\b&lt;/code&gt;. I typed out what I assumed would be a boring assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKED   Time for a walk
PASSES    We are going walking soon      ← this would have been spoken aloud
PASSES    She walked away
PASSES    Two walks today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Walk is the most reactive word on that list&lt;/strong&gt;, and three of its four forms went straight through to the speaker. The prompt promised one rule, the code enforced a narrower one, and nothing anywhere compared them.&lt;/p&gt;

&lt;p&gt;The obvious fix is worse than the bug: match a bare prefix and &lt;code&gt;car&lt;/code&gt; swallows &lt;em&gt;carpet&lt;/em&gt; and &lt;em&gt;careful&lt;/em&gt;, so the app can't say "careful now" to a dog. It matches a short set of real inflections instead, and there's a test asserting that a dog named &lt;strong&gt;Walker&lt;/strong&gt; is still called by its own name.&lt;/p&gt;

&lt;p&gt;The next test found another one. Questions were only caught by their punctuation, so &lt;code&gt;"Are you alright in there"&lt;/code&gt; sailed through. Still a question to a dog, and a question makes a dog get up.&lt;/p&gt;

&lt;p&gt;Every test is a regression for something that actually went wrong. No test framework: Node 22's built-in runner and native TypeScript stripping, so the project still has five dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running an AudioWorklet in CI
&lt;/h3&gt;

&lt;p&gt;The detector is written against the AudioWorklet API, so it could only ever run inside a page. That left the one component whose worst failure mode is &lt;em&gt;silence&lt;/em&gt; with no automated check. And I already knew what that costs, because the first version detected nothing on a real barking dog and looked perfectly healthy doing it.&lt;/p&gt;

&lt;p&gt;But the worklet is only a class. It touches three globals, so shimming those runs it unmodified under Node. &lt;strong&gt;The tests drive the exact file the browser loads&lt;/strong&gt;, not a copy of it. Then the signals are generated in code, each shaped to isolate one rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Asserts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three voiced bursts&lt;/td&gt;
&lt;td&gt;An episode opens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One burst, then two&lt;/td&gt;
&lt;td&gt;It doesn't. A bang is an event, not a pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Four loud broadband transients&lt;/td&gt;
&lt;td&gt;Still nothing. &lt;strong&gt;The door slam case&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A sustained tone&lt;/td&gt;
&lt;td&gt;Opens via the continuous route: whining, not barking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noise that never stops&lt;/td&gt;
&lt;td&gt;Silent throughout, then answers the quiet afterwards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22s unbroken&lt;/td&gt;
&lt;td&gt;Answered anyway, on the ceiling rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A second episode inside 90s&lt;/td&gt;
&lt;td&gt;Logged as &lt;code&gt;held&lt;/code&gt;, not answered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faint barking at two sensitivities&lt;/td&gt;
&lt;td&gt;The slider really does move the operating point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No browser, no audio files, nothing to license.&lt;/p&gt;

&lt;p&gt;The last test is my favourite: it asserts that &lt;strong&gt;a speech-shaped signal still triggers the detector.&lt;/strong&gt; That's the documented limitation written down as an assertion, so nobody can quietly "fix" the television problem without noticing the whining case went with it.&lt;/p&gt;

&lt;p&gt;Building it taught me something too. My first synthetic "speech" was a bass-heavy tone and it &lt;em&gt;didn't&lt;/em&gt; trigger, because a 300 Hz highpass strips a 150 Hz fundamental. Real speech carries its energy in the &lt;strong&gt;formants&lt;/strong&gt;, inside the dog band. Modelling it wrong would have deleted the limitation from the test bench while leaving it in the product.&lt;/p&gt;

&lt;p&gt;94 tests, green in CI on every push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making it work on a free ElevenLabs account
&lt;/h3&gt;

&lt;p&gt;I have no budget for this, so I started by finding out what a free key can actually do. Less than the docs suggest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;TTS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;library&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;voice&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"Free users cannot use library voices via the API"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Voice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Design&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;API&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"Creating a voice through the API is only available
                                  on a paid plan"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a voice you create yourself in the ElevenLabs dashboard is a &lt;em&gt;personal&lt;/em&gt; voice, not a library one, and driving that through the API works fine. I confirmed it with a real synthesis before building anything on top.&lt;/p&gt;

&lt;p&gt;So Stay asks your key what it's allowed to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCapabilities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// GET /v1/user/subscription&lt;/span&gt;
&lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canCreateVoice&lt;/span&gt;   &lt;span class="c1"&gt;// ← can_use_instant_voice_cloning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paid key:&lt;/strong&gt; record three minutes, clone it in-app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free key:&lt;/strong&gt; Stay writes the voice description for you, you paste it into Voice Lab, hand back the voice ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No key:&lt;/strong&gt; demo mode, everything pre-rendered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three complete paths instead of one crippled one. &lt;strong&gt;Stay runs on a free account&lt;/strong&gt;, never asks you for money, and never stores your key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the audio is made before it's needed
&lt;/h3&gt;

&lt;p&gt;Generating a line and synthesising it &lt;em&gt;after&lt;/em&gt; the dog goes quiet costs two to six seconds. The moment's gone and the app looks broken.&lt;/p&gt;

&lt;p&gt;So at session start Stay writes ten lines, renders them all, and holds them as decoded buffers. When the dog settles, playback is instant. It refills in the background, and underneath sits a bank of twenty hand-written pre-rendered lines, so a rate limit or a dropped connection never leaves the app silent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it runs
&lt;/h3&gt;

&lt;p&gt;Detection runs in an &lt;strong&gt;AudioWorklet&lt;/strong&gt; on the audio thread, with every decision timed by counting samples rather than reading a clock. Away Mode means an unattended tab for hours, and browsers throttle timers and animation frames in background tabs. A render-loop detector would quietly stop working in exactly the situation the product exists for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The number I never show the model
&lt;/h3&gt;

&lt;p&gt;A session leaves behind something a camera can't produce: a record of a dog that was alone. When each upset started, how long it ran, how loud it peaked. That's what an owner actually wants, and as a list of timestamps it's unreadable. So Gemini writes the summary.&lt;/p&gt;

&lt;p&gt;My first attempt handed it the figures with a prompt saying &lt;em&gt;don't recite these, say what they mean&lt;/em&gt;. It recited them anyway, every time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Biscuit got upset 3 times during the 42.5 minute session. The upsets lasted 14.2, 9.6 and 4.8 seconds, with peak volumes of -19 dBFS, -23 dBFS and -28 dBFS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the table read aloud, in units no dog owner should ever see. I rewrote the prompt twice, harder each time, and it kept happening. &lt;strong&gt;Telling a model to ignore what's in front of it isn't a design. It's a wish.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I stopped sending numbers. A pure function turns the timestamps into statements that are already true, and &lt;em&gt;those sentences are the entire prompt input&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;The session lasted about 45 minutes.
The dog got upset three times.
Each upset was shorter than the one before it.
The upsets also got quieter as the session went on.
There was one long stretch of about 20 minutes with nothing at all.
Stay answered every upset.
Every answer came after the noise had already stopped, never during it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which comes back as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Three upsets, getting shorter and quieter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Biscuit got upset three times during the session, which lasted about 45 minutes. Each upset became shorter and quieter as the session went on, and there was a long, quiet stretch in the middle. Stay answered every upset, always after the noise had already stopped.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Worth knowing:&lt;/em&gt; The upsets did get shorter and quieter, but three of them is a thin basis for believing that means very much yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A model can't misreport a number it was never shown.&lt;/strong&gt; And whether a direction can be described at all is decided in code from the episode count. Under three upsets, the prompt is told outright that no trend exists. The model can't be talked past that, because it isn't in the prompt. The page shows the exact input under a disclosure, so you can check rather than take my word for it.&lt;/p&gt;

&lt;p&gt;It also caught a bug in my own code. Loudness direction was first computed as a &lt;em&gt;ratio&lt;/em&gt; of dBFS values. Decibels are logarithmic and negative, so &lt;code&gt;-28 / -19&lt;/code&gt; isn't a ratio of anything, and a dog that was getting quieter got reported as getting louder. It's a difference test with a 4 dB floor now. Honesty scaffolding is only worth having if the numbers underneath it are right.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt; and &lt;strong&gt;Google AI&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ElevenLabs
&lt;/h3&gt;

&lt;p&gt;The voice isn't a feature of Stay, it's the reason it exists. The whole premise is that the dog hears &lt;em&gt;its own person&lt;/em&gt; saying something it has never said before.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subscription introspection&lt;/strong&gt; (&lt;code&gt;/v1/user/subscription&lt;/code&gt;) picks which of three onboarding paths you get, before you hit a wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant Voice Cloning&lt;/strong&gt; for paid keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A guided Voice Design flow&lt;/strong&gt; for free keys. Stay writes the description, and the trailing clause &lt;em&gt;"speaking slowly and gently, as if calming an animal"&lt;/em&gt; makes a real difference to what comes back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification by actual synthesis.&lt;/strong&gt; Pasting a voice ID doesn't just check it exists. Stay speaks two words with it and plays them to you, because a free plan can &lt;em&gt;read&lt;/em&gt; the whole voice library and still get a 402 when it tries to &lt;em&gt;speak&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery matched to the moment.&lt;/strong&gt; The three moments Stay speaks into aren't the same moment, and a person's voice wouldn't be either.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Moment&lt;/th&gt;
&lt;th&gt;stability&lt;/th&gt;
&lt;th&gt;speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;calm&lt;/code&gt;, nothing is wrong&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;settle&lt;/code&gt;, the dog just went quiet&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;reassure&lt;/code&gt;, 20s of distress with no sign of stopping&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Higher stability means less expressive variation, which is normally a cost and is exactly the goal when the listener is an animal deciding whether the room is safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google AI
&lt;/h3&gt;

&lt;p&gt;Gemini does four jobs, and the fourth is the one I'd point at first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads the session back to you&lt;/strong&gt;, the summary described above, where the model is never shown a number and so can't misreport one. It runs on the shared key, so &lt;strong&gt;you can try it in demo mode with no account.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes every line&lt;/strong&gt;, under hard constraints, with server-side validation that rejects and regenerates anything breaking them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classifies vocalisation from audio.&lt;/strong&gt; Whining and howling are distress; a short burst of sharp barks usually means something walked past the window. Different problems, different responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores a clip of your dog alone:&lt;/strong&gt; how much of it was spent pacing, how often it went to the door, how long until it lay down and stayed down.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same pattern in all four: JSON schema mode, a prompt that defines every term exactly, and a deterministic check in code between the model and anything that reaches you or the dog. &lt;strong&gt;Gemini writes the sentences. It never gets to decide the facts.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Being straight with you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is a prototype, not a treatment.&lt;/strong&gt; A badly anxious dog needs a veterinary behaviourist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I don't own a dog.&lt;/strong&gt; So rather than film one dog once and call it proof, I published the rates and &lt;strong&gt;shipped all 34 clips in the repo&lt;/strong&gt; so you can re-run the sweep yourself at &lt;a href="https://stay-swart.vercel.app/test" rel="noopener noreferrer"&gt;https://stay-swart.vercel.app/test&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Held-out performance is 56% detection and 31% false alarms&lt;/strong&gt;, not the 5/5 I got on the set I tuned against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A television will trigger it.&lt;/strong&gt; Human speech is voiced and in-band, and this detector can't tell a person from a dog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;34 clips is still small&lt;/strong&gt;, and every clip is a clean recording of one thing. Real rooms layer sounds. A dog whining over a washing machine isn't represented at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no room in the measurement.&lt;/strong&gt; Clips go straight into the audio graph: no speaker, no microphone, no distance, no reverb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no before-and-after demo&lt;/strong&gt;, and there won't be a fake one. A real comparison needs the same dog in two states, and putting two unrelated dogs side by side to imply a result would be a lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The session summary is narration, not analysis.&lt;/strong&gt; A handful of upsets in one afternoon can't tell you whether anything is working, and the summary says so out loud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The microphone audio never leaves your browser.&lt;/strong&gt; Stay measures loudness and a frequency ratio on-device. Nothing is recorded, buffered or uploaded. That's a property of how it's built, not a promise I'm asking you to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This was built with AI assistance.&lt;/strong&gt; Claude wrote the code. My part was the idea, the architecture and the direction: what to build, what to cut, what to ship, and what to own up to in this section. Research and sourcing came through Gemini and Perplexity.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Proper validation on real dogs, with owners who can run the before-and-after I can't. A bigger, more varied test set the detector has never seen. And an on-device audio classifier, which is where judging by periodicity runs out of road.&lt;/p&gt;

&lt;p&gt;If you have a dog and twenty minutes, I'd genuinely like the footage.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built 15 to 17 August 2026 for the DEV Weekend Challenge: Dog Days Edition. Every commit falls inside the challenge window, and the history is public and timestamped.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>gemini</category>
      <category>elevenlabs</category>
    </item>
    <item>
      <title>I built a workflow-aware verification layer for AI coding agents — open source, MCP-native</title>
      <dc:creator>Hriday Vig</dc:creator>
      <pubDate>Tue, 19 May 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/vighriday/i-built-a-workflow-aware-verification-layer-for-ai-coding-agents-open-source-mcp-native-10df</link>
      <guid>https://dev.to/vighriday/i-built-a-workflow-aware-verification-layer-for-ai-coding-agents-open-source-mcp-native-10df</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Autonomous coding agents are good at writing code. They are bad at knowing &lt;strong&gt;what's actually risky&lt;/strong&gt; about the code they just wrote.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/vighriday/Veris" rel="noopener noreferrer"&gt;Veris&lt;/a&gt;&lt;/strong&gt; -- an MCP-native verification intelligence layer. You point it at a repo, it builds a behavioral graph, groups functions into semantic workflows (Authentication, Payments, Webhooks, Caching, Queue, etc.), and emits &lt;em&gt;concrete adversarial probes&lt;/em&gt; per workflow.&lt;/p&gt;

&lt;p&gt;Install + run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx veris-core analyze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Open the HTML dashboard, see what could break.&lt;/p&gt;

&lt;p&gt;MIT. Sponsor-funded. No telemetry. Local SQLite. &lt;a href="https://github.com/vighriday/Veris" rel="noopener noreferrer"&gt;Repo here.&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Every coding agent I use -- Claude Code, Cursor, Aider, you name it -- has the same blindspot.&lt;/p&gt;

&lt;p&gt;I ask it to "add a new Stripe charge flow." It writes the code, runs the unit tests, says it's done. Tests pass. The PR merges. Three days later, prod has duplicate charges because there's no idempotency key.&lt;/p&gt;

&lt;p&gt;The agent didn't miss a &lt;em&gt;bug&lt;/em&gt;. It missed an entire &lt;em&gt;category&lt;/em&gt; of failure that doesn't show up in unit tests: idempotency under retry. There is no test for "the same request hits us twice in 500ms because of network retry." Until prod surfaces it.&lt;/p&gt;

&lt;p&gt;Veris exists to make that category visible &lt;strong&gt;before&lt;/strong&gt; the PR merges.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  1. Behavioral graph
&lt;/h3&gt;

&lt;p&gt;Veris parses every TS/JS/JSX/TSX/MJS/CJS file in your repo via &lt;code&gt;ts-morph&lt;/code&gt;. For each, it extracts symbols (classes, functions, methods, top-level arrow assignments, &lt;code&gt;module.exports.X = function&lt;/code&gt;, &lt;code&gt;Foo.prototype.method = function&lt;/code&gt;) and resolves cross-module imports + invocations into edges.&lt;/p&gt;

&lt;p&gt;On Express (141 files) it produces 93 nodes and 71 edges in under a second. On Next.js (2,444 files) it builds the graph in the same timeframe via basename pre-indexing and local-import filtering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodes  = symbols (Class / Method / Function)
edges  = DependsOn (file import) | Invokes (call)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Semantic workflow grouping
&lt;/h3&gt;

&lt;p&gt;This is the moat. Raw graphs are noise. Veris classifies each node into one of 25 workflow domains using a weighted vote across three signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path tokens&lt;/strong&gt; -- &lt;code&gt;src/payments/charge.ts&lt;/code&gt; becomes strongly Payments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import tokens&lt;/strong&gt; -- &lt;code&gt;import stripe from 'stripe'&lt;/code&gt; becomes Payments-adjacent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbol tokens&lt;/strong&gt; -- &lt;code&gt;processPayment&lt;/code&gt;, &lt;code&gt;refund&lt;/code&gt; match via word-boundary + camelCase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exact-segment path matches outrank import-token matches, which outrank symbol matches. Test/sample/fixture dirs get a 30% multiplier so a real &lt;code&gt;src/auth/login.ts&lt;/code&gt; always beats &lt;code&gt;tests/auth.spec.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rules live in &lt;code&gt;data/workflow-rules.json&lt;/code&gt;. Override per repo at &lt;code&gt;.veris/data/workflow-rules.json&lt;/code&gt;. Add new domains via &lt;code&gt;.veris/plugins/*.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Adversarial probe templates
&lt;/h3&gt;

&lt;p&gt;Each workflow has a deck of concrete probes. Examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payments:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Submit charge twice with the same idempotency key inside a 500ms window. &lt;em&gt;Expected: exactly one ledger entry; second call returns the first result.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Capture succeeds at gateway, response times out before reaching us. &lt;em&gt;Expected: reconciliation eventually marks order paid; no orphan charge.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Webhooks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replay a 24-hour-old signed payload with the original signature. &lt;em&gt;Expected: replay rejected by timestamp window even though signature is valid.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Caching:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mass cache expiry triggers thundering herd on origin. &lt;em&gt;Expected: single-flight or jittered refresh; origin not overwhelmed.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Veris never &lt;em&gt;runs&lt;/em&gt; the probe. It emits the directive. Your agent (or human) runs it and calls &lt;code&gt;report_execution&lt;/code&gt; via MCP to feed results back into the confidence model.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Behavioral drift detection
&lt;/h3&gt;

&lt;p&gt;Veris fingerprints each workflow (SHA-256 of sorted edges + members + key signals) and stores fingerprints in &lt;code&gt;.veris/state.db&lt;/code&gt;. Run again later -- drift is the diff of fingerprints. A workflow that silently changed (member set identical, edges shifted) is the most dangerous kind because nobody's looking.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Confidence model
&lt;/h3&gt;

&lt;p&gt;Per-workflow risk score = weighted blast radius + runtime criticality + dependency fragility. Math weights live in &lt;code&gt;data/risk-config.json&lt;/code&gt;. Every number visible and explainable. Confidence decays with a 14-day half-life; execution feedback restores it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you actually see
&lt;/h2&gt;

&lt;p&gt;I ran Veris on a self-contained synthetic app with 17 planted bugs across 11 workflows. (&lt;a href="https://github.com/vighriday/Veris/tree/main/examples/demo-app" rel="noopener noreferrer"&gt;Demo app + ground truth here.&lt;/a&gt;)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Planted bug&lt;/th&gt;
&lt;th&gt;Workflow detected&lt;/th&gt;
&lt;th&gt;Probe fired&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JWT expiry check uses &lt;code&gt;&amp;lt;&lt;/code&gt; not &lt;code&gt;&amp;lt;=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;"Refresh token at the exact expiry boundary while two requests in flight"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe charge has no idempotency key&lt;/td&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;"Submit charge twice with the same idempotency key inside a 500ms window"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook handler not idempotent&lt;/td&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;"Sender delivers 50 retries of the same event id within 1 minute"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;updateProduct&lt;/code&gt; doesn't invalidate cache&lt;/td&gt;
&lt;td&gt;Caching&lt;/td&gt;
&lt;td&gt;"Invalidation event arrives out of order with the write"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker side-effects not idempotent&lt;/td&gt;
&lt;td&gt;Queue&lt;/td&gt;
&lt;td&gt;"Worker crashes after side effect but before ack"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N+1 in &lt;code&gt;getOrdersWithItems&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;"Two transactions update the same row; commit order non-deterministic"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/admin/users&lt;/code&gt; has no auth middleware&lt;/td&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;"Middleware order changes -- unauthenticated request reaches handler"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every planted bug got a matching probe. Veris doesn't read function bodies to find &lt;code&gt;&amp;lt;&lt;/code&gt; vs &lt;code&gt;&amp;lt;=&lt;/code&gt; -- it surfaces the workflow and the probe directive. The agent (or human) runs the probe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validated on real OSS repos
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Nodes&lt;/th&gt;
&lt;th&gt;Edges&lt;/th&gt;
&lt;th&gt;Workflows&lt;/th&gt;
&lt;th&gt;Probes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Express&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;2,400+&lt;/td&gt;
&lt;td&gt;~30k&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;3,696&lt;/td&gt;
&lt;td&gt;25,046&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NestJS&lt;/td&gt;
&lt;td&gt;3,712&lt;/td&gt;
&lt;td&gt;31,890&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;6,982&lt;/td&gt;
&lt;td&gt;40,027&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each one surfaced real bugs in Veris itself that I then fixed. The shakedown is the dev loop -- running Veris on Express revealed missing CommonJS extraction; Next.js revealed an O(N^2) edge explosion; Prisma revealed a Windows MAX_PATH crash; NestJS revealed AI false positives from CLI prompt scaffolding. All shipped fixes are in the CHANGELOG.&lt;/p&gt;




&lt;h2&gt;
  
  
  MCP integration
&lt;/h2&gt;

&lt;p&gt;17 tools exposed via stdio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;analyze_repository        export_behavioral_graph    analyze_pr_behavior
generate_verification_plan identify_unverified_behaviors
list_workflows            analyze_workflow           detect_drift
generate_adversarial_probes allocate_budget          what_if_revert
report_execution          confidence_history         node_history
export_onboarding         cross_repo_snapshot        register_repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire into any MCP-compatible client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"veris"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"veris-core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask the agent: &lt;em&gt;"List the workflows in this repo affected by my current PR. For the highest-risk one, give me the adversarial probes I should run before merging."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Also discoverable via the official &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP Registry&lt;/a&gt; as &lt;code&gt;io.github.vighriday/veris&lt;/code&gt;, and via &lt;code&gt;npx skills add vighriday/Veris&lt;/code&gt; for the skills.sh ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privacy + posture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MIT.&lt;/strong&gt; No paid tier. No license gating. No telemetry endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;VERIS_STATE_DISABLED=1&lt;/code&gt;&lt;/strong&gt; for zero-retention mode (skips all SQLite writes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first.&lt;/strong&gt; No network calls. State lives at &lt;code&gt;&amp;lt;projectRoot&amp;gt;/.veris/state.db&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No analytics.&lt;/strong&gt; No phone-home.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Funding: GitHub Sponsors when I get them. Until then, my own time.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;More language adapters (Python next, then Go).&lt;/li&gt;
&lt;li&gt;More workflow domains via community plugins (&lt;code&gt;.veris/plugins/*.js&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Tighter Cursor integration.&lt;/li&gt;
&lt;li&gt;Public registry of community probe libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this resonates: star the &lt;a href="https://github.com/vighriday/Veris" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, file issues with your false positives, or contribute a workflow rule.&lt;/p&gt;

&lt;p&gt;If you find a planted bug in the demo app that Veris missed -- open an issue with the workflow + missing probe. That's the loop.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/vighriday/Veris" rel="noopener noreferrer"&gt;github.com/vighriday/Veris&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NPM:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/veris-core" rel="noopener noreferrer"&gt;npmjs.com/package/veris-core&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Registry:&lt;/strong&gt; &lt;code&gt;io.github.vighriday/veris&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
