<?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: Ibn Web3</title>
    <description>The latest articles on DEV Community by Ibn Web3 (@ibnweb3).</description>
    <link>https://dev.to/ibnweb3</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%2F4158605%2Fbc71d605-66a5-45a3-a953-98ea932cd49b.jpg</url>
      <title>DEV Community: Ibn Web3</title>
      <link>https://dev.to/ibnweb3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ibnweb3"/>
    <language>en</language>
    <item>
      <title>Your bibliography might be standing on a retracted paper</title>
      <dc:creator>Ibn Web3</dc:creator>
      <pubDate>Sat, 17 Oct 2026 10:45:14 +0000</pubDate>
      <link>https://dev.to/ibnweb3/the-retraction-two-citations-back-building-a-citation-path-audit-on-falkordb-3cjk</link>
      <guid>https://dev.to/ibnweb3/the-retraction-two-citations-back-building-a-citation-path-audit-on-falkordb-3cjk</guid>
      <description>&lt;p&gt;&lt;strong&gt;I tried to predict retracted papers. I failed, and built something better.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bedrock-audit.ibnweb3lab.workers.dev/blog/" rel="noopener noreferrer"&gt;bedrock-audit.ibnweb3lab.workers.dev/blog&lt;/a&gt;. Built for the Graph Hacks hackathon (WeMakeDevs x FalkorDB), Track 01.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine you've spent a year on a literature review. Every source on your list checks out, because your reference manager flags nothing. But two steps back, one of those sources leans on a paper that was retracted years ago, and no tool you use was ever going to tell you.&lt;/p&gt;

&lt;p&gt;That gap is what &lt;strong&gt;Bedrock&lt;/strong&gt; is for. You paste a list of DOIs, and it follows every citation path, up to two steps, from your papers to retracted research. For each finding it shows the exact chain, the retraction date and the reasons on record. An AI agent explains the results and proposes edits. It can never apply them.&lt;/p&gt;

&lt;p&gt;This is the story of building it in three days: the parts that went well, the part that went badly, and the part where my own agent miscounted.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/bqgNoxluHjw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code (MIT):&lt;/strong&gt; &lt;a href="https://github.com/ibnweb3/bedrock" rel="noopener noreferrer"&gt;https://github.com/ibnweb3/bedrock&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://bedrock-audit.ibnweb3lab.workers.dev" rel="noopener noreferrer"&gt;https://bedrock-audit.ibnweb3lab.workers.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The idea: a question about paths, not rows
&lt;/h2&gt;

&lt;p&gt;Tools that flag retractions typically check the papers you cite. They don't ask what those papers cite. That second question is a graph question: start at a paper, follow &lt;code&gt;CITES&lt;/code&gt; edges, stop when you reach a retracted node, and report the path.&lt;/p&gt;

&lt;p&gt;Here is a real path from the corpus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your reference ──cites──▶ not flagged ──cites──▶ RETRACTED 2014-10-01
10.1016/j.neubiorev       10.1111/ejn.13603      10.1016/j.jpsychires
.2019.09.040                                     .2014.06.018
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middle paper isn't retracted, which is exactly why nothing warns you.&lt;/p&gt;

&lt;p&gt;Two rules shaped everything. First, &lt;strong&gt;Bedrock states facts about a fixed corpus, it doesn't guess.&lt;/strong&gt; A paper with "no exposure found" is never called safe, only not found within two hops. Second, &lt;strong&gt;the person decides.&lt;/strong&gt; The agent proposes, a human approves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: public, and kept that way
&lt;/h2&gt;

&lt;p&gt;The retractions come from the Retraction Watch database, distributed by Crossref. The citations come from OpenAlex, which is CC0. I pulled works from 213 journals, 251,645 articles in all, and joined them to 61,299 retracted works. The hackathon rules ask for public data only, so authors were hashed with a secret salt in memory before anything touched disk, and names, emails and ORCIDs were dropped. The graph I publish has &lt;strong&gt;no author data at all&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retraction Watch ─┐
                  ├─▶ FalkorDB (Cypher) ─▶ audit queries ─▶ read-only agent ─▶ you approve
OpenAlex (CC0) ───┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graph produces every finding. The model only writes the summary, and a person approves any change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph: a small model, sharp queries
&lt;/h2&gt;

&lt;p&gt;The model is deliberately plain: &lt;code&gt;Paper&lt;/code&gt; nodes carrying &lt;code&gt;doi&lt;/code&gt;, &lt;code&gt;retracted_at&lt;/code&gt; and &lt;code&gt;retraction_reasons&lt;/code&gt;, &lt;code&gt;Venue&lt;/code&gt; nodes, and two relationships, &lt;code&gt;CITES&lt;/code&gt; and &lt;code&gt;PUBLISHED_IN&lt;/code&gt;. A retraction is a property, not its own node, which keeps the graph small and makes "as of this date" filters trivial. The two-hop question is one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;UNWIND&lt;/span&gt; &lt;span class="n"&gt;$ids&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Paper&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CITES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;x:&lt;/span&gt;&lt;span class="n"&gt;Paper&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;x.retracted_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CITES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;Paper&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r.retracted_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CITES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x.doi&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.doi&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.retracted_at&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.retraction_reasons&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;$limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it aloud: the paper cites &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt; isn't retracted, &lt;code&gt;x&lt;/code&gt; cites a retracted &lt;code&gt;r&lt;/code&gt;, and the paper doesn't already cite &lt;code&gt;r&lt;/code&gt; directly. Everything Bedrock shows, the direct hits, the two-hop paths and the "papers cited alongside it" suggestions for replacements, is a query of this size, run by FalkorDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 59-second query: the planner started in the wrong place
&lt;/h2&gt;

&lt;p&gt;My first audit of nine papers took &lt;strong&gt;59 seconds&lt;/strong&gt;. The data was small and the question was simple, so something was off. The cause: FalkorDB's planner began at the 61,299 retracted papers and worked back toward my input, instead of starting at the few papers I'd asked about. The fix was the humble &lt;code&gt;WITH p&lt;/code&gt; you can see above. It forces the traversal to start from the input papers and expand their handful of references.&lt;/p&gt;

&lt;p&gt;Re-timed after the fix: 0.05 to 0.4 seconds for nine papers, and 0.15 to 0.4 seconds for 150.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What I took from it:&lt;/strong&gt; a graph database makes the shape of the query matter more than the size of the data. When an indexed lookup can anchor a traversal, say so in the query. It's the difference between a demo and a product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The test that failed: I tried to predict retractions
&lt;/h2&gt;

&lt;p&gt;Bedrock's first idea was ambitious: use graph signals, like who a paper cites, who it shares authors with and which community it sits in, to predict which papers will be retracted. Before looking at any result I wrote down the pass mark: the graph method had to beat the best simple baseline, and the gap had to survive removing the largest journal. Then I ran it on 134,798 articles from journals with a retraction history, 140 of which were retracted within the next two years (cutoff 2020-12-31).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Average precision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Random ranking&lt;/td&gt;
&lt;td&gt;0.0012&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Journal's past retraction rate&lt;/td&gt;
&lt;td&gt;0.0066&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cites a retracted work (one hop)&lt;/td&gt;
&lt;td&gt;0.0066&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two citation hops&lt;/td&gt;
&lt;td&gt;0.0021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All graph signals combined&lt;/td&gt;
&lt;td&gt;0.0041&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Journal history plus graph&lt;/td&gt;
&lt;td&gt;0.0074&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The verdict: no reliable improvement.&lt;/strong&gt; The best combination beat the journal baseline by less than the uncertainty. The 140 retractions were concentrated in 20 to 27 journals, so the study has low power, and the multi-hop signals did not beat their one-hop versions. The single signal that stood out, "this paper already cites a retracted one", made a paper 5 to 12 times more likely than average to be retracted later, depending on the cutoff. That is a lookup, not a forecast.&lt;/p&gt;

&lt;p&gt;I could have shipped a "retraction risk score" anyway. It would have looked impressive and been unearned. Instead I changed the product: Bedrock stopped predicting and started proving. Exact provenance, with the failed test on the front page. That decision, more than any query, is what I'd defend in front of anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent: useful, on a short leash
&lt;/h2&gt;

&lt;p&gt;The agent runs on Groq's free tier with &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;. It has three tools, &lt;code&gt;audit_bibliography&lt;/code&gt;, &lt;code&gt;explain_finding&lt;/code&gt; and &lt;code&gt;find_dois&lt;/code&gt;, and every one only reads. There's no edit tool, and a test makes sure it stays that way. The guardrails came from watching it misbehave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding.&lt;/strong&gt; Every DOI in the answer must come from your text or a tool result. Models retyped DOIs and corrupted some, so an invented one now raises a visible warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness.&lt;/strong&gt; It once skipped a flagged paper. If the summary omits one, the agent gets a single retry; anything still missing is appended in plain words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts.&lt;/strong&gt; The same run opened with "one retracted paper" when the audit had found two. Models miscount, so the opening line, "Checked 9 DOIs: 2 retracted, 2 cite a retracted work directly...", is written by code from the audit, never by the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback.&lt;/strong&gt; The findings are computed independently of the model. If the key is missing or Groq is rate-limited, you still get the full audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the approval step: the server re-runs the audit itself and only applies proposal ids it generated. The browser can't hand it a result to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping it: 2.5 million nodes down to 318,942
&lt;/h2&gt;

&lt;p&gt;The research graph, with authors and two derived edge types for the backtest (1.69M &lt;code&gt;COAUTHOR&lt;/code&gt; and 2.68M &lt;code&gt;BIB_COUPLED&lt;/code&gt;, both built in Cypher inside FalkorDB), reaches about 2.5 million nodes. Too big to hand to anyone. But an exposure path can only follow &lt;code&gt;CITES&lt;/code&gt; between papers I hold, so for the audit most of it is dead weight.&lt;/p&gt;

&lt;p&gt;So I built a slim graph with just papers, venues and the citations between held papers: &lt;strong&gt;318,942 nodes and 530,894 edges.&lt;/strong&gt; Then I tested the claim, not just made it. A script audited 2,501 DOIs on both graphs and compared the complete results: statuses, paths, reasons, coverage and alternatives. &lt;strong&gt;Zero differences.&lt;/strong&gt; (That test also caught a real bug: self-citations had inflated a reference count by exactly one in 37 papers.)&lt;/p&gt;

&lt;p&gt;The slim graph ships as a 17 MB &lt;code&gt;DUMP&lt;/code&gt; snapshot in the repo. In a container limited to 512 MB and a tenth of a CPU, it restores in about a minute and sits at roughly 300 MB. A judge can run the whole thing with:&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/ibnweb3/bedrock &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;bedrock
docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; demo up &lt;span class="nt"&gt;--build&lt;/span&gt; app
&lt;span class="c"&gt;# open http://127.0.0.1:8765&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked that from a fresh clone with no keys: a no-cache build took just over two minutes and the app answered five seconds after starting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It knows only its corpus: 213 journals, 251,645 articles and the papers they cite, with a Retraction Watch snapshot from Oct 15, 2026.&lt;/li&gt;
&lt;li&gt;It looks two hops back. "No exposure found" means nothing was found within that, not that a paper is sound.&lt;/li&gt;
&lt;li&gt;Reasons and dates come from Retraction Watch. A flag is a prompt for a person to read, never a finding about an author.&lt;/li&gt;
&lt;li&gt;The live demo runs on one small AWS server, so it can be slow if many people try it at once. The one-command setup above always works.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Write the pass mark before you run the test. It's the only reason a failure was a result instead of an argument.&lt;/li&gt;
&lt;li&gt;In a graph database, anchor your traversals. Query shape beats data size.&lt;/li&gt;
&lt;li&gt;Let code state the facts and let the model write the prose. Check its numbers anyway.&lt;/li&gt;
&lt;li&gt;Prove your shortcuts. "The slim graph is equivalent" became a claim I could stand behind only after the 2,501-DOI test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this was built
&lt;/h2&gt;

&lt;p&gt;I built it solo in three days, with Claude Code writing most of the code and docs from my instructions. I chose the idea, set the rules, approved the experiment's size, decided the pivot from prediction to provenance, picked Groq for the agent, and signed off every publication and deployment. The decision log is in the repo at &lt;code&gt;docs/DECISIONS.md&lt;/code&gt;. The code is MIT licensed; the data keeps its own terms (Retraction Watch via Crossref, OpenAlex CC0).&lt;/p&gt;

&lt;p&gt;Photo: sedimentary rock layers at Zabriskie Point, Death Valley, by Brigitte Werner, CC0 (&lt;a href="https://commons.wikimedia.org/wiki/File:Sedimentary_Rock_Layers_Zabriskie_Point_Death_Valley_USA.jpg" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>database</category>
      <category>ai</category>
      <category>python</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
