<?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: Ernesto Arias</title>
    <description>The latest articles on DEV Community by Ernesto Arias (@ernesto_arias_148b35bc25d).</description>
    <link>https://dev.to/ernesto_arias_148b35bc25d</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%2F2767341%2Fda332cd5-8211-479b-8a77-0a328ddb7111.jpg</url>
      <title>DEV Community: Ernesto Arias</title>
      <link>https://dev.to/ernesto_arias_148b35bc25d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ernesto_arias_148b35bc25d"/>
    <language>en</language>
    <item>
      <title>Two audits of my own knowledge graph found two unrelated silent failures</title>
      <dc:creator>Ernesto Arias</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:02:17 +0000</pubDate>
      <link>https://dev.to/ernesto_arias_148b35bc25d/two-audits-of-my-own-knowledge-graph-found-two-unrelated-silent-failures-4nfp</link>
      <guid>https://dev.to/ernesto_arias_148b35bc25d/two-audits-of-my-own-knowledge-graph-found-two-unrelated-silent-failures-4nfp</guid>
      <description>&lt;p&gt;I'm a systems administrator in the Dominican Republic, and for the past several months I've been building &lt;a href="https://github.com/ernestoariasdiaz/animus-ai" rel="noopener noreferrer"&gt;ANIMUS&lt;/a&gt; on the side — a Rust-based autonomous knowledge graph system running over a real regulatory corpus (Dominican banking regulation PDFs, 800+ documents from the Superintendencia de Bancos). No team, no funding, just evenings and weekends after a day job in application support.&lt;/p&gt;

&lt;p&gt;Twice now, auditing my own pipeline has turned up a failure mode I wasn't looking for. Neither one threw an error. Both were silently "successful" right up until I went and actually counted things that should have been fine.&lt;/p&gt;

&lt;p&gt;I think the pattern is more interesting than either bug on its own, so here's both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit #1 — 52% of the graph was duplicate nodes
&lt;/h3&gt;

&lt;p&gt;I'd been re-running the ingestion pipeline after fixes, with no uniqueness check in place. The same regulatory circular got added to the graph multiple times under slightly different labels — once from the original ingestion, again after a re-run, sometimes a third time after a partial reprocess.&lt;/p&gt;

&lt;p&gt;Node count looked great. Over half of it was redundant. The graph &lt;em&gt;looked&lt;/em&gt; bigger and more capable than it actually was, and retrieval was quietly skewed toward whichever duplicate happened to match on a given query.&lt;/p&gt;

&lt;p&gt;The fix was a deduplication pass keyed on document similarity, not just exact filename matches — duplicates didn't always share a name. I wrote up the full debugging story when I found it: &lt;a href="https://dev.to/ernesto_arias_148b35bc25d/-how-i-found-out-52-of-my-knowledge-graph-was-duplicates-and-what-i-did-about-it-3coh"&gt;How I Found Out 52% of My Knowledge Graph Was Duplicates&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit #2 — 32% of the source corpus never made it into the graph at all
&lt;/h3&gt;

&lt;p&gt;This one was worse, because the failure was invisible &lt;em&gt;by design&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;My PDF ingestion script marks a document as successfully processed if the extracted text is longer than 100 characters. That's a reasonable-looking sanity check — empty or near-empty extractions usually mean something went wrong. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anything under that threshold gets silently dropped, with no integration into the graph&lt;/li&gt;
&lt;li&gt;the source file still gets moved into the "processed" folder regardless of whether it succeeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the folder said "done" for every single file. The graph quietly had 262 out of 817 documents — 32% of the corpus — missing, with no log entry explaining why, because as far as the pipeline was concerned, nothing had failed.&lt;/p&gt;

&lt;p&gt;Root cause: my text extraction (PyMuPDF) only reads text that's already embedded in the PDF. A meaningful chunk of the corpus turned out to be scanned or digitally signed documents with no real text layer — old circulars digitized as images, signed PDFs with the signature flattening the page into a scan. Those come back nearly empty, fail the 100-character check, and vanish without a trace.&lt;/p&gt;

&lt;p&gt;Adding an OCR fallback (pytesseract + Tesseract, Spanish language pack) recovered 259 of the 262. The first pass used a low render resolution and produced text with a lot of noise; bumping the DPI and adding basic image preprocessing before OCR helped, though I haven't fully solved the noise problem — more on that below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The follow-up problem I haven't solved yet
&lt;/h3&gt;

&lt;p&gt;Some documents that &lt;em&gt;passed&lt;/em&gt; the 100-character check — meaning my pipeline considered them fine — turned out to have corrupted native text anyway. My best guess is that the original PDF was digitized with low-quality OCR years ago, by whoever scanned it originally, and that corrupted text got permanently baked into the file. PyMuPDF extracts it faithfully; the extraction isn't broken, the source already was.&lt;/p&gt;

&lt;p&gt;One real example from the corpus: a circular about reporting suspicious transactions to the financial intelligence unit extracts with "Entidades" rendered as "Entídodes" and "financiero" as "finonciero." A human can read past that. Keyword-based retrieval can't — the words in the question never match the corrupted words in the document, so the system reports "no information found" about a document that's sitting right there in the graph.&lt;/p&gt;

&lt;p&gt;Detecting "text exists but is garbage" turns out to be a meaningfully different problem than detecting "text doesn't exist." I don't have a clean check for it yet — my first instinct (looking for repeated consecutive words, a common OCR artifact) caught exactly one case out of hundreds. I'd genuinely like to hear how other people doing retrieval over scanned or historical document corpora have approached this, because it feels like it should be a solved problem somewhere and I just haven't found the right keyword to search for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this did to actual benchmark numbers
&lt;/h3&gt;

&lt;p&gt;I run a 36-question evaluation set against the corpus — factual questions answerable from a single document, multi-source questions requiring two or more related documents, and adversarial "trap" questions where the correct answer is "this information isn't in the corpus," designed to catch hallucination.&lt;/p&gt;

&lt;p&gt;Corpus coverage went from 68.5% to 99.6% of source documents after the OCR recovery pass. Overall benchmark score went from 58.3% to 63.9%. Honesty on the trap questions hit 100% — zero hallucinated answers.&lt;/p&gt;

&lt;p&gt;Multi-source questions, interestingly, didn't move much in aggregate. Several individually went from completely failing to partially succeeding (finding one of two required documents instead of zero), but that was offset by a handful of regressions traced to a separate, unrelated issue: low-value "reflection" nodes — essentially the system's own past answers, including "I couldn't find anything" responses — were crowding out real source documents in a retrieval step that's biased toward recent nodes. That's a different rabbit hole than the one in this post, but it's a good reminder that fixing one silent failure can expose another one sitting right behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disclosure
&lt;/h3&gt;

&lt;p&gt;This post was drafted with the help of an AI assistant, working from my own debugging notes and logs. The investigation, the fixes, and the numbers are mine — I'm not a native English speaker and writing this entirely from scratch in English isn't something I can do quickly, so I used AI assistance for the writing itself.&lt;/p&gt;




&lt;p&gt;Paper and dataset: &lt;a href="https://doi.org/10.5281/zenodo.20674981" rel="noopener noreferrer"&gt;Zenodo&lt;/a&gt;&lt;br&gt;
Code: &lt;a href="https://github.com/ernestoariasdiaz/animus-ai" rel="noopener noreferrer"&gt;github.com/ernestoariasdiaz/animus-ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've hit either of these — silent ingestion failures, or "passed the check but the content is garbage" — I'd like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dataengineering</category>
      <category>rag</category>
      <category>rust</category>
    </item>
    <item>
      <title>How I Found Out 52% of My Knowledge Graph Was Duplicates (and What I Did About It)</title>
      <dc:creator>Ernesto Arias</dc:creator>
      <pubDate>Thu, 25 Jun 2026 00:50:17 +0000</pubDate>
      <link>https://dev.to/ernesto_arias_148b35bc25d/-how-i-found-out-52-of-my-knowledge-graph-was-duplicates-and-what-i-did-about-it-3coh</link>
      <guid>https://dev.to/ernesto_arias_148b35bc25d/-how-i-found-out-52-of-my-knowledge-graph-was-duplicates-and-what-i-did-about-it-3coh</guid>
      <description>&lt;p&gt;I've spent the last several months building &lt;a href="https://github.com/ernestoariasdiaz/animus-ai" rel="noopener noreferrer"&gt;ANIMUS&lt;/a&gt;, an autonomous system in Rust that gives a local LLM persistent memory. The idea is simple: a knowledge graph that grows on its own, cycle after cycle, as the system reads documents, detects gaps in its knowledge, and fills them in.&lt;/p&gt;

&lt;p&gt;For months, the metric I watched most closely was the node count of the graph. It kept climbing. I felt good about that.&lt;/p&gt;

&lt;p&gt;Until I ran a full audit and found out that &lt;strong&gt;52% of those nodes were undetected duplicates&lt;/strong&gt;. Of 1,892 reported nodes, only 911 were actually unique.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did this happen?
&lt;/h2&gt;

&lt;p&gt;ANIMUS's autonomous loop actively looks for "gaps" — holes in its knowledge that the system decides to fill on its own. The problem: an overly aggressive filter was excluding certain categories from the gap pool, which trapped the system in a loop of re-exploring the same ~40 topics for thousands of cycles. Each pass generated content that was &lt;em&gt;similar&lt;/em&gt; but not identical to the last — different enough to avoid triggering any exact-duplicate check, but substantially the same information rephrased.&lt;/p&gt;

&lt;p&gt;The node count kept climbing. Actual knowledge, not so much.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rust engineering side
&lt;/h2&gt;

&lt;p&gt;The fix wasn't magic, it was audit work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reopening the gap filter that had been closed too aggressively, so the system would explore genuinely new topics instead of repeating itself.&lt;/li&gt;
&lt;li&gt;Fixing a recency bias in the semantic search (&lt;code&gt;Brain::search&lt;/code&gt;): it walked the graph from node 0 with &lt;code&gt;.take(2)&lt;/code&gt;, which meant it almost always returned stale content from earlier versions of the system. A simple &lt;code&gt;.rev()&lt;/code&gt; fixed it.&lt;/li&gt;
&lt;li&gt;Building an "auto-census" process that runs every 37 cycles and generates real statistics about the graph by category — so the system itself (and I) could see with numbers, not intuition, whether it was growing in a healthy way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Along the way, I also migrated the inference engine: from a Python wrapper to a &lt;code&gt;llama-server.exe&lt;/code&gt; launched directly from Rust, and from the original model to a quantized Gemma 4 E2B, running at ~77 tokens/second on a consumer GPU (RTX 3050, 4GB). None of this required the cloud or paid APIs — everything runs locally.&lt;/p&gt;

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

&lt;p&gt;The most valuable part of this whole episode wasn't fixing the bug. It was realizing that &lt;strong&gt;a metric that only goes up never warns you that something is wrong&lt;/strong&gt;. Node count was a proxy for "the system is learning," but optimizing that one proxy, with nothing to balance it, ended up producing the opposite: inflated content, not new knowledge.&lt;/p&gt;

&lt;p&gt;ANIMUS now runs on several cross-checked signals (verified uniqueness, recency-weighted relevance, source validation) instead of one vanity metric. If two signals start to diverge, the system stops and re-audits instead of continuing to generate.&lt;/p&gt;

&lt;p&gt;If you're curious about the full picture (architecture, benchmarks, comparison against a simple vector RAG baseline), the technical paper is open access with a DOI: &lt;a href="https://doi.org/10.5281/zenodo.20674981" rel="noopener noreferrer"&gt;10.5281/zenodo.20674981&lt;/a&gt;. Code is on &lt;a href="https://github.com/ernestoariasdiaz/animus-ai" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;ANIMUS is an independent project, developed in Santo Domingo, Dominican Republic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"This post was written with the assistance of an LLM, based on my own project, data, and experience."&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rust</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
