<?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: Bradley Grubbs</title>
    <description>The latest articles on DEV Community by Bradley Grubbs (@bgrubbs1).</description>
    <link>https://dev.to/bgrubbs1</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%2F4215823%2Fa66f6720-19df-49ab-a825-883b195f6a43.png</url>
      <title>DEV Community: Bradley Grubbs</title>
      <link>https://dev.to/bgrubbs1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bgrubbs1"/>
    <language>en</language>
    <item>
      <title>HauntScope: the alarm, the silence, and the ghost</title>
      <dc:creator>Bradley Grubbs</dc:creator>
      <pubDate>Fri, 30 Oct 2026 21:47:39 +0000</pubDate>
      <link>https://dev.to/bgrubbs1/hauntscope-the-alarm-the-silence-and-the-ghost-22f5</link>
      <guid>https://dev.to/bgrubbs1/hauntscope-the-alarm-the-silence-and-the-ghost-22f5</guid>
      <description>&lt;p&gt;By Brad Grubbs.&lt;/p&gt;

&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;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;At 08:01, a learned detector flags a combination of sensor readings that a simple rule passes. At 05:28, the same model stays quiet while temperature reaches 61.749 °C, well outside its training normal. At 06:16, both detectors raise an alarm even though the generator injected nothing.&lt;/p&gt;

&lt;p&gt;HauntScope puts those three minutes in front of you. Read the evidence, predict which detector will flag, reveal the answer, and try changing one reading. The point is to discover what an alarm can support before turning it into a story.&lt;/p&gt;

&lt;p&gt;The setting is a fictional pump room on the night shift. Four synthetic channels describe its imagined behavior. An open-source Isolation Forest supplies learned scores; a simple baseline asks whether any individual sensor is far enough from its training mean.&lt;/p&gt;

&lt;p&gt;This is my interpretation of &lt;em&gt;Ghost in the Machine&lt;/em&gt;: an unexplained trace invites a story, and a persuasive detector can become part of that story too. An optional fiction layer uses hand-authored templates, clearly labeled and off by default. Its spooky prose is never a diagnosis or model explanation.&lt;/p&gt;

&lt;p&gt;All readings and labels are synthetic. No microphone, machine, customer data, or industrial safety claim is involved.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/bgrubbs1/hauntscope/main/media/hauntscope-v2-walkthrough.mp4" rel="noopener noreferrer"&gt;Watch the 76-second walkthrough (MP4)&lt;/a&gt; · &lt;a href="https://github.com/bgrubbs1/hauntscope/blob/main/media/hauntscope-v2-walkthrough.mp4" rel="noopener noreferrer"&gt;Video repository page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The silent walkthrough records the actual local interface, with captions added below it. It shows the predictions, real re-scoring, reset, failure cases and unchanged comparison.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;python preview.py&lt;/code&gt; from the project directory and open the printed &lt;code&gt;http://127.0.0.1:.../&lt;/code&gt; address. The README explains setup. This local preview serves only named project assets on loopback. Direct &lt;code&gt;file://&lt;/code&gt; launch is not the verified route.&lt;/p&gt;

&lt;p&gt;The short investigation has three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose a case and inspect its sensor readings before revealing the detector outcomes or injected label.&lt;/li&gt;
&lt;li&gt;Choose “Forest only,” “Baseline only,” “Both detectors,” or “Neither detector,” then select “Reveal the evidence.”&lt;/li&gt;
&lt;li&gt;In the 08:01 case, test one narrow hypothesis by changing only current and re-scoring the hypothetical row with the frozen model.&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%2Fsvifwtydycobhx2icxq1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsvifwtydycobhx2icxq1.jpg" alt="Current-only sensitivity result: original and hypothetical scores, unchanged cutoffs, and an explicit warning that the edit cannot establish a physical cause." width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/bgrubbs1/hauntscope/main/output/playwright/case-study.jpg" rel="noopener noreferrer"&gt;Full console view&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The minute where the detectors disagree
&lt;/h3&gt;

&lt;p&gt;At 08:01, vibration is 2.138 mm/s, temperature is 50.365 °C, and acoustic level is 57.987 dB. All three exceed their training means. Current, at 6.043 A, falls below its training mean. None of the four absolute standardized deviations crosses the baseline cutoff of 1.999, so the baseline does not flag the minute.&lt;/p&gt;

&lt;p&gt;The forest's score is 0.609, just over its 0.599 cutoff. Revealing the synthetic answer key shows that the generator altered current. This makes a useful teaching case: a combination can look unusual even when no individual reading crosses the marginal rule. It does not establish which feature caused the forest's decision.&lt;/p&gt;

&lt;p&gt;Across the entire altered-current window, the forest flags 16 of 30 minutes and the baseline flags one. These are neighboring, correlated minutes from one constructed event—not 30 independent incidents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change one reading, keep the model still
&lt;/h3&gt;

&lt;p&gt;The sensitivity experiment asks a smaller question than “what caused the anomaly?”: &lt;strong&gt;what happens to these detector scores if current changes while the other three readings stay fixed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose a bounded current value or select “Use reference current.” Neither action changes the forest, training means and standard deviations, or calibrated cutoffs. The app scores a separate hypothetical row through the exported learned trees locally in the browser. The original log, labels, and benchmark stay untouched.&lt;/p&gt;

&lt;p&gt;For the reference button, the exporter finds the normal training row closest in standardized vibration, temperature, and acoustic level, excluding current from that match. Training index 89 supplies a reference current of 10.384690 A. Substituting only that value produces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;th&gt;Forest score / flag&lt;/th&gt;
&lt;th&gt;Baseline score / flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original 08:01&lt;/td&gt;
&lt;td&gt;6.043 A&lt;/td&gt;
&lt;td&gt;0.609 / flagged&lt;/td&gt;
&lt;td&gt;1.647 / unflagged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypothetical reference current&lt;/td&gt;
&lt;td&gt;10.385 A&lt;/td&gt;
&lt;td&gt;0.519 / unflagged&lt;/td&gt;
&lt;td&gt;1.654 / unflagged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A lower score would establish sensitivity to that input under this fitted model. It would not establish a repaired fault, the correct physical current, or a causal mechanism. Even a value inside a sensor's training range can form an unfamiliar combination with the other readings. The experiment therefore labels edits hypothetical and keeps uncertainty visible.&lt;/p&gt;

&lt;p&gt;Resetting to the original reading supplies a no-change control. If the scores or flags do not change meaningfully, the useful outcome is “no decisive change,” not a more elaborate story. For a physical cause, the available evidence remains insufficient: there is no independent load measurement, maintenance record, or reference sensor.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/bgrubbs1/hauntscope" rel="noopener noreferrer"&gt;HauntScope source code — MIT licensed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository contains the generator, detector, baseline, exported forest, browser scorer, log, tests, and interface. No API key, paid service, language model download, or GPU is required.&lt;/p&gt;

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

&lt;p&gt;The core is &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.IsolationForest.html" rel="noopener noreferrer"&gt;scikit-learn's IsolationForest&lt;/a&gt;. Its randomized partition trees tend to isolate unusual observations in fewer steps. HauntScope negates &lt;code&gt;score_samples&lt;/code&gt;, making larger displayed scores more unusual. These scores are not probabilities of failure, danger, or haunting.&lt;/p&gt;

&lt;p&gt;Training uses 2,048 synthetic normal observations. A separate set of 1,024 normal observations establishes each detector's cutoff at its own 98th percentile. The demonstration log contains 720 minutes, including 92 injected-event minutes. Only the four numeric sensor columns enter scoring; the labels are used for evaluation and reveal controls.&lt;/p&gt;

&lt;p&gt;The forest has 128 trees and at most 256 training observations per tree. Training, calibration, the demonstration log, and the forest have fixed seeds: 1729, 2718, 31415, and 42. The sensitivity experiment uses the original model configuration, calibration, and evaluation fixture.&lt;/p&gt;

&lt;p&gt;Python remains responsible for fitting and exporting the model. The browser's small scorer traverses the exported trees for hypothetical inputs. The original timeline displays the saved Python results. Sensor z-deviations help inspect the readings; they are not formal feature attributions.&lt;/p&gt;

&lt;p&gt;Against Python, the JavaScript scorer matched all flags across 2,157 test vectors, including unseen normal data and split-boundary probes. Maximum forest-score error was 1.11 × 10⁻¹⁶; baseline scores matched exactly. That comparison runs in Node, separately from browser interaction checks.&lt;/p&gt;

&lt;p&gt;Automated tests cover the numerical pipeline and export. Browser interaction checks are executed separately and saved as a checklist; that receipt is not a shipped executable browser regression suite. The evidence report distinguishes those checks and their limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The baseline wins overall
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detector&lt;/th&gt;
&lt;th&gt;True alerts&lt;/th&gt;
&lt;th&gt;False alerts&lt;/th&gt;
&lt;th&gt;Correct normal&lt;/th&gt;
&lt;th&gt;Missed labeled minutes&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IsolationForest&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;620&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;86.2%&lt;/td&gt;
&lt;td&gt;54.3%&lt;/td&gt;
&lt;td&gt;0.667&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum absolute z-score&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;622&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;90.6%&lt;/td&gt;
&lt;td&gt;63.0%&lt;/td&gt;
&lt;td&gt;0.744&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both cutoffs produce 21 exceedances among the 1,024 normal calibration observations. On the separate demonstration log, the baseline finds more labeled minutes and raises fewer false alerts. The learned detector contributes an interesting disagreement, but the overall result does not justify preferring it on accuracy.&lt;/p&gt;

&lt;p&gt;The other two cases make that limitation concrete. At &lt;strong&gt;05:28&lt;/strong&gt;, temperature is about 5.82 standard deviations above its training mean. The baseline flags it; the forest's 0.570 score stays below its cutoff. Across that whole heat window, the forest misses all 28 minutes while the baseline detects 23.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;06:16&lt;/strong&gt;, temperature is 40.663 °C, about 2.23 standard deviations below the training mean. Both methods flag it, but its generator label is normal. An unusual reading can arise from the normal process. “Insufficient evidence to name a cause” is a legitimate conclusion, including when two detectors agree.&lt;/p&gt;

&lt;p&gt;These are controlled synthetic results, not field performance estimates. Training and evaluation share a deliberately simple normal generator, and adjacent samples are correlated. A future evaluation would need separately designed operating regimes and real measurement evidence before making engineering claims.&lt;/p&gt;

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

&lt;p&gt;Openness makes the disagreement inspectable. The data recipe, trained trees, baseline, and thresholds are available together. Exporting the forest enables local what-if inference and creates an obligation to verify agreement with Python.&lt;/p&gt;

&lt;p&gt;With the documented dependencies present, rebuilding and previewing use local files and the CPU. There are no API charges or external runtime requests. Readers control the experiment that produced the scores, including its less flattering result.&lt;/p&gt;

&lt;p&gt;scikit-learn, NumPy, and SciPy are credited in the versioned license inventory. Codex assisted implementation, tests, interface work, and this article; it is not a runtime dependency. The scenarios, interface, and fictional templates are project-specific work.&lt;/p&gt;

&lt;p&gt;The machine room can keep its ghost story. The investigation has to keep its uncertainty.&lt;/p&gt;

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