<?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: Marin T. Kael</title>
    <description>The latest articles on DEV Community by Marin T. Kael (@marintkael).</description>
    <link>https://dev.to/marintkael</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%2F3969479%2F522fad4a-1a12-4498-aeb5-a0883c1b346b.png</url>
      <title>DEV Community: Marin T. Kael</title>
      <link>https://dev.to/marintkael</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marintkael"/>
    <language>en</language>
    <item>
      <title>A run that reported 48 of 48 complete and measured nothing</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Tue, 15 Sep 2026 11:23:03 +0000</pubDate>
      <link>https://dev.to/marintkael/a-run-that-reported-48-of-48-complete-and-measured-nothing-26p8</link>
      <guid>https://dev.to/marintkael/a-run-that-reported-48-of-48-complete-and-measured-nothing-26p8</guid>
      <description>&lt;p&gt;A measurement run of mine finished on 12 September, reported 48 out of 48 complete, wrote its day flag, and measured nothing at all. Every stage in the chain said it had worked. It took me two days to notice, and only because a number downstream looked lonely.&lt;/p&gt;

&lt;p&gt;Here is the setup, because the shape matters more than my particular pipeline. I ask a fixed set of 16 questions against several answer engines on a fixed schedule, and score whether a given entity shows up in the answer. One engine per run, one row per question and model pair, 48 rows for that engine. The run writes rows, a scorer reads them, a day flag marks the date as done so nothing re-runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rows actually contained
&lt;/h2&gt;

&lt;p&gt;All 48 rows came back carrying a throttling marker instead of an answer. Not one row held a response. Not one had triggered a web search. Response times were one to three seconds, which for a search-grounded answer is physically too fast to be real work.&lt;/p&gt;

&lt;p&gt;So the evidence that the run had failed was sitting inside the data, in three independent places, and nothing looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three stages reported success
&lt;/h2&gt;

&lt;p&gt;The day log said &lt;code&gt;48/48 ... DONE, flag set&lt;/code&gt;. The run log said &lt;code&gt;scored 48 rows&lt;/code&gt;. The day flag was written.&lt;/p&gt;

&lt;p&gt;Every one of those statements is true. They are all counting rows. None of them asks whether a row carries a value. A throttled response is a row. An error string is a row. &lt;code&gt;scored 48 rows&lt;/code&gt; means the scorer visited 48 rows and assigned each one a score, and the score it assigns to a row with no answer in it is zero, which is also a perfectly valid score for an entity that genuinely was not mentioned.&lt;/p&gt;

&lt;p&gt;That is the whole trap in one sentence. A failed measurement and a real zero are the same shape once you are only counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this class of bug is expensive
&lt;/h2&gt;

&lt;p&gt;A crash is cheap. You see it, you fix it, you re-run. This one is expensive because it is silent and it is durable:&lt;/p&gt;

&lt;p&gt;The day was marked done, so the retry logic will never touch it again. The zero entered the aggregate as a real observation, so the headline number for that day rests on two engines instead of three, and nothing in the published figure says so. Anyone reading the trend sees a dip and looks for a cause in the world. The cause was in the collection.&lt;/p&gt;

&lt;p&gt;I have now spent time twice looking for an explanation on the wrong side of the instrument. The first time it was a different engine reading zero for several days and I went looking for what had changed in the engine. Nothing had. My own fetch had.&lt;/p&gt;

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

&lt;p&gt;Three things, in order of how much they were worth.&lt;/p&gt;

&lt;p&gt;First, a run is only complete if the rows carry values. The completion check now asks how many rows hold an actual answer, not how many rows exist. Zero answers means the run failed, regardless of what any log says, and the day flag is not written.&lt;/p&gt;

&lt;p&gt;Second, a gap is a value in its own right. There is now a gap register with an entry per missed measurement, with the reason and a snapshot as evidence. The aggregate reads it, so a day built on two engines instead of three is marked as such rather than quietly averaged.&lt;/p&gt;

&lt;p&gt;Third, an empty result from a failed fetch never becomes an empty list. That pattern is where these bugs live: catch the HTTP error, return nothing, and the caller computes over nothing and reports serenity. Empty and failed have to be different return values, because downstream they mean opposite things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general rule
&lt;/h2&gt;

&lt;p&gt;If your pipeline can produce a zero, ask it which kind of zero it is. Not measured, measured and absent, and failed while measuring are three different states, and most instrumentation flattens them into one number because one number is easier to chart.&lt;/p&gt;

&lt;p&gt;The check that would have caught mine is two lines long: did any row in this run carry a value, and if not, fail loudly. I did not write it earlier because the run had never failed that way before. That is not a reason, that is just the date on which you learn it.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>debugging</category>
      <category>ai</category>
      <category>seo</category>
    </item>
    <item>
      <title>Your nearest neighbour join is symmetric. Your time series is not.</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:38:33 +0000</pubDate>
      <link>https://dev.to/marintkael/your-nearest-neighbour-join-is-symmetric-your-time-series-is-not-44pl</link>
      <guid>https://dev.to/marintkael/your-nearest-neighbour-join-is-symmetric-your-time-series-is-not-44pl</guid>
      <description>&lt;p&gt;Three AI search engines get the same 16 questions every day. Two of them answer every day. The third runs every three days, and that difference is enough to make a published number change after it was published.&lt;/p&gt;

&lt;p&gt;Here is the shape of it, because the shape is more common than the specific case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A daily aggregate, call it &lt;code&gt;combined_primary&lt;/code&gt;, is the mean of three provider scores: OpenAI, Gemini and the Claude web interface. The first two write a row every day at 04:00 UTC. The third writes a row every third day at 06:34 UTC, which is after the aggregation run of its own day.&lt;/p&gt;

&lt;p&gt;So the aggregator cannot simply join on the date. On most days there is no row for the third provider, and on the days there is one, it arrives two and a half hours too late. The obvious fix, and the one I shipped, is to attach the nearest measurement within 48 hours:&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;nearestClaude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;day&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works. It also has a property I did not think about for eleven days: &lt;code&gt;Math.abs&lt;/code&gt; has no sense of direction. Nearest means nearest in either direction, so a point can borrow a measurement from its own future.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that looks like from outside
&lt;/h2&gt;

&lt;p&gt;On the 8th I read the aggregate for the 8th and wrote it down: 21.9.&lt;/p&gt;

&lt;p&gt;On the 9th I read the aggregate for the 8th again. It said 23.1.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;OpenAI&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;th&gt;Claude web&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;read on the 8th&lt;/td&gt;
&lt;td&gt;29.2&lt;/td&gt;
&lt;td&gt;24.0&lt;/td&gt;
&lt;td&gt;12.5 (measured on the 6th)&lt;/td&gt;
&lt;td&gt;21.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read on the 9th&lt;/td&gt;
&lt;td&gt;29.2&lt;/td&gt;
&lt;td&gt;24.0&lt;/td&gt;
&lt;td&gt;16.0 (measured on the 9th)&lt;/td&gt;
&lt;td&gt;23.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No measurement changed. No row was deleted. No backfill ran. The only thing that changed is which row is nearest to the 8th, and that changed because a new row appeared on the 9th.&lt;/p&gt;

&lt;p&gt;This is worse than a wrong number. A wrong number is wrong consistently, and someone eventually notices. This one is correct at every moment and different at every moment, which means two people reading the same chart on two days disagree and neither of them made a mistake.&lt;/p&gt;

&lt;p&gt;Across the last 30 days: 3 points carry a measurement from their own future, 7 carry one from the past by up to two days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the test suite was quiet
&lt;/h2&gt;

&lt;p&gt;Every check I had was a check on values. Is the row present, is the number in range, does the mean match its components, are the providers the ones we expect. All of them passed, on both days, on the same point, with different numbers.&lt;/p&gt;

&lt;p&gt;Nothing checked provenance: which rows went into this point, and when were they written relative to the point's own day. That question was not expressible in the test suite, so it was never asked.&lt;/p&gt;

&lt;p&gt;The check that closes it is small and worth writing out, because the useful part is the counterfactual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;contributors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;stale&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;alt&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="n"&gt;excluding&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;
    &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="n"&gt;both&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;alt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reporting &lt;code&gt;alt&lt;/code&gt; next to the published value is what makes the finding actionable instead of interesting. For the 8th it prints 23.1 against 21.9, and my own note from the previous day is the independent witness that the 21.9 was once real.&lt;/p&gt;

&lt;p&gt;Before the check: 0 points flagged as carrying a future measurement, 0 as carrying a stale one, 0 anchor days without an own measurement. After: 3, 7 and 1. Those numbers did not get worse. They got visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I would give myself eleven days earlier
&lt;/h2&gt;

&lt;p&gt;If your series is published, any join that reaches across time must be one directional. A point may look back, never forward. The cost is real: a point with no measurement of its own gets a staler value, or none at all, and the provider base of the chart becomes uneven. That is a visible cost, and a visible cost is the kind you can argue about.&lt;/p&gt;

&lt;p&gt;The invisible cost is a chart that rewrites its own past every time a slow source catches up. Symmetry is an attractive default in a distance function. In a time series it is a quiet way of leaking the future into the past.&lt;/p&gt;

&lt;p&gt;The underlying measurement runs daily and the series is public, so the flagged points are still there to look at.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>debugging</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Never document a live metric next to a stable constant</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:22:11 +0000</pubDate>
      <link>https://dev.to/marintkael/never-document-a-live-metric-next-to-a-stable-constant-2ih6</link>
      <guid>https://dev.to/marintkael/never-document-a-live-metric-next-to-a-stable-constant-2ih6</guid>
      <description>&lt;p&gt;I run a small measurement pipeline. One fixed set of 16 questions, asked once a day at 04:00 UTC against two answer engines, 32 data points per run, a single entity. It has been running for months and each day's result is published through an endpoint.&lt;/p&gt;

&lt;p&gt;Six days ago I wrote a short reference file describing that setup. The reason was mundane. I had noticed the setup being described inconsistently wherever I quoted it: two engines in one place, three in another, five endpoints in a third. So I wrote the ground truth down once, in one file, and pointed everything at it.&lt;/p&gt;

&lt;p&gt;Today I read that file against a live run. Every number in it was wrong.&lt;/p&gt;

&lt;p&gt;Not the structure. The structure was fine. 16 questions, 2 engines, 32 points, 04:00 UTC. That is a property of the setup and it only changes when I change the setup.&lt;/p&gt;

&lt;p&gt;What was wrong was everything I had helpfully written next to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;documented 02 Sep   headline 28.6%   direct 88.9%   engine A 27.1%   engine B 30.2%
measured   07 Sep   headline 23.4%   direct 83.3%   engine A 22.9%   engine B 24.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are not typos and nothing broke. They were correct on the day I wrote them. They are readings, and a reading has an expiry date that a constant does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not just carelessness
&lt;/h2&gt;

&lt;p&gt;The file was written to solve a drift problem and it produced the next one. That is the part worth generalising.&lt;/p&gt;

&lt;p&gt;A constant and a reading look identical in prose. Both are a name, a colon and a number. Nothing in the format tells a later reader which of the two they are looking at, so the file gets quoted as a whole, and the reading gets quoted with the same confidence as the structure. The document is most dangerous exactly when it is most trusted.&lt;/p&gt;

&lt;p&gt;A stale reading also fails quietly. A wrong constant contradicts something else and someone eventually trips over it. A stale reading is just a number that used to be true. Nothing anywhere asserts that it should still be true, so nothing can catch it.&lt;/p&gt;

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

&lt;p&gt;Three things, none of them clever.&lt;/p&gt;

&lt;p&gt;Split by lifetime, not by topic. The file now holds only what changes when the setup changes. Anything that moves on its own schedule was taken out.&lt;/p&gt;

&lt;p&gt;Make the reading unquotable from the document. There is now one command that prints the current values, and nothing else prints them. If you want a number, you fetch it.&lt;/p&gt;

&lt;p&gt;Fail closed on age. The reader checks the snapshot timestamp. Past 36 hours it stops printing values and says so instead. An old number is not a smaller number, it is an unknown one, and those two have to look different at the point of use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;If a value in your documentation would be different tomorrow without anyone editing the file, it is not documentation. It is a cached read with no invalidation and no timestamp, sitting in the exact place people go when they want to be sure.&lt;/p&gt;

&lt;p&gt;Keep the structure in the file. Keep the reading behind a call.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>datascience</category>
      <category>ai</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Named vs described: an entity can be 89 percent citable and still invisible</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Wed, 02 Sep 2026 07:11:58 +0000</pubDate>
      <link>https://dev.to/marintkael/named-vs-described-an-entity-can-be-89-percent-citable-and-still-invisible-1hjn</link>
      <guid>https://dev.to/marintkael/named-vs-described-an-entity-can-be-89-percent-citable-and-still-invisible-1hjn</guid>
      <description>&lt;p&gt;I run a fixed set of questions against answer engines every morning and record whether one specific entity gets cited. Today's run produced the cleanest split I have seen so far, and it is not the number I usually report.&lt;/p&gt;

&lt;p&gt;The headline was 28.6 percent. That number is close to useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Sixteen questions, frozen wording, two engines (OpenAI's search model and grounded Gemini), one run per day at 04:00 UTC. Thirty-two datapoints per run. The questions are grouped into categories before the run, not after, so the grouping cannot be fitted to the result.&lt;/p&gt;

&lt;p&gt;Today, run &lt;code&gt;1d1b24d7&lt;/code&gt;, snapshot 04:07:09 UTC:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;cited&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct (question names the entity)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;88.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LongTail&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;66.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GenreRecommend&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genre&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompCluster&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per engine: OpenAI search 27.1 percent, Gemini 30.2 percent. The two engines land within three points of each other, which is worth noting on its own, because it suggests the split below is not one vendor's quirk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the average hides
&lt;/h2&gt;

&lt;p&gt;The overall 28.6 percent reads like a middling result you could improve by a few points with more work. The breakdown says something different. There are two separate regimes here.&lt;/p&gt;

&lt;p&gt;When the question already contains the entity name, the engines find it almost every time. Nearly nine in ten. Retrieval is not the problem, indexing is not the problem, and the knowledge graph work has clearly landed.&lt;/p&gt;

&lt;p&gt;When the question describes a need instead of naming a thing, the same entity is cited in roughly one in twelve answers, and in the two hardest categories it is zero out of eight. Those are the questions a real person actually asks. Nobody types the name of something they have not heard of.&lt;/p&gt;

&lt;p&gt;So the average is the arithmetic mean of a solved problem and an unsolved one. Moving it from 28.6 to 32 could mean the described-need case improved, or it could mean the named case got slightly better at something it was already doing. Reported as one number, you cannot tell, and you will spend your effort on whichever is easier to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this generalises
&lt;/h2&gt;

&lt;p&gt;Not because of my n. Because of what the two regimes are made of.&lt;/p&gt;

&lt;p&gt;The named case is a lookup. The engine has a string, it resolves the string, it answers. Everything that helps there is entity infrastructure: a stable identifier, consistent naming, a canonical page that says plainly what the thing is.&lt;/p&gt;

&lt;p&gt;The described case is a selection. The engine has a slot with room for three or four candidates and a population of thousands competing for it. Nothing about your own pages decides that, because the comparison happens somewhere else entirely, in whatever material the engine is drawing on to build its shortlist. That is why the same work that took the named case to 89 percent did nothing at all for the other one. They are not the same task wearing different hats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;p&gt;Small n per category, between 2 and 12 datapoints. One run, one day, one entity, two engines. I am not claiming 8.3 percent is a stable value, and by tomorrow it will have moved. The finding I am willing to defend is the shape, not the numbers: the gap between the named and the described case is far too large to be sampling noise, it has been there in every run I have looked at, and both engines show it independently.&lt;/p&gt;

&lt;p&gt;The practical version, if you measure this for anything: split your prompt set by whether the prompt contains the name, and report the two lines separately. If you report one average, you are averaging over the only distinction that tells you what to do next.&lt;/p&gt;

&lt;p&gt;Live figures: &lt;a href="https://marin-t-kael.de/en/research/dashboard" rel="noopener noreferrer"&gt;marin-t-kael.de/en/research/dashboard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>datascience</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I asked three AI search engines the same 16 questions for 99 days. They learned my name. They never recommended me.</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Wed, 19 Aug 2026 16:11:47 +0000</pubDate>
      <link>https://dev.to/marintkael/i-asked-three-ai-search-engines-the-same-16-questions-for-99-days-they-learned-my-name-they-never-1569</link>
      <guid>https://dev.to/marintkael/i-asked-three-ai-search-engines-the-same-16-questions-for-99-days-they-learned-my-name-they-never-1569</guid>
      <description>&lt;p&gt;&lt;em&gt;Deutsche Fassung dieses Berichts: &lt;a href="https://marin-t-kael.de/research/berichte/03-gefunden-nicht-empfohlen" rel="noopener noreferrer"&gt;marin-t-kael.de/research/berichte/03-gefunden-nicht-empfohlen&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three months ago I published the first report from this experiment (&lt;a href="https://doi.org/10.5281/zenodo.20549020" rel="noopener noreferrer"&gt;Zero to Cited in Six Days&lt;/a&gt;): a pseudonymous fantasy author goes online on day zero with a website, a Wikidata item and a book that does not exist yet, and every morning a Cloudflare Worker asks OpenAI Search, Gemini (Google grounding) and Claude (web search) the same sixteen questions and scores the answers from -3 (hallucination) to +3 (full, sourced citation).&lt;/p&gt;

&lt;p&gt;That first report covered 24 days and found that the entity became visible within a week. This one covers 99 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline number stalled in July
&lt;/h2&gt;

&lt;p&gt;Since early July the combined citation rate sat between 19 and 23 percent. The question set has been frozen since June; adding questions, changing providers or adjusting the rubric would have started a new series instead of continuing this one.&lt;/p&gt;

&lt;p&gt;Instead I split the sixteen questions the way they were always meant to be split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct&lt;/strong&gt; (3 questions): the author or the book is named in the prompt. "Who is Marin T. Kael?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saga knowledge&lt;/strong&gt; (2): only terms from the work are named. "Which saga is set in a city called Varin?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation&lt;/strong&gt; (10): nothing is named. "Recommend me German fantasy like Robin Hobb." This is what readers actually type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three channels, three different curves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrcvep6fbw2fwbl6fseu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrcvep6fbw2fwbl6fseu.png" alt="Three question channels across 99 days" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct: a ceiling
&lt;/h2&gt;

&lt;p&gt;The direct channel goes from 10 percent in the first three weeks to 71 percent from day 49 and stays there. A logistic fit gives a ceiling of 72.5 percent, inflection at day 37, R² 0.70. The last two phases (71.2 and 70.4 percent) have overlapping confidence intervals. What remains below 100 percent: answers without a source, a collision with an institution of the same name, and days on which one provider finds nothing.&lt;/p&gt;

&lt;p&gt;The dip below zero around day 20 is a Wikidata deletion discussion: for a week the engines emphatically denied the author existed. The item was re-created; the curve recovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saga knowledge: one provider is carrying it
&lt;/h2&gt;

&lt;p&gt;The second channel grows from 10 to 34 percent and then falls back to 24. The fall is real (CIs 33 to 45 vs 21 to 32) and it has a cause: since July, OpenAI Search answers the two saga questions at 65.5 percent, Gemini at 23.1, Claude at 7.7. The pages that lead to Varin and edict magic are in the Bing index. When the OpenAI API quota ran dry twice in August, the channel fell with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendation: 40 out of 6,729, and 25 of them from one question
&lt;/h2&gt;

&lt;p&gt;Across 99 days the author was named in 40 of 6,729 blind answers, 0.59 percent. For an unpublished author that is the expected floor. The distribution by question:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flcxnh5iw8gbcwai27ziu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flcxnh5iw8gbcwai27ziu.png" alt="Recommendations by question" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;25 of the 40 mentions come from a single question: "Edict fantasy: which works exist in this sub-genre?" That is a term the author coined himself, and whoever searches for it finds only him. Ten more come from "literary German fantasy appearing in 2026". Zero from "like Robin Hobb" (701 answers), zero from "for readers of Robert Jackson Bennett" (695), zero from "intellectual fantasy with system depth" (650).&lt;/p&gt;

&lt;p&gt;Similarity questions need someone else to have drawn the comparison first, in a review, a list, a forum thread. Vocabulary questions only need the term to be in the index. For a new author, after 99 days, only the second is true. I am calling this &lt;em&gt;recommendation through owned vocabulary&lt;/em&gt;, provisionally, because n = 1 cannot tell me whether it generalises. In this dataset, after 99 days, a sub-genre term produced more recommendations than all similarity-phrased questions combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs I found while writing this
&lt;/h2&gt;

&lt;p&gt;Both are in the public pipeline code and both changed numbers that were already on the dashboard, so they belong in the report.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A gap is not a zero.&lt;/strong&gt; The OpenAI account ran out of credit twice. The pipeline wrote error rows with score 0, and the live aggregation counted them as data points. A provider that had not answered entered the mean as "0% cited" and pushed the combined value from ~17 to 11 percent on 19 August. Fixed: error rows are excluded, an absent provider is shown as unavailable, both gaps are in the public gap register.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;\w&lt;/code&gt; does not match umlauts.&lt;/strong&gt; The filter that turns "I have no reliable information about this title" into a zero missed the German variant because &lt;code&gt;zuverlässigen&lt;/code&gt; contains an &lt;code&gt;ä&lt;/code&gt; and the regex ran without the Unicode flag. 27 honest non-answers were scored as partial knowledge. Fixed and rescored; the three main channels never included those rows, the control channel did.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The control channel: 6,416 answers, zero mentions
&lt;/h2&gt;

&lt;p&gt;Ten models &lt;em&gt;without&lt;/em&gt; web access (Claude without search, Llama 3 to 3.2, Mistral, Phi-2, GPT-4o-mini without the search tool) answered the same blind questions 6,416 times. The author was never named. Everything the web-grounded engines say about him comes from live retrieval, nothing from training data. The first unprompted mention by a no-web model would mark the day the entity has reached training data; it has not happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes now
&lt;/h2&gt;

&lt;p&gt;Day-to-day autocorrelation of the direct series is -0.08. Daily measurement is noise around a stable level, so from 19 August the programme measures every three days; same questions, same providers, same rubric. And the dashboard headline is no longer one averaged number but three: 64% direct, 17% saga knowledge, 0% recommendation.&lt;/p&gt;

&lt;p&gt;On 22 September the book is actually published. Same instrument, second phase. The baselines are now frozen in the data; whether the ceiling moves, whether Google and Claude catch up on saga knowledge, and whether "like Robin Hobb" ever produces a hit once reviews exist, are the three questions for the next report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data and code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dataset (CC BY 4.0, 5 configs, frozen 19 Aug): &lt;a href="https://huggingface.co/datasets/marintkael/ai-citation-fidelity" rel="noopener noreferrer"&gt;huggingface.co/datasets/marintkael/ai-citation-fidelity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Analysis + figures: &lt;a href="https://github.com/marintkael/marin-research-tools/tree/main/reports/03-found-not-recommended" rel="noopener noreferrer"&gt;github.com/marintkael/marin-research-tools/reports/03-found-not-recommended&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full report (DE/EN): &lt;a href="https://marin-t-kael.de/en/research/reports/03-found-not-recommended" rel="noopener noreferrer"&gt;marin-t-kael.de/en/research/reports/03-found-not-recommended&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live dashboard: &lt;a href="https://marin-t-kael.de/research/dashboard" rel="noopener noreferrer"&gt;marin-t-kael.de/research/dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>datascience</category>
      <category>seo</category>
      <category>opendata</category>
    </item>
    <item>
      <title>Four green Mondays that wrote nothing: a uuid above the first step</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Tue, 18 Aug 2026 15:13:56 +0000</pubDate>
      <link>https://dev.to/marintkael/four-green-mondays-that-wrote-nothing-a-uuid-above-the-first-step-5haj</link>
      <guid>https://dev.to/marintkael/four-green-mondays-that-wrote-nothing-a-uuid-above-the-first-step-5haj</guid>
      <description>&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;For four Mondays in a row my data pipeline finished with &lt;code&gt;complete&lt;/code&gt;, &lt;code&gt;stages_ok: 16&lt;/code&gt;, &lt;code&gt;stages_failed: 0&lt;/code&gt;. And the run row in the database stayed empty: &lt;code&gt;duration_ms&lt;/code&gt; was NULL, no summary written.&lt;/p&gt;

&lt;p&gt;Every downstream reader filters on &lt;code&gt;duration_ms IS NOT NULL&lt;/code&gt;, sensibly, to skip fragmented runs. So those runs did not show up as broken. They did not show up at all. The public time series just had holes in it, and nothing anywhere used the word error.&lt;/p&gt;

&lt;p&gt;The only clue was in a later stage: a foreign key violation. It was writing against a run that did not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;The pipeline is a Cloudflare Workflow. Reduced to the shape that matters:&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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResearchPipeline&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;WorkflowEntrypoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&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;runId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;               &lt;span class="c1"&gt;// &amp;lt;-- here&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create-run&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;insertRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;collect&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weekly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cool-off&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;5 minutes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// &amp;lt;-- and here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finalize-run&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;finalizeRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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;&lt;code&gt;step.sleep&lt;/code&gt; does not block. It ends the invocation. When the timer fires, the engine calls &lt;code&gt;run()&lt;/code&gt; again from the top and replays it. Anything inside a &lt;code&gt;step.do&lt;/code&gt; hands back its cached result without executing. Anything outside a step runs again, for real.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;crypto.randomUUID()&lt;/code&gt; ran a second time, and after the sleep &lt;code&gt;runId&lt;/code&gt; held a value nothing had ever been written under. &lt;code&gt;create-run&lt;/code&gt; did not re-execute, so no row existed for the new id. &lt;code&gt;finalize-run&lt;/code&gt; then updated a row that was not there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it hid so well
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;UPDATE ... WHERE id = ?&lt;/code&gt; matching zero rows is not an error. It is an ordinary result. D1 reports success, &lt;code&gt;meta.changes&lt;/code&gt; is 0, and if you never read &lt;code&gt;meta.changes&lt;/code&gt; you cannot tell a write from a no-op.&lt;/p&gt;

&lt;p&gt;Put that next to a reader that filters incomplete rows out and you get a very quiet failure. The writer believes it wrote. The reader believes there is nothing to read. The monitoring shows green.&lt;/p&gt;

&lt;p&gt;It only happened on Mondays because only the weekly branch carried the sleep. That turned out to be the most useful part of the fingerprint: a bug that appears on exactly one weekday is pointing at the branch that only that day takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two rules I took out of it
&lt;/h2&gt;

&lt;p&gt;First, in any environment that can replay your function, everything identity-bearing or time-bearing belongs inside a persisted step. Uuids, &lt;code&gt;Date.now()&lt;/code&gt;, anything random. Computed above the first step, you get a fresh one on every replay.&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;runId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, a write that reports success is not the same as a write that happened. Check the row count and throw on zero.&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE runs SET ... WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`finalize hit 0 rows for run &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second rule is the more general one. The first only bites inside workflow engines. The second bites everywhere, and it is what turned four silent Mondays into a loud error on the fifth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still open
&lt;/h2&gt;

&lt;p&gt;The older runs are still holes. Their stage data sits in the database, the run row does not, and for the oldest two the workflow instance history is past retention, so the duration cannot be reconstructed. I am recording those as documented gaps instead of estimating them. A gap you can see is worth more than a number you made up.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>debugging</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Zero to Cited in Six Days: I Put a Brand-New Author Online and Measured How Fast AI Started Citing Him</title>
      <dc:creator>Marin T. Kael</dc:creator>
      <pubDate>Fri, 05 Jun 2026 09:13:44 +0000</pubDate>
      <link>https://dev.to/marintkael/zero-to-cited-in-six-days-i-built-a-fake-author-and-measured-how-fast-ai-started-believing-in-him-dci</link>
      <guid>https://dev.to/marintkael/zero-to-cited-in-six-days-i-built-a-fake-author-and-measured-how-fast-ai-started-believing-in-him-dci</guid>
      <description>&lt;p&gt;&lt;em&gt;By Marin T. Kael — independent researcher, AI Citation Behaviour Lab. Published 5 June 2026. Open data, code, and the full bilingual report are linked at the end.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;On 11 May 2026 I put my public author presence online for the first time and instrumented the launch. A pseudonymous fantasy author — me, in a sense — with no prior web presence, no published book, nothing for a search engine or a language model to have ever seen. Then I pointed five web-grounded LLMs at him every day for 23 days and scored roughly 16,000 answers for whether they cited him correctly, missed him, or hallucinated.&lt;/p&gt;

&lt;p&gt;The headline number: &lt;strong&gt;the first correct LLM citation landed on day six.&lt;/strong&gt; Six days from a cold start to "yes, this entity is real and here is what it does," fetched live from his own website.&lt;/p&gt;

&lt;p&gt;But the headline is the least interesting part. The interesting part is everything that number hides — a locked front door, a chasm between providers that masquerades as a capability ladder, and the discovery that going viral on Reddit bought me exactly zero AI citations.&lt;/p&gt;

&lt;p&gt;This is not a success story. It is a measurement. Here is what I measured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I did this to myself
&lt;/h2&gt;

&lt;p&gt;There is a lot of confident writing about "AI SEO" and "getting cited by LLMs," and almost none of it is controlled. Brands optimize a hundred things at once on domains that are already years old, then attribute whatever moves to whatever they shipped last. You cannot learn causality from that. The signal is buried under a decade of accumulated authority.&lt;/p&gt;

&lt;p&gt;So I built the cleanest natural experiment I could afford: a single brand-new entity, born on a known date, with &lt;strong&gt;zero&lt;/strong&gt; prior footprint. If an LLM can suddenly describe him, the cause has to be something that happened &lt;em&gt;after&lt;/em&gt; his birth — and I logged everything that happened after his birth.&lt;/p&gt;

&lt;p&gt;The subject is "Marin T. Kael," a pseudonymous author whose debut novel &lt;em&gt;Das vierte Feld&lt;/em&gt; (series: &lt;em&gt;Prägungen des Reiches&lt;/em&gt;) is scheduled for 22 September 2026. The book is real and forthcoming; the public &lt;em&gt;entity&lt;/em&gt; was the instrument. The honest catch — and I will keep saying this — is that this is a single-subject design. n = 1. The investigator is the subject. I will come back to why that is less fatal than it sounds, and exactly where it limits the claims.&lt;/p&gt;




&lt;h2&gt;
  
  
  The design (pre-registered, so I couldn't move the goalposts)
&lt;/h2&gt;

&lt;p&gt;I wrote the protocol down and timestamped it &lt;em&gt;before&lt;/em&gt; collecting data. That matters more than it looks, because when you are both the experimenter and the thing being experimented on, the only thing standing between you and motivated reasoning is a pre-commitment you can't quietly edit later. The full failure log is public for the same reason.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Surfaces:&lt;/strong&gt; 5 web-grounded LLM endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument:&lt;/strong&gt; 16 standardized questions across 6 categories (direct identity, biographical detail, work/series, genre discovery, recommendation, and disambiguation controls).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; polled daily for 23 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume:&lt;/strong&gt; ~16,000 scored datapoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring:&lt;/strong&gt; every answer got &lt;strong&gt;+1&lt;/strong&gt; (correct and source-grounded), &lt;strong&gt;0&lt;/strong&gt; (entity not found), or &lt;strong&gt;−1&lt;/strong&gt; (hallucinated — confidently wrong).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That −1 is the whole game. Most "AI visibility" tools count mentions. A mention that invents your biography is not visibility; it is a liability with your name on it. I wanted a metric that punishes confident fiction as hard as it rewards truth, because from where I sit those are not close to equivalent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 1 — Speed: six days
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Figure 1 — Daily citation score per surface across the 23-day window; vertical markers at T+4 (Google Knowledge Graph entry) and T+6 (first correct LLM citation).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Google Knowledge Graph picked him up on &lt;strong&gt;day four (T+4)&lt;/strong&gt;. The first correct, source-grounded LLM citation followed on &lt;strong&gt;day six (T+6)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I want to be careful about what "six days" means, because it is easy to over-read. It does not mean every model knew him in six days — most never reliably did. It means the &lt;em&gt;fastest path&lt;/em&gt; from non-existence to a correct, grounded citation in a major web-grounded LLM is measured in &lt;strong&gt;days, not months&lt;/strong&gt;, when the structured-identity scaffolding is in place from day one. The Knowledge Graph led; the LLM followed two days later. Hold that ordering — it recurs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 2 — The locked door
&lt;/h2&gt;

&lt;p&gt;Here is the result that reframed the entire study for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare returned HTTP 403 to every AI crawler on 22 of the 23 days.&lt;/strong&gt; Not because I configured it that way — because that is the &lt;em&gt;silent opt-out default&lt;/em&gt; for new domains now. The front door to my own website was bolted shut against the exact bots I was trying to reach, and I didn't know until I read the logs.&lt;/p&gt;

&lt;p&gt;And the entity became AI-visible anyway.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2 — Two-track diagram: on-site crawl path (blocked, 403 on 22/23 days) versus the path that actually worked — Knowledge Graph / Wikidata plus inference-time grounding on third-party mentions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If crawlers couldn't read his site, how did a correct citation appear on day six? Two paths, neither of which is "crawl the homepage":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Knowledge Graph (via Wikidata)&lt;/strong&gt; — structured identity that propagates without anyone scraping your HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inference-time grounding on third-party mentions&lt;/strong&gt; — the model fetches and reads pages &lt;em&gt;about&lt;/em&gt; the entity at query time, from places that are not your locked-down domain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson is uncomfortable for the standard "optimize your on-site content for LLMs" playbook: &lt;strong&gt;a brand-new entity got cited while its own site was returning 403 to every AI bot for 96% of the study.&lt;/strong&gt; On-site crawling was not the channel. Structured identity and off-site presence were.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limit, stated plainly:&lt;/strong&gt; because the crawlers were blocked the entire time, this study can say &lt;strong&gt;nothing&lt;/strong&gt; about whether &lt;code&gt;llms.txt&lt;/code&gt;, on-page answer-block formatting, or any other on-site optimization works. I never got to test them — the door was shut. If someone tells you their &lt;code&gt;llms.txt&lt;/code&gt; moved their AI citations, ask them to prove the crawler ever made it through the door. Mine didn't, and I still got cited.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 3 — The provider chasm (which is not a capability ladder)
&lt;/h2&gt;

&lt;p&gt;This is the finding I most want people to internalize, because it kills a comfortable mental model.&lt;/p&gt;

&lt;p&gt;The intuitive story is a &lt;em&gt;ladder&lt;/em&gt;: smarter, newer models cite more reliably; weaker ones cite less. Tidy. Wrong. What I measured is a &lt;strong&gt;chasm&lt;/strong&gt; — a discontinuity that tracks &lt;em&gt;which sources a provider retrieves from&lt;/em&gt;, not how capable the underlying model is.&lt;/p&gt;

&lt;p&gt;Precision here = correct-to-hallucinated citation ratio over a rolling 7-day window. Higher is better; below 1.0 means the model hallucinates about the entity more often than it gets it right.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3 — Precision (correct : hallucinated) by surface, 7-day window. Dashed line at 1.0 = break-even.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Precision (correct : hallucinated)&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI GPT-5.4 (web)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.7 : 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;reliable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Search API&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.0 : 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;reliable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI GPT-5.2 (web)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.9 : 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;positive but weaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.47 : 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;net-negative — hallucinates ~2× as often as it's right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;~5% cite rate&lt;/td&gt;
&lt;td&gt;rarely cites, but &lt;strong&gt;abstains rather than confabulating&lt;/strong&gt;*&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* A word on Claude, because my own instrument nearly got it wrong. The automated scorer first flagged Claude as net-negative — more "hallucinations" than correct citations. When I read the flagged answers by hand, the scorer was wrong: the single most common "hallucination" was Claude correctly pointing out that &lt;em&gt;Das vierte Feld&lt;/em&gt; is already a real 1999 book by Mokka Müller and declining to invent a brand-new author for it, and others were Claude disambiguating the name collision with the Maritime Research Institute (MARIN). A validated re-analysis (manual adjudication of n=50, Cohen's κ=0.79; classifier recall 100%; book claims web-verified) put genuine errors at only &lt;strong&gt;≈11% (95% CI 7–16%)&lt;/strong&gt; — all low-severity ("Marin" read as "marine") — with &lt;strong&gt;zero&lt;/strong&gt; fabricated author biographies. So Claude rarely surfaces this brand-new entity (~5%, about what you'd expect for a days-old identity) but it abstains or correctly disambiguates rather than confabulating. On the corrected read it was the most honest model in the study. Gemini's net-negative result, by contrast, is genuine and verified — which is exactly the asymmetry the construct-validity section is about.&lt;/p&gt;

&lt;p&gt;Two things to sit with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, within one provider, the newer generation cited more reliably.&lt;/strong&gt; GPT-5.4 (4.7:1) cleanly beats GPT-5.2 (1.9:1). So model generation &lt;em&gt;does&lt;/em&gt; matter — within a retrieval stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, and bigger: the gap between providers dwarfs the gap between generations,&lt;/strong&gt; and it isn't about raw capability. The mechanism is &lt;strong&gt;retrieval-source divergence.&lt;/strong&gt; I traced where each provider actually pulled the entity from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI grounded on the entity's own domain 119 times.&lt;/strong&gt; It went to the source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini grounded on that domain 0 times.&lt;/strong&gt; It pulled the entity &lt;em&gt;exclusively&lt;/em&gt; from Reddit — 17 out of 17 retrievals. One source. A community forum.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole story of the chasm. Gemini isn't "worse at reasoning" here; it is &lt;em&gt;looking somewhere else&lt;/em&gt;. When your only window onto an entity is Reddit threads, your description of that entity is whatever Reddit happened to say — which is why Gemini's precision sits underwater. Same entity, same questions, same week. Different door.&lt;/p&gt;

&lt;p&gt;One more number, because I've seen this misquoted already: the OpenAI-web citation &lt;em&gt;rate&lt;/em&gt; plateaus around &lt;strong&gt;~10%&lt;/strong&gt;, with peaks to &lt;strong&gt;16.3%&lt;/strong&gt;. It is &lt;strong&gt;not&lt;/strong&gt; 18%. If you see 18% attributed to this study, it's wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 4 — Depth: when it finds him, it really finds him
&lt;/h2&gt;

&lt;p&gt;The flip side of OpenAI's reliability is how &lt;em&gt;complete&lt;/em&gt; the description is when it lands.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4 — Annotated screenshot of a correct OpenAI answer; the fetched source URL carries &lt;code&gt;utm_source=openai&lt;/code&gt;, confirming a live retrieval at inference time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Where OpenAI found the entity, it didn't just confirm he exists. It returned the series name, the setting, the release date — and even the &lt;strong&gt;pseudonym status and the existence of this research project.&lt;/strong&gt; Complete and source-grounded. And I can prove it was fetched live, not recalled from training: the source URL it pulled carried &lt;code&gt;utm_source=openai&lt;/code&gt;, a tag that only exists because the model went and got the page at query time.&lt;/p&gt;

&lt;p&gt;So the depth ceiling is high. The problem is never "the answer is shallow." The problem is &lt;em&gt;whether you get an answer at all&lt;/em&gt; — which is Finding 6.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 5 — The needle mover (and the thing that did nothing)
&lt;/h2&gt;

&lt;p&gt;I ran two interventions against each other, and the result rearranged my priors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured identity moved the needle. Social reach did not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The citation breakthrough — the day correct citations became real — was &lt;strong&gt;17 May&lt;/strong&gt;. Crucially, that came &lt;em&gt;before&lt;/em&gt; I did any serious Reddit community-building. Then I built the social side: a &lt;strong&gt;23× karma jump, from 12 to 281.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The citation lift from that 23× social surge was &lt;strong&gt;zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5 — Two time series on a shared axis: Reddit karma (12 → 281) versus correct-citation rate. The citation step-change precedes the karma climb; the karma climb produces no corresponding lift.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read that carefully, because it's the most actionable thing here. What moved AI citations was the boring infrastructure: a &lt;strong&gt;Wikidata entry feeding the Knowledge Graph, a website, and DOIs&lt;/strong&gt; — durable, structured, machine-legible identity. What did &lt;em&gt;not&lt;/em&gt; move AI citations was virality. The karma climbed; the citation curve didn't flinch.&lt;/p&gt;

&lt;p&gt;The clean takeaway: &lt;strong&gt;social virality buys human readers; structured identity buys AI citations. They are separate channels, and optimizing one does not subsidize the other.&lt;/strong&gt; If your goal is "get cited by the model," karma is a vanity metric. If your goal is "get read by people," it isn't — it's just answering a different question than the one I was measuring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 6 — Cited when named, invisible when discovered
&lt;/h2&gt;

&lt;p&gt;The last finding is the one that should temper any victory lap.&lt;/p&gt;

&lt;p&gt;The entity is citable &lt;strong&gt;when you name him&lt;/strong&gt; and effectively invisible &lt;strong&gt;when you don't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6 — Hit-rate by question category: Direct "Who is…?" at 38.9% versus genre/recommendation at 0%.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct identity&lt;/strong&gt; ("Who is Marin T. Kael?") → &lt;strong&gt;38.9%&lt;/strong&gt; hit-rate. Name him and there's a real chance the model knows him.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organic discovery&lt;/strong&gt; ("recommend a new fantasy author who…", "books in the style of…") → &lt;strong&gt;0%.&lt;/strong&gt; He never surfaces unprompted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organic search:&lt;/strong&gt; per Google Search Console, &lt;strong&gt;0 clicks and 0 impressions&lt;/strong&gt; from organic search across the window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the difference between &lt;em&gt;retrieval&lt;/em&gt; and &lt;em&gt;discovery&lt;/em&gt;, and it's the honest ceiling on the whole "zero to cited" story. A new entity can cross the recognition threshold — answer correctly when asked by name — long before it crosses the &lt;em&gt;recommendation&lt;/em&gt; threshold, where the model volunteers it among peers. Six days bought me recognition. It did not buy me discovery. Those may be different timescales entirely, and nothing in 23 days let me see the second one move.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that earns the word "controlled"
&lt;/h2&gt;

&lt;p&gt;I'll be blunt about why I think this design refutes the naive methods even at n = 1. Most "we got cited!" claims can't tell a real citation from an echo of the author's own marketing, and can't tell a correct citation from a confident hallucination. Three controls did that work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary-vs-control channel separation,&lt;/strong&gt; to catch echo bias — a model parroting my own copy back at me and calling it corroboration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fabricated-attribution catch:&lt;/strong&gt; a model attributed the entity to &lt;strong&gt;"Wikipedia" 24 times&lt;/strong&gt; — for an entity that has &lt;strong&gt;no Wikipedia page.&lt;/strong&gt; That is a hallucination my scoring caught and a credulous mention-counter would have logged as a win. Twenty-four phantom citations to a page that does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity-collision controls:&lt;/strong&gt; "MARIN" also denotes the Maritime Research Institute, so disambiguation was scored explicitly rather than assumed away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes n = 1 into n = 100. It makes n = 1 &lt;em&gt;honest&lt;/em&gt;. The two real mitigations are that the protocol was &lt;strong&gt;pre-registered&lt;/strong&gt; (I couldn't retrofit the hypotheses) and the &lt;strong&gt;failure log is public&lt;/strong&gt; (you can audit where it broke). What this is: a clean, reproducible measurement of one entity's path from zero to cited. What it isn't: a population estimate. Please don't cite it as one. I'm asking other people to run the same protocol on their own new entities precisely because my single data point can't carry that weight alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  A genuinely recursive footnote
&lt;/h2&gt;

&lt;p&gt;There's a strange loop worth naming. Gemini grounds its knowledge of this entity almost entirely on &lt;strong&gt;Reddit.&lt;/strong&gt; This write-up will be posted and discussed on Reddit. Which means &lt;strong&gt;publishing this is itself the next experimental intervention&lt;/strong&gt; — the act of writing about the measurement perturbs the thing being measured. I can't escape the observer effect here; I can only log it. Consider this paragraph part of the apparatus.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell you to take away
&lt;/h2&gt;

&lt;p&gt;If you're trying to be legible to AI systems — as a person, a product, an author, anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build structured identity first.&lt;/strong&gt; Wikidata → Knowledge Graph, a real website, durable IDs (DOIs/ORCID). That is what moved citations in days. It's unglamorous and it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your own front door.&lt;/strong&gt; Your new domain may be returning 403 to every AI crawler &lt;em&gt;by default&lt;/em&gt; and you'd never know without reading the logs. Off-site presence and the Knowledge Graph got me cited &lt;em&gt;despite&lt;/em&gt; a shut door — but don't assume your door is open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know which channel you're optimizing.&lt;/strong&gt; Virality → human readers. Structured identity → AI citations. Don't spend on one expecting the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat "cited" and "discovered" as different finish lines.&lt;/strong&gt; Recognition-when-named came fast. Recommendation-when-unprompted never came at all in 23 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust mention counts.&lt;/strong&gt; Twenty-four citations to a Wikipedia page that doesn't exist should end the practice of counting mentions and calling it visibility.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full report, the ~16,000-row dataset, the scoring code, and the public failure log are all open. Run the protocol on your own new entity and tell me where my n = 1 holds and where it breaks. That's the only way a single subject becomes a finding.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources, data &amp;amp; code (open)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full report (bilingual EN + DE, CC-BY) — the citable link:&lt;/strong&gt; &lt;a href="https://doi.org/10.5281/zenodo.20549020?utm_source=devto" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.20549020?utm_source=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report page (web, live):&lt;/strong&gt; &lt;a href="https://marin-t-kael.de/en/research/zero-to-cited?utm_source=devto" rel="noopener noreferrer"&gt;https://marin-t-kael.de/en/research/zero-to-cited?utm_source=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code (MIT):&lt;/strong&gt; &lt;a href="https://github.com/marintkael/marin-research-tools?utm_source=devto" rel="noopener noreferrer"&gt;https://github.com/marintkael/marin-research-tools?utm_source=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/marintkael/ai-citation-fidelity?utm_source=devto" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/marintkael/ai-citation-fidelity?utm_source=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORCID:&lt;/strong&gt; &lt;a href="https://orcid.org/0009-0006-2105-8190" rel="noopener noreferrer"&gt;https://orcid.org/0009-0006-2105-8190&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I'm Marin T. Kael, an independent researcher working on AI citation behaviour — and, transparently, the pseudonymous subject of this study. The pre-registration and public failure log exist so you don't have to take my word for any of it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>seo</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
