<?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: Ai-Q Labs</title>
    <description>The latest articles on DEV Community by Ai-Q Labs (@aiq_labs).</description>
    <link>https://dev.to/aiq_labs</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%2F4058521%2F5249f1b0-9454-4e1a-b10f-9b266868cce2.png</url>
      <title>DEV Community: Ai-Q Labs</title>
      <link>https://dev.to/aiq_labs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aiq_labs"/>
    <language>en</language>
    <item>
      <title>A default MCP connection hands an agent 11 tools. None of my 23 Actors is one of them.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:10:09 +0000</pubDate>
      <link>https://dev.to/apify/a-default-mcp-connection-hands-an-agent-11-tools-none-of-my-23-actors-is-one-of-them-65m</link>
      <guid>https://dev.to/apify/a-default-mcp-connection-hands-an-agent-11-tools-none-of-my-23-actors-is-one-of-them-65m</guid>
      <description>&lt;h1&gt;
  
  
  A default MCP connection hands an agent 11 tools. None of my 23 Actors is one of them.
&lt;/h1&gt;

&lt;p&gt;I have 23 audit Actors on the &lt;a href="https://apify.com/store" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. They all do a version of the same thing: take a public record, check whether what it still claims is true, and write the verdict into a dataset.&lt;/p&gt;

&lt;p&gt;I have also written six articles about what happens when an AI agent calls one of them through the &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;. Output fields that arrive empty. Input schemas that read differently to a form and to a tool. A &lt;code&gt;default&lt;/code&gt; that quietly fills a &lt;code&gt;required&lt;/code&gt; field. A connector that shows 44 tools and grants four.&lt;/p&gt;

&lt;p&gt;MCP is the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;, the interface that turns an Actor into a tool that clients like Claude and Cursor can call on their own. Every one of those six articles assumed the agent had that tool in hand. Last week I checked the assumption for the first time. It does not hold by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection string I never read
&lt;/h2&gt;

&lt;p&gt;I found the problem in my own machine before I found it anywhere else. This is the Apify entry in my Claude config, verbatim except that there is no token in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.apify.com?tools=aiqlabs/github-repository-audit"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote that line months ago to test one Actor in isolation, and then left it. My editor session had five Apify tools in it. One of them was an Actor, and it was the one named in that string.&lt;/p&gt;

&lt;p&gt;So I had spent six articles reasoning about how agents see my catalogue. The session I was reasoning from had been narrowed to a single Actor, by me, on purpose, and then forgotten. The first thing worth measuring was what a connection with nothing in it returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a default connection actually sends
&lt;/h2&gt;

&lt;p&gt;Twenty lines, no dependencies. This is the whole test:&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="c1"&gt;// reach.mjs — what does a default connection to the Apify MCP server actually send?&lt;/span&gt;
&lt;span class="c1"&gt;// Run: APIFY_TOKEN=... node reach.mjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://mcp.apify.com/?token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIFY_TOKEN&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HEAD&lt;/span&gt; &lt;span class="o"&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;content-type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json, text/event-stream&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sid&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;HEAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HEAD&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;text&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// the server answers as SSE, so the JSON sits on a "data:" line&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sid&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initialize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;protocolVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-11-05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;clientInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reach&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications/initialized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;sid&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;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`tools: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;span class="k"&gt;for &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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its output, on 12 August:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tools: 11
  search-actors
  fetch-actor-details
  call-actor
  get-actor-run
  get-dataset-items
  get-key-value-store-record
  abort-actor-run
  search-apify-docs
  fetch-apify-docs
  report-problem
  apify--rag-web-browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten of those are the server's own tools. One is an Actor: &lt;a href="https://apify.com/apify/rag-web-browser" rel="noopener noreferrer"&gt;RAG Web Browser&lt;/a&gt;, which belongs to Apify. &lt;strong&gt;Of the 23 Actors I have published, zero are in the list.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nothing is being hidden. Add &lt;code&gt;?actors=&lt;/code&gt; to the same URL and name all 23 slugs. The same server then returns &lt;strong&gt;27&lt;/strong&gt; tools: my 23 plus four storage tools. The Actors are reachable. They are not default.&lt;/p&gt;

&lt;p&gt;The default is also defensible. The Store holds thousands of Actors. A server that turned every one of them into a tool definition would blow out the context window of every client that connected. Eleven tools is a budget, not a snub.&lt;/p&gt;

&lt;p&gt;But it does mean that everything I had written was conditional on a step I had never measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways an Actor reaches an agent
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Who decides&lt;/th&gt;
&lt;th&gt;What an author can do about it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A&lt;/strong&gt; — named in the connection URL (&lt;code&gt;?actors=&lt;/code&gt; / &lt;code&gt;?tools=&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;whoever configures the agent&lt;/td&gt;
&lt;td&gt;Almost nothing. They have to know your slug before they can type it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;B&lt;/strong&gt; — included in the default set&lt;/td&gt;
&lt;td&gt;Apify&lt;/td&gt;
&lt;td&gt;Nothing. Today that set contains one Actor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;C&lt;/strong&gt; — found at runtime by &lt;code&gt;search-actors&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Store search ranking&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;This one.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A and B are not work I can do. C is the entire surface, so I measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the third route
&lt;/h2&gt;

&lt;p&gt;The first two attempts failed on the parameter name. &lt;code&gt;{"search": …}&lt;/code&gt; and &lt;code&gt;{"query": …}&lt;/code&gt; are both accepted and both silently ignored. The server echoes &lt;code&gt;Search query:&lt;/code&gt; back empty and returns its default list. Rather than guess a third time, I read the tool's own &lt;code&gt;inputSchema&lt;/code&gt; out of the &lt;a href="https://www.jsonrpc.org/specification" rel="noopener noreferrer"&gt;JSON-RPC&lt;/a&gt; reply to &lt;code&gt;tools/list&lt;/code&gt; above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keywords : string   default ""
limit    : integer  default 5, max 10
offset   : integer  default 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameter is &lt;code&gt;keywords&lt;/code&gt;. But the line that matters more is &lt;code&gt;limit&lt;/code&gt;. &lt;strong&gt;It defaults to 5 and caps at 10.&lt;/strong&gt; That is the window. An agent that runs one search and picks from it is choosing among five Actors out of the whole Store.&lt;/p&gt;

&lt;p&gt;The second trap is in the response. Each result set is introduced by a line like &lt;code&gt;**Number of Actors found:** 7&lt;/code&gt;, which reads as a total and is not one. It is the size of that page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Instagram&lt;/code&gt; keeps producing fresh results at &lt;code&gt;offset&lt;/code&gt; 0, 10, 20 and 30 — &lt;strong&gt;39 distinct Actors&lt;/strong&gt;, no repeats. One of those four pages came back with nine rather than ten, which is the second thing to know: the pages are ragged.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Chrome extensions&lt;/code&gt; returns &lt;strong&gt;7&lt;/strong&gt; at &lt;code&gt;offset&lt;/code&gt; 0 and a &lt;strong&gt;different 6&lt;/strong&gt; at &lt;code&gt;offset&lt;/code&gt; 10. A short page does not mean the list ended.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first pass treated the short page as the end of the list, so it never looked past page one. It reported seven of my Actors as absent when they were sitting at ranks 12, 16, 31, 41, 52, 55 and 57. Every number below comes from the corrected run. That run pages to rank 100 whatever the page count says, and computes rank as &lt;code&gt;offset + position in page&lt;/code&gt;, because the pages come back ragged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;p&gt;I paired each of 23 Actors with the query it was built for — 28 queries in all, since several Actors got both a specific phrase and the broader term a user might type instead. Then I asked how deep in the results that Actor sits.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where my Actor lands&lt;/th&gt;
&lt;th&gt;queries&lt;/th&gt;
&lt;th&gt;share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inside the &lt;strong&gt;default 5&lt;/strong&gt; the agent receives&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inside the &lt;strong&gt;maximum 10&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rank 11–100: indexed, past the window&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;not in the first 100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the queries these Actors were written for, an agent doing a default search finds one of them about one time in five. Half of them are not in the first hundred results at all.&lt;/p&gt;

&lt;p&gt;That was worse than I expected, but it was not the finding. The finding is which half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queries I can win are the queries nobody is making
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;search-actors&lt;/code&gt; result carries the Actor's usage with it, in a line like &lt;code&gt;**Stats:** 20 total users, 3 monthly users&lt;/code&gt;. So the demand on a query is readable from the same response that gives you the ranking: take the 30-day user count of whatever sits at rank 1.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;my rank&lt;/th&gt;
&lt;th&gt;queries&lt;/th&gt;
&lt;th&gt;median 30-day users of that query's rank-1 Actor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;reachable (top 10)&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unreachable (past 10, or absent)&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The unreachable side is where the users are. &lt;code&gt;App Store&lt;/code&gt; — 311. &lt;code&gt;Google Play&lt;/code&gt; — 295. &lt;code&gt;article extractor&lt;/code&gt; — 271. &lt;code&gt;Shopify&lt;/code&gt; — 186. My Actors for the first three of those are not in the first hundred results.&lt;/p&gt;

&lt;p&gt;And the one query where I rank &lt;strong&gt;#1&lt;/strong&gt; is &lt;code&gt;sitemap checker&lt;/code&gt;, where the rank-1 Actor has &lt;strong&gt;0&lt;/strong&gt; users in the last thirty days. That Actor is &lt;a href="https://apify.com/aiqlabs/sitemap-checker" rel="noopener noreferrer"&gt;mine&lt;/a&gt;. I am first on a shelf with no traffic, and invisible on every shelf with traffic.&lt;/p&gt;

&lt;p&gt;I had measured the same shape once before, from the other side. On 2 August I sampled the Store itself and found that Actors naming a well-known platform had a median of 5 monthly users against 2 for the rest. I read that as an argument for naming platforms. This measurement says the platform-named shelves are exactly the ones where I cannot be seen — 1 reachable query out of 14, against 6 out of 14 for the generic ones. Both results are true and they are not in conflict: platform names are where demand collects, demand attracts competitors, and competitors fill a window five deep. But I predicted the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool tells the agent to broaden the search, and broadening removes me
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;search-actors&lt;/code&gt; response ends with this, addressed to the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;IMPORTANT: You MUST always do a second search with broader, more generic keywords (e.g., just the platform name like "TikTok" instead of "TikTok posts") to make sure you haven't missed a better Actor.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is good advice for the user. It is also an instruction to run the query I do worst on. Same Actor, specific phrase against the broadened phrase:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;specific&lt;/th&gt;
&lt;th&gt;rank&lt;/th&gt;
&lt;th&gt;broadened&lt;/th&gt;
&lt;th&gt;rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sitemap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not in first 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PDF tables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PDF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not in first 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PDF text markdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PDF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not in first 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Chrome extensions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#16&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Chrome Web Store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not in first 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shopify apps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#41&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shopify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not in first 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GitHub repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#52&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GitHub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not in first 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;App Store apps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;App Store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Google Play apps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Google Play&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six worse, none better, two unchanged. The clearest one is the first: &lt;code&gt;sitemap checker&lt;/code&gt; puts me at #1, and deleting one word puts me past rank 100. Whatever advantage a precise name buys, the agent is under instructions to take a second look without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What decides the order
&lt;/h2&gt;

&lt;p&gt;Not popularity, at least not alone. Across every first page I collected, 157 of 238 adjacent pairs are in descending order of monthly users — 66%. A pure popularity sort would be 100%. Relevance carries real weight, which is how an Actor with no users reaches #1 on &lt;code&gt;sitemap checker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Apify documents what does decide it, and says so for both surfaces at once. From &lt;a href="https://docs.apify.com/actors/publishing/quality-score" rel="noopener noreferrer"&gt;Actor quality score&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Actors with higher quality scores tend to rank higher on both surfaces, though no specific position is guaranteed.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second surface named on that page is &lt;em&gt;"the Apify MCP server &lt;code&gt;search-actors&lt;/code&gt; tool used by external AI agents"&lt;/em&gt;. The score aggregates eight categories: reliability, popularity, feedback and community, ease of use, pricing transparency, trustworthiness, history of success, and congruency of texts.&lt;/p&gt;

&lt;p&gt;I have moved it once and can report the size of the move. On 2 August I added dataset output schemas across the catalogue; the score on the Actor I watched went from 74 to 78, and after that pass the whole catalogue sat between 78 and 81. There is no API for the number — I tried four endpoints and got 404 from each — so the only place to read it is &lt;strong&gt;Console &amp;gt; Insights &amp;gt; Actor quality&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is that panel today, for the Actor this article keeps returning to:&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%2F5lo3f2qhv0gtzt3rc3gr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lo3f2qhv0gtzt3rc3gr.jpg" alt="Apify Console's Actor quality panel for aiqlabs/sitemap-checker. The score reads 79 out of 100, with a badge saying " width="799" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;79 out of 100. Better than 71% of Actors on the platform, better than 99% on reliability, and one suggestion left in the panel.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the pairing worth sitting with. Console rates this Actor above 71% of the platform and has one cosmetic item left to suggest. The same Actor is absent from the first hundred results on every query with real demand behind it, and its one first place is on a query with none. Both readings are correct, and the documentation is careful enough to allow it: a higher score &lt;em&gt;tends&lt;/em&gt; to rank higher, with no position guaranteed. On a crowded shelf, tending is not the same as landing in the five results the agent receives.&lt;/p&gt;

&lt;p&gt;Which leaves the honest part. Three of the eight categories — popularity, feedback and community, history of success — are things you get from having users. For an Actor with none, part of the ranking that would bring users is held shut by not having them yet. That is not a complaint about the design; a store that ranked unproven tools first would be worse for everyone using it. But it is the actual shape of the problem, and no amount of description polishing changes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell an Actor author
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check what your connection sends before you tune anything.&lt;/strong&gt; The 20 lines above answer it. If your Actor is not in the list, none of your tool-definition work is reaching an agent yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure your rank on the query you were built for&lt;/strong&gt;, with &lt;code&gt;limit: 10&lt;/code&gt;, paging past the first short page. Anything past rank 10 is in the index and outside the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the demand off the same response.&lt;/strong&gt; The rank-1 Actor's monthly users are printed next to it. If that number is 0, being #1 buys nothing — I have the receipt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect the broad term to be worse.&lt;/strong&gt; It is also where the demand is. Decide which of those two facts you are building for, rather than discovering the trade after you publish.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;Three things, in the order I found them.&lt;/p&gt;

&lt;p&gt;I narrowed my own MCP connection to one Actor and then wrote six articles about how agents see my catalogue. The narrowing was in a config file I had not opened in months.&lt;/p&gt;

&lt;p&gt;I read a page count as a total, and my first pass reported seven Actors as unreachable that rank between 12 and 57. Had I not checked whether &lt;code&gt;offset&lt;/code&gt; kept returning results, this article would have carried seven false claims of the most flattering kind — the kind where the platform looks worse than it is.&lt;/p&gt;

&lt;p&gt;And I expected the platform-named Actors to be the findable ones. They are the least findable ones I own.&lt;/p&gt;

&lt;p&gt;None of that makes the earlier six articles wrong. It reorders them. An input schema that misleads an agent, an output schema that arrives empty, a &lt;code&gt;default&lt;/code&gt; that fills a &lt;code&gt;required&lt;/code&gt; field — all of those are real, and all of them start to matter at the moment the agent receives the tool. For 23 out of 23 of mine, that moment does not happen by default.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I pinned my Actors to the MCP URL, the way the docs recommend. The agent lost every way to see what a call costs.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:09:29 +0000</pubDate>
      <link>https://dev.to/apify/i-pinned-my-actors-to-the-mcp-url-the-way-the-docs-recommend-the-agent-lost-every-way-to-see-what-529h</link>
      <guid>https://dev.to/apify/i-pinned-my-actors-to-the-mcp-url-the-way-the-docs-recommend-the-agent-lost-every-way-to-see-what-529h</guid>
      <description>&lt;h1&gt;
  
  
  I pinned my Actors to the MCP URL, the way the docs recommend. The agent lost every way to see what a call costs.
&lt;/h1&gt;

&lt;p&gt;I publish 23 Actors on the Apify Store. All 23 are pay-per-event.&lt;/p&gt;

&lt;p&gt;Last week I measured how an AI agent actually reaches them. The answer was uncomfortable. A&lt;br&gt;
default MCP connection hands an agent 11 tools. None of mine is one of them.&lt;/p&gt;

&lt;p&gt;So you pin. You name your Actors in the connection URL. The&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP documentation&lt;/a&gt; tells you to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For production deployments, explicitly specify which tools to load rather than relying on&lt;br&gt;
defaults. This ensures consistent behavior across updates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is good advice for reliability. I followed it. Then I measured what the agent receives&lt;br&gt;
afterwards, and found something the docs do not mention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pinning removes the agent's ability to see what a call costs. It also removes its ability to&lt;br&gt;
cap what a call spends.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below are the measurements. Then the three tools that disappear. Then the half of this that is&lt;br&gt;
my own fault rather than the platform's.&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%2F39kpo9erq6v4purd8d3h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39kpo9erq6v4purd8d3h.jpg" alt="Two tool lists side by side. The default connection shows 11 tools. The same account pinned to two Actors shows 6. call-actor, search-actors and fetch-actor-details appear only on the left." width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What pinning actually changes
&lt;/h2&gt;

&lt;p&gt;I connected three ways and listed the tools each time. Same account, same day, 2026-08-12.&lt;br&gt;
Every listing came from a &lt;code&gt;tools/list&lt;/code&gt; call over the&lt;br&gt;
&lt;a href="https://modelcontextprotocol.io/specification" rel="noopener noreferrer"&gt;MCP HTTP transport&lt;/a&gt;. That is what the client&lt;br&gt;
library hands the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default connection&lt;/strong&gt;, no parameters. Eleven tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search-actors          get-key-value-store-record   report-problem
fetch-actor-details    abort-actor-run              apify--rag-web-browser
call-actor             search-apify-docs
get-actor-run          fetch-apify-docs
get-dataset-items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pinned with two of my Actors&lt;/strong&gt;, using the documented parameter&lt;br&gt;
&lt;code&gt;?tools=aiqlabs/seo-audit-tool,aiqlabs/pdf-inspector&lt;/code&gt;. Six tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aiqlabs--seo-audit-tool    get-actor-run                 abort-actor-run
aiqlabs--pdf-inspector     get-dataset-items
                           get-key-value-store-record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pinned with all 23.&lt;/strong&gt; Twenty-seven tools: my 23, plus the same four.&lt;/p&gt;

&lt;p&gt;The four survivors are all post-run tools. Each one reads a run, reads a dataset, reads a&lt;br&gt;
key-value record, or aborts something already running.&lt;/p&gt;

&lt;p&gt;Every tool that helps an agent decide &lt;em&gt;before&lt;/em&gt; it calls is gone. Three of the missing ones&lt;br&gt;
matter for money.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three tools that carried the price
&lt;/h2&gt;

&lt;p&gt;I searched the full JSON of each default tool definition for pricing language. These are the&lt;br&gt;
strings that came back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;search-actors&lt;/code&gt;&lt;/strong&gt; returns pricing in its results. Its own description says so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- **Pricing:** Details with pricing link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its output schema enumerates the models. &lt;code&gt;FREE&lt;/code&gt;, &lt;code&gt;PRICE_PER_DATASET_ITEM&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;FLAT_PRICE_PER_MONTH&lt;/code&gt;, and a &lt;code&gt;pricePerUnit&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetch-actor-details&lt;/code&gt;&lt;/strong&gt; takes a flag whose description reads, in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Include pricing model and costs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;call-actor&lt;/code&gt;&lt;/strong&gt; is the one that actually protects the caller. It accepts &lt;code&gt;maxTotalChargeUsd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pay-per-event Actors only — ignored otherwise.
Caps total USD billed; does NOT limit work. Prefer the Actor's own input fields to bound work.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a spending ceiling. The caller sets it, at call time. Note the second sentence. I will&lt;br&gt;
come back to it, because it is the sentence that indicts me.&lt;/p&gt;

&lt;p&gt;All three tools are in the default set. &lt;strong&gt;None of them survives pinning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An agent holding my Actors directly cannot look up what they cost. It cannot put a ceiling on&lt;br&gt;
what it spends. It can only call them.&lt;/p&gt;
&lt;h2&gt;
  
  
  What my own definitions tell the agent about price
&lt;/h2&gt;

&lt;p&gt;Nothing. I checked, and I checked wrong the first time.&lt;/p&gt;

&lt;p&gt;My first pass searched each tool definition for a dollar sign. It reported a hit on 23 of 23.&lt;br&gt;
For about a minute I believed my descriptions already carried prices.&lt;/p&gt;

&lt;p&gt;They do not. The dollar signs were &lt;code&gt;$id&lt;/code&gt;, the JSON Schema keyword:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"required":[],"$id":"https://apify.com/mcp/aiqlabs--app-store-audit"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I searched again for a dollar sign followed by a digit. An actual amount. The count was&lt;br&gt;
&lt;strong&gt;0 of 23&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The words &lt;code&gt;price&lt;/code&gt; and &lt;code&gt;cost&lt;/code&gt; do appear in a few of my definitions. Every one of them is about&lt;br&gt;
the subject matter, not the invoice.&lt;/p&gt;

&lt;p&gt;One Actor notes that prices and availability differ by storefront. Another explains that each&lt;br&gt;
archived URL costs a request to the live site.&lt;/p&gt;

&lt;p&gt;Those are useful sentences. Neither tells an agent what calling the tool will cost.&lt;/p&gt;

&lt;p&gt;Meanwhile every one of the 23 is metered. Each has two charge events, configured the way&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/actors/publishing/monetize" rel="noopener noreferrer"&gt;Apify's monetization docs&lt;/a&gt;&lt;br&gt;
describe:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apify-actor-start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.00005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apify-default-dataset-item&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.002 – $0.01 depending on the Actor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The per-item price is the one that moves.&lt;/p&gt;

&lt;p&gt;I wanted to be sure those events fire on their own. My Actors never call a charge function. So&lt;br&gt;
I read a real run record rather than assuming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run thLehE7kk3NVRAvqm  SUCCEEDED
chargedEventCounts: {"apify-actor-start":4,"apify-default-dataset-item":608}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They fire. Another run in the same batch recorded 1,000 dataset items. At my highest per-item&lt;br&gt;
price, one call like that is ten dollars.&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%2Fg5zy4epb2x9lnbb21dck.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5zy4epb2x9lnbb21dck.jpg" alt="The public pricing page for one of my Actors, showing the two configured charge events: $5.00 per 1,000 results and $0.00005 per Actor start." width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that is my fault
&lt;/h2&gt;

&lt;p&gt;Go back to that sentence in &lt;code&gt;call-actor&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prefer the Actor's own input fields to bound work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Apify is right about this. A billing cap stops the invoice, not the work. The real bound&lt;br&gt;
belongs in the&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema" rel="noopener noreferrer"&gt;input schema&lt;/a&gt;,&lt;br&gt;
where the author controls it.&lt;/p&gt;

&lt;p&gt;So I checked whether my own Actors carry that bound.&lt;/p&gt;

&lt;p&gt;All 23 have a field for it. &lt;code&gt;maxUrls&lt;/code&gt;, &lt;code&gt;maxPages&lt;/code&gt;, &lt;code&gt;maxApps&lt;/code&gt;, &lt;code&gt;maxPdfs&lt;/code&gt;, &lt;code&gt;maxVehicles&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;maxOrganizations&lt;/code&gt;, &lt;code&gt;maxStories&lt;/code&gt;. Twenty-three out of twenty-three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not one of them declares a JSON Schema &lt;code&gt;maximum&lt;/code&gt;.&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;pdf-inspector        maxPages(none), maxPdfs(none), maxFileMb(none), maxLinksToCheck(none)
sitemap-checker      maxUrlsToCheck(none), maxSitemaps(none), maxDepth(none)
http-status-checker  maxUrls(none), maxRedirects(none), maxConcurrency(none)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I named the fields as if they were limits. I never made them limits.&lt;/p&gt;

&lt;p&gt;An agent can pass &lt;code&gt;maxUrls: 1000000&lt;/code&gt;. The schema will accept it. I told the schema that any&lt;br&gt;
integer is fine.&lt;/p&gt;

&lt;p&gt;So the failure has two halves. Only one of them is the platform's.&lt;/p&gt;

&lt;p&gt;Pinning removes the caller-side ceiling. That is the platform's shape. My Actors have no&lt;br&gt;
author-side ceiling. That is mine, in 23 files I wrote myself.&lt;/p&gt;

&lt;p&gt;An agent holding my pinned tools has no cap available from either direction.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I got wrong on the way here
&lt;/h2&gt;

&lt;p&gt;I started this measurement expecting a different problem.&lt;/p&gt;

&lt;p&gt;My hypothesis was &lt;strong&gt;time&lt;/strong&gt;. Actor runs are slow. Agents time out waiting. Authors ship tools an&lt;br&gt;
agent cannot practically await.&lt;/p&gt;

&lt;p&gt;I pulled the run history for all 23 and computed durations from the API.&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;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median of per-Actor medians&lt;/td&gt;
&lt;td&gt;4.05 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slowest single run observed&lt;/td&gt;
&lt;td&gt;58.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actors with a median above 60 s&lt;/td&gt;
&lt;td&gt;0 of 23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no timeout problem. My hypothesis was wrong.&lt;/p&gt;

&lt;p&gt;I dropped that half of the article rather than stretching the numbers to fit it. Only five of&lt;br&gt;
the 23 had enough run history to produce a median, and that limit is worth stating. But nothing&lt;br&gt;
in the data pointed toward latency. I would rather report a dead hypothesis than a decorated&lt;br&gt;
one.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to change, concretely
&lt;/h2&gt;

&lt;p&gt;Three changes. The first two are the author's job. The third is a question for the platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Put a real ceiling in the input schema.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A field named &lt;code&gt;maxPages&lt;/code&gt; should refuse an absurd value. The&lt;br&gt;
&lt;a href="https://json-schema.org/understanding-json-schema/reference/numeric" rel="noopener noreferrer"&gt;&lt;code&gt;maximum&lt;/code&gt; keyword&lt;/a&gt; does&lt;br&gt;
this before your code runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"maxPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Max pages per PDF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hard cap. Each page produces one billed result row."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default&lt;/code&gt; is not a limit. &lt;code&gt;prefill&lt;/code&gt; is not a limit either, and the two are easy to confuse in&lt;br&gt;
Console. That distinction bit me once already, from the correctness side. This is the same&lt;br&gt;
lesson arriving from the money side.&lt;/p&gt;

&lt;p&gt;You can verify the deployed schema rather than trusting the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.apify.com/v2/acts/&amp;lt;user&amp;gt;~&amp;lt;actor&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"maximum":[0-9]*'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Say the price in the description the agent reads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you pin, the agent has no other source. One clause is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Each result row is one billed event ($0.002). A 500-URL input bills roughly $1.00.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sentence costs you nothing. It is the only pricing signal a pinned agent will ever see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The platform question.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;call-actor&lt;/code&gt; exists in the default set and carries &lt;code&gt;maxTotalChargeUsd&lt;/code&gt;. Pinned tools are&lt;br&gt;
invoked directly, so no wrapper carries it.&lt;/p&gt;

&lt;p&gt;A per-call spending cap that survived pinning would close the gap. It could be an optional&lt;br&gt;
argument on every metered tool, or a connection-level parameter. Either way it would spare each&lt;br&gt;
author from rediscovering this alone.&lt;/p&gt;

&lt;p&gt;I do not know whether that is on anyone's roadmap. I am reporting the shape I measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves an Actor author
&lt;/h2&gt;

&lt;p&gt;Two connection modes, and a cost to each.&lt;/p&gt;

&lt;p&gt;Leave the default. The agent can search, can read your pricing, can cap its spend. It will also&lt;br&gt;
almost certainly never receive your Actor. The default set is 11 tools and none of them is&lt;br&gt;
yours.&lt;/p&gt;

&lt;p&gt;Pin your Actors. The agent receives them and calls them directly. It does so without a price&lt;br&gt;
and without a ceiling.&lt;/p&gt;

&lt;p&gt;I do not think either mode is wrong. I think the second one quietly moves a responsibility onto&lt;br&gt;
the Actor author. The docs that recommend it do not mention the move.&lt;/p&gt;

&lt;p&gt;Until they do, the fix is the schema you already control. Mine was missing it in all 23 files.&lt;br&gt;
I only found out because I went looking for a different problem entirely.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Measurements were taken on 2026-08-12&lt;/strong&gt; against &lt;code&gt;mcp.apify.com&lt;/code&gt; with a single account. Tool&lt;br&gt;
listings came from &lt;code&gt;tools/list&lt;/code&gt; over the MCP HTTP transport. Run durations and charge counts&lt;br&gt;
came from&lt;br&gt;
&lt;a href="https://docs.apify.com/api/v2/actor-run-get" rel="noopener noreferrer"&gt;Apify API run records&lt;/a&gt;. No Actor was executed to&lt;br&gt;
produce these numbers, so nothing here cost anything to measure.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I removed the under maintenance label from my Actor twice. It came back in four hours, then in one.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:08:48 +0000</pubDate>
      <link>https://dev.to/apify/i-removed-the-under-maintenance-label-from-my-actor-twice-it-came-back-in-four-hours-then-in-one-48ge</link>
      <guid>https://dev.to/apify/i-removed-the-under-maintenance-label-from-my-actor-twice-it-came-back-in-four-hours-then-in-one-48ge</guid>
      <description>&lt;h1&gt;
  
  
  I removed the under maintenance label from my Actor twice. It came back in four hours, then in one.
&lt;/h1&gt;

&lt;p&gt;I publish 23 Actors on Apify. Twenty-two of them take a public record and check whether what it&lt;br&gt;
still claims is true.&lt;/p&gt;

&lt;p&gt;The twenty-third is different. It reads an audit dataset and opens a GitHub issue for each finding.&lt;br&gt;
It reaches GitHub through an &lt;a href="https://blog.apify.com/announcing-mcp-connectors/" rel="noopener noreferrer"&gt;Apify MCP connector&lt;/a&gt;,&lt;br&gt;
so the Actor never handles my token.&lt;/p&gt;

&lt;p&gt;That difference is the whole story. Two days after I published it, Apify flagged it as under&lt;br&gt;
maintenance. I spent an afternoon removing the label by hand and learned that the label is not a&lt;br&gt;
setting. It is a conclusion.&lt;/p&gt;
&lt;h2&gt;
  
  
  The email
&lt;/h2&gt;

&lt;p&gt;The notification arrived at 10:08 on 8 August. My Actor had failed the automated tests.&lt;/p&gt;

&lt;p&gt;Apify runs &lt;a href="https://docs.apify.com/platform/actors/publishing/test" rel="noopener noreferrer"&gt;a daily test on every Actor&lt;/a&gt;.&lt;br&gt;
The documentation is precise about what it does:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The test runs the Actor with its default input (defined by the prefill option in the input schema&lt;br&gt;
file) and expects it to finish with a Succeeded status and non-empty default dataset within 5&lt;br&gt;
minutes of the beginning of the run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My Actor cannot do that. Not on a bad day — on any day.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this Actor and not the other 22
&lt;/h2&gt;

&lt;p&gt;My other Actors take a URL, a domain, or a package name. Every one of those is a string. A string&lt;br&gt;
goes in the &lt;code&gt;prefill&lt;/code&gt; of the input schema, the test picks it up, and the run succeeds.&lt;/p&gt;

&lt;p&gt;An authorized connector is not a string. It is a grant, tied to an account, held by the platform.&lt;br&gt;
There is no value I can put in &lt;code&gt;prefill&lt;/code&gt; that gives the test account access to my GitHub&lt;br&gt;
installation. Nor should there be.&lt;/p&gt;

&lt;p&gt;So the run fails at the first step, every day, forever.&lt;/p&gt;

&lt;p&gt;The documentation says this outright, in a section I had not read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actors that require some sort of authentication will always fail the tests despite being fully&lt;br&gt;
functional.&lt;/p&gt;
&lt;/blockquote&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%2Fnh8wh85veus04pd0qprp.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%2Fnh8wh85veus04pd0qprp.png" alt="Apify documentation section titled What if my Actor cannot comply with the test logic, stating that Actors requiring authentication will always fail the tests despite being fully functional, and directing developers to contact support" width="685" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to be exact about what this means, because I got it wrong at first. I had assumed the cost&lt;br&gt;
belonged to my design choice of failing fast on empty input. It does not. Twenty-two of my Actors&lt;br&gt;
fail fast on empty input and none of them were flagged. The cost belongs to a narrower category:&lt;br&gt;
&lt;strong&gt;Actors whose only successful path requires a credential the test account cannot hold.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If that describes your Actor, the daily test is not a quality signal about your code. It is a&lt;br&gt;
structural mismatch, and it will not resolve itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I did instead of reading the docs
&lt;/h2&gt;

&lt;p&gt;I went to Console. Under Publication → Display information there is an Actor status control with an&lt;br&gt;
"Under maintenance" switch. I turned it off and saved. The banner disappeared. The public page&lt;br&gt;
stopped showing the badge.&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%2Fmai7jqefzmgp6qp9myr9.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%2Fmai7jqefzmgp6qp9myr9.png" alt="Apify Console Actor status section with three toggles, Custom status, Under maintenance and Deprecated, all switched off" width="740" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recorded it as fixed. It was not fixed. It was hidden.&lt;/p&gt;

&lt;p&gt;At 17:08 the same day, about four hours after I cleared it, the second notification arrived. The&lt;br&gt;
label was back.&lt;/p&gt;

&lt;p&gt;I removed it again. At 18:08 — inside an hour — the third notification arrived.&lt;/p&gt;

&lt;p&gt;Two removals, two returns, and the interval got shorter. That second number is what made me stop&lt;br&gt;
and read.&lt;/p&gt;
&lt;h2&gt;
  
  
  The label is not a setting
&lt;/h2&gt;

&lt;p&gt;Here is the mechanism, and it is stated plainly in the same documentation page I had skipped:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the Actor fails to complete successful runs for three consecutive days, the developer will be&lt;br&gt;
notified, and the Actor will be labeled under maintenance until it is fixed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the other direction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The best course of action is to fix the Actor and rebuild it. The automatic testing system will&lt;br&gt;
pick this up within 24 hours and mark it as healthy. In some cases, your Actor might break because&lt;br&gt;
of issues with the target website. In such a case, if your Actor passes the majority of test runs&lt;br&gt;
in the next 7 days, it will be marked as healthy automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read those two together. The label is &lt;strong&gt;derived from your run history&lt;/strong&gt;, and the system recomputes&lt;br&gt;
it. The switch in Console writes to a field that an evaluator overwrites on its own schedule.&lt;/p&gt;

&lt;p&gt;For a normal broken Actor, the switch is honest and useful. You fix the bug, you rebuild, you clear&lt;br&gt;
the label, and the next evaluation agrees with you. Your manual action and the computed state&lt;br&gt;
converge.&lt;/p&gt;

&lt;p&gt;My case had no convergence. The failures were still in the window and would be there tomorrow too.&lt;br&gt;
Every removal I made was a claim the evidence contradicted, so the evaluator restored it.&lt;/p&gt;

&lt;p&gt;I also stopped for a second reason, which has nothing to do with mechanics. Repeatedly toggling a&lt;br&gt;
flag that an automated system keeps restoring is not a fix. It is an argument with a robot, in&lt;br&gt;
public, on my own store listing. I did not want to find out how that reads from the other side.&lt;/p&gt;
&lt;h2&gt;
  
  
  The path that actually worked
&lt;/h2&gt;

&lt;p&gt;The documentation tells you where to go:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If that's the case with your Actor, contact support and explain your specific use case that&lt;br&gt;
justifies why the Actor should be excluded from the automated tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is also a form. While my Actor was flagged, Console opened a skip-test request straight from a&lt;br&gt;
query parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://console.apify.com/actors/&amp;lt;actorId&amp;gt;/publication?showSkipTestForm=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat I can only report, not explain. Two days later, with the exemption granted, that same URL&lt;br&gt;
renders the ordinary publication page for me. So treat it as something that is there while you are&lt;br&gt;
flagged, not as a permanent entry point. If it does not open, the documented route is support, and&lt;br&gt;
that is the route the docs tell you to take anyway.&lt;/p&gt;

&lt;p&gt;I filled it in with three sentences: what the Actor does, why the test account cannot authorize the&lt;br&gt;
connector, and the sentence from the docs that describes exactly my situation. Quoting their own&lt;br&gt;
documentation back to them felt lazy. It was the right call — it turned a request for special&lt;br&gt;
treatment into a request to apply a rule that already existed.&lt;/p&gt;

&lt;p&gt;The reply came in about 31 hours:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After reviewing your situation, we've approved your request. Your Actor will now not be checked by&lt;br&gt;
our tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I cleared the label a third time. This one held, because there is no longer an evaluator&lt;br&gt;
producing a verdict to overwrite it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The order matters and I had it backwards.&lt;/strong&gt; Get the exemption first. Clear the label second. Doing&lt;br&gt;
it the other way round produces exactly what I got: a label that returns on a schedule you do not&lt;br&gt;
control.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to check the flag without lying to yourself
&lt;/h2&gt;

&lt;p&gt;While I was doing this I needed a reliable way to answer "is it flagged right now?" I tried three&lt;br&gt;
and only one of them is trustworthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Console banner&lt;/strong&gt; is fine, but it is one Actor at a time and it needs a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The public Store page is not reliable.&lt;/strong&gt; After my third removal I fetched the page and grepped for&lt;br&gt;
the phrase. The badge was still in the HTML, on a page that was already correct in Console. When I&lt;br&gt;
checked again later the same day, it was gone. So the page is not wrong — it lags, and on that day&lt;br&gt;
the lag was hours. If you grep the page right after a change, you will read your own stale render&lt;br&gt;
and conclude the change failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API is the source of truth.&lt;/strong&gt; The Actor object carries a top-level &lt;code&gt;notice&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/aiqlabs~dataset-to-github-issues"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; print(json.load(sys.stdin)['data']['notice'])"&lt;/span&gt;
&lt;span class="c"&gt;# NONE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fciiz8k0ahl1lfcgq564y.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%2Fciiz8k0ahl1lfcgq564y.png" alt="Selected fields from the live API response for the Actor, showing notice set to NONE alongside isPublic true and three categories" width="700" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The values I have observed are &lt;code&gt;UNDER_MAINTENANCE&lt;/code&gt; and &lt;code&gt;NONE&lt;/code&gt;. This endpoint answers for public&lt;br&gt;
Actors without a token, which means you can check anyone's — including, before you fork it, the one&lt;br&gt;
you are about to depend on.&lt;/p&gt;

&lt;p&gt;I did not find &lt;code&gt;notice&lt;/code&gt; documented as the maintenance flag. I found it by reading the object.&lt;/p&gt;
&lt;h2&gt;
  
  
  A check you can run on everything you publish
&lt;/h2&gt;

&lt;p&gt;After this, I stopped trusting my memory of which Actors were healthy. This prints the flag for a&lt;br&gt;
list of slugs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Print the maintenance flag for every Actor you name. No token needed for public Actors.&lt;/span&gt;
&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"aiqlabs"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;slug &lt;span class="k"&gt;in &lt;/span&gt;dataset-to-github-issues github-repository-audit pdf-table-extractor&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;notice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;~&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;slug&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; d=json.load(sys.stdin).get('data') or {}; print(d.get('notice','MISSING'))"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-32s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$notice&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it across the Actors I care about after the approval. All &lt;code&gt;NONE&lt;/code&gt;, including the twenty-two&lt;br&gt;
that were never at risk. That last part matters: it confirmed the flag had not spread from a shared&lt;br&gt;
cause.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap next door
&lt;/h2&gt;

&lt;p&gt;One warning that cost me three rounds of rework, because it sits on the same Console screen.&lt;/p&gt;

&lt;p&gt;The Actor status switch lives under Publication → Display information. &lt;strong&gt;Saving that section dropped&lt;br&gt;
one of my categories, three times in a row.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had set three categories through the API. After each save, &lt;code&gt;OPEN_SOURCE&lt;/code&gt; was gone, and I put it&lt;br&gt;
back through the API.&lt;/p&gt;

&lt;p&gt;Three for three is not an accident. But I want to be exact about the limit of what I know: when I&lt;br&gt;
opened that same form today to write this, all three categories were sitting in the field. So I can&lt;br&gt;
report the outcome I measured and not the cause.&lt;/p&gt;

&lt;p&gt;So if you go anywhere near this screen, re-apply your categories through the API afterwards, and&lt;br&gt;
read them back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/aiqlabs~dataset-to-github-issues"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; print(json.load(sys.stdin)['data']['categories'])"&lt;/span&gt;
&lt;span class="c"&gt;# ['DEVELOPER_TOOLS', 'AUTOMATION', 'OPEN_SOURCE']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not confirm this in the UI you just used. Confirm it in the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to ignore this
&lt;/h2&gt;

&lt;p&gt;The label is not cosmetic, and the timeline is published:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After another 14 days of failing runs, you will receive another notification. Finally, if the runs&lt;br&gt;
continue to fail after yet another 14 days, the Actor will be deprecated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three days of failures to get labeled. Roughly a month of them to get deprecated. For an Actor that&lt;br&gt;
structurally cannot pass, that clock starts on the day you publish and never stops. Nothing in your&lt;br&gt;
code will change it, which is precisely why manual removal feels like it works and is the most&lt;br&gt;
expensive thing you can do with the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I now apply
&lt;/h2&gt;

&lt;p&gt;Before I publish an Actor that needs an authorized connector, I ask one question: &lt;strong&gt;is there any&lt;br&gt;
input the test account can supply that produces a successful run?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is yes, I make sure that path is what &lt;code&gt;prefill&lt;/code&gt; describes, and the daily test becomes&lt;br&gt;
a free health check I get for nothing.&lt;/p&gt;

&lt;p&gt;If the answer is no, I file the skip request in the same session as the publish, before the first&lt;br&gt;
notification arrives. Then I never touch the status switch, because by then there is nothing left&lt;br&gt;
for it to argue with.&lt;/p&gt;

&lt;p&gt;The Actor in this story is &lt;a href="https://apify.com/aiqlabs/dataset-to-github-issues" rel="noopener noreferrer"&gt;dataset-to-github-issues&lt;/a&gt;.&lt;br&gt;
Its source is public under ISC at&lt;br&gt;
&lt;a href="https://github.com/ai-q-labs/dataset-to-github-issues" rel="noopener noreferrer"&gt;github.com/ai-q-labs/dataset-to-github-issues&lt;/a&gt;,&lt;br&gt;
and the connector it uses speaks the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;.&lt;br&gt;
The rest of what I publish is at &lt;a href="https://apify.com/aiqlabs" rel="noopener noreferrer"&gt;apify.com/aiqlabs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>automation</category>
      <category>apify</category>
    </item>
    <item>
      <title>I gave my Actor a GitHub MCP connector. It could see 44 tools and use 4.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:08:07 +0000</pubDate>
      <link>https://dev.to/apify/i-gave-my-actor-a-github-mcp-connector-it-could-see-44-tools-and-use-4-31ih</link>
      <guid>https://dev.to/apify/i-gave-my-actor-a-github-mcp-connector-it-could-see-44-tools-and-use-4-31ih</guid>
      <description>&lt;h1&gt;
  
  
  I gave my Actor a GitHub MCP connector. It could see 44 tools and use 4.
&lt;/h1&gt;

&lt;p&gt;I publish 22 audit Actors on Apify. They all answer one question: is this public record still true?&lt;/p&gt;

&lt;p&gt;The registry serves a package with no warning on it. Its repository has been archived. My Actors&lt;br&gt;
find that gap and write it into a dataset.&lt;/p&gt;

&lt;p&gt;Then nothing happens. A dataset is a place findings go to be correct in private.&lt;/p&gt;

&lt;p&gt;So I built the missing half. It reads an audit dataset, decides what deserves attention, and opens&lt;br&gt;
a GitHub issue for each finding.&lt;/p&gt;

&lt;p&gt;It reaches GitHub through an&lt;br&gt;
&lt;a href="https://blog.apify.com/announcing-mcp-connectors/" rel="noopener noreferrer"&gt;Apify MCP connector&lt;/a&gt;. That is a Model Context&lt;br&gt;
Protocol (MCP) server, wired into the Actor's input by the platform. My Actor never touches my&lt;br&gt;
token.&lt;/p&gt;

&lt;p&gt;I expected the interesting part to be the writing. It was not. The interesting part was how little&lt;br&gt;
the platform let my Actor do, and how much the connector &lt;em&gt;appeared&lt;/em&gt; to offer.&lt;/p&gt;

&lt;p&gt;At authorization, Console listed &lt;strong&gt;44 tools&lt;/strong&gt;. My run could see &lt;strong&gt;four&lt;/strong&gt;. Both numbers are correct,&lt;br&gt;
and the gap between them is the whole design.&lt;/p&gt;

&lt;p&gt;Here is what I built, where in the run the connector fires, and the four things that surprised me.&lt;br&gt;
One of them broke an assumption the Actor was designed around.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I built, and why it is a separate Actor
&lt;/h2&gt;

&lt;p&gt;The obvious move was to add issue-filing to&lt;br&gt;
&lt;a href="https://apify.com/aiqlabs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;github-repository-audit&lt;/code&gt;&lt;/a&gt; directly. I did not,&lt;br&gt;
for a boring reason: that Actor was under an unrelated measurement, and changing its build would have&lt;br&gt;
destroyed the data.&lt;/p&gt;

&lt;p&gt;The boring reason turned out to be the right architecture. Filing issues has nothing to do with&lt;br&gt;
auditing. It needs a dataset, a tracker, and a policy. It does not need to know what npm is.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;dataset-to-github-issues&lt;/code&gt; takes a dataset ID and a connector, and nothing about my audit is&lt;br&gt;
hard-coded into it.&lt;/p&gt;

&lt;p&gt;The input schema is the interesting file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"datasetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Audit dataset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resourcePicker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dataset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourcePermissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"READ"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"githubConnector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHub connector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcpConnector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.githubcopilot.com/mcp/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"issue_read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"list_issues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search_issues"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two declarations sit side by side. One names what the run may read. The other names what it may call&lt;br&gt;
through the connector. The platform enforces both. My code enforces neither.&lt;/p&gt;

&lt;p&gt;That symmetry is easy to miss when you are writing it. It matters later.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up the connector: the docs and Console disagree
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.apify.com/platform/integrations/mcp-connectors" rel="noopener noreferrer"&gt;MCP connectors documentation&lt;/a&gt; says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Supported services include Notion, Slack, GitHub, Sentry, and Supabase.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;strong&gt;Add new MCP connector&lt;/strong&gt; dropdown in Console offers three presets. I scrolled to be sure. The&lt;br&gt;
list does not grow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://mcp.sentry.dev/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://mcp.notion.com/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://mcp.linear.app/sse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Slack, GitHub, and Supabase are not presets. Linear is a preset and is not in that sentence.&lt;/p&gt;

&lt;p&gt;This is not a bug. The URL field accepts free text, so the presets are a convenience, not an&lt;br&gt;
allowlist. I typed the URL of&lt;br&gt;
&lt;a href="https://github.com/github/github-mcp-server" rel="noopener noreferrer"&gt;GitHub's own MCP server&lt;/a&gt; and waited.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.githubcopilot.com/mcp/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About four seconds later the field turned green and an auth panel appeared. Apify had gone and asked&lt;br&gt;
the server what it supports. It offered &lt;strong&gt;API key&lt;/strong&gt;, selected by default. It rendered &lt;strong&gt;OAuth&lt;/strong&gt;&lt;br&gt;
grayed out, with this text:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This server doesn't support dynamic client registration. Your own OAuth client is recommended.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I like that the platform probes rather than assumes. It also told me exactly which path to take on a&lt;br&gt;
free account: a personal access token used as a bearer credential.&lt;/p&gt;
&lt;h3&gt;
  
  
  The token I actually issued
&lt;/h3&gt;

&lt;p&gt;I used a &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;fine-grained personal access token&lt;/a&gt;,&lt;br&gt;
scoped as tightly as GitHub allows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope element&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository access&lt;/td&gt;
&lt;td&gt;Only select repositories → one repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Issues: read and write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Metadata: read-only (added automatically, marked &lt;em&gt;Required&lt;/em&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expiry&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No contents write. No administration. No second repository. Remember this table; it is the point of&lt;br&gt;
the next section.&lt;/p&gt;
&lt;h3&gt;
  
  
  The trap: the URL reverted while I was not looking
&lt;/h3&gt;

&lt;p&gt;Between filling in the dialog and pasting the API key, the server URL changed under me. It went back&lt;br&gt;
to &lt;code&gt;https://mcp.sentry.dev/mcp&lt;/code&gt;, the first preset in the list. The field label also changed from&lt;br&gt;
"MCP server URL" to "MCP server".&lt;/p&gt;

&lt;p&gt;My API key field kept its contents. The URL showed a green validation check. Save was enabled.&lt;/p&gt;

&lt;p&gt;If I had saved that, a GitHub token would have been registered as the bearer credential for&lt;br&gt;
&lt;strong&gt;Sentry's&lt;/strong&gt; MCP server. Nothing in the dialog would have complained.&lt;/p&gt;

&lt;p&gt;Re-typing the GitHub URL and picking it from the suggestion fixed it, and the key survived the&lt;br&gt;
change.&lt;/p&gt;

&lt;p&gt;I have a rule now: read the server URL again immediately before you save. Typing it once is not&lt;br&gt;
enough. A connector dialog holds a live credential, and it deserves the same paranoia as a payment&lt;br&gt;
form.&lt;/p&gt;
&lt;h2&gt;
  
  
  44 tools discovered, 4 of them visible
&lt;/h2&gt;

&lt;p&gt;Saving the connector produced an ID and a list. Console showed the tools it had discovered at&lt;br&gt;
authorization time. All 44:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add_comment_to_pending_review  add_issue_comment  add_reply_to_pull_request_comment
create_branch  create_or_update_file  create_pull_request  create_repository  delete_file
fork_repository  get_commit  get_file_contents  get_label  get_latest_release  get_me
get_release_by_tag  get_tag  get_team_members  get_teams  issue_read  issue_write
list_branches  list_commits  list_issue_fields  list_issue_types  list_issues
list_pull_requests  list_releases  list_repository_collaborators  list_tags
merge_pull_request  pull_request_read  pull_request_review_write  push_files
request_copilot_review  run_secret_scanning  search_code  search_commits  search_issues
search_pull_requests  search_repositories  search_users  sub_issue_write
update_pull_request  update_pull_request_branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F4mhig2gbe3yj0lk49v90.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mhig2gbe3yj0lk49v90.jpg" alt="Apify Console listing the 44 tools discovered on the GitHub MCP connector at authorization time" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read that list against my token's permissions. &lt;code&gt;create_repository&lt;/code&gt; is there. &lt;code&gt;delete_file&lt;/code&gt; is there.&lt;br&gt;
&lt;code&gt;merge_pull_request&lt;/code&gt;, &lt;code&gt;push_files&lt;/code&gt;, &lt;code&gt;fork_repository&lt;/code&gt;, &lt;code&gt;run_secret_scanning&lt;/code&gt; — all there, all far&lt;br&gt;
outside a token that can only read and write issues in one repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The discovered tool list describes the server, not the token.&lt;/strong&gt; It is a menu, not a grant. Every&lt;br&gt;
one of those calls would die upstream, at GitHub itself.&lt;/p&gt;

&lt;p&gt;That is a fine outer boundary. It is also the worst possible place to learn about it. So I looked at&lt;br&gt;
what my run actually got. From the run log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Read 12 row(s) from dataset iBbLEMf3NUSJcU0cZ.
INFO  The proxy exposes 4 tool(s) to this run: issue_read, issue_write, list_issues, search_issues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four. Exactly the four names in &lt;code&gt;mcpServers[0].tools.required&lt;/code&gt;, and nothing else. I changed nothing&lt;br&gt;
about the connector between those two observations. The&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema/specification/v1" rel="noopener noreferrer"&gt;input schema declaration&lt;/a&gt;&lt;br&gt;
is what narrowed it.&lt;/p&gt;

&lt;p&gt;The docs put it in one sentence, and now I have watched it happen:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The proxy enforces that an Actor can only call tools it explicitly declared in its input schema.&lt;/p&gt;
&lt;/blockquote&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%2F005f7w5noerw499s8veh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F005f7w5noerw499s8veh.jpg" alt="Apify run log showing the line: The proxy exposes 4 tool(s) to this run, naming issue_read, issue_write, list_issues and search_issues" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So there are three layers, and they do different jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The token.&lt;/strong&gt; What the upstream service will honor. Enforced by GitHub, discovered on failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The connector.&lt;/strong&gt; What server this credential belongs to. Chosen once, in Console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The input schema.&lt;/strong&gt; What &lt;em&gt;this Actor&lt;/em&gt; may call. Enforced by the proxy, before the request leaves
Apify.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Layer 3 is the one that ties a tool to a specific Actor. It is also the only one a reader of my Actor&lt;br&gt;
can see. Anyone can open my input schema and learn the worst thing my code can do to their GitHub&lt;br&gt;
account.&lt;/p&gt;

&lt;p&gt;I value that as much as the enforcement itself. A limit nobody can inspect is a promise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the connector fires, and why not at the end
&lt;/h2&gt;

&lt;p&gt;The easy design writes at the end. Audit, decide, file, done.&lt;/p&gt;

&lt;p&gt;I fire the connector in the middle, twice, and the first call is a read. The Actor lists the open&lt;br&gt;
issues in the tracker before it decides anything:&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="c1"&gt;// This is the whole reason the connector fires here and not at the end: the decision of&lt;/span&gt;
&lt;span class="c1"&gt;// whether to open an issue depends on what is already open.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;argsFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;methodValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/list/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&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="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readError&lt;/span&gt; &lt;span class="o"&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;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&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;data&lt;/span&gt;&lt;span class="p"&gt;)&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;data&lt;/span&gt; &lt;span class="p"&gt;:&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;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;??&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="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &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;issue&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// a title like "[dep-drift] npm:left-pad - deprecated..." gives back "npm:left-pad"&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;keyFromTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;openKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identity lives in the issue title. Every issue my Actor opens is tagged &lt;code&gt;[dep-drift]&lt;/code&gt;, and the&lt;br&gt;
finding key follows the tag. A later run recovers the key by parsing titles it wrote itself. No&lt;br&gt;
state file, no external store, nothing to fall out of sync.&lt;/p&gt;

&lt;p&gt;I proved it by running the same input twice, live, with the same settings.&lt;/p&gt;

&lt;p&gt;First run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  0 finding(s) already have an open issue (read 0 title(s) over 1 page(s)).
INFO  Opened 3 issue(s).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second run, identical input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  3 finding(s) already have an open issue (read 3 title(s) over 1 page(s)).
INFO  Opened 3 issue(s).
INFO  Done. skipped: 7, filed: 3, deferred: 2.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Actor recognized its own three issues from the first run and skipped them. It filed the next&lt;br&gt;
three. Two more hit the per-run ceiling, and it reported them as &lt;code&gt;deferred&lt;/code&gt; instead of dropping&lt;br&gt;
them.&lt;/p&gt;

&lt;p&gt;Every row says why, in the dataset, whether or not it became an issue:&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%2Fwer9c65nvdywncn0lfdz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwer9c65nvdywncn0lfdz.jpg" alt="Apify dataset table listing each finding with its decision and reason: three skipped because an open issue already covers them, three filed with issue numbers 4, 5 and 6, two deferred over the ceiling" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is what a connector buys that a separate script does not.&lt;/strong&gt; A write-only integration would&lt;br&gt;
have re-filed the same three issues. Then it would do it again every scheduled run, and the tracker&lt;br&gt;
becomes noise inside a week. Reading and writing in the same run, through the same authorization,&lt;br&gt;
is what makes re-running safe.&lt;/p&gt;

&lt;p&gt;The Actor also refuses to write blind. If the read fails, it stops:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readError&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;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`Reading the existing issues failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;readError&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Stopping before any write, because `&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;without that list this run cannot tell a new finding from one it filed last time.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicates in someone's tracker cost more than a failed run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assumption that broke
&lt;/h2&gt;

&lt;p&gt;My audit answers in three states. It found a defect. It found nothing. Or it could not check —&lt;br&gt;
usually because GitHub's hourly allowance ran out mid-run.&lt;/p&gt;

&lt;p&gt;The third state is the one I care about. Filing it turns "I don't know" into an alarm. Dropping it&lt;br&gt;
silently turns an unanswered question into a clean bill of health.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;decide()&lt;/code&gt; gives it its own outcome:&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;function&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;minRiskLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;openKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findingKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&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;real&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;realCodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// codes that mean "we found something"&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unverified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unverifiedCodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// codes that mean "the check did not run"&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;real&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="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;unverified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;withheld&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`not verified (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;unverified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;skipped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nothing to report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="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;openKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;skipped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;an open issue already covers this&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&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;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;riskLevel&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;span class="nx"&gt;real&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="nx"&gt;key&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;To exercise that path honestly, I needed an audit that really ran out of allowance. So I pointed the&lt;br&gt;
audit Actor at &lt;code&gt;gatsby&lt;/code&gt;'s &lt;code&gt;package.json&lt;/code&gt; — &lt;strong&gt;166 dependencies&lt;/strong&gt;. It duly ran out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requestsMade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"distinctRepositories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;131&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"repositoriesNotChecked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rateLimited"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"not_checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repository_not_on_github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repo_moved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"deprecated_on_registry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repo_archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"silent_abandonment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventy-six unverified rows. I expected a dry run to report dozens of withheld findings. It reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Done. skipped: 161, would-file: 5.
0 finding(s) were withheld because the audit could not verify them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero.&lt;/p&gt;

&lt;p&gt;I read the rows instead of guessing at the cause. &lt;strong&gt;Every single &lt;code&gt;not_checked&lt;/code&gt; row also carried&lt;br&gt;
&lt;code&gt;repository_not_on_github&lt;/code&gt;.&lt;/strong&gt; Not one row in 166 had an unverified code and nothing else.&lt;/p&gt;

&lt;p&gt;That kills the assumption. I had built the decision around "a finding is either established or&lt;br&gt;
unestablished". Real data says a row is usually &lt;strong&gt;both&lt;/strong&gt;: something the audit established, plus a&lt;br&gt;
caveat about a check that did not run. The whole-row case turns out to be the rare one.&lt;/p&gt;

&lt;p&gt;The behavior that actually mattered was the other one. The caveat has to travel &lt;em&gt;with&lt;/em&gt; the finding,&lt;br&gt;
into the issue body. Here is issue #11, read back from the public GitHub API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gs"&gt;**npm:string-similarity**&lt;/span&gt; — risk: &lt;span class="sb"&gt;`high`&lt;/span&gt;

&lt;span class="gu"&gt;### What the audit found&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`deprecated_on_registry`&lt;/span&gt; (high) — The registry marks this deprecated: "Package no longer supported..."
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`repository_not_on_github`&lt;/span&gt; (low) — The registry links to git://github.com/aceakash/string-similarity.git,
  which is not a GitHub repository.

&lt;span class="gu"&gt;### What the audit could not check&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`not_checked`&lt;/span&gt; — aceakash/string-similarity was not checked: GitHub's hourly allowance ran out.

These are open questions, not clean results. Re-run the audit to close them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reader cannot mistake that last section for a clean result. The design goal held. The code path I&lt;br&gt;
built to demonstrate it never fired.&lt;/p&gt;

&lt;p&gt;I am leaving &lt;code&gt;withheld&lt;/code&gt; in, with its unit tests, and saying plainly that no live audit has produced&lt;br&gt;
one yet. It is designed behavior. I have never watched it happen.&lt;/p&gt;
&lt;h2&gt;
  
  
  The reply that looked complete and was not
&lt;/h2&gt;

&lt;p&gt;Run 4 opened three issues and then crashed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Opened 3 issue(s).
ApifyApiError: Schema validation failed   clientMethod: DatasetClient.pushItems
  instancePath: '/issueNumber'  message: 'must be integer'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issues were real. I checked them from outside the run with an unauthenticated &lt;code&gt;curl&lt;/code&gt;. The&lt;br&gt;
&lt;em&gt;report&lt;/em&gt; is what failed, because &lt;code&gt;issueNumber&lt;/code&gt; came back &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had stored the first raw reply in the key-value store on purpose, so I could look instead of&lt;br&gt;
guess. This is &lt;code&gt;issue_write&lt;/code&gt;'s answer, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"5078084249"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/ai-q-labs/github-repository-audit/issues/4"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is &lt;strong&gt;no &lt;code&gt;number&lt;/code&gt; field&lt;/strong&gt;. &lt;code&gt;id&lt;/code&gt; is a string, and it holds GitHub's internal identifier. The&lt;br&gt;
issue number — 4 — exists only inside the URL.&lt;/p&gt;

&lt;p&gt;Reaching for &lt;code&gt;id&lt;/code&gt; would have recorded issue "5078084249". Nothing would have errored. My dataset&lt;br&gt;
would have looked complete and been wrong in a way no type check catches.&lt;/p&gt;

&lt;p&gt;The fix reads the number where it actually lives, and refuses to invent one:&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;function&lt;/span&gt; &lt;span class="nf"&gt;findIssueRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;reply&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;issue_number&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// note: a string id fails this on purpose&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;html_url&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;htmlUrl&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&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;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;github&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;com&lt;/span&gt;&lt;span class="se"&gt;\/[\w&lt;/span&gt;&lt;span class="sr"&gt;.-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/[\w&lt;/span&gt;&lt;span class="sr"&gt;.-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;issues&lt;/span&gt;&lt;span class="se"&gt;\/(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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;Number.isInteger&lt;/code&gt; is doing real work there. It rejects the string &lt;code&gt;id&lt;/code&gt; that would otherwise sail&lt;br&gt;
through.&lt;/p&gt;

&lt;p&gt;If you build against a connector, capture the first reply from every tool you call. A tool result is&lt;br&gt;
whatever the upstream server decided to send. Assuming a field name and discovering the truth in&lt;br&gt;
production is exactly the failure this prevents.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two more failures worth designing for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A run cannot open an arbitrary dataset.&lt;/strong&gt; My very first run died here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTOR: Running under "LIMITED_PERMISSIONS".
ERROR Could not read dataset iBbLEMf3NUSJcU0cZ: Insufficient permissions for the dataset.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same account, same owner, still refused. The fix is not a token — it is the &lt;code&gt;resourceType&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;resourcePermissions&lt;/code&gt; declaration from the schema at the top of this article. The schema names the&lt;br&gt;
thing being read and the thing being written through. The platform grants exactly that, and no more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A run that changed the outside world must not fail on its own bookkeeping.&lt;/strong&gt; That crash after&lt;br&gt;
"Opened 3 issue(s)" marked the whole run FAILED. FAILED reads as "nothing happened". That is the&lt;br&gt;
opposite of the truth, and it sends the next run straight back at the same findings.&lt;/p&gt;

&lt;p&gt;I now guard each dataset write on its own. Anything that fails lands in the summary instead of&lt;br&gt;
killing the run:&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;for &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;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;reportProblems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order the work so the irreversible part happens last, and make everything after it non-fatal.&lt;/p&gt;

&lt;p&gt;Seven runs got me here, and the first two are the ones worth reading:&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%2Fgzx5sb1oyfrnw778c5sx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzx5sb1oyfrnw778c5sx.jpg" alt="Apify run list for the Actor showing seven runs, including one that failed on dataset permissions and one that failed on a mistyped connector id" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug my new Actor found in my old one
&lt;/h2&gt;

&lt;p&gt;Look again at issue #11. The registry link is &lt;code&gt;git://github.com/aceakash/string-similarity.git&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That &lt;strong&gt;is&lt;/strong&gt; on GitHub. The same row resolves &lt;code&gt;aceakash/string-similarity&lt;/code&gt; and queues it for a GitHub&lt;br&gt;
lookup. One row, two statements, contradicting each other.&lt;/p&gt;

&lt;p&gt;My first explanation was that the URL parser rejects the &lt;code&gt;git://&lt;/code&gt; scheme. I ran the parser to check,&lt;br&gt;
and it does not. It accepts &lt;code&gt;git://&lt;/code&gt;, &lt;code&gt;git+ssh://&lt;/code&gt;, &lt;code&gt;git@host:path&lt;/code&gt; and &lt;code&gt;github:owner/name&lt;/code&gt; alike.&lt;/p&gt;

&lt;p&gt;The real cause is one line further on. The finding fires when the GitHub record is missing:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registryFound&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repoUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no_repository_link&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;The registry record does not link to any source repository, so nothing can be verified against it.&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="k"&gt;else&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;registryFound&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repoUrl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repository_not_on_github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`The registry links to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repoUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;, which is not a GitHub repository.`&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;The record is also missing when the lookup never ran. GitHub allows an unauthenticated caller 60&lt;br&gt;
requests an hour. Past that, my Actor stops asking and stores &lt;code&gt;null&lt;/code&gt;:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rateLimited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skipped&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;So &lt;code&gt;!repo&lt;/code&gt; means two different things, and the code reads only one of them. Every row cut off by the&lt;br&gt;
rate limit is reported as a repository that is not on GitHub. That is why all 76 unverified rows were&lt;br&gt;
paired. The pairing tracks the rate limit. The URL scheme has nothing to do with it.&lt;/p&gt;

&lt;p&gt;It is the same mistake as the one in my first article, made a fifth time. A missing answer is not a&lt;br&gt;
negative answer.&lt;/p&gt;

&lt;p&gt;I have not fixed it. &lt;code&gt;github-repository-audit&lt;/code&gt; is inside an unrelated observation window, and&lt;br&gt;
changing its build would destroy that measurement. It is recorded, and it goes in next.&lt;/p&gt;

&lt;p&gt;I did not expect the reader of an Actor's output to become the best test of that Actor. It is&lt;br&gt;
obvious in hindsight. A dataset you only look at is a dataset nobody checks.&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%2Fko87pva87czrybn10uze.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko87pva87czrybn10uze.jpg" alt="GitHub issues list showing eleven dep-drift issues opened by the Actor across two audit datasets" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost, and what I would do differently
&lt;/h2&gt;

&lt;p&gt;Eleven issues, none duplicated, across four live runs and two audit datasets. All of it on the&lt;br&gt;
&lt;strong&gt;free plan&lt;/strong&gt;. When I stopped, my account usage read $0.61 against the $5 monthly credit. MCP&lt;br&gt;
connectors are not a paid feature, which surprised me enough to check twice.&lt;/p&gt;

&lt;p&gt;Three things I would tell myself before starting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Declare the smallest tool set you can name.&lt;/strong&gt; Not for safety theater — because the declaration
is public. Anyone can read what my Actor is allowed to do without reading my code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire the connector where the decision is, not where the output is.&lt;/strong&gt; Read before you write, and
stop if the read fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store the first reply from every tool.&lt;/strong&gt; The one that cost me a run was shaped nothing like I
assumed. It failed quietly, in the one direction a type check cannot catch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one I got wrong: I designed a decision path around a data shape I had never seen. A branch, a&lt;br&gt;
reason string, and unit tests, for a row that has not appeared once in 178 audited packages. Next&lt;br&gt;
time I look at the data before I write the branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run it yourself
&lt;/h3&gt;

&lt;p&gt;The full source of the Actor in this article is at&lt;br&gt;
&lt;a href="https://github.com/ai-q-labs/dataset-to-github-issues" rel="noopener noreferrer"&gt;&lt;code&gt;ai-q-labs/dataset-to-github-issues&lt;/code&gt;&lt;/a&gt; —&lt;br&gt;
the input schema, the decision function, the connector client, and the unit tests, including the one&lt;br&gt;
covering the case that has never fired.&lt;/p&gt;

&lt;p&gt;You will need three things to run it: an audit dataset with &lt;code&gt;riskLevel&lt;/code&gt; and &lt;code&gt;issueCodes&lt;/code&gt; on each&lt;br&gt;
row, an MCP connector authorized against GitHub, and a repository you are willing to file issues in.&lt;br&gt;
A free Apify account covers the rest.&lt;/p&gt;

&lt;p&gt;Where the findings came from:&lt;br&gt;
&lt;a href="https://apify.com/aiqlabs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;github-repository-audit&lt;/code&gt;&lt;/a&gt; is public and free.&lt;br&gt;
Everything it produced through the connector is open in the&lt;br&gt;
&lt;a href="https://github.com/ai-q-labs/github-repository-audit/issues" rel="noopener noreferrer"&gt;tracker repository&lt;/a&gt;, tagged&lt;br&gt;
&lt;code&gt;[dep-drift]&lt;/code&gt;, including the one that exposed my own parser bug.&lt;/p&gt;

&lt;p&gt;Full-size versions of every screenshot in this article sit in the source repository above, under&lt;br&gt;
&lt;code&gt;docs/screenshots&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The shortest version of the whole thing: your connector shows you a menu. Your Actor declares what it&lt;br&gt;
eats. The proxy is what makes the difference real.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I connected my audit Actor to Claude, and it audited three packages nobody asked for</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:07:21 +0000</pubDate>
      <link>https://dev.to/apify/i-connected-my-audit-actor-to-claude-and-it-audited-three-packages-nobody-asked-for-3m1d</link>
      <guid>https://dev.to/apify/i-connected-my-audit-actor-to-claude-and-it-audited-three-packages-nobody-asked-for-3m1d</guid>
      <description>&lt;h1&gt;
  
  
  I connected my audit Actor to Claude, and it audited three packages nobody asked for
&lt;/h1&gt;

&lt;p&gt;I maintain 22 audit Actors on Apify. They all do a version of the same thing. They take a public&lt;br&gt;
record, and they check whether what it still claims is true.&lt;/p&gt;

&lt;p&gt;Last week I connected one of them to Claude through the Apify Model Context Protocol (MCP) server. I&lt;br&gt;
expected the interesting part to be the agent. It was not. The interesting part was my own input&lt;br&gt;
schema. It had been lying to every non-human caller since the day I published it.&lt;/p&gt;

&lt;p&gt;Then I made a second assumption. I decided I knew how an agent would read the result. I gave the&lt;br&gt;
question to four of them to prove it, and all four proved the opposite.&lt;/p&gt;

&lt;p&gt;This is what I found, what the agents did with it, and what I changed.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the Actor does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/aiqlabs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;github-repository-audit&lt;/code&gt;&lt;/a&gt; takes package names or&lt;br&gt;
repository names. For each one it asks two sources the same question and compares the answers.&lt;/p&gt;

&lt;p&gt;The registry says a package points at a repository. The repository says whether it is archived, moved,&lt;br&gt;
or relicensed. Those two records disagree more often than you would think.&lt;/p&gt;

&lt;p&gt;The finding I built it for is the quiet one. Here is real output from a run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;npm&lt;/code&gt; still serves &lt;code&gt;cross-env@10.1.0&lt;/code&gt; with no deprecation notice, while its repository&lt;br&gt;
&lt;code&gt;kentcdodds/cross-env&lt;/code&gt; has been archived. Installing it looks completely normal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing in your terminal tells you. &lt;code&gt;npm install&lt;/code&gt; prints no warning. The archive banner sits on&lt;br&gt;
&lt;a href="https://github.com/kentcdodds/cross-env" rel="noopener noreferrer"&gt;the repository&lt;/a&gt; where nobody installing the package will&lt;br&gt;
look. The maintainer said goodbye in the only place they could. The registry never passed the message&lt;br&gt;
on.&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%2Fwwssoj47wxvwtm1azii8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwssoj47wxvwtm1azii8.jpg" alt="The Actor's page in Apify Console, showing its pay-per-event price and source files." width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Connecting it through the Apify MCP server
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt; turns an Actor into a tool an&lt;br&gt;
agent can call. The connection itself took one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http apify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://mcp.apify.com?tools=aiqlabs/github-repository-audit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;?tools=&lt;/code&gt; parameter matters more than it looks. Without it, the server exposes its whole surface.&lt;br&gt;
With it, the agent sees exactly one tool. I wanted a clean experiment, so I scoped it to one Actor.&lt;/p&gt;

&lt;p&gt;Authentication runs over OAuth with dynamic client registration and PKCE. I never handled a token.&lt;br&gt;
The consent screen is worth reading rather than clicking through. Apify prints the honest version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This application was registered dynamically, and wasn't verified by Apify. Make sure you trust it.&lt;br&gt;
It's allowed to redirect you to following URL(s): &lt;code&gt;http://localhost/callback&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the right warning to show. Dynamic registration means nobody vetted the client.&lt;/p&gt;
&lt;h3&gt;
  
  
  The detour
&lt;/h3&gt;

&lt;p&gt;My first three attempts failed, and the reason had nothing to do with Apify.&lt;/p&gt;

&lt;p&gt;I was driving the CLI from a non-interactive shell. &lt;code&gt;claude mcp login&lt;/code&gt; prints the authorization URL,&lt;br&gt;
starts waiting, and then gives up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Waiting for authorization… (^C to cancel)
Couldn't complete authentication: stdin isn't a terminal.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The login flow wants a real terminal even when the callback would arrive over the network. I tried&lt;br&gt;
&lt;code&gt;winpty&lt;/code&gt; next. &lt;code&gt;winpty&lt;/code&gt; refused for the same reason one level down. It needs a console of its own.&lt;/p&gt;

&lt;p&gt;What worked was a batch file, launched in a fresh console window, with stdout sent to a file. The&lt;br&gt;
console satisfies the terminal check. The redirect lets me read the authorization URL. Then I opened&lt;br&gt;
that URL, approved it, and the callback landed on &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Write that down if you automate this. The blocker is the terminal, not the network.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first run returned four rows for a one-repository question
&lt;/h2&gt;

&lt;p&gt;Before handing the tool to an agent, I wanted to see the smallest possible call. So I sent one field&lt;br&gt;
and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"repos"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"facebook/create-react-app"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 39 bytes. The run recorded an &lt;code&gt;inputBodyLen&lt;/code&gt; of 328.&lt;/p&gt;

&lt;p&gt;Something had grown my input by a factor of eight. This is what the platform actually stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"facebook/create-react-app"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npm:request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:babel-eslint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:left-pad"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaultRegistry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifestType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifestGroups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"staleAfterDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeContributors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeReleases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"onlyIssues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxTargets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestTimeoutSecs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2Fi919njh296th9hwtxtlx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi919njh296th9hwtxtlx.jpg" alt="The INPUT record Apify saved for the run, shown as JSON in the Console. It holds the single repository I sent plus a packages array of three npm packages I never sent." width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three packages I had not mentioned were now part of the run. The results came back like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;input&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;source&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;riskLevel&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;issueCodes&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;facebook/create-react-app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;repos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;repo_moved&lt;/code&gt;, &lt;code&gt;stale_no_push&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm:request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;high&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deprecated_on_registry&lt;/code&gt;, &lt;code&gt;stale_no_push&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm:babel-eslint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;critical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deprecated_on_registry&lt;/code&gt;, &lt;code&gt;repo_archived&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm:left-pad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;critical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deprecated_on_registry&lt;/code&gt;, &lt;code&gt;repo_moved&lt;/code&gt;, &lt;code&gt;repo_archived&lt;/code&gt;, &lt;code&gt;license_mismatch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fw2w627omoyaypka6jtqi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw2w627omoyaypka6jtqi.jpg" alt="The run's output table. Row one, the repository I asked about, shows null for package name and medium risk. Rows two to four are npm packages I did not ask about, shown as high and critical." width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I asked about one repository. I got four rows. Every high and critical row belongs to something the&lt;br&gt;
caller never mentioned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/request/request" rel="noopener noreferrer"&gt;&lt;code&gt;request&lt;/code&gt;&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/left-pad/left-pad" rel="noopener noreferrer"&gt;&lt;code&gt;left-pad&lt;/code&gt;&lt;/a&gt; are not neutral filler either. They are two of the&lt;br&gt;
most famously abandoned packages in the npm registry. They produce findings by design.&lt;/p&gt;
&lt;h2&gt;
  
  
  prefill and default are not the same word
&lt;/h2&gt;

&lt;p&gt;The cause is one line in my own input schema, written months earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"facebook/create-react-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"babel/babel-eslint"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npm:request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:babel-eslint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:left-pad"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had used the two keys interchangeably. I thought both were examples for the form. The&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema/specification/v1" rel="noopener noreferrer"&gt;input schema specification&lt;/a&gt;&lt;br&gt;
is unambiguous, and I had simply never read this paragraph carefully:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Default&lt;/strong&gt; — If the user omits the value when starting the Actor via any means (API, CLI,&lt;br&gt;
scheduler, or user interface), the platform automatically passes the Actor this default value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefill&lt;/strong&gt; — this field is only used in the user interface but does not affect the Actor&lt;br&gt;
functionality and API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;prefill&lt;/code&gt; is a suggestion. &lt;code&gt;default&lt;/code&gt; is a promise the platform keeps on your behalf.&lt;/p&gt;

&lt;p&gt;There is a second edge to this, and I only saw it once the Actor was a tool. The MCP server turns&lt;br&gt;
the input schema into a JSON Schema and hands it to the model. Here is what arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npm:request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:babel-eslint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:left-pad"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"facebook/create-react-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"babel/babel-eslint"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default&lt;/code&gt; is a JSON Schema keyword. It means "the value used when this is omitted", and a model&lt;br&gt;
reading the tool definition can act on it. &lt;code&gt;prefill&lt;/code&gt; is not a JSON Schema keyword at all. It passes&lt;br&gt;
through as an unrecognised key.&lt;/p&gt;

&lt;p&gt;So the field nobody asked about announces itself in the language of the specification. The field that&lt;br&gt;
answers the question carries a word the model has no rule for. I had put the standard keyword on the&lt;br&gt;
wrong field.&lt;/p&gt;

&lt;p&gt;My Actor's own code is innocent here. It destructures with &lt;code&gt;packages = []&lt;/code&gt;. The injection happens&lt;br&gt;
above it, before &lt;code&gt;Actor.getInput()&lt;/code&gt; ever returns.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I assumed would happen next
&lt;/h2&gt;

&lt;p&gt;Open the Actor in Apify Console and the bug is visible immediately. The Packages box has three values&lt;br&gt;
sitting in it. You delete them, or you leave them, and either way you decided.&lt;/p&gt;

&lt;p&gt;The form is doing something important. It shows you the whole input, including the parts you did not&lt;br&gt;
supply.&lt;/p&gt;

&lt;p&gt;An agent never sees a form. It sends the fields it decided to send. It receives rows.&lt;/p&gt;

&lt;p&gt;My rows do carry a &lt;code&gt;source&lt;/code&gt; field, so the information needed to separate them exists. An agent that&lt;br&gt;
reads it can tell the four rows apart. I want to be exact about that. It is the part I got right by&lt;br&gt;
accident, because I did not add &lt;code&gt;source&lt;/code&gt; for this reason.&lt;/p&gt;

&lt;p&gt;But an agent does not read fifty-four fields per row back to a user. It reads the summary. So I&lt;br&gt;
opened the two records my Actor writes for exactly that purpose.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SUMMARY&lt;/code&gt; counts the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"byRiskLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"licenseComparison"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"comparable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mismatchRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.333&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"notComparable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two critical and one high, for a question about one repository. No count in that block says which&lt;br&gt;
findings belong to the caller's target. The mismatch rate is worse. It is a fraction whose&lt;br&gt;
denominator the caller never chose.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ACTION_LIST&lt;/code&gt; is the record I named for what to do next. Its entries carry &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;registry&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;repo&lt;/code&gt;, &lt;code&gt;riskLevel&lt;/code&gt; and &lt;code&gt;issues&lt;/code&gt;. It has no &lt;code&gt;source&lt;/code&gt; field at all. It is sorted by severity:&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;&lt;code&gt;target&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;riskLevel&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;did the caller ask for it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;babel-eslint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;left-pad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;facebook/create-react-app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one entry that answers the question sits at the bottom. Above it are three the caller never&lt;br&gt;
mentioned. The record offers no field that would tell them apart.&lt;/p&gt;

&lt;p&gt;That is the shape of the trap. Attribution survives in the raw rows, which an agent skims. It&lt;br&gt;
disappears from both records built to be read instead.&lt;/p&gt;

&lt;p&gt;So I had a prediction, and it was a tidy one. Ask an agent whether create-react-app is safe to depend&lt;br&gt;
on. It will read &lt;code&gt;critical: 2&lt;/code&gt; and pass that on.&lt;/p&gt;
&lt;h2&gt;
  
  
  I was wrong four times in a row
&lt;/h2&gt;

&lt;p&gt;I had already read the schema, so my own answer proved nothing. I gave the question to four fresh&lt;br&gt;
agents instead. None had seen any of this. The Actor was still unfixed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is facebook/create-react-app safe to depend on?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;All four got it right.&lt;/strong&gt; Every one of them reported medium risk for create-react-app, which is the&lt;br&gt;
correct answer. Not one passed on the critical count.&lt;/p&gt;

&lt;p&gt;Three of the four went further and told the user my tool was broken.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One thing worth flagging about the tool: my first run asked about a single repository but produced&lt;br&gt;
4 rows. The extra 3 are the Actor's built-in defaults for the &lt;code&gt;packages&lt;/code&gt; field […] Those three&lt;br&gt;
carry the alarming findings — 2 critical, 1 high […] &lt;strong&gt;Reading that summary at face value would&lt;br&gt;
have produced a badly wrong answer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another said the same thing in different words, then added a line I did not want to read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;when you pass only &lt;code&gt;repos&lt;/code&gt; to this Actor, you have to set &lt;code&gt;packages&lt;/code&gt; to empty explicitly or&lt;br&gt;
unrelated rows get mixed in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a workaround for my Actor, written by an agent, addressed to my user.&lt;/p&gt;
&lt;h3&gt;
  
  
  The trap still cost something
&lt;/h3&gt;

&lt;p&gt;Nobody was misled. I want to be careful not to soften that. But four correct answers is not the same&lt;br&gt;
as no harm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two of the four ran the audit twice.&lt;/strong&gt; Double the compute units, double the GitHub requests against&lt;br&gt;
a 60-per-hour unauthenticated allowance, double the wait. The caller paid to undo my mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One told the user the tool was defective.&lt;/strong&gt; That paragraph is now part of what my Actor looks like&lt;br&gt;
to somebody deciding whether to use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One said nothing at all.&lt;/strong&gt; The fourth agent gave a clean, correct answer and never mentioned the&lt;br&gt;
three rows it had discarded. The caller was billed for four rows and got the use of one. Nothing in&lt;br&gt;
the exchange told them.&lt;/p&gt;

&lt;p&gt;And the wrong answer was sitting there the whole time. &lt;code&gt;byRiskLevel&lt;/code&gt; still reads &lt;code&gt;critical: 2&lt;/code&gt; with&lt;br&gt;
nothing beside it to say whose. Four agents declined to take it. That is not the same as it not being&lt;br&gt;
there.&lt;/p&gt;
&lt;h3&gt;
  
  
  One of them found a second bug I had missed
&lt;/h3&gt;

&lt;p&gt;The agent that ran the audit twice explained why:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I ran the audit a second time because the repo-only run left &lt;code&gt;registryDeprecated&lt;/code&gt;, &lt;code&gt;licenseMatch&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;silentAbandonment&lt;/code&gt; all &lt;code&gt;null&lt;/code&gt; — &lt;strong&gt;it never consulted npm, so it couldn't have answered "safe&lt;br&gt;
to depend on" as asked.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is right. A repository has no registry side, so three checks cannot run on it. One of them is&lt;br&gt;
&lt;code&gt;silent_abandonment&lt;/code&gt;, the finding this Actor exists for. A caller who names a repository never gets&lt;br&gt;
it, and nothing said so. The fields just came back &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had written the rule against this in my own README. Return "broken", "fine" and "could not check"&lt;br&gt;
as three different values. Then I returned the third as a bare &lt;code&gt;null&lt;/code&gt; and let it read like the second.&lt;/p&gt;

&lt;p&gt;Two of the four agents worked around it the same way, without being asked. That is a design gap, not&lt;br&gt;
a coincidence.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;



&lt;p&gt;Five changes, in order of how much they mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I removed &lt;code&gt;default&lt;/code&gt; from every field that names a target.&lt;/strong&gt; Sample values moved to &lt;code&gt;prefill&lt;/code&gt;, which&lt;br&gt;
keeps the Console form useful and never reaches the API. A field that decides &lt;em&gt;what to audit&lt;/em&gt; must&lt;br&gt;
come from the caller. A field that decides &lt;em&gt;how to audit&lt;/em&gt; can have a default, and &lt;code&gt;staleAfterDays&lt;/code&gt;&lt;br&gt;
still does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I made the empty call fail loudly.&lt;/strong&gt; With no targets the Actor now stops with a message that names&lt;br&gt;
the three ways in. It used to be impossible to reach that path, because the default guaranteed there&lt;br&gt;
was always something to audit. That was the bug hiding the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I moved the relevance rule into the description.&lt;/strong&gt; The &lt;code&gt;source&lt;/code&gt; field is now documented where an&lt;br&gt;
agent reads it, not only in the README.&lt;/p&gt;

&lt;p&gt;That last one had a second layer I only found while writing this. &lt;code&gt;source&lt;/code&gt; did not appear anywhere in&lt;br&gt;
my &lt;code&gt;dataset_schema.json&lt;/code&gt; either. So the default Console table did not show it, and neither did the&lt;br&gt;
"Problems only" view.&lt;/p&gt;

&lt;p&gt;Opening that table made it worse. My view leads with &lt;code&gt;packageName&lt;/code&gt;, and the row I asked for came from&lt;br&gt;
&lt;code&gt;repos&lt;/code&gt;, so it has no package name. The Console printed it as &lt;code&gt;null&lt;/code&gt;.&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;Package Name&lt;/th&gt;
&lt;th&gt;Registry&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Resolved Repo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;null&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;react/create-react-app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;request&lt;/td&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;request/request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;babel-eslint&lt;/td&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;babel/babel-eslint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;left-pad&lt;/td&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;left-pad/left-pad&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one row that answers the question looks like the broken one. The three rows nobody asked for look&lt;br&gt;
authoritative. I had built a view that ranked my own output by how little the reader wanted it.&lt;/p&gt;

&lt;p&gt;Both views now lead with the two columns that answer "did I ask for this row?": &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I made the summaries carry attribution too.&lt;/strong&gt; This is the change I would have missed if I had&lt;br&gt;
stopped at the schema. Removing the default stops this particular injection. It does not make the&lt;br&gt;
output attributable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ACTION_LIST&lt;/code&gt; now begins each entry with &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt;. &lt;code&gt;SUMMARY&lt;/code&gt; now carries a &lt;code&gt;bySource&lt;/code&gt;&lt;br&gt;
block beside the flat counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bySource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"byRiskLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flat line still says two critical. Now something next to it says whose.&lt;/p&gt;

&lt;p&gt;That block is eight lines in &lt;code&gt;src/audit.js&lt;/code&gt;, inside the function that builds &lt;code&gt;SUMMARY&lt;/code&gt;:&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="c1"&gt;// A single call can mix targets the caller typed with targets that arrived some&lt;/span&gt;
&lt;span class="c1"&gt;// other way - a manifest that expanded into forty dependencies, or a schema&lt;/span&gt;
&lt;span class="c1"&gt;// default. A flat count of "2 critical" cannot be acted on, because it does not&lt;/span&gt;
&lt;span class="c1"&gt;// say whose.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bySource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;for &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;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;bySource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;bySource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;riskLevel&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;bySource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;bySource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;riskLevel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;ACTION_LIST&lt;/code&gt; took two lines: &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt; moved to the front of each entry, above&lt;br&gt;
&lt;code&gt;target&lt;/code&gt;. That ordering is the whole change. A reader who cannot answer "did I ask for this?"&lt;br&gt;
cannot act on the row.&lt;/p&gt;

&lt;p&gt;This matters beyond the bug that started it. My Actor takes repositories, packages and a manifest in&lt;br&gt;
one call. A manifest URL can expand into forty dependencies from a single field. Any of those&lt;br&gt;
mixes produces a count the caller cannot take apart. The default was one way in. It was not the only&lt;br&gt;
one.&lt;/p&gt;

&lt;p&gt;Going through the rest of the output found one more. &lt;code&gt;LICENSE_REPORT&lt;/code&gt; ends with a list of licences&lt;br&gt;
that could not be resolved. That list is an instruction to open files by hand. It held bare names. So&lt;br&gt;
I gave those entries &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt; as well. Sending someone to read a licence for a dependency&lt;br&gt;
they never named wastes the same afternoon a false finding does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I made "could not check" say so out loud.&lt;/strong&gt; This is the one an agent found for me. A&lt;br&gt;
repository-only row now produces a note in &lt;code&gt;SUMMARY&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1 target(s) were given as repositories, so no registry was consulted for them. registryDeprecated,&lt;br&gt;
licenseMatch and silentAbandonment are null on those rows because they could not be checked, not&lt;br&gt;
because they came back clean. Pass the package name to check them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same sentence went into the &lt;code&gt;repos&lt;/code&gt; field description, where an agent reads it before calling.&lt;br&gt;
&lt;code&gt;null&lt;/code&gt; was already the honest value. It was not a legible one.&lt;/p&gt;

&lt;p&gt;Four tests hold this. Three check the new fields on the three records. The fourth is a control. A run&lt;br&gt;
from a single source must report one group, and its numbers must equal the flat totals. So the&lt;br&gt;
breakdown cannot invent structure that is not there. The suite went from 48 to 52.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually changed, measured the same way
&lt;/h2&gt;

&lt;p&gt;I pushed build 0.1.8 and sent the same one-field call again.&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;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rows returned&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stored &lt;code&gt;INPUT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;328 bytes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;267 bytes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;byRiskLevel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;critical 2, high 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;critical 0, high 0&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bySource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;present&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;first &lt;code&gt;ACTION_LIST&lt;/code&gt; entry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;babel-eslint&lt;/code&gt;, critical&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;the repository I asked about&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fkxkf4pcc64ufw1s72kfv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxkf4pcc64ufw1s72kfv.jpg" alt="The run's output table after the fix, on the Actor whose Store title now reads GitHub Scraper. One row, for the repository I asked about, with You asked for and Came from as the first two columns, and nothing I did not send." width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I ran the agent trial again, same question, same plain prompt, two fresh agents.&lt;/p&gt;

&lt;p&gt;Both answered correctly, as before. &lt;strong&gt;Neither mentioned a defect in the tool.&lt;/strong&gt; Before the change,&lt;br&gt;
three of four had. That is the entire measurable result, and it is a modest one. Nobody was ever&lt;br&gt;
given a wrong answer, so nothing about correctness improved.&lt;/p&gt;

&lt;p&gt;What improved is that my Actor stopped asking its callers to compensate for it.&lt;/p&gt;

&lt;p&gt;One thing did not change. Both agents still ran the audit twice, once by repository and once by&lt;br&gt;
package. That is now the right behaviour, and the output asks for it in writing. I should say that my&lt;br&gt;
new note may be causing the second call rather than merely permitting it. One agent made the same&lt;br&gt;
second call before the note existed, which argues against that. With four trials I cannot separate&lt;br&gt;
the two.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then I checked the other twenty-one
&lt;/h2&gt;

&lt;p&gt;One bad field is a typo. I wanted to know whether it was a habit. So I read every input schema I&lt;br&gt;
have published and sorted the defaults into two piles.&lt;/p&gt;

&lt;p&gt;A default is safe when it decides &lt;strong&gt;how&lt;/strong&gt; the work is done. It is dangerous when it decides &lt;strong&gt;what&lt;/strong&gt;&lt;br&gt;
the work is done to.&lt;/p&gt;

&lt;p&gt;Twenty-two published Actors. Every one of them sets a &lt;code&gt;default&lt;/code&gt; somewhere. Ten of those defaults&lt;br&gt;
still name a target, and I had just removed an eleventh.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;what it audits when the caller says nothing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;startUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulk-domain-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain-availability-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tech-stack-detector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dead-link-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docs.apify.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http-status-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apify.com/store&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pdf-inspector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdfUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a US tax form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pdf-to-text-markdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdfUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the same tax form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hacker-news-link-rot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the Hacker News &lt;code&gt;topstories&lt;/code&gt; list&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Call any of the first nine with an empty input and you get a finished report. It covers Apify's own&lt;br&gt;
website, or a US tax form. Nothing in the response says it is a sample. An agent has a well-formed&lt;br&gt;
answer to a question nobody asked.&lt;/p&gt;

&lt;p&gt;Then I noticed the part that embarrassed me most. Nine of those ten fields also appear under&lt;br&gt;
&lt;code&gt;required&lt;/code&gt; in the same schema.&lt;/p&gt;

&lt;p&gt;I had been reading &lt;code&gt;required&lt;/code&gt; as a promise that the caller named the target. So I tested that&lt;br&gt;
reading. I called &lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt; with an empty object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; empty.json
apify call GuuMKUiWcaUUqGGhG &lt;span class="nt"&gt;--input-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run succeeded and audited &lt;code&gt;apify.com&lt;/code&gt;. Here is what the platform stored as my input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"crawlSite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkBrokenLinks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxLinksToCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proxyConfiguration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"useApifyProxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I sent &lt;code&gt;{}&lt;/code&gt;. The Actor received a target, and nothing rejected the call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema/specification/v1" rel="noopener noreferrer"&gt;The specification&lt;/a&gt;&lt;br&gt;
treats the two settings as alternatives rather than as a pair. &lt;code&gt;required&lt;/code&gt; is for fields that "don't&lt;br&gt;
have a reasonable default". &lt;code&gt;default&lt;/code&gt; is passed by the platform whenever the caller omits the field,&lt;br&gt;
"via any means". Put both on one field and the second one decides. &lt;code&gt;required&lt;/code&gt; survives as a note to&lt;br&gt;
whoever is reading the form.&lt;/p&gt;

&lt;p&gt;The other twelve Actors are fine. &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;outputFormats&lt;/code&gt;, &lt;code&gt;robotsAgent&lt;/code&gt;, &lt;code&gt;manifestGroups&lt;/code&gt; —&lt;br&gt;
omitting those does not invent work.&lt;/p&gt;

&lt;p&gt;One row in the table made me think harder. &lt;code&gt;hacker-news-link-rot&lt;/code&gt; defaults its &lt;code&gt;list&lt;/code&gt; field to&lt;br&gt;
&lt;code&gt;topstories&lt;/code&gt;, and that does pick the target. But the Actor has no other way in. An empty call has to&lt;br&gt;
mean something. It is also the one row I never marked &lt;code&gt;required&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the rule is not "never use &lt;code&gt;default&lt;/code&gt;". It is narrower than that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A caller who says nothing must not receive results they cannot tell apart from results they asked&lt;br&gt;
for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;hacker-news-link-rot&lt;/code&gt; satisfies that with one sentence in its description. The other nine did not&lt;br&gt;
satisfy it at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same mistake, four times, before an agent was involved
&lt;/h2&gt;

&lt;p&gt;Here is what stung. This was not a new class of error for me. It was the fourth time.&lt;/p&gt;

&lt;p&gt;Every audit Actor I have written has produced a confident finding that was really a gap in my own&lt;br&gt;
knowledge. Each time, the fix was the same shape: split one value into two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rate limit that looked like deletion.&lt;/strong&gt; My Chrome extension auditor read 130 listings from&lt;br&gt;
Google's own sitemap, one every 600 milliseconds. Ninety-three came back as "the store has never&lt;br&gt;
heard of this ID". Every one of them was a healthy extension I had listed minutes earlier.&lt;/p&gt;

&lt;p&gt;Google serves its rate-limit interstitial as a redirect away from the store. A reader that only asks&lt;br&gt;
"did I land on a listing page?" sees exactly what a deleted extension looks like. The Actor now checks&lt;br&gt;
for that first. It never treats the interstitial as a fact about the extension. It stops the run&lt;br&gt;
instead of producing ninety-three more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 404 that was an advertising server refusing.&lt;/strong&gt; My podcast auditor reported a 1.7% episode death&lt;br&gt;
rate. Nine of those failures came from one host, and all nine shows were running fine.&lt;/p&gt;

&lt;p&gt;A per-listener redirect service was serving that audio, and stitching in advertising as it went. It&lt;br&gt;
would not build a redirect for an automated request. So it answered 404, with the body &lt;code&gt;Missing&lt;br&gt;
redirect URL&lt;/code&gt;. I had a real death rate of 0.5% and a fake one three times larger.&lt;/p&gt;

&lt;p&gt;Those cases became &lt;code&gt;undetermined&lt;/code&gt;, not dead. The finding text says what I actually know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether the audio is there cannot be established without behaving like a listener, which this&lt;br&gt;
Actor does not do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A licence warning on lodash.&lt;/strong&gt; GitHub could not match lodash's LICENSE to a standard licence, so it&lt;br&gt;
returned &lt;code&gt;NOASSERTION&lt;/code&gt;. My first version called that &lt;code&gt;license_non_standard&lt;/code&gt; and raised it.&lt;/p&gt;

&lt;p&gt;lodash, jQuery UI and UglifyJS all land there. They are ordinary MIT and BSD projects whose LICENSE&lt;br&gt;
carries an extra paragraph. A warning that fires on healthy rows does not add information. It buries&lt;br&gt;
the rows that matter. The code comment I left says it better than I can paraphrase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Raising this would put a warning on healthy rows and bury the ones that matter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A severity I set before I measured anything.&lt;/strong&gt; I once shipped "no update in three years" as a high&lt;br&gt;
severity finding. Then I measured the base rate across 1,312 App Store apps. It is 18.4%.&lt;/p&gt;

&lt;p&gt;A finding that fires on one row in five describes the ecosystem. It does not describe a problem with&lt;br&gt;
your dependency. It sits at medium now.&lt;/p&gt;

&lt;p&gt;For the Shopify auditor I dropped the check entirely. Shopify publishes a launch date and no update&lt;br&gt;
date. Building the check anyway would have meant guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I ended up with
&lt;/h2&gt;

&lt;p&gt;Every one of these is the same rule, arrived at four times the slow way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return "broken", "fine", and "I could not check" as three different values.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used to justify that by saying an agent cannot tell the third from the second. My own trials say&lt;br&gt;
otherwise. Four agents pulled the distinction out of a &lt;code&gt;source&lt;/code&gt; field I had not documented. Two&lt;br&gt;
worked out on their own that a repository-only row never touches the registry.&lt;/p&gt;

&lt;p&gt;So the reason is not that agents cannot cope. It is what coping costs.&lt;/p&gt;

&lt;p&gt;Every ambiguity you leave in your output is work you have handed to the caller. Sometimes they pay it&lt;br&gt;
in a second run against a rate-limited API. Sometimes they pay it by writing a paragraph explaining&lt;br&gt;
your tool's quirk to their user. Sometimes they pay it silently, by throwing away three quarters of&lt;br&gt;
what you charged them for.&lt;/p&gt;

&lt;p&gt;None of that shows up as an error. It shows up as your Actor being slightly more expensive and&lt;br&gt;
slightly less trusted than the one next to it.&lt;/p&gt;

&lt;p&gt;The input schema is the other half. If a field can change what gets audited, the caller must set it.&lt;br&gt;
Silence has to mean silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your own schema
&lt;/h2&gt;

&lt;p&gt;Five things, in the order that cost me the most time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search your schema for &lt;code&gt;"default"&lt;/code&gt;.&lt;/strong&gt; For each hit, ask what happens when an API caller omits
that field. If the answer changes &lt;em&gt;what&lt;/em&gt; the Actor works on, move it to &lt;code&gt;prefill&lt;/code&gt;. Listing the
field under &lt;code&gt;required&lt;/code&gt; will not do this for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call your Actor with the minimum viable input, then read the stored &lt;code&gt;INPUT&lt;/code&gt; record.&lt;/strong&gt; Do not
read the input you sent. Read what the platform saved. That is what your code receives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make sure an empty call fails.&lt;/strong&gt; If your Actor can always find something to do, you cannot tell
an empty call from a real one. I checked what that costs, because the platform runs published
Actors on its own schedule. Twelve of my twenty-two already have nothing to do on an empty call,
three of them because a &lt;code&gt;required&lt;/code&gt; field carries no &lt;code&gt;default&lt;/code&gt; at all. All twenty-two show zero
failed runs across 159 platform runs in the last thirty days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your limits into &lt;code&gt;description&lt;/code&gt;, not the README.&lt;/strong&gt; The description is what an agent reads.
Mine now says why &lt;code&gt;pyproject.toml&lt;/code&gt; is unsupported: half-parsing a manifest produces findings about
dependencies you do not have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand the tool to an agent and read what it says about you.&lt;/strong&gt; This found more than my own review
did. An agent that works around your quirk will usually explain the quirk to its user, in writing,
in the answer. That paragraph is a free bug report. It is also what your Actor looks like to a
prospective user.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The full source for the Actor in this article is on GitHub:&lt;br&gt;
&lt;a href="https://github.com/ai-q-labs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;ai-q-labs/github-repository-audit&lt;/code&gt;&lt;/a&gt;. That is&lt;br&gt;
the code behind the published Actor at&lt;br&gt;
&lt;a href="https://apify.com/aiqlabs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;apify.com/aiqlabs/github-repository-audit&lt;/code&gt;&lt;/a&gt;. It&lt;br&gt;
includes the input schema this article is about, 52 unit tests, and a live check against the real&lt;br&gt;
GitHub, npm and PyPI APIs.&lt;/p&gt;

&lt;p&gt;Clone it, run &lt;code&gt;npm install&lt;/code&gt;, then &lt;code&gt;npm test&lt;/code&gt; for the unit tests or &lt;code&gt;npm run test:live&lt;/code&gt; for the live&lt;br&gt;
check. The live check needs no key, but GitHub allows unauthenticated callers 60 requests an hour, so&lt;br&gt;
set &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; if you run it more than once.&lt;/p&gt;

&lt;p&gt;Full-size versions of every screenshot in this article sit in the same repository, under&lt;br&gt;
&lt;code&gt;docs/screenshots&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Actor ranked #1 in an agent's search. No one else's agent could see it at all.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:07:12 +0000</pubDate>
      <link>https://dev.to/apify/my-actor-ranked-1-in-an-agents-search-no-one-elses-agent-could-see-it-at-all-2jf1</link>
      <guid>https://dev.to/apify/my-actor-ranked-1-in-an-agents-search-no-one-elses-agent-could-see-it-at-all-2jf1</guid>
      <description>&lt;h1&gt;
  
  
  My Actor ranked #1 in an agent's search. No one else's agent could see it at all.
&lt;/h1&gt;

&lt;p&gt;Four days ago I published a number I was pleased with. I had measured how often an AI agent finds one of my 23 Actors when it searches the &lt;a href="https://apify.com/store" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt; through the &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;. The answer came out at roughly one query in five, and on &lt;code&gt;sitemap checker&lt;/code&gt; my Actor came back first.&lt;/p&gt;

&lt;p&gt;Both figures were wrong. On the queries I re-tested this morning, an agent that is not mine finds my Actors zero times out of six.&lt;/p&gt;

&lt;p&gt;The arithmetic was fine. The problem was the connection I measured through, and it is a problem that any Actor author can have without noticing, because the platform gives you no obvious way to measure it any other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement I trusted
&lt;/h2&gt;

&lt;p&gt;My script for that article built one URL:&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://mcp.apify.com/?token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;Then it called &lt;code&gt;search-actors&lt;/code&gt; with a keyword, paged through the results and recorded where my Actor landed. &lt;code&gt;sitemap checker&lt;/code&gt; put it at rank 1. &lt;code&gt;PDF tables&lt;/code&gt; put it at rank 2. I wrote both numbers down and reasoned from them for a week.&lt;/p&gt;

&lt;p&gt;I never asked whose search that was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same query, with the token and without
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;search-actors&lt;/code&gt; needs a token, so I could not run it anonymously. The Store also has a plain REST endpoint that serves the same shelf, and that one takes the &lt;code&gt;Authorization&lt;/code&gt; header or does without it. So I ran the same query twice:&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="c1"&gt;// store-auth-diff.mjs — is my Actor in the results, or only in *my* results?&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withAuth&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.apify.com/v2/store?search=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=100`&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;withAuth&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&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;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;ok&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;`HTTP &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;status&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&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;username&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;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&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;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aiqlabs/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;returned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mineAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &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;q&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sitemap checker&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;pdf table extractor&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;github repository audit&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  auth:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  anon:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&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;Its output on 16 August:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sitemap checker
  auth: { total: 339, returned: 78, mineAt: 1 }
  anon: { total: 339, returned: 77, mineAt: null }
pdf table extractor
  auth: { total: 757, returned: 86, mineAt: 2 }
  anon: { total: 757, returned: 84, mineAt: null }
github repository audit
  auth: { total: 417, returned: 71, mineAt: 1 }
  anon: { total: 417, returned: 69, mineAt: null }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the columns in order. &lt;code&gt;total&lt;/code&gt; is the same number with and without the token — 339 stays 339. The count of items actually returned is one to three higher when the token is present. The extra items are mine.&lt;/p&gt;

&lt;p&gt;I ran it over five queries. With the token my Actors sit at ranks 1, 2, 1, 1 and 5. Without it, none of them is anywhere in the first hundred results.&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%2F57sn0imq75s26vy4c76p.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%2F57sn0imq75s26vy4c76p.png" alt="Apify Store search results compared with and without an API token, across five queries. The total count is identical in both calls — 339, 757, 417, 5102, 3189 — while the number of items returned rises by one to three when the token is present. With the token the author's Actors rank 1, 2, 1, 1 and 5; without it they are absent from all one hundred results." width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same endpoint, same query, one HTTP header apart.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP tool reads the same shelf
&lt;/h2&gt;

&lt;p&gt;That only bears on the earlier article if &lt;code&gt;search-actors&lt;/code&gt; ranks the same way the REST endpoint does. It does. Six queries through both, comparing the top five:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;search-actors&lt;/code&gt; (token)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;/v2/store&lt;/code&gt; (token)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;/v2/store&lt;/code&gt; (no token)&lt;/th&gt;
&lt;th&gt;top 5 identical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PDF tables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GitHub repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent to 30&lt;/td&gt;
&lt;td&gt;#40&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;broken links&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent to 30&lt;/td&gt;
&lt;td&gt;#29&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RSS feed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent to 30&lt;/td&gt;
&lt;td&gt;#47&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tech stack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The top five agree on every query. Where my Actor appears in both channels, the rank is the same integer. So the first place I published — first place on &lt;code&gt;sitemap checker&lt;/code&gt;, for an AI agent — was first place for an agent carrying my token. There is one such agent and I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One query parameter says why
&lt;/h2&gt;

&lt;p&gt;At this point I assumed the token was doing something to the ranking. It is not. The Store endpoint documents a parameter that tells you exactly what is happening, and you can use it without any credentials at all.&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://docs.apify.com/api/v2/store-get" rel="noopener noreferrer"&gt;&lt;code&gt;GET /v2/store&lt;/code&gt; reference&lt;/a&gt;, on &lt;code&gt;includeUnrunnableActors&lt;/code&gt;, describing what the default excludes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actors from developers who haven't passed KYC, or full-permission Actors without a large user base&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I ran every query anonymously, twice — once plain, once with that flag on:&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="c1"&gt;// unrunnable-probe.mjs — no credentials anywhere in this file&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github repository audit&lt;/span&gt;&lt;span class="dl"&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.apify.com/v2/store?search=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=100`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &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;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;base&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;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;includeUnrunnableActors=true`&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&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;username&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;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&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;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aiqlabs/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unrunnable ON &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;default      &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;total:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mine at:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;default       total: 417 mine at: —
unrunnable ON total: 502 mine at: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across five queries, unauthenticated both times:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query&lt;/th&gt;
&lt;th&gt;default&lt;/th&gt;
&lt;th&gt;&lt;code&gt;includeUnrunnableActors=true&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent, total 339&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;#3&lt;/strong&gt;, total &lt;strong&gt;352&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pdf table extractor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent, total 757&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;#2&lt;/strong&gt;, total &lt;strong&gt;877&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;github repository audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent, total 417&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;#1&lt;/strong&gt;, total &lt;strong&gt;502&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google play audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent, total 5,102&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;#1&lt;/strong&gt;, total &lt;strong&gt;5,386&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http status checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent, total 3,189&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;#6&lt;/strong&gt;, total &lt;strong&gt;3,379&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fji0r2ef8vuokniqzap0k.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%2Fji0r2ef8vuokniqzap0k.png" alt="Apify Store search run twice without any credentials, with and without includeUnrunnableActors=true. In the default call the author's Actors are absent on all five queries. With the flag on they return at ranks 3, 2, 1, 1 and 6, and the total result count rises from 339 to 352, 757 to 877, 417 to 502, 5102 to 5386 and 3189 to 3379." width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Both calls are unauthenticated. The only difference is one documented query parameter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The totals move. Between 13 and 284 more Actors enter each result set, and mine come back near the top of them. My Actors were never ranked badly. They were filtered out before ranking, for everyone except me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which of the two exclusions is mine
&lt;/h2&gt;

&lt;p&gt;The documented filter covers two groups, and it is worth knowing which one you are in, because only one of them is fixed by paperwork. The permission level of any public Actor is readable without a token:&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.apify.com/v2/acts/aiqlabs~sitemap-checker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorPermissionLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPublic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// LIMITED_PERMISSIONS true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five Actors I checked come back &lt;code&gt;LIMITED_PERMISSIONS&lt;/code&gt;, so the full-permission half of the filter does not apply to them. That leaves the other half, and Apify Console states it plainly:&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%2Fr3p4yyh1gh1pndbvdob6.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%2Fr3p4yyh1gh1pndbvdob6.png" alt="A notice in Apify Console reading " start="" width="795" height="37"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Insights → Payouts, 16 August 2026. Billing details are registered on the same screen; the identity check is not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The document I hold keeps being rejected by the verification vendor, which is an open thread with Apify support and not this article's subject.&lt;/p&gt;

&lt;p&gt;What matters here is the shape of it. &lt;strong&gt;An incomplete account setting removed my Actors from every search except my own, and eleven days of work on titles, categories and output schemas went into Actors that were not in the result set while I was tuning them.&lt;/strong&gt; Every one of those changes was aimed at &lt;a href="https://docs.apify.com/actors/publishing/quality-score" rel="noopener noreferrer"&gt;Actor quality score&lt;/a&gt;, which Apify documents as the ranking input for both Store search and &lt;code&gt;search-actors&lt;/code&gt;. Ranking inputs do nothing for a row that never reaches the ranking stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actor works. That was never the problem.
&lt;/h2&gt;

&lt;p&gt;It is worth being clear about what is and is not broken here, because "invisible in search" and "unusable as a tool" are different failures and only one of them applies.&lt;/p&gt;

&lt;p&gt;Name the Actor in the connection string and an agent picks it up as a tool immediately. Mine is pinned into my editor's MCP config, so I asked the agent to audit two well-known repositories. It called the Actor itself, waited for the run, and read the dataset back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run m3PNCMD8HAAIGFpAq   SUCCEEDED in 2.402s   0.0027 compute units   2 items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;requested&lt;/th&gt;
&lt;th&gt;resolved to&lt;/th&gt;
&lt;th&gt;archived&lt;/th&gt;
&lt;th&gt;moved&lt;/th&gt;
&lt;th&gt;last push&lt;/th&gt;
&lt;th&gt;risk&lt;/th&gt;
&lt;th&gt;issues&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;facebook/create-react-app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;react/create-react-app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;547 days&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;repo_moved&lt;/code&gt;, &lt;code&gt;stale_no_push&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;babel/babel-eslint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;babel/babel-eslint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;1,823 days&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;critical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;repo_archived&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two and a half seconds, a fraction of a cent, and the agent had something it could act on: one dependency whose repository has quietly moved owner while the old URL still answers, and one that is archived outright. That is the whole point of exposing an Actor over MCP — the agent reaches for it mid-task instead of asking me to go and look.&lt;/p&gt;

&lt;p&gt;So the tool definition, the input schema and the output fields all do their job. An agent that has this Actor uses it fine. The failure is one step earlier and entirely invisible from here: an agent that does not already have the name will not find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your own MCP session cannot tell you
&lt;/h2&gt;

&lt;p&gt;The obvious control is to connect to the MCP server without a token and search again. That is not available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -X POST https://mcp.apify.com/ -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
{"error":"invalid_token","error_description":"Missing or invalid access token. Pass an Apify API
token in the Authorization: Bearer &amp;lt;token&amp;gt; header. ..."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 401, with no token and with an empty &lt;code&gt;?token=&lt;/code&gt; alike. The server speaks &lt;a href="https://www.jsonrpc.org/specification" rel="noopener noreferrer"&gt;JSON-RPC&lt;/a&gt; over HTTP, as &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; requires, and every session belongs to somebody.&lt;/p&gt;

&lt;p&gt;This is the part I would ask you to sit with, because it is structural and not personal. If you publish an Actor and want to know whether an agent can find it, the natural move is to connect your own client and search. That test is authenticated by construction. It will show you your Actor whether or not anyone else can see it. Your test passes, and it passes for the one reason that cannot generalise.&lt;/p&gt;

&lt;p&gt;Nothing in the MCP documentation warned me about this, and I do not think it should have to. Returning an author their own Actors is a reasonable thing for a store to do — it is what makes testing an unpublished or unreviewed Actor possible at all. The gap is not in the behaviour. It is that the only search surface an Actor author naturally reaches for is the one that cannot be run anonymously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-line check to run before you tune anything
&lt;/h2&gt;

&lt;p&gt;No token, no dependencies, no cost:&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="c1"&gt;// visible.mjs — run: node visible.mjs your-username sitemap checker&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.apify.com/v2/store?search=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=100&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;extra&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;json&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;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&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="o"&gt;=&amp;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;username&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;absent&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;span class="s2"&gt;  (total &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&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;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;as everyone sees it :&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;including filtered  :&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;includeUnrunnableActors=true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against my own account and against a publisher who is not filtered, on the same query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node visible.mjs aiqlabs sitemap checker
as everyone sees it : absent  (total 339)
including filtered  : #3  (total 352)

$ node visible.mjs automation-lab sitemap checker
as everyone sees it : #1  (total 339)
including filtered  : #6  (total 352)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different diagnoses from the same two lines. Mine is absent until the filter comes off. The other publisher is first without it, and slips to sixth with it — which is what a healthy result looks like, because turning the filter on adds thirteen competitors to that shelf.&lt;/p&gt;

&lt;p&gt;So: if the first line says &lt;code&gt;absent&lt;/code&gt; and the second gives you a rank, your Actor is being filtered out of search for every user but you, and no amount of ranking work will change that until the filter lifts. If both lines say &lt;code&gt;absent&lt;/code&gt;, you have an ordinary ranking problem. If the first line gives you a rank, you are visible and can trust the numbers you measure.&lt;/p&gt;

&lt;p&gt;I would rather have run those four lines on 5 August than on 16 August.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;I published a reachability figure and a rank without asking whose view produced them. The script that produced them had &lt;code&gt;?token=&lt;/code&gt; written into the URL, by me.&lt;/p&gt;

&lt;p&gt;It is the second time in two weeks that I have measured my own visibility from inside my own session. The first was a rank I read off the Store web UI in a browser I was logged into. I caught that one, corrected it privately, and then made the same mistake again in a script — which tells me the lesson is not "be careful with browsers" but something duller: &lt;strong&gt;if a measurement can only be taken while authenticated, the number it returns is a statement about you, not about your users.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The earlier article's other findings survive. A default MCP connection really does hand an agent eleven tools with a single Actor among them; &lt;code&gt;limit&lt;/code&gt; really does default to 5 and cap at 10; the result pages really are ragged, so a short page is not the end of the list; and broadening a keyword really does push my Actor down — that comparison sits inside one channel, so its direction holds even though both ranks are token-local.&lt;/p&gt;

&lt;p&gt;What has to be withdrawn is the headline. One query in five is what I could see. For anyone else's agent, on the queries I re-tested, it was none of them.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I had never read my own Actors the way an agent reads them</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 11 Sep 2026 05:18:24 +0000</pubDate>
      <link>https://dev.to/apify/i-had-never-read-my-own-actors-the-way-an-agent-reads-them-1h6d</link>
      <guid>https://dev.to/apify/i-had-never-read-my-own-actors-the-way-an-agent-reads-them-1h6d</guid>
      <description>&lt;p&gt;I have 23 Actors on the Apify Store. I have read their Console input forms hundreds of times - every&lt;br&gt;
field label, every hint, every checkbox, because I wrote them and then fixed them and then fixed them&lt;br&gt;
again.&lt;/p&gt;

&lt;p&gt;I had never once read the tool definition an AI agent receives when it calls them through the&lt;br&gt;
&lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;.&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%2F6r10gkpgbpllj23rrlon.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6r10gkpgbpllj23rrlon.jpg" alt="The Console input form for one of my Actors: two array fields, each pre-populated with the prefill values I wrote for human users - facebook/create-react-app, babel/babel-eslint, npm:request. This is the interface I reviewed dozens of times." width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is what I kept looking at. It is not what my callers see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last week I did. Of the 246 field descriptions I have written across those 23 Actors, &lt;strong&gt;53 arrive at&lt;br&gt;
the agent exactly as I typed them&lt;/strong&gt;. That is 21.5%. The rest are altered on the way out.&lt;/p&gt;

&lt;p&gt;Most of those alterations are documented, sensible, and probably improvements. That is not the point.&lt;br&gt;
The point is that I shipped 23 tools without ever looking at what my callers actually receive, and&lt;br&gt;
when I finally looked, I found three things the docs do not mention and one mistake that was entirely&lt;br&gt;
mine.&lt;/p&gt;

&lt;p&gt;Here is how to look, and what I found.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading your own tool definitions
&lt;/h2&gt;

&lt;p&gt;The Apify MCP server speaks Streamable HTTP, the transport defined by the&lt;br&gt;
&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;. You post JSON-RPC to it, you get&lt;br&gt;
&lt;code&gt;tools/list&lt;/code&gt; back, and that response is the ground truth for what an agent sees.&lt;/p&gt;

&lt;p&gt;The first trap costs a minute: &lt;strong&gt;the endpoint is &lt;code&gt;/&lt;/code&gt;, not &lt;code&gt;/mcp&lt;/code&gt;.&lt;/strong&gt; Post to &lt;code&gt;/mcp&lt;/code&gt; and the server&lt;br&gt;
tells you off in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is nothing at route POST /mcp?actors=... This Model Context Protocol (MCP) server supports the Streamable HTTP transport.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second trap costs a little more: &lt;code&gt;notifications/initialized&lt;/code&gt; answers with an empty body, so a&lt;br&gt;
naive &lt;code&gt;JSON.parse&lt;/code&gt; on every response throws. I wrote that bug while writing this article.&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="c1"&gt;// Read your own Actors the way an AI agent reads them.&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: APIFY_TOKEN=... node read-my-tools.mjs aiqlabs/sitemap-checker aiqlabs/pdf-inspector&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIFY_TOKEN&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;actors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://mcp.apify.com/?token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;actors=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actors&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;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&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;headers&lt;/span&gt; &lt;span class="o"&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;content-type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json, text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&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;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionId&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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;sid&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;sessionId&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;text&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// notifications answer 202 with no body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initialize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;protocolVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-11-05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;clientInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read-my-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications/initialized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at three of mine - say &lt;a href="https://apify.com/aiqlabs/sitemap-checker" rel="noopener noreferrer"&gt;&lt;code&gt;sitemap-checker&lt;/code&gt;&lt;/a&gt; and two&lt;br&gt;
others - and it prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aiqlabs--github-repository-audit  (490 chars, 16 params)
  injected by the server : waitSecs
  prefill left in schema : repos, packages
  truncated at 500 chars : repos

aiqlabs--sitemap-checker  (421 chars, 12 params)
  injected by the server : waitSecs
  prefill left in schema : domains
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of output, three things worth knowing. Take them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget nobody tells you about
&lt;/h2&gt;

&lt;p&gt;The description an agent reads is not your description. It is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This tool calls the Actor "&amp;lt;user&amp;gt;/&amp;lt;slug&amp;gt;" and retrieves its output results.
Use this tool instead of the "call-actor" if user requests this specific Actor.
Actor description: &amp;lt;your description&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README states that descriptions are truncated at 500 characters (&lt;code&gt;MAX_DESCRIPTION_LENGTH&lt;/code&gt;). That&lt;br&gt;
limit applies to the whole string above, preamble included - so what you actually get is 500 minus&lt;br&gt;
the preamble.&lt;/p&gt;

&lt;p&gt;And the preamble is not a fixed cost. Your Actor's full name sits inside it, which means &lt;strong&gt;a longer&lt;br&gt;
slug buys you a shorter description&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;budget = 338 − length("&amp;lt;user&amp;gt;/&amp;lt;slug&amp;gt;")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across my 23 that works out to &lt;strong&gt;303 to 317 characters&lt;/strong&gt;. &lt;code&gt;domain-availability-checker&lt;/code&gt; - my longest&lt;br&gt;
slug at 35 characters - gets 303. &lt;code&gt;pdf-inspector&lt;/code&gt; gets 317. Nothing anywhere told me that naming an&lt;br&gt;
Actor spends part of its description.&lt;/p&gt;

&lt;p&gt;None of mine are over. But the margins are thinner than I would have guessed: my tightest is&lt;br&gt;
&lt;code&gt;github-repository-audit&lt;/code&gt; at 10 characters of headroom, then &lt;code&gt;hacker-news-link-rot&lt;/code&gt; at 11 and&lt;br&gt;
&lt;code&gt;chrome-extension-audit&lt;/code&gt; at 12. Median description across the 23 is 278. Three of them are one&lt;br&gt;
average sentence away from being delivered with the last clause replaced by &lt;code&gt;...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I found the real numbers late, and only because I checked. My first draft of this section said the&lt;br&gt;
preamble was "174 characters, fixed" and the budget was 326 - measured off a single Actor, which&lt;br&gt;
happened to be one of the longest-named ones, and then stated as a constant.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the server changes, in numbers
&lt;/h2&gt;

&lt;p&gt;Across 23 Actors and 246 author-written fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Fields&lt;/th&gt;
&lt;th&gt;Documented&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Example values: ...&lt;/code&gt; appended&lt;/td&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;items.description&lt;/code&gt; generated where I wrote none&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prefill&lt;/code&gt; kept in the exposed JSON Schema&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;waitSecs&lt;/code&gt; parameter added&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;**REQUIRED**&lt;/code&gt; prefix added&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Possible values: ...&lt;/code&gt; appended&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description truncated at 500 chars&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero of my 246 fields had an empty description, so none of this is blanks being filled in. It is text&lt;br&gt;
I wrote being changed.&lt;/p&gt;

&lt;p&gt;The documented half is in the &lt;a href="https://github.com/apify/actors-mcp-server" rel="noopener noreferrer"&gt;server's README&lt;/a&gt; and it is&lt;br&gt;
worth reading once:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions&lt;/strong&gt; are truncated to 500 characters (as defined in &lt;code&gt;MAX_DESCRIPTION_LENGTH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required fields&lt;/strong&gt; are explicitly marked with a &lt;code&gt;REQUIRED&lt;/code&gt; prefix in their descriptions for compatibility with frameworks that may not handle the JSON schema properly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;I checked the second claim rather than trusting it: 17 fields are declared &lt;code&gt;required&lt;/code&gt; in my schemas,&lt;br&gt;
and &lt;strong&gt;all 17&lt;/strong&gt; carry the prefix in the tool definition. It does what it says.&lt;/p&gt;
&lt;h2&gt;
  
  
  The truncation that undoes itself
&lt;/h2&gt;

&lt;p&gt;Exactly one of my 246 fields is longer than 500 characters: the &lt;code&gt;repos&lt;/code&gt; field on&lt;br&gt;
&lt;a href="https://apify.com/aiqlabs/github-repository-audit" rel="noopener noreferrer"&gt;&lt;code&gt;github-repository-audit&lt;/code&gt;&lt;/a&gt;, at 624. Its top-level description arrives cut, with the last 124 characters&lt;br&gt;
replaced by &lt;code&gt;...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those 124 characters read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;...they could not be checked, not because they came back clean. Give the package name in Packages instead if you need those three.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not decoration. That sentence exists to stop a caller reading a &lt;code&gt;null&lt;/code&gt; as a clean result - the&lt;br&gt;
single misreading this Actor was built to prevent. Truncation ate exactly the warning.&lt;/p&gt;

&lt;p&gt;Except the agent still gets it. The server had also generated an &lt;code&gt;items.description&lt;/code&gt; for that array&lt;br&gt;
field - something I never wrote - containing the &lt;strong&gt;full 624 characters, byte for byte&lt;/strong&gt;. A documented&lt;br&gt;
truncation was quietly cancelled by an undocumented generation.&lt;/p&gt;

&lt;p&gt;I want to be precise about how much this shows. &lt;strong&gt;One field in 246 was long enough to test it.&lt;/strong&gt; I am&lt;br&gt;
reporting a mechanism, not a rate. But the mechanism is worth knowing in both directions: if your long&lt;br&gt;
description sits on an &lt;code&gt;array&lt;/code&gt;, the full text survives in &lt;code&gt;items&lt;/code&gt;; if it sits on a &lt;code&gt;string&lt;/code&gt;, nothing&lt;br&gt;
catches it.&lt;/p&gt;

&lt;p&gt;Where does &lt;code&gt;items.description&lt;/code&gt; come from? Of my 39 array fields, 36 got one. Twenty-four are exact&lt;br&gt;
copies of the parent description, eleven are the parent plus the &lt;code&gt;**REQUIRED**&lt;/code&gt; prefix, and one is a&lt;br&gt;
structural expansion of &lt;code&gt;editor: requestListSources&lt;/code&gt; into its &lt;code&gt;url&lt;/code&gt; / &lt;code&gt;method&lt;/code&gt; / &lt;code&gt;payload&lt;/code&gt; / &lt;code&gt;headers&lt;/code&gt;&lt;br&gt;
shape. The three that got nothing all have an &lt;code&gt;enum&lt;/code&gt; on their items.&lt;/p&gt;

&lt;p&gt;That last case caught me out while I was measuring. I first recorded it as "my 138-character&lt;br&gt;
description was replaced by a 19-character string" and nearly published that. It was wrong: the parent&lt;br&gt;
description arrives intact, and the 19 characters are the title of the expanded object type. Nothing&lt;br&gt;
was lost. I mention it because it is the kind of error that reads perfectly well in a draft.&lt;/p&gt;
&lt;h2&gt;
  
  
  The parameter you did not write
&lt;/h2&gt;

&lt;p&gt;Every one of my 23 tools carries a parameter I have never declared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Max seconds (0–45, default 30) to cap the wait for the Actor run to reach terminal state. For long-running Actors the response returns at the cap with the current run status; follow `nextStep` to poll via get-actor-run. Set to 0 to fire-and-forget."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is on the server's own tools too - &lt;code&gt;get-actor-run&lt;/code&gt; carries it - so this is a server-wide&lt;br&gt;
convention rather than something done to Actors specifically.&lt;/p&gt;

&lt;p&gt;I am not reporting this as a defect. The description tells the agent precisely how to recover, which&lt;br&gt;
is more than most timeouts do. I am reporting it because &lt;strong&gt;an author who has never read their tool&lt;br&gt;
definition does not know their Actor now has a 45-second contract with its callers.&lt;/strong&gt; If your Actor&lt;br&gt;
takes four minutes on a realistic input, every agent call returns before it finishes and the caller's&lt;br&gt;
experience depends on whether their client follows &lt;code&gt;nextStep&lt;/code&gt;. That is a design constraint on your&lt;br&gt;
Actor, and it arrived without you.&lt;/p&gt;

&lt;h2&gt;
  
  
  One key, two layers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prefill&lt;/code&gt; survives into the exposed JSON Schema on 33 of my fields. It is not a&lt;br&gt;
&lt;a href="https://json-schema.org/understanding-json-schema/reference/array" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt; keyword; nothing in&lt;br&gt;
the spec says what a consumer should do with it. An agent reading the schema sees a value&lt;br&gt;
sitting next to my field and can reasonably read it as a suggestion.&lt;/p&gt;

&lt;p&gt;I have met this key before. In an earlier article I traced a run that returned four rows when I had&lt;br&gt;
asked about one repository: the platform was treating my &lt;code&gt;prefill&lt;/code&gt; as a &lt;code&gt;default&lt;/code&gt; and injecting it&lt;br&gt;
into every call that omitted the field. Same key, different layer, different behaviour - and neither&lt;br&gt;
behaviour is one I asked for when I filled in a Console form hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I let an agent choose between all 23
&lt;/h2&gt;

&lt;p&gt;Reading the definitions tells you what arrives. It does not tell you whether the catalog works. So I&lt;br&gt;
ran a second trial: give an agent all 23 descriptions and one realistic task, and see which tool it&lt;br&gt;
reaches for.&lt;/p&gt;

&lt;p&gt;I wrote seven tasks, fixed the intended answer for each one in a file &lt;strong&gt;before launching anything&lt;/strong&gt;,&lt;br&gt;
and wrote down what I expected to happen. Two independent agents per task, fourteen trials, no Actor&lt;br&gt;
executed.&lt;/p&gt;

&lt;p&gt;I expected failures. My catalog has obvious traps in it - three PDF tools, three tools that all touch&lt;br&gt;
broken links, two that both check domains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fourteen out of fourteen picked the tool I intended.&lt;/strong&gt; Three of my seven predictions were wrong, and&lt;br&gt;
every one of them was wrong in the same direction: the agents were more careful than I gave them&lt;br&gt;
credit for. Task 7 is a fair example. I expected &lt;code&gt;http-status-checker&lt;/code&gt; to pull votes away from&lt;br&gt;
&lt;code&gt;dead-link-checker&lt;/code&gt;; instead both agents explained, unprompted, that a status checker can only test&lt;br&gt;
URLs you already have and therefore cannot recover the URLs of a blog that moved two years ago.&lt;/p&gt;

&lt;p&gt;So the selection layer was not the story. This was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence fell to &lt;code&gt;medium&lt;/code&gt; in exactly 4 of the 14 trials, and a tie was declared in exactly those&lt;br&gt;
same 4.&lt;/strong&gt; The other ten were unanimous, &lt;code&gt;high&lt;/code&gt;, and tie-free. Those four trials are the two pairs&lt;br&gt;
where I had written overlapping descriptions.&lt;/p&gt;

&lt;p&gt;Here is one of them, in my own words, from the live catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt; - "classic SEO health and for &lt;strong&gt;AI/LLM discoverability - AI crawler access in robots.txt, llms.txt&lt;/strong&gt;, structured data, metadata"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seo-audit-tool&lt;/code&gt; - "on-page and technical SEO, and adds four checks general auditors skip: &lt;strong&gt;AI crawler access in robots.txt, llms.txt&lt;/strong&gt;, redirects that drop the path, and noindex sent via X-Robots-Tag"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The phrase "AI crawler access in robots.txt, llms.txt" appears verbatim in both. I wrote both&lt;br&gt;
descriptions, months apart, and had never once read them next to each other.&lt;/p&gt;

&lt;p&gt;Asked "is my robots.txt blocking AI crawlers like GPTBot", both agents picked&lt;br&gt;
&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt;, and both told me why the pick was thin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;both entries explicitly list "AI crawler access in robots.txt" as a check, so either would answer&lt;br&gt;
the question; I picked seo-ai-visibility-auditor because AI/LLM discoverability is its stated&lt;br&gt;
primary purpose rather than one of four add-on checks. Neither description names GPTBot&lt;br&gt;
specifically, so the match rests on the "AI crawler access in robots.txt" wording alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a correct answer arrived at by elimination, with the reasoning shown and the weakness&lt;br&gt;
declared. The agent did the honest thing. The duplication it had to route around is mine.&lt;/p&gt;

&lt;p&gt;What I wanted to conclude here was: &lt;strong&gt;the signal is the confidence, not the choice&lt;/strong&gt; - every &lt;code&gt;medium&lt;/code&gt;&lt;br&gt;
landed on a real duplication, so watch where the agent hesitates. I had the sentence written. Then I&lt;br&gt;
measured it and had to take it out. That is the next section.&lt;/p&gt;

&lt;p&gt;Two caveats regardless. &lt;strong&gt;n is 2 per task&lt;/strong&gt;, all Claude, all in one harness - this is a check on my&lt;br&gt;
catalog, not evidence about agents in general. And the agents saw name and description only; a real&lt;br&gt;
client also reads the input schema, which might break a tie my prose leaves open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it, and measuring the fix the same way
&lt;/h2&gt;

&lt;p&gt;The two SEO Actors were never duplicates in the code. One takes a starting URL and crawls&lt;br&gt;
(&lt;code&gt;startUrls&lt;/code&gt;, &lt;code&gt;crawlSite&lt;/code&gt;, &lt;code&gt;maxPages&lt;/code&gt;); the other takes a list and audits it page by page (&lt;code&gt;urls&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;maxUrls&lt;/code&gt;). They are genuinely different tools. My descriptions simply never said so - they both led&lt;br&gt;
with the checks, and the checks overlap.&lt;/p&gt;

&lt;p&gt;So I rewrote both to lead with the input shape, and to name each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt; - "...&lt;strong&gt;Give it a domain to crawl&lt;/strong&gt; - for a fixed list of pages, use SEO Audit Tool instead."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seo-audit-tool&lt;/code&gt; - "&lt;strong&gt;Give it a list of URLs&lt;/strong&gt;... One row per URL - to crawl a whole site instead, use SEO &amp;amp; AI Visibility Auditor."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I pulled &lt;code&gt;tools/list&lt;/code&gt; again to confirm the agent-facing text had actually changed - no tool in&lt;br&gt;
the catalog still carries the shared phrase - and ran the same task past three fresh agents. All&lt;br&gt;
three: &lt;code&gt;high&lt;/code&gt;, no tie. One explained it without being asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;aiqlabs/seo-audit-tool also covers "AI crawler rules" but is scoped to a supplied list of URLs, and&lt;br&gt;
both descriptions explicitly cross-reference each other to resolve that split, so this is not a tie.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clean result. I nearly stopped there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The control that ruined it
&lt;/h3&gt;

&lt;p&gt;I also re-ran task 4 - the domain pair - as a control. &lt;strong&gt;I had not touched either of those two&lt;br&gt;
descriptions.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pair&lt;/th&gt;
&lt;th&gt;edited?&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;✅ yes&lt;/td&gt;
&lt;td&gt;medium ×2, tie ×2&lt;/td&gt;
&lt;td&gt;high ×3, tie none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;domain&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;no&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;medium ×2, tie ×2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;high ×2, tie none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pair I did not fix improved exactly as much as the pair I did. So the confidence change cannot be&lt;br&gt;
attributed to my rewrite, and the sentence I wanted to publish - &lt;em&gt;watch where the agent hesitates&lt;/em&gt; - &lt;br&gt;
does not survive its own control.&lt;/p&gt;

&lt;p&gt;The cause is a flaw in my design, not a mystery. My before and after prompts were not identical. The&lt;br&gt;
before agents were told "answer in exactly this structure and nothing else"; they produced answers and&lt;br&gt;
then sat on them, and I had to chase them with a second message asking them to actually send it. The&lt;br&gt;
after agents had the delivery instruction from the start. One changed variable, sitting right next to&lt;br&gt;
the one I was trying to measure.&lt;/p&gt;

&lt;p&gt;What survives is narrower and still worth having. The duplication was real: the same clause, verbatim,&lt;br&gt;
in two live descriptions I wrote months apart. Four of fourteen before-trials declared a tie and &lt;strong&gt;all&lt;br&gt;
four landed on the two pairs where my descriptions genuinely overlapped&lt;/strong&gt; - no false alarms among the&lt;br&gt;
other ten. The fix shipped, and the shared phrase is gone from what agents receive. What I cannot tell&lt;br&gt;
you is whether fixing it changed anything measurable, because I broke my own instrument while using it.&lt;/p&gt;

&lt;p&gt;If I had measured only the pair I edited - which was my plan until I added the control as an&lt;br&gt;
afterthought - I would have published a causal claim resting on a changed prompt.&lt;/p&gt;

&lt;p&gt;One more honest note on that edit. While writing the new description I listed "soft 404s answering 200" as&lt;br&gt;
one of the four checks. The Actor does detect those - there is a &lt;code&gt;soft_404&lt;/code&gt; code in its source - but&lt;br&gt;
my own README defines the four as AI crawler rules, &lt;code&gt;/llms.txt&lt;/code&gt;, path-dropping redirects, and&lt;br&gt;
&lt;code&gt;X-Robots-Tag&lt;/code&gt; noindex. I had quietly swapped one out. I caught it by reading the README before&lt;br&gt;
publishing rather than after, which is the only reason it is a footnote instead of a correction.&lt;/p&gt;

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

&lt;p&gt;Four things, none of them large:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I compute the budget per Actor&lt;/strong&gt; - &lt;code&gt;338 − length("&amp;lt;user&amp;gt;/&amp;lt;slug&amp;gt;")&lt;/code&gt; - instead of assuming one
number. Three of mine sit within 12 characters of losing a sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No field description over 500 characters unless the field is an &lt;code&gt;array&lt;/code&gt;.&lt;/strong&gt; On an array, &lt;code&gt;items&lt;/code&gt;
catches the overflow. On a string, it does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools/list&lt;/code&gt; before publish.&lt;/strong&gt; It takes ten seconds and it is the only view of your Actor that
your non-human callers actually have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read your descriptions as a set, not one at a time.&lt;/strong&gt; This is the one that actually cost me
something. I wrote two SEO Actors months apart, gave them the same clause, and never once put the
two sentences side by side. A catalog is a document; mine was 23 documents that had never met.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The wider point is smaller than a bug and more annoying than one. I built 23 tools for AI agents to&lt;br&gt;
call and spent all of my review time in a form built for humans. The one interface my actual callers&lt;br&gt;
use, I had never opened.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>agents</category>
      <category>webdev</category>
    </item>
    <item>
      <title>prefill and default look the same in Console. Only one of them lets an agent skip your required field.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 11 Sep 2026 05:18:13 +0000</pubDate>
      <link>https://dev.to/apify/prefill-and-default-look-the-same-in-console-only-one-of-them-lets-an-agent-skip-your-required-4bc</link>
      <guid>https://dev.to/apify/prefill-and-default-look-the-same-in-console-only-one-of-them-lets-an-agent-skip-your-required-4bc</guid>
      <description>&lt;p&gt;I have 23 audit Actors on the &lt;a href="https://apify.com/store" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt;. They all do a version of the same thing: take a public record, check whether what it still claims is true, and write the verdict into a dataset.&lt;/p&gt;

&lt;p&gt;Last week I set out to prove they were unsafe for AI agents to chain together. I had a specific accusation in mind, I built the experiment to demonstrate it, and the experiment refused. What I found instead was a one-line schema fix that I have now applied, and a distinction between two input-schema settings that I had treated as interchangeable for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accusation I was going to make
&lt;/h2&gt;

&lt;p&gt;Chaining means running Actor A, taking something out of its output, and passing it into Actor B. For an agent to do that through the &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;, B has to be callable with a value that only A can supply. MCP is the Model Context Protocol, the interface that exposes your Actor to clients like Claude and Cursor as a tool they can call on their own.&lt;/p&gt;

&lt;p&gt;So I read my own catalogue with a script instead of from memory. Of my 23 published Actors, &lt;strong&gt;9&lt;/strong&gt; declare a URL or a domain in their input schema's &lt;code&gt;required&lt;/code&gt; array. Counting every ordered pair where A emits the kind of identifier B requires, I get &lt;strong&gt;61&lt;/strong&gt; possible chains: 36 joined by a URL, 25 by a domain.&lt;/p&gt;

&lt;p&gt;A tenth Actor requires an identifier too, and it is the interesting one. &lt;code&gt;dataset-to-github-issues&lt;/code&gt; takes a dataset ID and a repository name. Nothing else in my catalogue emits either, so no agent can ever reach it from another Actor's output - it is a second stage with no possible first stage. I had not noticed that until the script told me.&lt;/p&gt;

&lt;p&gt;Then I looked at what those 9 do when the value never arrives.&lt;/p&gt;

&lt;p&gt;I had swept my catalogue for target-naming defaults once before and found them, so the count itself was not the surprise. Restricting it to the Actors that can actually be a second stage sharpens it: &lt;strong&gt;seven of the nine carry a &lt;code&gt;default&lt;/code&gt; that names a concrete target.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulk-domain-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dead-link-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["docs.apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;domain-availability-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http-status-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["https://apify.com/store"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seo-ai-visibility-auditor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;startUrls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"url":"https://apify.com"}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap-checker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tech-stack-detector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["apify.com"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All seven are also listed in &lt;code&gt;required&lt;/code&gt;. I had assumed that word did some work.&lt;/p&gt;

&lt;p&gt;My accusation was going to be this: in a two-stage workflow, an agent that drops the value between stages gets no error. The second Actor runs on &lt;code&gt;apify.com&lt;/code&gt;, returns a well-formed dataset, and the agent reports a confident answer about a website the user never asked about. A wrong answer with no failure attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the part that turned out to be true
&lt;/h2&gt;

&lt;p&gt;Before involving any agent, I called two of my own Actors with an empty input from the command line.&lt;/p&gt;

&lt;p&gt;The first, &lt;a href="https://apify.com/aiqlabs/seo-audit-tool" rel="noopener noreferrer"&gt;&lt;code&gt;seo-audit-tool&lt;/code&gt;&lt;/a&gt;, has &lt;code&gt;urls&lt;/code&gt; in &lt;code&gt;required&lt;/code&gt; and &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apify call aiqlabs/seo-audit-tool &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run: Calling Actor aiqlabs/seo-audit-tool (ZjU3YtyaWgqpNriNj)

Error: Input is not valid: Field input.urls is required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No run was created. No dataset, no compute, no partial result to misread. The platform named the missing field and stopped.&lt;/p&gt;

&lt;p&gt;The second, &lt;a href="https://apify.com/aiqlabs/http-status-checker" rel="noopener noreferrer"&gt;&lt;code&gt;http-status-checker&lt;/code&gt;&lt;/a&gt;, has &lt;code&gt;urls&lt;/code&gt; in &lt;code&gt;required&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; a &lt;code&gt;default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apify call aiqlabs/http-status-checker &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Checking 1 URL(s) with 8 in flight. HTML analysis: on.
INFO  Done. 1 URL(s): 0 error, 0 warning, 0 info, 1 clean.

Apify call result: SUCCEEDED
Run ID: sMLok9a5KMSSdtnD1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I sent nothing. It audited something. Reading the input the platform stored for that run shows what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"analyzeHtml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"respectRobotsTxt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"robotsAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Googlebot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"onlyIssues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"requestTimeoutSecs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxRedirects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"slowResponseMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"maxUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same platform, same account, same minute, same field name, both fields marked &lt;code&gt;required&lt;/code&gt;. One call was refused and one was silently completed. The only difference is the &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a second thing in that pair worth pulling out. &lt;code&gt;seo-audit-tool&lt;/code&gt; does carry a &lt;code&gt;prefill&lt;/code&gt; on &lt;code&gt;urls&lt;/code&gt; - two example URLs that appear in the Console form. It was still rejected. &lt;strong&gt;&lt;code&gt;prefill&lt;/code&gt; populates the form for a human; &lt;code&gt;default&lt;/code&gt; is substituted for whoever omits the field.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema/specification/v1" rel="noopener noreferrer"&gt;input schema specification&lt;/a&gt; is not vague about this. On &lt;code&gt;prefill&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;this field is only used in the user interface but does not affect the Actor functionality and API&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and, in the same paragraph:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the Prefill value won't be used by existing integrations such as Actor tasks or API calls, but the Default will be if specified&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On &lt;code&gt;default&lt;/code&gt;, it names the callers explicitly: the platform passes the value when the user omits it "via any means (API, CLI, scheduler, or user interface)". An agent calling through the MCP server is one of those means.&lt;/p&gt;

&lt;p&gt;I had read that page. I read it as a description of two features and used both, on the same field, for months.&lt;/p&gt;

&lt;p&gt;There is a detail in &lt;code&gt;http-status-checker&lt;/code&gt; that makes the gap concrete. The two settings on &lt;code&gt;urls&lt;/code&gt; do not even hold the same value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/this-page-does-not-exist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"http://apify.com/"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three URLs for the form, one URL for anyone who omits the field. I wrote both, months apart, and never put them side by side. A caller that skips &lt;code&gt;urls&lt;/code&gt; does not get the demo I built - it gets a shorter thing I stopped thinking about, and no part of the interface ever shows the two together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the accusation collapsed
&lt;/h2&gt;

&lt;p&gt;To test the chain I needed a question that one Actor cannot answer on its own. That took two attempts.&lt;/p&gt;

&lt;p&gt;My first idea was &lt;code&gt;sitemap-checker&lt;/code&gt; into &lt;code&gt;http-status-checker&lt;/code&gt;: enumerate a site's sitemap, then check the URLs. It is a bad example, because &lt;code&gt;sitemap-checker&lt;/code&gt; has &lt;code&gt;checkUrlStatus&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt; by default and already requests every URL. My second idea was &lt;code&gt;hacker-news-link-rot&lt;/code&gt; into &lt;code&gt;domain-availability-checker&lt;/code&gt;, and that fails the same way - the first Actor already emits &lt;code&gt;domainRegistrationStatus&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is worth pausing on. I had built every one of these Actors to answer a question completely, which is right for a standalone Store listing and is exactly what makes them poor chain stages. Composable and self-contained are not the same design, and I had only ever aimed at one of them.&lt;/p&gt;

&lt;p&gt;The question I settled on works because it needs HTML analysis, which the sitemap Actor does not do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Among the URLs listed in crawlee.dev's sitemap, which ones are marked noindex, and which ones are soft 404s?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A &lt;a href="https://developers.google.com/search/docs/crawling-indexing/block-indexing" rel="noopener noreferrer"&gt;noindex&lt;/a&gt; page is one that tells search engines to keep it out of the index; a &lt;a href="https://developers.google.com/search/docs/crawling-indexing/http-network-errors" rel="noopener noreferrer"&gt;soft 404&lt;/a&gt; is a missing page that returns 200 anyway. Both need the HTML of the page. &lt;code&gt;sitemap-checker&lt;/code&gt; emits 11 fields and none of them is either. &lt;code&gt;http-status-checker&lt;/code&gt; has both, behind an &lt;code&gt;analyzeHtml&lt;/code&gt; switch that is on by default. The chain is necessary, and &lt;a href="https://crawlee.dev" rel="noopener noreferrer"&gt;crawlee.dev&lt;/a&gt; is a real documentation site with a large sitemap, which makes it a fair test.&lt;/p&gt;

&lt;p&gt;I gave that question, word for word, to four fresh agents with both Actors exposed through the Apify MCP server, and no other instructions. I did not tell them to be careful, and I did not mention inputs or defaults - in an earlier experiment I learned that asking an agent to count its rows is the same as telling it where to look.&lt;/p&gt;

&lt;p&gt;Then I ignored what they told me and read what the platform stored.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stage-2 run&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;urls&lt;/code&gt; passed&lt;/th&gt;
&lt;th&gt;first entry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p5qwOESMQB0Vwj0ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.11/playwright-crawler/...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NPM7mW5oC848wn2ZL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;174&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.16/puppeteer-crawler/...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3fO6vTbKnLlkDwGnw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/blog&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;H0HkakmGE2bfkAdWU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.15/core/interface/HttpResponse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2cJHKXtsrapw9aikV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.14/browser-pool/class/PuppeteerPlugin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oGbGCrfPEM4uDuOBY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.12/puppeteer-crawler&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sDQnaUb7NxX9iz88B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.11/core/class/SystemStatus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QDpb6PG685ptVthUN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/blog&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bB20ynAk9ymZafaiY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;724&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://crawlee.dev/js/api/3.14/utils/function/downloadListOfUrls&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nine stage-2 runs. Every one carries real crawlee.dev URLs. Not one carries &lt;code&gt;https://apify.com/store&lt;/code&gt;. They also batched - 174 to 724 URLs per call - rather than making one call per URL.&lt;/p&gt;

&lt;p&gt;The trap I had spent a day setting never closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it never closed
&lt;/h2&gt;

&lt;p&gt;The mechanism is in the result once you stop arguing with it. A &lt;code&gt;default&lt;/code&gt; stands in for a value the caller never had. Inside a chain, the agent produced that value itself one step earlier and is still holding it. The omission the default is waiting for does not occur.&lt;/p&gt;

&lt;p&gt;That reconciles this with the case where the same defect does bite. When an agent calls one of these Actors cold - no preceding step, nothing in hand - the missing field is real, the platform fills it, and the run proceeds on a target nobody chose. The danger lives at the entry point of a workflow, not in the joins.&lt;/p&gt;

&lt;p&gt;I would rather publish that than the article I planned, because it changes where you should spend attention. If you are worried about agents mangling your Actor, the risk is concentrated in the first call, not in the handoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is wrong with my own experiment
&lt;/h2&gt;

&lt;p&gt;A reviewer would find this, so I will say it first.&lt;/p&gt;

&lt;p&gt;The question I picked has a null answer. Here is the summary record from one 500-URL batch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"broken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"soft404s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"noindexOnLivePages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"metaRefreshRedirects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;crawlee.dev has no soft 404s and no noindex pages in those URLs. "Which ones are noindex?" answers to "none". An agent that had quietly audited &lt;code&gt;apify.com/store&lt;/code&gt; instead would probably also have answered "none".&lt;/p&gt;

&lt;p&gt;So the agents' answers cannot tell a careful run from a careless one here. Only the stored inputs can. That is the one thing I got right by accident of habit: I built the measurement on records the platform keeps, not on what the agents said about themselves. It mattered more than I expected - &lt;strong&gt;all four agents finished without returning a report to me at all&lt;/strong&gt;, and every number above survived that because none of them came from an agent.&lt;/p&gt;

&lt;p&gt;A stronger version of this trial would use a site with known noindex pages, so a substituted target produces a &lt;em&gt;different&lt;/em&gt; answer rather than the same one. If you repeat this, pick your target site for that property.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and what it costs
&lt;/h2&gt;

&lt;p&gt;For any input field that names &lt;em&gt;what&lt;/em&gt; to work on - a URL, a domain, a repository, an account - delete the &lt;code&gt;default&lt;/code&gt; and keep the &lt;code&gt;prefill&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URLs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stringList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;"required": ["urls"]&lt;/code&gt; alongside it. The Console form still opens with an example filled in, so nothing changes for a human clicking through your Actor. What changes is that a caller who omits the field now gets &lt;code&gt;Field input.urls is required&lt;/code&gt; instead of a clean dataset about somebody else's website.&lt;/p&gt;

&lt;p&gt;The specification anticipated this too. Its guidance for &lt;code&gt;required&lt;/code&gt; reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use for fields that don't have a reasonable default and MUST be entered by the user (e.g., API token, password)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fields that don't have a reasonable default. Seven of mine had both, which is a combination the sentence quietly rules out. Marking a field &lt;code&gt;required&lt;/code&gt; and then supplying the answer yourself is not a stricter setting; it is two instructions that cancel.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;default&lt;/code&gt; for settings that describe &lt;em&gt;how&lt;/em&gt; to work - concurrency, timeouts, depth limits, feature switches. Those are genuinely safe to assume, and an empty call that inherits them is behaving correctly.&lt;/p&gt;

&lt;p&gt;The distinction is worth stating as a rule: &lt;strong&gt;&lt;code&gt;default&lt;/code&gt; on a method is a convenience; &lt;code&gt;default&lt;/code&gt; on a subject is a substitution.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking your own Actors
&lt;/h2&gt;

&lt;p&gt;I wrote the catalogue sweep as a script so I would stop trusting my memory of my own schemas. It reads every local &lt;code&gt;.actor/&lt;/code&gt; directory, classifies each input and output field into an identifier kind, and reports which of your Actors can be a chain stage and which of them will accept an empty call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node composability.mjs           &lt;span class="c"&gt;# summary and pair matrix&lt;/span&gt;
node composability.mjs &lt;span class="nt"&gt;--rules&lt;/span&gt;   &lt;span class="c"&gt;# print the classifier so you can audit it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--rules&lt;/code&gt; flag exists because the classifier is regular expressions over field names, and a regex table you cannot see is a number you should not trust. Mine misfired the first time I ran it: it reported that &lt;code&gt;github-repository-audit&lt;/code&gt; requires no identifier, which is nonsense for an Actor that audits repositories. The cause was real and boring - that schema has no &lt;code&gt;required&lt;/code&gt; key at all, so every field is optional and the defaults decide everything.&lt;/p&gt;

&lt;p&gt;If you want a single check with no script, call your own Actor with an empty input and see what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apify call &amp;lt;your-actor&amp;gt; &lt;span class="nt"&gt;--input&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it succeeds, look at what it audited. That is what an agent gets when it forgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed and what I did not
&lt;/h2&gt;

&lt;p&gt;I have not stripped the defaults from all seven Actors yet. The measurement says the harm is concentrated in cold calls, and I want to measure that path on these specific Actors before I change seven live listings - the last time I acted on a confident prediction here, the prediction was wrong twice.&lt;/p&gt;

&lt;p&gt;What I have changed is the rule I apply to new fields, and the check I run before publishing. Both of them came out of an experiment that failed to prove its own thesis, which is becoming a habit I am not entirely unhappy about.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My screener recommended every job it could not read</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Sat, 22 Aug 2026 08:05:41 +0000</pubDate>
      <link>https://dev.to/aiq_labs/my-screener-recommended-every-job-it-could-not-read-3ean</link>
      <guid>https://dev.to/aiq_labs/my-screener-recommended-every-job-it-could-not-read-3ean</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;Today I wrote a screener for a freelance job board. One category alone holds ~800 open postings, most of which I can rule out from the text: the ones that require a video interview, the ones restricted by age or gender, the ones that want your personal anecdotes, and — the one that actually matters to me — the ones that state the work must be written &lt;strong&gt;without&lt;/strong&gt; AI assistance.&lt;/p&gt;

&lt;p&gt;The tool is two stages, run from the browser console on a listing page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the listing DOM for id, pay, applicant count, open slots, deadline. Drop anything with no slots left or a bad applicant-to-slot ratio.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch()&lt;/code&gt; each survivor's detail page and test the body text against nine disqualifier regexes. Whatever survives both stages is the shortlist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It ran. It produced a shortlist. I was about to apply off that shortlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bug 1: a failed fetch was an endorsement
&lt;/h3&gt;

&lt;p&gt;The whole second stage was this:&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&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="nf"&gt;then&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="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;- here&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&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;hard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HARD&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="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&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;k&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;hard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ngc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ngc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// reject&lt;/span&gt;
&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;                                            &lt;span class="c1"&gt;// everything else: recommend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the failure path. &lt;code&gt;fetch&lt;/code&gt; rejects, &lt;code&gt;t&lt;/code&gt; is the empty string, no regex matches an empty string, &lt;code&gt;hard.length === 0&lt;/code&gt;, and the posting lands in &lt;code&gt;ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The function had exactly two outcomes: &lt;em&gt;found a disqualifier&lt;/em&gt; and &lt;em&gt;recommend&lt;/em&gt;. There was no outcome for &lt;strong&gt;I could not look&lt;/strong&gt;. So a network error, a 404, an expired session, a rate-limited response — each one silently converted into a positive recommendation. The worse the fetch went, the cleaner the posting looked.&lt;/p&gt;

&lt;p&gt;Nothing throws. Nothing logs. The shortlist just quietly gets longer.&lt;/p&gt;

&lt;p&gt;Here it is, measured, against a posting id that does not exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old implementation -&amp;gt; response body: 58 characters (a 404 page)
                   -&amp;gt; disqualifiers matched: none
                   -&amp;gt; verdict: RECOMMENDED

new implementation -&amp;gt; verdict: unreadable (HTTP 404)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Give "I could not look" its own outcome, and check twice:&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;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&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="o"&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;ok&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&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;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;unreadable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;span class="nx"&gt;id&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;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;span class="c1"&gt;// not "clean" - unjudged&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// A 200 is not proof you got the page you asked for.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/DETAIL_MARKER/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unreadable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (no body)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&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;The second guard is the one I would have skipped a year ago. A redirect to a login wall is a perfectly successful HTTP response, and it contains none of my disqualifier phrases — which under the old code made it a great job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: zero results, silently
&lt;/h3&gt;

&lt;p&gt;Same shape, different surface.&lt;/p&gt;

&lt;p&gt;I also wanted a quick count per keyword, so I fetched the search URLs directly instead of navigating. Eight keywords, one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Claude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ChatGPT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Python automation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scraping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"SEO writing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight zeros. I nearly wrote &lt;em&gt;this board has nothing in my areas&lt;/em&gt; into my notes and moved on.&lt;/p&gt;

&lt;p&gt;Then I navigated to the first URL in a real tab. &lt;strong&gt;99 results.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The listing pages are client-rendered; the detail pages are server-rendered. Same origin, same session, same cookies — different rendering, and only one of them survives &lt;code&gt;fetch&lt;/code&gt;. &lt;code&gt;fetch&lt;/code&gt; returned HTTP 200 and well-formed HTML every time. The HTML simply had no postings in it yet.&lt;/p&gt;

&lt;p&gt;Fix — make the parser prove it looked:&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;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;0-9,&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; of/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/,/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&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;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="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;`parsed 0 items but the page reports &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;total&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page states its own row count in its header. If it says 804 and my parser extracted 0, the shelf is not empty — I am not looking at it. Throwing is right here. Returning &lt;code&gt;[]&lt;/code&gt; is a lie with a plausible shape, and a plausible shape is what makes it survive review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these are one bug
&lt;/h3&gt;

&lt;p&gt;Both are the same failure: &lt;strong&gt;a check that can only answer "found" or "nothing" cannot tell you the difference between "nothing is there" and "I never looked."&lt;/strong&gt; Absence of evidence gets recorded as evidence of absence, and it gets recorded in the format that means &lt;em&gt;good news&lt;/em&gt; — a clean posting, an empty shelf.&lt;/p&gt;

&lt;p&gt;Every silent-zero bug I have shipped has had this shape. A collector of mine once exited 0 holding 27% of its rows. A link checker of mine once reported 9 of 9 while three links in the file were already dead. Same skeleton, different clothes: the failure path produced a value indistinguishable from success.&lt;/p&gt;

&lt;p&gt;The rule I now apply: &lt;strong&gt;if a function's failure path can produce a value, that value must be a third kind of answer, not a copy of the happy one.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;p&gt;One page, re-run after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;total:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;804&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;listed:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cand:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ok:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;ngc:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;own-experience-required:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;interview:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pay-unspecified:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;unreadable:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unreadable: []&lt;/code&gt; now carries information. Before the fix it carried none, because it did not exist — and its contents were being counted as job recommendations.&lt;/p&gt;

&lt;p&gt;Two more regexes went in as well, both false negatives I caught by hand while spot-checking: one posting disqualified itself with the phrasing "without using AI tools" (my pattern only looked for the word &lt;em&gt;prohibited&lt;/em&gt;), and another asked for "your own impressions" (my pattern only looked for &lt;em&gt;personal experience&lt;/em&gt;). Those two are ordinary pattern gaps. The two above are the ones worth writing down, because no amount of adding patterns would have found them.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>I paginated by 100 and lost 39 of 422 rows. At 99 and 101, nothing was missing.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Sat, 22 Aug 2026 02:15:13 +0000</pubDate>
      <link>https://dev.to/aiq_labs/i-paginated-by-100-and-lost-39-of-422-rows-at-99-and-101-nothing-was-missing-hf</link>
      <guid>https://dev.to/aiq_labs/i-paginated-by-100-and-lost-39-of-422-rows-at-99-and-101-nothing-was-missing-hf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;I wanted one number: what share of the entries in this challenge declare a particular prize category. It decides whether writing another entry is worth the evening.&lt;/p&gt;

&lt;p&gt;I got that number wrong three times in one morning. Every wrong answer looked finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First wrong answer: 19.&lt;/strong&gt; The listing endpoint returns titles and tags but not bodies, and the prize category is declared by a heading &lt;em&gt;inside&lt;/em&gt; the body. So I counted titles containing "gemini" or "google". That found 19 of the 66 entries that actually carry the heading — and one of the 47 it missed was my own most recent entry, whose title mentions neither word. I had been quoting that proxy for days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second wrong answer: 23.9%.&lt;/strong&gt; So I fetched all 412 bodies, six at a time, no delay. My loader was this:&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="n"&gt;arts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;arts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;277 of the 412 responses had the body &lt;code&gt;Retry later&lt;/code&gt;. Two words, plain text, where JSON was expected. The &lt;code&gt;except&lt;/code&gt; branch counted them and moved on, and I printed a clean table from 134 items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                items   coverage   category share
naive             134      32.5%          23.9%
complete          411     100.0%          18.5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5.4 points off, with nothing anomalous in it. &lt;strong&gt;The number was not noisy. It was wrong, and it was plausible&lt;/strong&gt; — which is the only combination that actually costs you anything.&lt;/p&gt;

&lt;p&gt;The third wrong answer is the interesting one, and I only found it because I asked something else to look at my fix. That's further down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 1: "did not parse" is not "not there"
&lt;/h3&gt;

&lt;p&gt;The loader has no bug in it. It does exactly what it says. The bug is in what it &lt;em&gt;means&lt;/em&gt;: an exception during parse was being treated as evidence about the item, when it was evidence about the transport.&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;def&lt;/span&gt; &lt;span class="nf"&gt;collect_strict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Same fetch, two extra rules: unparsed is not absent, and slow down on refusal.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;still&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;still&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;
        &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;still&lt;/span&gt;
        &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;          &lt;span class="c1"&gt;# back off: fewer requests per unit time
&lt;/span&gt;    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ids never resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Against the real API — 0.45s between requests, doubling to a 2s ceiling — that recovered all 277 in four rounds: &lt;strong&gt;134, then 77, then 63, then 3, then zero unresolved&lt;/strong&gt;. One id stayed unresolved and turned out to be a genuine &lt;code&gt;404&lt;/code&gt; (an entry deleted between the listing call and the fetch), so the assertion permits &lt;em&gt;resolved-as-404&lt;/em&gt; alongside &lt;em&gt;parsed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The offline reproducer at the end of this post runs the same two collectors against a fake server that refuses in bursts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       items   coverage   in category    share
ground truth             412     100.0%            97    23.5%
naive collector          157      38.1%            39    24.8%
strict collector         412     100.0%            97    23.5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note how boring the naive row is. 24.8% against a true 23.5%. Nobody looks twice at that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2, which I would not have found on my own
&lt;/h3&gt;

&lt;p&gt;With the numbers in hand, I handed the whole thing to &lt;strong&gt;Gemini&lt;/strong&gt; and asked, among other things: &lt;em&gt;what can still silently truncate the dataset underneath my completeness assertion?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Its first-ranked answer was that my assertion validates the fetched set against &lt;strong&gt;the listing&lt;/strong&gt;, and says nothing about whether the listing is complete.&lt;/p&gt;

&lt;p&gt;I went to check. This is the same endpoint, same tag, same minute, walked page by page until an empty page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; per_page  pages  unique ids   note
       25     17         422   empty page 18
       50                      non-JSON at page 10: 'Retry later'
       75      6         422   empty page 7
       99      5         422   empty page 6
      100      4         383   empty page 5
      101      5         422   empty page 6
      125      4         422   empty page 5
      150      3         422   empty page 4
      200      3         422   empty page 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Only &lt;code&gt;per_page=100&lt;/code&gt; loses data.&lt;/strong&gt; 99 is fine. 101 is fine. 100 comes up &lt;strong&gt;39 short of 422&lt;/strong&gt; and announces it by returning an empty page 5 — the universal signal for &lt;em&gt;you have reached the end&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It gets better. Those four pages of 100:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows returned : 400
unique ids    : 383
ids appearing on more than one page: 17
  id 4267457 appears 2x on pages [2, 3]
  id 4228478 appears 2x on pages [2, 3]
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four pages of 100 returned exactly 400 rows — the count you would sanity-check against — while containing &lt;strong&gt;383 articles, 17 of them twice, and 39 not at all&lt;/strong&gt;. The 39 are not random: the &lt;code&gt;per_page=100&lt;/code&gt; walk reaches back to 2026-07-14, the full listing reaches back to 2026-06-21. &lt;strong&gt;It is the oldest entries that vanish&lt;/strong&gt;, which is precisely the population you would use to say anything about how the challenge has changed over time.&lt;/p&gt;

&lt;p&gt;I want to be careful about what I am claiming here. I did not find the cause inside DEV's code; I have no access to it. What I measured is that &lt;strong&gt;the same query returns 422 or 383 items depending only on page size, and the short answer terminates cleanly.&lt;/strong&gt; That is reproducible from any machine, in about sixty requests, with no credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 3: the check I now put in front of the other checks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_listing_stable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A listing you cannot reproduce at two page sizes is not a population.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_page=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; yields &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ids, per_page=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; yields &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine lines, and it fails the build on an API defect I could not have guessed at.&lt;/p&gt;

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

&lt;p&gt;The offline reproducer is self-contained and needs nothing but the standard library. It builds a corpus whose property-of-interest is correlated with position — which is the case that matters — and runs both collectors against a server that refuses in bursts.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A rate limiter that answers with the words &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry later&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; is not an error your
parser will notice. It is a sampler you did not know you installed.

Runs offline against a fake server, so the numbers are reproducible without
touching anyone&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s API.

  collect_naive()   drops anything that fails to parse and returns what it got
  collect_strict()  treats &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;did not parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; as &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not fetched yet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, slows down,
                    retries, and refuses to return until every id is accounted for

Requires: nothing but the standard library.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconfigure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;412&lt;/span&gt;              &lt;span class="c1"&gt;# items in the listing
&lt;/span&gt;&lt;span class="n"&gt;BURST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;            &lt;span class="c1"&gt;# requests served before the limiter trips
&lt;/span&gt;&lt;span class="n"&gt;COOLDOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;        &lt;span class="c1"&gt;# requests refused before it serves again
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Ground truth: does item i belong to the category being counted?

    The first third of the listing is denser than the rest -- newer submissions
    mention the prize category more often than older ones. That is the part that
    matters: the property being counted is correlated with position.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Serves JSON in bursts. When the limiter trips it answers &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Retry later&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.

    The refusal is a 200 with a plain-text body. Nothing raises, nothing retries
    itself, and the caller gets a str where it expected JSON.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cooldown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COOLDOWN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cooldown&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;served&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BURST&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BURST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refused&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry later&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;served&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;collect_naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What I actually wrote. There is no bug in it -- it does exactly what it says.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;collect_strict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Same fetch, two extra rules: unparsed is not absent, and slow down on refusal.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;still&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;still&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;
        &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;still&lt;/span&gt;
        &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;          &lt;span class="c1"&gt;# back off: fewer requests per unit time
&lt;/span&gt;    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ids never resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;records&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;naive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_strict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;coverage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;in category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;share&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;naive collector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;naive&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict collector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;10.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;naive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;The naive collector reported &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;% from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;naive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The answer is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It raised nothing, logged nothing, and its table looked complete.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The strict collector resolved every id in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rounds.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page-size sweep is fifteen lines of &lt;code&gt;curl&lt;/code&gt; around the same idea and is quoted in full above.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;The collector treats an unparsed response as unfetched, backs off, retries, and asserts coverage before anything computes a share. Recovered 277 of 277.&lt;/li&gt;
&lt;li&gt;The completeness assertion permits &lt;em&gt;resolved as 404&lt;/em&gt; as well as &lt;em&gt;parsed&lt;/em&gt;, because "deleted since the listing" is a real state and silently dropping it is the same bug one level down.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assert_listing_stable&lt;/code&gt; runs before the fetch. On this tag it fails, correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Done because the review said so:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everything in the previous section. The listing defect was Gemini's first-ranked answer and I had not considered it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not done, and I'd rather say so:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I still cannot tell you what HTTP status those 277 refusals carried. I wrote the bodies to disk with &lt;code&gt;curl -o&lt;/code&gt; and &lt;strong&gt;never recorded the status codes&lt;/strong&gt;, so the question of whether a plain &lt;code&gt;resp.raise_for_status()&lt;/code&gt; would have caught the whole thing is one I destroyed the evidence for. Sequential requests do not reproduce it, and I am not going to hammer a free API until it stops talking to me just to find out. The lesson stands on its own: &lt;em&gt;store the status next to the body, or you cannot debug the fetch afterwards.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The fix lives in the loader. It belongs in the fetch layer, raising on a transport failure before anything is written to disk. That is a rewrite, not an edit, and I have not done it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Left as a guard, deliberately:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recomputing the denominator at two page sizes every time, even when nothing has changed. It costs about sixty requests and it is the only reason I know the number at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;Gemini&lt;/strong&gt; (free tier, Flash) once: after the fix worked, with the numbers already in hand, to ask what my fix still could not see. I gave it both wrong answers, the loader, the strict collector, the assertion, and four questions. I did not adopt any of it — I measured each claim.&lt;/p&gt;

&lt;p&gt;It named the class first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a &lt;strong&gt;Silent Partial Sample&lt;/strong&gt; (or Silent Truncation) resulting from &lt;strong&gt;Plausible Degradation&lt;/strong&gt;. [...] The system experienced soft failures—surrogate metrics (Attempt 1) and swallowed rate-limit payloads (Attempt 2)—that degraded the data quality into a plausible subset rather than triggering an explicit system error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one sentence covers both of my wrong answers, which is what I had asked for and had not managed to write myself. Then four ranked failure modes, and a verdict on the fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claim                                             Verdict        Measurement
Rank 1  the listing itself is truncated           held           per_page=100 -&amp;gt; 383 of 422,
                                                                 17 duplicated rows, empty page 5
Rank 2  valid JSON that lacks the key fields      held           the deleted entry parses fine and
                                                                 has no body_markdown
Rank 4  HTTP 200 carrying an error payload        not observed   the deleted entry returns a real 404
Q3      backoff+assert is a patch, not a fix      accepted       and see "Not done" above
Q4      I may not call the 32.5% sample biased    conceded       claim retracted, see below
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rank 1 is the reason this post exists.&lt;/strong&gt; I asked what my completeness check could not see, and the answer was: the thing it checks against. Sixty requests later I had an API defect that is reproducible by anyone, in which the page size everybody reaches for first is the only one that loses data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 4 cost me a claim I liked.&lt;/strong&gt; I had written that the 32.5% sample was &lt;em&gt;biased&lt;/em&gt;, not merely small, because rate-limit refusals arrive in bursts and bursts land on neighbours in a listing. Gemini pointed out that I overwrote the failed responses with the successful refetch, so I no longer have the evidence for that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You cannot claim the 32.5% sample was definitively biased due to listing-order burst clustering. You cannot claim spatial, chronological, or network locality for the failed items, because you lost the exact temporal sequence and per-request metadata.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What it left me is narrower and I think stronger:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The 32.5% sample is unvalidated and methodologically unreliable because the sampling mechanism was governed by server load-shedding rather than random selection. [...] the sample cannot be assumed to be Missing Completely at Random (MCAR).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So: 23.9% versus 18.5% is an empirical divergence of 5.4 points, and the sample was drawn by a server deciding what it felt like answering. That is enough to throw the number away. It is not enough to say &lt;em&gt;why&lt;/em&gt; it leaned the way it did, and I have edited that claim out of my notes.&lt;/p&gt;

&lt;p&gt;One last thing, which happened while I was measuring the above. My sweep script crashed decoding &lt;code&gt;curl&lt;/code&gt;'s output — the console here is cp932 and one of the titles was not — and my error handler, which was written to notice &lt;code&gt;Retry later&lt;/code&gt;, reported it as &lt;strong&gt;"rate-limited at page 1"&lt;/strong&gt; for all nine page sizes. I spent a minute believing the API had cut me off, in the middle of writing a post about mistaking a local failure for an absent record.&lt;/p&gt;

&lt;p&gt;That is the whole thing, really. Six of my submissions this month are the same shape: &lt;em&gt;every check passed and the answer was still wrong.&lt;/em&gt; A ranking only I could see. A collector reporting success on 27% of the data. A rate limiter counting the retries. A verifier that read every character back while three links in the document pointed at nothing. And now a denominator that was wrong three ways, where the third way was invisible until I asked something outside my own head what my check was not looking at.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>The checker said 9 of 9. Three links in the file were already 404.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:05:14 +0000</pubDate>
      <link>https://dev.to/aiq_labs/the-checker-said-9-of-9-three-links-in-the-file-were-already-404-4f4o</link>
      <guid>https://dev.to/aiq_labs/the-checker-said-9-of-9-three-links-in-the-file-were-already-404-4f4o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;I generate a PDF from a plain-text source with reportlab, and I verify it before it leaves the machine. The verifier is the obvious one: open the PDF with PyMuPDF, pull the text back out, assert that every required item is in there.&lt;/p&gt;

&lt;p&gt;Nine probes. Nine passes. I sent the file.&lt;/p&gt;

&lt;p&gt;The file I sent was fine, but only because I had also looked at it. When I rendered the pages to PNG and put my eyes on them, two things were wrong that the checker had not mentioned: a section heading was set in the wrong face, and a long URL ran off the right-hand side of the paper.&lt;/p&gt;

&lt;p&gt;That second one is the interesting one, because of what "off the paper" does to a text-extraction check.&lt;/p&gt;

&lt;p&gt;The URL was 115 characters. The text column is 511.3pt wide. At 8.2pt Courier, indented, the line needed 575.6pt. The last few characters were drawn past x=595.3 — the edge of an A4 sheet — and they are not on the page in any sense: not in the print, not in the render, not in the extracted text.&lt;/p&gt;

&lt;p&gt;So the extractor returned the URL &lt;strong&gt;without its tail&lt;/strong&gt;. And a dev.to URL without its tail is a different URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://dev.to/aiq_labs/...-only-one-of-them-was-real-1jh0   -&amp;gt;  200
https://dev.to/aiq_labs/...-only-one-of-them-was-real-       -&amp;gt;  404
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My checker asserted the URL was present. It &lt;em&gt;was&lt;/em&gt; present — four characters short, pointing at nothing. None of my nine probes reached far enough into the string to notice, because when you write a probe by hand you write the part that identifies the item, not the part that makes it resolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A text-extraction check cannot see the right-hand edge of the paper.&lt;/strong&gt; It reads what the content stream says. It does not know where any of it landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Measuring it instead of asserting it
&lt;/h3&gt;

&lt;p&gt;The original naive generator was never committed, so I reconstructed it from the fixed one by removing exactly the two changes I had made: the shrink-to-fit loop and the structural heading test. Then I ran both versions of the real document through both kinds of check.&lt;/p&gt;

&lt;p&gt;Same source text, A4, 42pt margins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            content check   spans off-column   URLs truncated   chars lost
  pre-fix (reconstructed)      9/9 PASS            4              3 of 10           23
  shipped                      9/9 PASS            0              0 of 10            0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of ten links silently truncated, twenty-three characters gone, and the content check reports &lt;strong&gt;9/9 PASS on both&lt;/strong&gt;. It is not that the check is weak. It is that the check is answering a different question than the one I thought I was asking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two ways to leave the column, and only one of them loses data
&lt;/h3&gt;

&lt;p&gt;Building a minimal reproducer made the shape clearer. There are two regimes, and the difference matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1, spans that left the text column:
  p1  + 44.7pt  past the paper   https://dev.to/aiq_labs/i-wrote-two-fixes-for-the-sa
  p1  + 34.8pt  into the margin  Verification tooling: deterministic geometry checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line ran past the sheet, so its tail is gone from the extracted text too — the content check &lt;em&gt;could&lt;/em&gt; have caught it with a luckier probe. The second stopped inside the paper but outside the column. Nothing is lost there. The characters extract perfectly. The document just looks broken, and &lt;strong&gt;no content check of any kind will ever tell you&lt;/strong&gt;, because from the extractor's point of view nothing happened.&lt;/p&gt;

&lt;p&gt;In the reproducer, both versions score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             content check   geometry check   URL fidelity
v1 naive       10/10 PASS     2 off-column    4 chars lost
v2 fitted      10/10 PASS     0 off-column    0 chars lost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The content column is constant. Every bit of signal is in the other two.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Two changes, on two different layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generator:&lt;/strong&gt; shrink the line until it fits the column, with a floor.&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="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BODY_SIZE&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MIN_SIZE&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;stringWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Courier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;COL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Courier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verifier:&lt;/strong&gt; stop asking what the document says and start asking where it put it.&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;def&lt;/span&gt; &lt;span class="nf"&gt;check_geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every drawn span must lie inside the text column.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;offenders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&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;right_edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LEFT&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                        &lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;right_edge&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="n"&gt;offenders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overshoot_pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;right_edge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;past_paper_pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;offenders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a dozen lines and it turns "looks fine to me" into a number that fails a build. &lt;code&gt;past_paper_pt&lt;/code&gt; is what separates the two regimes: positive means the glyphs are gone, zero or less means they printed somewhere they should not have.&lt;/p&gt;

&lt;p&gt;I also added a fidelity check, because it is cheap and it catches the failure that actually costs something: pull every URL out of the source and every URL out of the PDF, compare them positionally, and report the character delta. Twenty-three, in the pre-fix document.&lt;/p&gt;

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

&lt;p&gt;Everything above is reproducible from one file with two dependencies (&lt;code&gt;reportlab&lt;/code&gt;, &lt;code&gt;pymupdf&lt;/code&gt;). Measured on Python 3.14.2, reportlab 4.5.1, PyMuPDF 1.27.2.2, Windows. It builds the same source document twice and runs all three checks over both — copy, run, and you get the table above.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A text-extraction check cannot see page geometry.

Builds one source document twice:
  v1  naive   - fixed font size (what a first draft does)
  v2  fitted  - shrink each line until it fits the text column

Then runs two verifiers over both PDFs:
  check_content()   - &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is every field present?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;      (PyMuPDF text extraction)
  check_geometry()  - &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is every glyph on the paper?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (PyMuPDF span bboxes)
  check_fidelity()  - compares extracted URLs against the source, character for character

Requires: reportlab, pymupdf
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconfigure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.lib.pagesizes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.pdfbase.pdfmetrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stringWidth&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.pdfgen&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;

&lt;span class="n"&gt;HERE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;                       &lt;span class="c1"&gt;# 595.3 x 841.9 pt
&lt;/span&gt;&lt;span class="n"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BOTTOM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;
&lt;span class="n"&gt;COL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;LEFT&lt;/span&gt;              &lt;span class="c1"&gt;# the text column: 511.3 pt
&lt;/span&gt;&lt;span class="n"&gt;BODY_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MIN_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;8.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.4&lt;/span&gt;

&lt;span class="n"&gt;SOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Ai-Q Labs
independent developer - remote

----------------------------------------------------------------
Selected writing
----------------------------------------------------------------
Race conditions in parallel agent sessions
  https://dev.to/aiq_labs/i-wrote-two-fixes-for-the-same-race-condition-gemini-told-me-only-one-of-them-was-real-1jh0
A collector that reported success on 27% of the data
  https://dev.to/aiq_labs/my-collector-reported-success-it-had-27-of-the-data-2n0d

----------------------------------------------------------------
Tooling
----------------------------------------------------------------
Apify actors published            23
dev.to articles published          6
  Verification tooling: deterministic geometry checks for generated PDFs, so a broken layout cannot pass a test
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# What a person actually writes when asked "check the resume came out right".
# Nine probes, one per thing that must appear. Note how each one names the
# item -- none of them reaches for the tail end of a URL.
&lt;/span&gt;&lt;span class="n"&gt;PROBES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ai-Q Labs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;independent developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Selected writing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Race conditions in parallel agent sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/aiq_labs/i-wrote-two-fixes-for-the-same-race-condition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/aiq_labs/my-collector-reported-success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tooling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Apify actors published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev.to articles published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deterministic geometry checks for generated PDFs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;3&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fitted&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;pagesize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;nxt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;BOTTOM&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStrokeGray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLineWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStrokeGray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;LEAD&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;is_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;is_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nxt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Helvetica-Bold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;9.6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;LEAD&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BODY_SIZE&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fitted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MIN_SIZE&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;stringWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Courier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;COL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Courier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;LEAD&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out_path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The check I actually ran: extract text, assert every field is present.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROBES&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROBES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROBES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every drawn span must lie inside the text column.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;offenders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&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;right_edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LEFT&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                        &lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;right_edge&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="n"&gt;offenders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overshoot_pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;right_edge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;past_paper_pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;offenders&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_fidelity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compare each URL in the PDF against the URL at the same position in the source.

    Positional, not prefix-matched: an earlier attempt paired them by prefix and
    happily matched a truncated URL against a shorter unrelated one.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;url_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://\S+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HERE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out_v1_naive.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fitted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HERE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out_v2_fitted.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fitted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content check&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;geometry check&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;URL fidelity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1 naive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2 fitted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;check_fidelity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; PASS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; FAIL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0 off-column&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; off-column&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; chars lost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;v1, spans that left the text column:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;check_geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;past the paper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;past_paper_pt&lt;/span&gt;&lt;span class="sh"&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="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;into the margin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  p&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  +&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;overshoot_pt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;pt  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;v1, URLs as extracted:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;check_fidelity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  -&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chars: ...&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;              ...&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(nothing extracted)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The claims in the last section are a second file, &lt;code&gt;check_gemini_claims.py&lt;/code&gt;, built the same way: each claim becomes a deliberately broken PDF, then a measurement.&lt;/p&gt;

&lt;p&gt;One caveat about the vertical bounds check, found while writing that file: as written it flags the &lt;em&gt;first&lt;/em&gt; line of a page, because a baseline placed exactly at the top margin puts the glyph ascenders above it. That is arguably a real defect in my generator rather than a false positive, but it is a real thing you will hit on the first run.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;Shrink-to-fit in the generator; 0 off-column spans in the shipped document.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_geometry&lt;/code&gt; in the verifier, run on every build.&lt;/li&gt;
&lt;li&gt;URL fidelity check, source vs. PDF, positional, character-exact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Done because the review said so:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vertical bounds and line-collision checks, both verified to catch cases the horizontal check reports clean (numbers below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not done, and I'd rather say so:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The generator fix is a patch, and I now have the measurement that proves it (below). The real fix is to stop drawing long strings as single un-wrapped &lt;code&gt;drawString&lt;/code&gt; calls and let a layout engine break them. I have not done that yet, because I have exactly one document and the verifier now fails the build if the patch is ever insufficient. When there is a second document, the patch stops being defensible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Left as a guard, deliberately:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The render-and-look step. It is the only reason I found any of this, and no check I have written since would have caught the wrong-typeface heading — that one has no numeric signature at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;Gemini&lt;/strong&gt; (free tier, Flash) at one point: after both fixes were written and the numbers were in, before I decided the job was done. I handed it the measurement table, both check functions, both fixes, and three questions — state the general class of defect, name what my new check still misses, and tell me for each of my two fixes whether it is a fix or a patch.&lt;/p&gt;

&lt;p&gt;It gave the class cleanly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A purely semantic text-extraction test [...] cannot detect &lt;strong&gt;geometric presentation defects&lt;/strong&gt; [...] Text extractors read the stream's character data (the &lt;em&gt;what&lt;/em&gt;); they do not simulate clipping paths, viewports, or bounding-box intersections with canvas boundaries (the &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then four ranked failure modes my check still misses, each with code, and a verdict on each of my two fixes: &lt;strong&gt;shrink-to-fit — PATCH. Geometry check — GENUINE FIX.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did not adopt any of it. I built each claim as a deliberately broken PDF and measured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claim                                          Verdict        Measurement
shrink-to-fit still fails at the size floor    held           202-char line shrank to 5.35pt
                                                              (floor 5.4), still outside the column
Rank 1  bottom overflow                        held           horizontal check 0, vertical check 6
Rank 2  overlapping lines                      held           both bounds checks 0, collision check
                                                              5 pairs, overlap up to 7.0pt
stringWidth disagrees with PyMuPDF metrics     did not hold   Courier +0.00pt, embedded TTF -0.03pt
Rank 4  text hidden by a clip path             did not hold   and it inverted, see below
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three held. Two did not, and the two failures are the useful part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The font-metrics claim was wrong on my setup.&lt;/strong&gt; The argument was that &lt;code&gt;stringWidth&lt;/code&gt; uses target font metrics while PyMuPDF recomputes bounding boxes from embedded metrics, so my shrink loop is built on a number that lies. Measured, they agree to 0.00pt for base-14 Courier and to 0.03pt for an embedded TrueType face. Plausible mechanism, real in principle, not happening here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The clip-path claim inverted.&lt;/strong&gt; The prediction was that &lt;code&gt;get_text()&lt;/code&gt; returns text that graphics commands have clipped away, so my content check would pass while the page printed only part of the string. I built exactly that document — 74 characters drawn inside a 150pt-wide clip rectangle — and got the opposite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extracted : 'PAYMENT TERMS: net 30 days from'
text check: FAIL  (the string is not there)
geometry  : PASS  (the clipped bbox is inside the margins)
rendered  : ink stops at 150pt of a 153pt span
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PyMuPDF 1.27 honours the clip during extraction. So the check that got called &lt;em&gt;weak&lt;/em&gt; is the one that catches this, and the check that got called a &lt;em&gt;genuine fix&lt;/em&gt; is the one that reports the page clean. My geometry check does not compare the span bbox against the clipping path — it never sees one.&lt;/p&gt;

&lt;p&gt;That is worth more to me than the three that held. The three that held told me where to add code. The one that inverted told me that &lt;strong&gt;my new check has the same shape of blind spot as the old one&lt;/strong&gt;: it trusts a number the document hands it, and that number has already been through the machinery it is supposed to be auditing.&lt;/p&gt;

&lt;p&gt;The thread running through all of my submissions this month is the same, and I keep walking into it: &lt;em&gt;every check passed and the answer was still wrong.&lt;/em&gt; A ranking only I could see. A collector reporting success while holding 27% of the data. A rate limiter counting the retries. A guard that had stopped guarding. Two fixes where one was theatre. And now a verifier that read every character back correctly while three of the links in the document it had just approved pointed at nothing.&lt;/p&gt;

&lt;p&gt;What breaks the pattern is never a stricter version of the same check. It is a second fact, derived a different way, that is allowed to disagree with the first. Here that was pixels versus characters — and, once I had run out of ways to doubt myself, a second reader willing to hand me four things to go break.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>I wrote two fixes for the same race condition. Gemini told me only one of them was real.</title>
      <dc:creator>Ai-Q Labs</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:56:51 +0000</pubDate>
      <link>https://dev.to/aiq_labs/i-wrote-two-fixes-for-the-same-race-condition-gemini-told-me-only-one-of-them-was-real-1jh0</link>
      <guid>https://dev.to/aiq_labs/i-wrote-two-fixes-for-the-same-race-condition-gemini-told-me-only-one-of-them-was-real-1jh0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;This morning my conversation log was saved under a different project's name.&lt;/p&gt;

&lt;p&gt;I run several AI coding-agent sessions in parallel on one machine — same OS user, sometimes the same repository. One of them has a skill that archives the current conversation into a notes vault. Because non-ASCII arguments get mangled on this shell, the metadata for that archive (title, destination project) is &lt;em&gt;not&lt;/em&gt; passed on the command line. It is written to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;~/.claude/temp/obsidian-meta.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a separate script reads that file back a moment later.&lt;/p&gt;

&lt;p&gt;Read that path again. There is no session id in it. There is one such file per machine, and I was running more than one session per machine.&lt;/p&gt;

&lt;p&gt;Here is what the two sessions did, interleaved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session A   write meta.json  {"title": "paid-writing pitches", "project": "self-catering"}
session B   write meta.json  {"title": "MC mail triage",       "project": "Rtoner-Google"}
session A   run archiver  -&amp;gt;  reads meta.json  -&amp;gt;  gets B's title, B's folder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session A's conversation was written to disk with session B's title, in session B's project directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing failed.&lt;/strong&gt; The file existed. The JSON parsed. &lt;code&gt;json.load()&lt;/code&gt; returned a dict with exactly the keys the archiver expected. Every check passed, and the answer was somebody else's.&lt;/p&gt;

&lt;p&gt;I only caught it because the archiver stamps frontmatter into the file it writes — the real &lt;code&gt;session_id&lt;/code&gt;, the real working directory — and those contradicted the filename it had just chosen. The body of the file said one thing and the name of the file said another. Without that redundancy I would still not know.&lt;/p&gt;

&lt;p&gt;Then I remembered that four days earlier, the same thing had happened somewhere else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The second surface: git's index
&lt;/h3&gt;

&lt;p&gt;Four days before, I had run &lt;code&gt;git add -- &amp;lt;my-paths&amp;gt;/&lt;/code&gt; and then paused to ask the user whether to commit. While I waited, a &lt;em&gt;different&lt;/em&gt; agent session in the same repository ran &lt;code&gt;git commit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My eleven staged files went into that session's commit, under a message about an unrelated subproject.&lt;/p&gt;

&lt;p&gt;Nothing was lost. But my changes are now buried in a commit whose title does not describe them, which is its own kind of data loss — the kind you discover six months later while running &lt;code&gt;git log&lt;/code&gt; and finding nothing.&lt;/p&gt;

&lt;p&gt;What is worth noticing is which safeguard did &lt;em&gt;not&lt;/em&gt; help. I had a rule, written after an earlier incident: &lt;em&gt;always name the paths explicitly when you stage; never &lt;code&gt;git add .&lt;/code&gt;&lt;/em&gt;. I followed it. I even machine-checked that nothing outside my scope was staged.&lt;/p&gt;

&lt;p&gt;That rule protects the contents of &lt;strong&gt;my&lt;/strong&gt; commit. It says nothing about &lt;strong&gt;another&lt;/strong&gt; session's commit consuming my staged state. I had written a rule about the wrong half of the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the window measurable
&lt;/h3&gt;

&lt;p&gt;Two anecdotes are not a bug report, so I built the smallest thing that would show the window is real: two threads, each playing one agent session, each doing write-then-read-back 200 times. One run with a shared filename, one with a per-session filename. Count how often a session reads back data that isn't its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mode=fixed     handoffs=400  wrong-owner=219  rate=54.8%
  sess-A: 114/200 reads returned another session's data
  sess-B: 105/200 reads returned another session's data

mode=isolated  handoffs=400  wrong-owner=0    rate=0.0%
  sess-A: 0/200 reads returned another session's data
  sess-B: 0/200 reads returned another session's data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared-name number moves between runs, because thread scheduling decides it. Over 7 runs it landed between &lt;strong&gt;54.8% and 62.3%&lt;/strong&gt; (219–249 of 400). The per-session number did not move: &lt;strong&gt;0 of 400, in all 6 runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither of those is the real-world rate, and I want to be explicit about it.&lt;/strong&gt; Two threads in a tight loop maximise the overlap on purpose. In production this has bitten me exactly once, over weeks of parallel sessions. The percentage does not tell you how often it happens — it tells you the window is real, and that closing it takes the count to a hard zero rather than merely lowering it. The gap between "a number that wanders around 60" and "a number that is 0 every time" is the entire finding; the number itself is not evidence of anything.&lt;/p&gt;

&lt;p&gt;The interesting column is the failure &lt;em&gt;mode&lt;/em&gt;, not the rate. Look at what a wrong-owner read looks like from inside the process:&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="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# no exception. valid JSON. all expected keys present.
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;- the only thing that would ever notice
&lt;/span&gt;    &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in the normal path checks that line. A crash would have been a gift.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two fixes, and the one I got wrong
&lt;/h3&gt;

&lt;p&gt;I wrote both fixes before asking anyone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Handoff file&lt;/strong&gt; — use a per-session name, &lt;code&gt;obsidian-meta-&amp;lt;session_id&amp;gt;.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt; — never leave anything staged across a pause. Emit &lt;code&gt;git add -- &amp;lt;paths&amp;gt; &amp;amp;&amp;amp; git commit -F -&lt;/code&gt; as a single command, and get approval &lt;em&gt;before&lt;/em&gt; staging rather than between.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both looked equally good to me. I gave the whole thing to Gemini — both incidents, the reproduction numbers, and both fixes — and asked five questions, one of which was "is the per-session filename a genuine fix or an accident-avoidance patch?"&lt;/p&gt;

&lt;p&gt;I expected a yes on both. I got a split verdict:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Failure 1 (Per-session filename): Genuine Fix.&lt;/strong&gt;&lt;br&gt;
By scoping the path to &lt;code&gt;&amp;lt;session_id&amp;gt;&lt;/code&gt;, you eliminate the shared state altogether.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 2 (Atomicity via atomic chaining): A Patch, Not a Fix.&lt;/strong&gt;&lt;br&gt;
Getting approval first and running &lt;code&gt;git add ... &amp;amp;&amp;amp; git commit&lt;/code&gt; shrinks the window of vulnerability, but it does not fix the underlying lack of isolation. If Session B executes its &lt;code&gt;git commit&lt;/code&gt; in the precise millisecond between Session A's &lt;code&gt;git add&lt;/code&gt; and Session A's &lt;code&gt;git commit&lt;/code&gt;, Session B will still sweep up Session A's staged files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is right, and I had not seen it. Fix 1 removes the shared resource. Fix 2 keeps the shared resource and runs past it faster. Those are not the same kind of thing, and I had filed them under the same heading because they made the same symptom stop appearing in my testing.&lt;/p&gt;

&lt;p&gt;The generalisation is the part I'll actually keep:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The failure of Git's index highlights that reducing time-at-risk is merely a mitigation; physical or logical isolation is the only general solution. When a shared resource cannot be renamed or parameterised at the application level, you must isolate the environment itself. For Git, the true structural fix is to give each agent session its own isolated working tree and index (e.g., using git worktree or running sessions inside separate container/VM environments).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Renaming a temp file is available to me because I own the code that names it. &lt;code&gt;.git/index&lt;/code&gt; is at a fixed path by design — I cannot parameterise my way out of it, so the fix has to move up a level, to the environment. Same root cause, two different fixes, and the reason they differ is not the bug but how much of the surface I control.&lt;/p&gt;

&lt;p&gt;The annoying part: my agent runtime already supports per-session git worktrees. I had a working isolation primitive sitting in the toolbox and reached for a stopwatch instead.&lt;/p&gt;

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

&lt;p&gt;Full reproduction, dependency-free, stdlib only:&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Minimal reproduction of the handoff-file race that mislabelled my conversation log.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;

&lt;span class="n"&gt;TRIALS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="n"&gt;TMP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gettempdir&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;race_repro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TMP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;meta_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The only difference between the bug and the fix is this function.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TMP&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                    &lt;span class="c1"&gt;# one file for everybody
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TMP&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# one file per session
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One agent session: write my metadata, then read it back to use it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TRIALS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;meta_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# step 1 - hand off my own metadata
&lt;/span&gt;        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# step 2 - the downstream script reads it back
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# a torn read also counts as a loss, but it is the *loud* failure;
&lt;/span&gt;            &lt;span class="c1"&gt;# the dangerous one is the silent branch below
&lt;/span&gt;            &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="c1"&gt;# the read succeeded and the JSON parsed. Is it mine?
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wrong&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage: race_repro.py [fixed|isolated]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self-catering&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rtoner-Google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TRIALS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; handoffs=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  wrong-owner=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  rate=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TRIALS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; reads returned another session&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;python race_repro.py fixed | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="go"&gt;mode=fixed     handoffs=400  wrong-owner=243  rate=60.8%
mode=fixed     handoffs=400  wrong-owner=239  rate=59.8%
mode=fixed     handoffs=400  wrong-owner=232  rate=58.0%
mode=fixed     handoffs=400  wrong-owner=248  rate=62.0%
mode=fixed     handoffs=400  wrong-owner=249  rate=62.3%

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;python race_repro.py isolated | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="go"&gt;mode=isolated  handoffs=400  wrong-owner=0  rate=0.0%
mode=isolated  handoffs=400  wrong-owner=0  rate=0.0%
mode=isolated  handoffs=400  wrong-owner=0  rate=0.0%
mode=isolated  handoffs=400  wrong-owner=0  rate=0.0%
mode=isolated  handoffs=400  wrong-owner=0  rate=0.0%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole diff between "broken" and "correct" is one f-string in &lt;code&gt;meta_path()&lt;/code&gt;. That is what makes this class of bug worth writing about: the fix is trivial once you see it, and completely invisible until something contradicts itself in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;Per-session handoff filenames (&lt;code&gt;obsidian-meta-&amp;lt;session_id&amp;gt;.json&lt;/code&gt;). 0 of 400 in every one of the 6 harness runs.&lt;/li&gt;
&lt;li&gt;The mislabelled conversation log was located and removed. I could identify it because the frontmatter inside disagreed with the filename — the redundancy that caught the bug in the first place.&lt;/li&gt;
&lt;li&gt;Approval for a commit is now requested &lt;em&gt;before&lt;/em&gt; staging, not between staging and committing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Changed because of the review, not yet shipped:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git isolation via per-session worktrees, instead of trying to be fast between &lt;code&gt;add&lt;/code&gt; and &lt;code&gt;commit&lt;/code&gt;. My runtime supports this natively; I had simply never turned it on for the parallel case. Shipping it means changing how sessions start, which is not a thing to rush at the end of a session that has already found two bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not done, and I'd rather say so:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The skill's own instruction file still documents the fixed path. The per-session name works, but the written procedure has not been updated to require it, so the next session that follows the documentation reintroduces the bug. Changing that file is a harness change and my own rules require reading the design docs before touching it — which is correct, and which I have not done yet. A fix that lives only in my head is not a fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Left as a guard, deliberately:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The archiver writes the true &lt;code&gt;session_id&lt;/code&gt; and working directory into the output. That redundancy is the only reason I ever found out. When a handoff crosses a process boundary, having the payload carry its own identity — and having &lt;em&gt;something&lt;/em&gt; downstream compare it — turns a silent wrong answer into a detectable one. Every check in the normal path passed. Only the disagreement between two independently-derived facts caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;Gemini&lt;/strong&gt; (free tier, Flash) once, at one specific point: after I had reproduced the bug and written both fixes, and before I shipped either.&lt;/p&gt;

&lt;p&gt;What I handed over was deliberately boring — the two incidents, the interleaving, the harness numbers, both proposed fixes, and one instruction: &lt;em&gt;if my reasoning is wrong somewhere, say so directly.&lt;/em&gt; No code, no repo access. Five questions, of which the load-bearing one was whether my fixes were fixes.&lt;/p&gt;

&lt;p&gt;Three things came back that I did not have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A split verdict where I expected a pair.&lt;/strong&gt; Fix 1 genuine, fix 2 a patch — with the exact interleaving that defeats fix 2 spelled out (&lt;code&gt;commit&lt;/code&gt; landing in the millisecond between my &lt;code&gt;add&lt;/code&gt; and my &lt;code&gt;commit&lt;/code&gt;). I had tested fix 2 by not being able to make it fail, which is not the same as it not failing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The category the two fixes actually belong to.&lt;/strong&gt; &lt;em&gt;"Reducing time-at-risk is merely a mitigation; physical or logical isolation is the only general solution."&lt;/em&gt; That sentence is why I stopped writing rules about how fast to move and started looking at worktrees — a primitive I already had.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The next five surfaces, concretely.&lt;/strong&gt; Local TCP ports (&lt;code&gt;EADDRINUSE&lt;/code&gt;, or worse, silently reaching the wrong session's dev server), shared build caches, global CLI config and credential files being rewritten mid-run by another session, local SQLite/Postgres and migrations, and lock/socket files in &lt;code&gt;/tmp&lt;/code&gt;. Each one is a fixed path that exactly one process was ever expected to own.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also gave me the vocabulary I had been talking around: &lt;strong&gt;TOCTOU&lt;/strong&gt;, and &lt;em&gt;state interleaving / cross-talk&lt;/em&gt; for the multi-tenant framing. The class is older than agents by decades. What is new is who is racing — not threads I wrote, but sessions I started, in tools that were designed when "one developer, one machine, one thing at a time" was too obvious to state.&lt;/p&gt;

&lt;p&gt;Two of my earlier submissions this month were about measurements that were quietly wrong — a ranking only I could see, a collector that reported success holding 27% of the data. This one has the same shape and I keep walking into it: &lt;strong&gt;every check passed and the answer was still wrong.&lt;/strong&gt; The thing that broke the pattern here was not a better check. It was a second, independently-derived fact that could disagree with the first — and, at the point where I'd stopped being able to see my own reasoning, a second opinion that was willing to tell me one of my two fixes was theatre.&lt;/p&gt;

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