<?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: Waleed Arshad</title>
    <description>The latest articles on DEV Community by Waleed Arshad (@waleed_arshad_ada1184b196).</description>
    <link>https://dev.to/waleed_arshad_ada1184b196</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%2F4054236%2F79787e37-2491-48b5-82b9-05d559f5b4ed.png</url>
      <title>DEV Community: Waleed Arshad</title>
      <link>https://dev.to/waleed_arshad_ada1184b196</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/waleed_arshad_ada1184b196"/>
    <language>en</language>
    <item>
      <title>Version AI-Visibility Prompt Sets Without Breaking Before/After Comparisons</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Tue, 11 Aug 2026 23:10:13 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/version-ai-visibility-prompt-sets-without-breaking-beforeafter-comparisons-kdg</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/version-ai-visibility-prompt-sets-without-breaking-beforeafter-comparisons-kdg</guid>
      <description>&lt;p&gt;A prompt set is part of an AI-visibility measurement instrument. If you edit that instrument between two collection runs, a change in the output may reflect the prompts rather than a change in the brand, sources, or answer system.&lt;/p&gt;

&lt;p&gt;That sounds obvious, yet prompt libraries often live in a spreadsheet where wording, intent labels, and sampling rules are overwritten in place. The team then compares “before” and “after” percentages as if both runs used the same test.&lt;/p&gt;

&lt;p&gt;The fix is not to freeze prompts forever. It is to version them like code and make comparability an explicit property of every report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the prompt set as an immutable release
&lt;/h2&gt;

&lt;p&gt;Give each release a stable identifier and never mutate it after it has produced observations. A compact JSON representation can look like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "prompt_set_id": "retail-ai-discovery-us",
  "version": "1.2.0",
  "parent_version": "1.1.0",
  "effective_at": "2026-08-11",
  "locale": "en-US",
  "sampling": {
    "engines": ["engine-a", "engine-b"],
    "runs_per_prompt": 3
  },
  "prompts": [
    {
      "prompt_id": "local-discovery-001",
      "intent": "local_discovery",
      "template": "Which providers offer {{service}} in {{city}}?",
      "variables": ["service", "city"],
      "status": "active"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;prompt_set_id identifies the long-lived measurement program. version identifies one immutable release. parent_version creates a traceable lineage. The sampling block belongs in the same release because a different engine list or run count can change what the aggregate means even when every prompt string is identical.&lt;/p&gt;

&lt;p&gt;Keep prompt IDs stable across releases only when the underlying measurement intent is stable. A prompt ID is not a row number; it is the join key for matched comparisons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a change policy that reflects analytical impact
&lt;/h2&gt;

&lt;p&gt;Semantic versioning is useful if its rules are defined for measurement rather than package compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patch&lt;/strong&gt;: metadata corrections that do not alter the generated prompt, such as fixing a description or owner field. A typo correction should be treated cautiously; if it changes the submitted string, store the new string and hash even when the team believes the meaning is unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minor&lt;/strong&gt;: additive changes that preserve the existing prompt IDs and sampling policy. New prompts may expand coverage, but before/after reporting must separate the matched core from the expanded full set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Major&lt;/strong&gt;: any change that edits, removes, reweights, or reinterprets an existing prompt; changes variables in a way that alters intent; or modifies the engine, locale, geography, or sampling policy used for the headline comparison.&lt;/p&gt;

&lt;p&gt;This policy prevents a new set of high-intent prompts from being blended into an old baseline and presented as brand growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record a real change log
&lt;/h2&gt;

&lt;p&gt;A useful change log needs more than a sentence saying “updated prompts.” Capture these fields for every release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;version and parent version;&lt;/li&gt;
&lt;li&gt;release timestamp and owner;&lt;/li&gt;
&lt;li&gt;added, modified, deprecated, and removed prompt IDs;&lt;/li&gt;
&lt;li&gt;old and new normalized prompt hashes;&lt;/li&gt;
&lt;li&gt;intent, locale, and variable changes;&lt;/li&gt;
&lt;li&gt;sampling-policy changes;&lt;/li&gt;
&lt;li&gt;reason for the change;&lt;/li&gt;
&lt;li&gt;expected comparability impact;&lt;/li&gt;
&lt;li&gt;reviewer and approval timestamp;&lt;/li&gt;
&lt;li&gt;bridge-run requirement and completion state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A normalized hash should be deterministic, but do not normalize away meaningful punctuation, negation, ordering, or variable placement. Preserve the exact submitted text separately. The hash detects accidental mutation; the raw text explains it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A synthetic example: when a rewrite breaks the baseline
&lt;/h2&gt;

&lt;p&gt;Assume version 1.0 contains this prompt:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which providers offer laser engraving in Austin?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A proposed replacement says:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What is the best laser engraving company in Austin?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Both concern the same service and city, but they do not measure the same thing. The first is a discovery query. The second introduces an evaluative superlative and a narrower company label. Reusing the old prompt ID would hide an intent change.&lt;/p&gt;

&lt;p&gt;The safer release creates a new prompt ID, deprecates the old one without deleting it, and increments the major version. During a bridge period, collect both prompts under the same engine, locale, and sampling conditions. Report three views:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the matched legacy core, using only unchanged prompt IDs;&lt;/li&gt;
&lt;li&gt;the new prompt’s results as a separate series;&lt;/li&gt;
&lt;li&gt;the full new set, clearly labeled as a new baseline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bridge does not make the prompts equivalent. It shows how the measurement behaves while both definitions are observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate releases before collection
&lt;/h2&gt;

&lt;p&gt;A lightweight release pipeline can stop most comparability failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Validate structure
&lt;/h3&gt;

&lt;p&gt;Require the schema fields, a valid version, unique prompt IDs, a declared parent, and an allowed intent value. Reject empty templates and undeclared variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compile every prompt
&lt;/h3&gt;

&lt;p&gt;Render all variable combinations used in the run. Fail on missing placeholders, unused variables, unexpected braces, or blank values. Store the compiled text that will actually be submitted.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Diff against the parent
&lt;/h3&gt;

&lt;p&gt;Compare IDs, exact text, normalized hashes, intent labels, variables, weights, locale, engine set, and sampling counts. Produce a machine-readable diff and a short reviewer summary.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Enforce the version rule
&lt;/h3&gt;

&lt;p&gt;If an existing prompt string or intent changed, a patch release should fail. If prompts were added, require reporting rules for the matched subset. If the engine list changed, require a new baseline or bridge plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Freeze an observation manifest
&lt;/h3&gt;

&lt;p&gt;Before collection, write a manifest containing the prompt-set version, compiled prompt hashes, run timestamp, engine labels, locale, and sampling configuration. Each observation row should reference that manifest and the stable prompt ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Test the reporting join
&lt;/h3&gt;

&lt;p&gt;Run the comparison code against a synthetic fixture with unchanged, added, modified, and removed prompts. Confirm that only unchanged IDs enter the matched before/after metric and that new prompts cannot silently enter the denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report comparability, not just movement
&lt;/h2&gt;

&lt;p&gt;Every chart should state which prompt-set versions it compares and assign a comparability class:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;direct&lt;/strong&gt;: prompt IDs, compiled text, locale, engine set, and sampling policy match;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bridged&lt;/strong&gt;: a controlled overlap exists, but at least one measurement component changed;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new baseline&lt;/strong&gt;: the change is too material for a defensible direct comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This label is more honest than adding another decimal place. It also gives reviewers a quick way to decide whether a movement claim is supported by the experiment design.&lt;/p&gt;

&lt;p&gt;For teams that need an initial snapshot before building this pipeline, the free AI-visibility audit at &lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt; can provide a practical starting point; subsequent internal runs should still preserve their own prompt, source, and configuration records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Prompt versioning does not remove model variability, engine updates, personalization, location effects, temporal source changes, or incomplete citation surfaces. A bridge run can still be noisy. Stable prompts also do not prove that a content or SEO change caused an answer change.&lt;/p&gt;

&lt;p&gt;Version control solves a narrower but important problem: it prevents the measurement instrument from changing invisibly. Once prompt releases, manifests, and matched-set rules are explicit, the team can separate “the answer changed” from “we changed the question” and make before/after reporting substantially more auditable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>analytics</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a Prompt–Source Gap Triage Queue in TypeScript (Without a Vanity Score)</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:04:46 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/build-a-prompt-source-gap-triage-queue-in-typescript-without-a-vanity-score-45hc</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/build-a-prompt-source-gap-triage-queue-in-typescript-without-a-vanity-score-45hc</guid>
      <description>&lt;p&gt;A visibility dashboard can look precise while hiding the question that actually matters: &lt;strong&gt;what useful source is missing from this answer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This tutorial builds a small, deterministic triage pipeline for AI-answer observations. It does not guess how a model works, scrape private interfaces, or collapse every signal into one “visibility score.” It accepts a frozen prompt set plus human-reviewed source roles and produces an editorial queue you can inspect.&lt;/p&gt;

&lt;p&gt;The examples are synthetic. They do not represent measured rankings or customer results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unit of analysis
&lt;/h2&gt;

&lt;p&gt;Keep one row per observed prompt run. A minimal input needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a stable prompt ID and version;&lt;/li&gt;
&lt;li&gt;an intent class;&lt;/li&gt;
&lt;li&gt;the answer system and timestamp;&lt;/li&gt;
&lt;li&gt;whether the brand was mentioned;&lt;/li&gt;
&lt;li&gt;whether the target domain was cited;&lt;/li&gt;
&lt;li&gt;source roles that a reviewer observed;&lt;/li&gt;
&lt;li&gt;a link to a permissioned evidence artifact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source roles describe the job a cited page performs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;definition&lt;/strong&gt; — explains what an entity or category is;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;evidence&lt;/strong&gt; — supports a factual claim or measurement;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;comparison&lt;/strong&gt; — helps distinguish options;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;implementation&lt;/strong&gt; — helps the reader act;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;identity&lt;/strong&gt; — verifies canonical entity facts;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;selection&lt;/strong&gt; — supports a shortlist or recommendation;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context&lt;/strong&gt; — adds background without carrying the central claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A role is not a quality judgment. Record support separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discovery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;diagnosis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comparison&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;implementation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;risk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SourceRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;definition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comparison&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;implementation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SupportStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;supports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partially_supports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does_not_support&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unclear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SourceObservation&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceRole&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;support&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupportStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PromptObservation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;promptVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;promptText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;answerSystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;runTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;brandMentioned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;targetDomainCited&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceObservation&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;evidenceArtifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isSynthetic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TriageItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;missingRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceRole&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Keep the answer system and timestamp visible. A repeated run is a sample, not a permanent ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map intent to expected source roles
&lt;/h2&gt;

&lt;p&gt;The mapping is an editorial hypothesis. It should describe which roles would make an answer more useful, not which publisher the model must cite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedRoles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SourceRole&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;discovery&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="s2"&gt;definition&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="s2"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;diagnosis&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="s2"&gt;evidence&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="s2"&gt;context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;comparison&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="s2"&gt;comparison&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="s2"&gt;evidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;implementation&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="s2"&gt;implementation&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="s2"&gt;evidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;selection&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="s2"&gt;selection&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="s2"&gt;comparison&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="s2"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;risk&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="s2"&gt;evidence&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="s2"&gt;context&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version this mapping. If you change it after seeing the results, your new output is not directly comparable with the old one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add transparent priority weights
&lt;/h2&gt;

&lt;p&gt;A useful priority is not a claim about model probability. It is simply an ordered work queue. Keep the formula visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;discovery&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;diagnosis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;selection&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="na"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&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;roleWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SourceRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;definition&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;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;identity&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;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values reflect editorial urgency in this example only. Do not present them as a validated industry benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate missing roles
&lt;/h2&gt;

&lt;p&gt;We count a role as present only when a reviewer found at least partial support. A citation that does not support its attached claim should not make the gap disappear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;supportedRoles&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;PromptObservation&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SourceRole&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;support&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;supports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;support&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partially_supports&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;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;source&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;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&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;function&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PromptObservation&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;TriageItem&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;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TriageItem&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="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="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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promptId&lt;/span&gt; &lt;span class="o"&gt;||&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;promptVersion&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Every row needs a stable prompt ID and version&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;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;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runTimestamp&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid timestamp for &lt;/span&gt;&lt;span class="dl"&gt;"&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;promptId&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;present&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;supportedRoles&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="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;role&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;expectedRoles&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;intent&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;present&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;role&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;citationGap&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;targetDomainCited&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;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;mentionGap&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;brandMentioned&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;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;queue&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;promptId&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;promptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;intent&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;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;missingRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;intentWeight&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;intent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="nx"&gt;roleWeight&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="nx"&gt;citationGap&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="nx"&gt;mentionGap&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="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expected &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; support for &lt;/span&gt;&lt;span class="dl"&gt;"&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;intent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; intent&lt;/span&gt;&lt;span class="dl"&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;brandMentioned&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brand mentioned&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="s2"&gt;brand not mentioned&lt;/span&gt;&lt;span class="dl"&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;targetDomainCited&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;target domain cited&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="s2"&gt;target domain not cited&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="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;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="o"&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;priority&lt;/span&gt; &lt;span class="o"&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;promptId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;missingRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingRole&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;The deterministic tie-breakers matter. Two people running the same input should receive the same ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test with synthetic observations
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PromptObservation&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="na"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example-selection-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;promptVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="na"&gt;promptText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What should a mid-market team evaluate in an AI visibility platform?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;answerSystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example Answer System&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-11T20:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brandMentioned&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="na"&gt;targetDomainCited&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="na"&gt;sources&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.org/category-overview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;support&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;supports&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="na"&gt;evidenceArtifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.org/evidence/example-selection-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;isSynthetic&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example-implementation-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;promptVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="na"&gt;promptText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What evidence should an AI visibility audit preserve?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;implementation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;answerSystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example Answer System&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-11T20:05:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brandMentioned&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="na"&gt;targetDomainCited&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="na"&gt;sources&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.org/audit-method&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;support&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partially_supports&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="na"&gt;evidenceArtifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.org/evidence/example-implementation-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;isSynthetic&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="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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the selection prompt, the queue will preserve the missing selection and comparison roles. The identity page confirms the entity; it does not independently justify a recommendation.&lt;/p&gt;

&lt;p&gt;For the implementation prompt, evidence is present, but an actionable implementation source is still missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert a gap into an asset brief
&lt;/h2&gt;

&lt;p&gt;A missing role is a research lead, not automatic permission to publish another article. Confirm the prompt genuinely calls for the role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assetBrief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TriageItem&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SourceRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;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;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;concise definition page with scope and exclusions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;methodology or dataset with dates and limitations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;criteria-led comparison with explicit tradeoffs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reproducible tutorial, template, or checklist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canonical entity profile with consistent facts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use-case matrix with qualification criteria&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background explainer tied to the answer claim&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Prompt: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Role gap: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Suggested format: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingRole&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Priority: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reason: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is small enough for an editor to review. That is the point. Automation should make the judgment legible, not hide it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production guardrails
&lt;/h2&gt;

&lt;p&gt;Before using this in a reporting workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Freeze and version the prompt portfolio.&lt;/li&gt;
&lt;li&gt;Use a real CSV parser instead of splitting arbitrary CSV on commas.&lt;/li&gt;
&lt;li&gt;Validate timestamps, URLs, enum values, and duplicate run keys.&lt;/li&gt;
&lt;li&gt;Keep synthetic rows physically or logically separate from observations.&lt;/li&gt;
&lt;li&gt;Preserve answer-level evidence where terms and platform policy allow it.&lt;/li&gt;
&lt;li&gt;Store the source claim and reviewer note, not only the role label.&lt;/li&gt;
&lt;li&gt;Measure reviewer agreement for consequential analyses.&lt;/li&gt;
&lt;li&gt;Re-run the same prompt set before interpreting change.&lt;/li&gt;
&lt;li&gt;Keep mentions, citations, recommendations, support, and prominence separate.&lt;/li&gt;
&lt;li&gt;Never describe a correlation as proof that one backlink caused an answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this queue cannot tell you
&lt;/h2&gt;

&lt;p&gt;This method cannot reveal hidden retrieval, training data, or causal attribution. It cannot guarantee that publishing a missing asset will change an answer. Interfaces, citations, personalization, geography, and model versions can change between runs.&lt;/p&gt;

&lt;p&gt;It also cannot replace editorial judgment. A definition page may be the wrong asset for a selection question, and a vendor-authored comparison may not be independent enough to support a recommendation.&lt;/p&gt;

&lt;p&gt;Use the queue to choose the next evidence question. Then publish the smallest truthful asset that answers it, distribute it where the intended audience already participates, and re-test with the frozen prompt set.&lt;/p&gt;

&lt;p&gt;If you want a broader baseline before building the queue, &lt;a href="https://corank.ai/" rel="noopener noreferrer"&gt;Corank&lt;/a&gt; offers a free AI visibility audit across major answer-engine surfaces.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build a Reproducible AI Visibility Evidence Table with JSON</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 10 Aug 2026 03:09:52 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/build-a-reproducible-ai-visibility-evidence-table-with-json-4742</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/build-a-reproducible-ai-visibility-evidence-table-with-json-4742</guid>
      <description>&lt;p&gt;AI visibility audits often collapse into a single score. That makes the report easy to scan, but hard to reproduce.&lt;/p&gt;

&lt;p&gt;A better technical workflow keeps the raw evidence first, calculates summaries second, and preserves enough context for another analyst to re-run the same test.&lt;/p&gt;

&lt;p&gt;This post shows a compact JSON model for that workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Store the test, not just the answer
&lt;/h2&gt;

&lt;p&gt;For every prompt, record the exact conditions under which the answer was observed:&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;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-09-us-en-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-09T18:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-ai-surface"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"market"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which tools help measure brand visibility in AI answers?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mentioned"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linked"&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;"position"&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;"sources"&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://example.com/resource"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"supporting-evidence"&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;The important design choice is separating &lt;code&gt;mentioned&lt;/code&gt; from &lt;code&gt;linked&lt;/code&gt;. An entity can appear without a citation, and a cited source can appear without a direct brand mention. Treating those as different fields prevents a lot of misleading reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Define source roles
&lt;/h2&gt;

&lt;p&gt;Not every cited page is doing the same job. A small controlled vocabulary makes later analysis much more useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;definition&lt;/code&gt;: explains what a category or method is&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evidence&lt;/code&gt;: contains data, research, or a reproducible result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;comparison&lt;/code&gt;: helps a user evaluate alternatives&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;implementation&lt;/code&gt;: shows how to do the work&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entity&lt;/code&gt;: confirms who a company or product is&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supporting-evidence&lt;/code&gt;: adds context without carrying the main claim&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets you ask a better question than “Were we cited?” You can ask, “Which source role do competitors own, and which role is missing from our content?”&lt;/p&gt;

&lt;p&gt;That gap is often the next linkable asset to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Normalize URLs before counting
&lt;/h2&gt;

&lt;p&gt;The same source can appear with tracking parameters, fragments, mixed casing, or multiple trailing-slash variants. Normalize before aggregation:&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;normalizeSourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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="nx"&gt;hash&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;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;key&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;key&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="s2"&gt;utm_&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;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ref&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="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="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&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;\/&lt;/span&gt;&lt;span class="sr"&gt;+$/&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="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;return&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;toString&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;Do not remove query parameters blindly. Some parameters identify a genuinely different public resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Calculate transparent metrics
&lt;/h2&gt;

&lt;p&gt;Once raw observations are saved, summaries become simple and auditable:&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;mentionRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;runs&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;run&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mentioned&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="nx"&gt;runs&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;citationRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;runs&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;run&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linked&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="nx"&gt;runs&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceRoleCoverage&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="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&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;source&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&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;Keep the numerator and denominator beside every percentage. “42% visibility” is not meaningful unless the reader can see whether that means 42 of 100 prompts or 5 of 12.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use a fixed prompt set
&lt;/h2&gt;

&lt;p&gt;For trend analysis, freeze a prompt set for a period of time. Re-run it on a schedule, but avoid rewriting the prompts every time the result changes.&lt;/p&gt;

&lt;p&gt;A useful prompt set usually mixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;category discovery questions&lt;/li&gt;
&lt;li&gt;comparison and alternative questions&lt;/li&gt;
&lt;li&gt;“how do I” workflow questions&lt;/li&gt;
&lt;li&gt;vendor or tool evaluation questions&lt;/li&gt;
&lt;li&gt;branded confirmation questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Version the set when you intentionally change it. Otherwise, a chart may be measuring prompt drift instead of visibility drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Turn the evidence gap into an asset
&lt;/h2&gt;

&lt;p&gt;If the table shows that competitors are repeatedly cited for implementation guidance, build an implementation guide. If comparison pages dominate, create a fair, sourced comparison framework. If original data wins citations, publish the methodology and dataset.&lt;/p&gt;

&lt;p&gt;The evidence table is not only a report. It is a content roadmap.&lt;/p&gt;

&lt;p&gt;For a practical starting point, Corank publishes a &lt;a href="https://corank.ai/tools/ai-visibility-audit" rel="noopener noreferrer"&gt;free AI visibility audit&lt;/a&gt; and guides to &lt;a href="https://corank.ai/guides/what-is-aeo" rel="noopener noreferrer"&gt;answer engine optimization&lt;/a&gt; and &lt;a href="https://corank.ai/guides/what-is-geo" rel="noopener noreferrer"&gt;generative engine optimization&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before calling a run complete, verify that you preserved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact prompt&lt;/li&gt;
&lt;li&gt;engine or product surface&lt;/li&gt;
&lt;li&gt;date, market, and language&lt;/li&gt;
&lt;li&gt;mention and link status separately&lt;/li&gt;
&lt;li&gt;normalized source URLs&lt;/li&gt;
&lt;li&gt;source roles&lt;/li&gt;
&lt;li&gt;raw counts behind percentages&lt;/li&gt;
&lt;li&gt;the prompt-set version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That structure makes AI visibility work easier to audit, easier to compare, and much harder to turn into a vanity metric.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Audit Backlink Domain Concentration with Node.js (Without Fooling Yourself)</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Sat, 08 Aug 2026 04:43:01 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/audit-backlink-domain-concentration-with-nodejs-without-fooling-yourself-36b5</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/audit-backlink-domain-concentration-with-nodejs-without-fooling-yourself-36b5</guid>
      <description>&lt;p&gt;A spreadsheet can contain hundreds of valid backlink URLs and still describe a fragile off-page footprint.&lt;/p&gt;

&lt;p&gt;I ran into this while auditing 1,023 verified public placements. About 84.8% of them lived on four hosts. The URLs were real, but the graph was concentrated.&lt;/p&gt;

&lt;p&gt;This tutorial builds a small Node.js audit that answers four practical questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many unique placement URLs do we have?&lt;/li&gt;
&lt;li&gt;How many unique referring hosts?&lt;/li&gt;
&lt;li&gt;What share belongs to the largest one and four hosts?&lt;/li&gt;
&lt;li&gt;Which hosts should be paused before the next publishing sprint?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code uses only built-in Node APIs, so it is easy to run in CI or adapt to a Google Sheets export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input format
&lt;/h2&gt;

&lt;p&gt;Export the tracker as a CSV with two columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name,url
Medium article,https://medium.com/@example/article
Company profile,https://directory.example.com/company
Technical guide,https://dev.to/example/guide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a production tracker, use a proper CSV parser because titles can contain commas. The parsing below is intentionally small so the concentration logic is easy to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize URLs before counting
&lt;/h2&gt;

&lt;p&gt;Tracking parameters, fragments, casing, and www variants can make identical destinations look different.&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;normalizeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&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;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;key&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;key&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;utm_&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;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ref&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="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="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;/^www&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="dl"&gt;''&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="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&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;\/&lt;/span&gt;&lt;span class="sr"&gt;+$/&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="o"&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="k"&gt;return&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;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;referringHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalizeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;hostname&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;Do not remove every query parameter blindly. Some platforms use query parameters as part of the canonical identity of a public page. Strip only parameters you have classified as tracking noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate concentration
&lt;/h2&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;concentrationReport&lt;/span&gt;&lt;span class="p"&gt;(&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;uniqueUrls&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;Map&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="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;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeUrl&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;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;uniqueUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Keep invalid or non-public URLs out of verified totals.&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;byHost&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;Map&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;normalized&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;uniqueUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;referringHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;byHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byHost&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="nx"&gt;host&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;byHost&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="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;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;placements&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="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;placements&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;placements&lt;/span&gt; &lt;span class="o"&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;placements&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;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uniqueUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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;topOne&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hosts&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;placements&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topFour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hosts&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;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&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;placements&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;totalPlacements&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;uniqueHosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hosts&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;topOneShare&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="nx"&gt;topOne&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&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;topFourShare&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="nx"&gt;topFour&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&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;hosts&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;h2&gt;
  
  
  Turn the result into a publishing control
&lt;/h2&gt;

&lt;p&gt;A report is useful only if it changes the queue.&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;allocationDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;host&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hosts&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;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;host&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;host&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;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;placements&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalPlacements&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;share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAUSE&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;share&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REVIEW&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ELIGIBLE&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thresholds are policy choices, not search-engine rules. Their purpose is to keep an automated publishing process from selecting the lowest-friction host forever.&lt;/p&gt;

&lt;p&gt;One useful sprint rule is stricter: give the current top four hosts a temporary allocation of zero. That forces research into new editorial, technical, professional, and reviewed-directory surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add source types
&lt;/h2&gt;

&lt;p&gt;Hostname diversity alone is not enough. Ten unrelated directories still represent one kind of evidence.&lt;/p&gt;

&lt;p&gt;Add a sourceType field such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;editorial&lt;/li&gt;
&lt;li&gt;technical&lt;/li&gt;
&lt;li&gt;company_profile&lt;/li&gt;
&lt;li&gt;reviewed_directory&lt;/li&gt;
&lt;li&gt;social_post&lt;/li&gt;
&lt;li&gt;community_answer&lt;/li&gt;
&lt;li&gt;earned_media&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then calculate the same distribution by source type. Your goal is a portfolio of independent sources doing different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify links before counting
&lt;/h2&gt;

&lt;p&gt;A row should enter the verified corpus only when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The URL returns a public page in a signed-out session.&lt;/li&gt;
&lt;li&gt;The page contains a clickable link to the intended site.&lt;/li&gt;
&lt;li&gt;The placement is stable, relevant, and not duplicated.&lt;/li&gt;
&lt;li&gt;Pending review and drafts remain in separate states.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For dynamic sites, a raw HTTP fetch may miss client-rendered content. Use a real browser check as a fallback, but do not treat an authenticated preview as proof that the page is public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for SEO and AEO
&lt;/h2&gt;

&lt;p&gt;A raw backlink total is a production metric. Referring-domain and source-type diversity are closer to evidence metrics.&lt;/p&gt;

&lt;p&gt;For answer-engine optimization, the external page also needs to contribute something worth citing: a definition, method, dataset, checklist, or defensible point of view. The linked destination should match the off-site context.&lt;/p&gt;

&lt;p&gt;For example, a technical post about entity consistency should link to the relevant guide rather than defaulting to a homepage.&lt;/p&gt;

&lt;p&gt;I am applying this measurement approach to the public SEO/AEO work documented by &lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard I would ship
&lt;/h2&gt;

&lt;p&gt;At minimum, expose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verified placement URLs&lt;/li&gt;
&lt;li&gt;unique referring hosts&lt;/li&gt;
&lt;li&gt;net-new hosts this sprint&lt;/li&gt;
&lt;li&gt;top-one and top-four shares&lt;/li&gt;
&lt;li&gt;source-type distribution&lt;/li&gt;
&lt;li&gt;deep-link distribution&lt;/li&gt;
&lt;li&gt;indexed placements&lt;/li&gt;
&lt;li&gt;referral visits&lt;/li&gt;
&lt;li&gt;non-branded impressions&lt;/li&gt;
&lt;li&gt;AI answer mentions and cited-source appearances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes a failure mode visible before another few hundred URLs accumulate on the same host.&lt;/p&gt;

&lt;p&gt;The point is not to replace judgment with a formula. It is to give the agent enough memory and constraints that judgment survives scale.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>javascript</category>
      <category>node</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Encode a Publish-or-Suppress Policy for AI Visibility Scores</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 18:09:25 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-encode-a-publish-or-suppress-policy-for-ai-visibility-scores-3fip</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-encode-a-publish-or-suppress-policy-for-ai-visibility-scores-3fip</guid>
      <description>&lt;p&gt;AI-search visibility dashboards are very good at producing numbers. They are usually less explicit about the conditions under which a number should not be published.&lt;/p&gt;

&lt;p&gt;That is a reliability problem.&lt;/p&gt;

&lt;p&gt;If collection coverage collapses, citations cannot be traced, or classification cannot be replayed, a polished score can create more confidence than the evidence deserves. A safer system treats publication as a policy decision, not the automatic last step of a calculation.&lt;/p&gt;

&lt;p&gt;This guide shows how to encode that decision as data, evaluate it consistently, and test the failure paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with five output states
&lt;/h2&gt;

&lt;p&gt;A binary pass/fail flag is too coarse. A practical publication policy can return one of five states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;publish&lt;/strong&gt; — the score and its components are decision-grade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;publish_with_warning&lt;/strong&gt; — the score is usable, but a named reliability issue must travel with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;components_only&lt;/strong&gt; — underlying measurements may be shown, but the composite score is withheld.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hold_last_valid&lt;/strong&gt; — the current run is unreliable, so the UI keeps the most recent valid score and labels its age.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;suppress&lt;/strong&gt; — no score should be shown until the evidence is repaired.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A temporary drop in one model's completion rate may justify a warning. Missing raw answers across half the prompt set may require suppression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the evidence contract first
&lt;/h2&gt;

&lt;p&gt;A publication policy cannot inspect evidence that the pipeline never stored. At minimum, every observation should expose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;observationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;promptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;modelVersion&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;scheduledAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;completedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parse_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;excluded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rawAnswer&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;citations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;observedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;resolvedUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;resolutionReason&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;classification&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;brandMentioned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;recommended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;methodologyVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Notice what is not erased:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;failed observations remain rows;&lt;/li&gt;
&lt;li&gt;the raw citation survives URL normalization;&lt;/li&gt;
&lt;li&gt;methodology has a version;&lt;/li&gt;
&lt;li&gt;eligibility is explicit;&lt;/li&gt;
&lt;li&gt;collection status is separate from brand performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation prevents a timeout from quietly becoming "brand absent."&lt;/p&gt;

&lt;h2&gt;
  
  
  Express the policy as configuration
&lt;/h2&gt;

&lt;p&gt;Thresholds should be reviewable without reading application code. YAML is convenient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-08-03"&lt;/span&gt;
&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;max_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;36&lt;/span&gt;
  &lt;span class="na"&gt;min_eligible_observations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;

&lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;warn_below&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.95&lt;/span&gt;
  &lt;span class="na"&gt;suppress_below&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.80&lt;/span&gt;

&lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;min_raw_answer_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.98&lt;/span&gt;
  &lt;span class="na"&gt;min_citation_trace_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.95&lt;/span&gt;
  &lt;span class="na"&gt;min_replay_success_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.97&lt;/span&gt;

&lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;min_mean_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.90&lt;/span&gt;
  &lt;span class="na"&gt;max_methodology_versions_per_window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;low_coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish_with_warning&lt;/span&gt;
  &lt;span class="na"&gt;critical_coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hold_last_valid&lt;/span&gt;
  &lt;span class="na"&gt;missing_raw_evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;suppress&lt;/span&gt;
  &lt;span class="na"&gt;broken_citation_lineage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;components_only&lt;/span&gt;
  &lt;span class="na"&gt;mixed_methodology&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;suppress&lt;/span&gt;
  &lt;span class="na"&gt;stale_window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hold_last_valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers are examples, not universal benchmarks. A high-stakes workflow may require stricter thresholds. An exploratory internal dashboard may tolerate more warnings.&lt;/p&gt;

&lt;p&gt;The important part is that thresholds and consequences are defined before the team reviews a surprising score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate policy separately from scoring
&lt;/h2&gt;

&lt;p&gt;Keep publication logic outside the function that calculates visibility. That makes it harder for scoring changes to silently weaken quality gates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rawAnswerRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;citationTraceRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;replaySuccessRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;meanConfidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;methodologyVersions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Decision&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish_with_warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;components_only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold_last_valid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decidePublication&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="nx"&gt;Metrics&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Decision&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;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="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="nx"&gt;methodologyVersions&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasons&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="s2"&gt;Window mixes multiple methodology versions&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;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="nx"&gt;rawAnswerRate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.98&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasons&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="s2"&gt;Too many completed observations lack raw evidence&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;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="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;36&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold_last_valid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasons&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="s2"&gt;Current evidence window is stale&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;coverage&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="nx"&gt;eligible&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="mi"&gt;0&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="nx"&gt;completed&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="nx"&gt;eligible&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;coverage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.80&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold_last_valid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasons&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="s2"&gt;Collection coverage is below the critical threshold&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;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="nx"&gt;citationTraceRate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.95&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;components_only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasons&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="s2"&gt;Citation lineage is incomplete&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;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="nx"&gt;replaySuccessRate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reasons&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Some classifications cannot be reproduced&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;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="nx"&gt;meanConfidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reasons&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Classification confidence is below target&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reasons&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Collection coverage is below target&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;return&lt;/span&gt; &lt;span class="nx"&gt;reasons&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="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="s2"&gt;publish_with_warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reasons&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="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="s2"&gt;publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reasons&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;In production, validate the configuration against a schema and include the policy version in every decision record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the decision auditable
&lt;/h2&gt;

&lt;p&gt;Store the result as its own object:&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;"scoreWindowId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly-brand-2026-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policyVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evaluatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T17:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"components_only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reasons"&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;"Citation lineage is incomplete"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&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;"coverage"&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.96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rawAnswerRate"&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;"citationTraceRate"&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.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"replaySuccessRate"&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.99&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;"lastValidScoreWindowId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly-brand-2026-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;Now a reviewer can answer which policy produced the state, which threshold failed, what evidence window was evaluated, and which prior score remains valid.&lt;/p&gt;

&lt;p&gt;Do not overwrite the original decision after a repair. Append a new evaluation and preserve the history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test counterfactuals, not only happy paths
&lt;/h2&gt;

&lt;p&gt;A score can be numerically correct while the publication policy is broken. Add tests that deliberately damage the evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publication policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppresses when raw answers are missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decidePublication&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;eligible&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="na"&gt;completed&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="na"&gt;rawAnswerRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;citationTraceRate&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;replaySuccessRate&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;meanConfidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;methodologyVersions&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;ageHours&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="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppress&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does not convert timeouts into brand absence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eligible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;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;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;75&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;mentionCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mentionCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mentionCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;holds the last valid score when evidence is stale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decidePublication&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;eligible&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="na"&gt;completed&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="na"&gt;rawAnswerRate&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;citationTraceRate&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;replaySuccessRate&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;meanConfidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;methodologyVersions&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;ageHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold_last_valid&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;Other valuable counterfactual tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove a detected brand mention and confirm the mention metric changes;&lt;/li&gt;
&lt;li&gt;mark a completed answer as failed and confirm it leaves performance scoring;&lt;/li&gt;
&lt;li&gt;replace a canonical URL while preserving the observed URL;&lt;/li&gt;
&lt;li&gt;mix two methodology versions and confirm publication stops;&lt;/li&gt;
&lt;li&gt;expire the evidence window and confirm the UI labels the last valid score.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Design the UI around reliability state
&lt;/h2&gt;

&lt;p&gt;Do not hide policy output in a tooltip. The interface should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the current publication state;&lt;/li&gt;
&lt;li&gt;every reason code;&lt;/li&gt;
&lt;li&gt;eligible, completed, failed, and excluded counts;&lt;/li&gt;
&lt;li&gt;the policy and methodology versions;&lt;/li&gt;
&lt;li&gt;evidence freshness;&lt;/li&gt;
&lt;li&gt;a link from the aggregate score to raw observations;&lt;/li&gt;
&lt;li&gt;the timestamp and age of any held score.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Score: 64" and "Last valid score: 64, collected nine days ago; current run suppressed because 43% of raw answers are missing" are not equivalent messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid three common mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failing open
&lt;/h3&gt;

&lt;p&gt;If the policy service errors, the dashboard should not quietly publish. Choose a safe fallback such as holding the last valid score or suppressing the current one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hiding failures in the denominator
&lt;/h3&gt;

&lt;p&gt;Keep scheduled, eligible, completed, failed, and excluded counts separate. A collection failure is an operational fact, not negative brand performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repairing history with a fresh model response
&lt;/h3&gt;

&lt;p&gt;A live replay may be useful, but it cannot replace what was actually observed. Store the original answer and the replay as separate events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operating principle
&lt;/h2&gt;

&lt;p&gt;A visibility score earns trust when the system can explain both why it published the number and what failure would have stopped publication.&lt;/p&gt;

&lt;p&gt;I expanded the measurement side of this idea into a practical falsification protocol:&lt;br&gt;
&lt;a href="https://www.linkedin.com/pulse/how-falsify-ai-visibility-score-before-you-trust-waleed-arshad-jqkdc/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/how-falsify-ai-visibility-score-before-you-trust-waleed-arshad-jqkdc/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Corank applies the same evidence-first principle to AI-search visibility and optimization:&lt;br&gt;
&lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;https://corank.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation can be small. The key is making the stop rule explicit, versioned, testable, and visible to the people who rely on the score.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>typescript</category>
      <category>seo</category>
    </item>
    <item>
      <title>How to Audit AI Crawler Access with robots.txt and Server Logs</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:51:12 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-audit-ai-crawler-access-with-robotstxt-and-server-logs-ng6</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-audit-ai-crawler-access-with-robotstxt-and-server-logs-ng6</guid>
      <description>&lt;p&gt;AI visibility work often starts with prompts, citations, and answer tracking. But there is a more basic question to settle first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the systems you care about reliably fetch the pages you expect them to use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A site can publish excellent research and still create avoidable discovery problems through a restrictive robots.txt rule, a CDN challenge, inconsistent redirects, or pages that return different status codes to different user agents.&lt;/p&gt;

&lt;p&gt;This guide shows a practical, repeatable audit for AI-crawler access using two evidence sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the site's declared policy in robots.txt, and&lt;/li&gt;
&lt;li&gt;observed requests in server or CDN logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to guarantee inclusion in any AI answer. It is to make access decisions visible, testable, and easier to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with an explicit crawler policy
&lt;/h2&gt;

&lt;p&gt;A short robots.txt file is easier to review than a long collection of overlapping rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your organization intentionally applies different rules to specific crawlers, document the business reason next to the configuration in version control. Robots.txt itself does not support inline operational context well, so keep a companion policy file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ai-crawler-policy.yml&lt;/span&gt;
&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;growth-platform&lt;/span&gt;
&lt;span class="na"&gt;review_interval_days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
&lt;span class="na"&gt;default_policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
&lt;span class="na"&gt;exceptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user_agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExampleBot&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/private-research/&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disallow&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unpublished source material&lt;/span&gt;
    &lt;span class="na"&gt;approved_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
    &lt;span class="na"&gt;review_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-01&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the exact schema. It is having an owner, a review date, and a reason for every exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Parse robots.txt as data
&lt;/h2&gt;

&lt;p&gt;Manual inspection is useful, but automated checks catch accidental changes during deployment.&lt;/p&gt;

&lt;p&gt;The following TypeScript example reads a robots.txt file and extracts simple allow/disallow rules. Production parsers should account for the full standard and the matching behavior of the crawlers you monitor, but even a small check can surface unexpected policy changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disallow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseRobots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Rule&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;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rule&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentAgents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="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;rawLine&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;input&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&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;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawLine&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="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&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;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;line&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;separator&lt;/span&gt; &lt;span class="o"&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;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;separator&lt;/span&gt; &lt;span class="o"&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="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;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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;separator&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="nf"&gt;toLowerCase&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;value&lt;/span&gt; &lt;span class="o"&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="nx"&gt;separator&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="nf"&gt;trim&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="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-agent&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;currentAgents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;currentAgents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="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="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disallow&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;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;userAgent&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;currentAgents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;rules&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="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;directive&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rules&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;A useful CI assertion is that important public paths remain allowed for the default agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseRobots&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./parse-robots&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;robots&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;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public/robots.txt&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="s2"&gt;utf8&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;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRobots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;robots&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;blockedPublicPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rules&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;directive&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disallow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/blog&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="s2"&gt;/research&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="s2"&gt;/docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;path&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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="nx"&gt;path&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="nx"&gt;blockedPublicPaths&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;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;`Public content blocked in robots.txt: &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;blockedPublicPaths&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not replace a standards-compliant parser. It creates a focused regression test for the paths your team has declared important.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test the real HTTP response
&lt;/h2&gt;

&lt;p&gt;Robots.txt is only one layer. A crawler can be allowed by policy and still receive a 403, 429, redirect loop, or JavaScript-only shell.&lt;/p&gt;

&lt;p&gt;Run a small set of HTTP checks from outside your production network:&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;-I&lt;/span&gt; https://example.com/robots.txt
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://example.com/sitemap.xml
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://example.com/research/
curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} %{url_effective}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/research/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each important URL, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initial status code&lt;/li&gt;
&lt;li&gt;final status code after redirects&lt;/li&gt;
&lt;li&gt;final canonical URL&lt;/li&gt;
&lt;li&gt;response content type&lt;/li&gt;
&lt;li&gt;cache or challenge headers&lt;/li&gt;
&lt;li&gt;response time&lt;/li&gt;
&lt;li&gt;whether the page contains meaningful server-rendered text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not rely on a single homepage test. Sample templates separately: documentation, articles, comparison pages, pricing pages, and structured research assets can behave differently at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Query logs for observed crawler traffic
&lt;/h2&gt;

&lt;p&gt;Server and CDN logs show what actually happened. Start with a bounded time window and retain only the fields needed for diagnosis.&lt;/p&gt;

&lt;p&gt;Example SQL for a generic request-log table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;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;unique_paths&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;edge_requests&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;request_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%bot%'&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%crawler%'&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%spider%'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&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="mi"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User-agent text is a discovery signal, not proof of identity. If identity matters, validate requests using the publisher's documented verification method, such as IP or reverse-DNS procedures where available. Do not create a universal list from memory and assume it stays current.&lt;/p&gt;

&lt;p&gt;Then inspect errors by path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&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;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_time&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;last_seen&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;edge_requests&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;request_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%bot%'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns a vague concern into a queue of concrete fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Separate access, discovery, and citation
&lt;/h2&gt;

&lt;p&gt;These are different layers and should have different metrics.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Example evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;Can the page be fetched?&lt;/td&gt;
&lt;td&gt;200 response, no challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovery&lt;/td&gt;
&lt;td&gt;Was the page requested?&lt;/td&gt;
&lt;td&gt;verified log event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interpretation&lt;/td&gt;
&lt;td&gt;Was usable content returned?&lt;/td&gt;
&lt;td&gt;rendered text and metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citation&lt;/td&gt;
&lt;td&gt;Did an answer reference the page?&lt;/td&gt;
&lt;td&gt;observed answer citation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A successful fetch does not prove citation. A citation does not prove that every page is accessible. Keeping the layers separate prevents teams from drawing causal conclusions that the data cannot support.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Build a small weekly audit
&lt;/h2&gt;

&lt;p&gt;A useful weekly job can be simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;fetch robots.txt and store a hash&lt;/li&gt;
&lt;li&gt;compare it with the previous version&lt;/li&gt;
&lt;li&gt;test a representative URL set&lt;/li&gt;
&lt;li&gt;aggregate verified crawler requests&lt;/li&gt;
&lt;li&gt;flag new 4xx/5xx patterns&lt;/li&gt;
&lt;li&gt;attach owners and deadlines&lt;/li&gt;
&lt;li&gt;review access changes alongside AI-visibility changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example audit record:&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;"runAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T07:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"robotsHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:replace-with-real-hash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"testedUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"successfulUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blockedUrls"&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;"newErrorPatterns"&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;"pathPattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/research/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edge-platform"&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;Use real values from your own audit. The placeholder hash above is intentionally labeled so it cannot be mistaken for production evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Review changes with an evidence checklist
&lt;/h2&gt;

&lt;p&gt;Before closing an access issue, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the robots.txt rule is intentional&lt;/li&gt;
&lt;li&gt;the live response matches the declared policy&lt;/li&gt;
&lt;li&gt;the canonical URL is stable&lt;/li&gt;
&lt;li&gt;the page returns meaningful content without a browser-only dependency&lt;/li&gt;
&lt;li&gt;bot-management rules are not creating unintended challenges&lt;/li&gt;
&lt;li&gt;log retention is long enough to compare periods&lt;/li&gt;
&lt;li&gt;verified and unverified user agents are reported separately&lt;/li&gt;
&lt;li&gt;access metrics are not presented as citation metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this fits in an AI-visibility program
&lt;/h2&gt;

&lt;p&gt;Crawler access is infrastructure hygiene. The next layer is measuring how a brand appears across AI answers, how fresh those observations are, and which cited sources recur.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corank.ai/" rel="noopener noreferrer"&gt;Corank&lt;/a&gt; is built around AI-visibility measurement. Its workflow can complement an access audit by helping teams observe the answer layer after the underlying pages are available and monitorable.&lt;/p&gt;

&lt;p&gt;The practical sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;make access policy explicit,&lt;/li&gt;
&lt;li&gt;verify real HTTP behavior,&lt;/li&gt;
&lt;li&gt;observe crawler requests,&lt;/li&gt;
&lt;li&gt;measure answer visibility and citations,&lt;/li&gt;
&lt;li&gt;review changes with dated evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence keeps technical fixes, content work, and visibility reporting connected without treating any one signal as proof of the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;An AI-crawler audit should be boring in the best way: versioned rules, repeatable tests, bounded log queries, clear owners, and explicit uncertainty.&lt;/p&gt;

&lt;p&gt;When those basics are in place, teams can spend less time guessing whether a page was reachable and more time improving the content and evidence that people—and answer engines—may choose to use.``&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Encode a Freshness Policy for AI Visibility Data</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:42:08 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-encode-a-freshness-policy-for-ai-visibility-data-156</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-encode-a-freshness-policy-for-ai-visibility-data-156</guid>
      <description>&lt;p&gt;AI visibility teams often start with a freshness rule buried inside a query, a notebook, or a dashboard formula. That works until the provider mix changes, a new reporting use case appears, or somebody needs to explain why yesterday's score moved.&lt;/p&gt;

&lt;p&gt;A better pattern is to treat freshness as a versioned policy. Store the assumptions in configuration, validate them at startup, and publish the policy version beside every reported score.&lt;/p&gt;

&lt;p&gt;This article shows one implementation pattern using YAML and TypeScript. All thresholds are illustrative, not universal benchmarks. Tune them to your provider volatility, collection cadence, and decision risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the policy must express
&lt;/h2&gt;

&lt;p&gt;A useful policy should answer five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How quickly does an observation lose weight?&lt;/li&gt;
&lt;li&gt;At what age is it excluded entirely?&lt;/li&gt;
&lt;li&gt;How much recent collection coverage is required?&lt;/li&gt;
&lt;li&gt;Which operational state should downstream systems receive?&lt;/li&gt;
&lt;li&gt;Which policy version produced the score?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not collapse all five into one "quality score." Freshness weighting and collection coverage represent different failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  An illustrative YAML policy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ai-visibility-freshness-v1&lt;/span&gt;
  &lt;span class="na"&gt;effective_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-08-03T00:00:00Z&lt;/span&gt;

&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;decay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exponential&lt;/span&gt;
    &lt;span class="na"&gt;half_life_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;168&lt;/span&gt;
    &lt;span class="na"&gt;maximum_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;720&lt;/span&gt;
  &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;window_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;168&lt;/span&gt;
    &lt;span class="na"&gt;minimum_ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.90&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maximum_median_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;48&lt;/span&gt;
    &lt;span class="na"&gt;aging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maximum_median_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;168&lt;/span&gt;
    &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maximum_median_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;720&lt;/span&gt;

&lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-fast-changing-provider&lt;/span&gt;
    &lt;span class="na"&gt;decay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;half_life_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;72&lt;/span&gt;
      &lt;span class="na"&gt;maximum_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;336&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;query_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;breaking-news&lt;/span&gt;
    &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;window_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;
      &lt;span class="na"&gt;minimum_ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.95&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider and query names above are placeholders. The value of this structure is not the numbers; it is the explicit separation between decay, exclusion, coverage, and state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define a narrow TypeScript model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DecayShape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tiered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FreshnessPolicy&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;effectiveFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DecayShape&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;halfLifeHours&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;maximumAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;windowHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;minimumRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;currentMaxMedianAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;agingMaxMedianAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;staleMaxMedianAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;type&lt;/span&gt; &lt;span class="nx"&gt;Observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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;type&lt;/span&gt; &lt;span class="nx"&gt;FreshnessState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppressed&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;Keep the model deliberately small. Provider-specific and query-class overrides can be resolved before this object reaches the scoring function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate the observation weight
&lt;/h2&gt;

&lt;p&gt;For an exponential policy, the half-life formula is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;exponentialWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;halfLifeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maximumAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;ageHours&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="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="s2"&gt;observedAt cannot be in the future&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maximumAgeHours&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="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;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&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="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;halfLifeHours&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;With a seven-day half-life, a seven-day-old observation receives 0.5 weight and a fourteen-day-old observation receives 0.25 weight. That is arithmetic from the configured formula, not a claim about how quickly any specific AI system changes.&lt;/p&gt;

&lt;p&gt;A linear policy can be useful when stakeholders want a simple deadline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;linearWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maximumAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;ageHours&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="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="s2"&gt;observedAt cannot be in the future&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;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maximumAgeHours&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;h2&gt;
  
  
  Produce a weighted visibility rate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;weightedVisibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observation&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FreshnessPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;weightedMentions&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalWeight&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;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;observation&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;observations&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;ageHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&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;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;exponentialWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;halfLifeHours&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maximumAgeHours&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="nf"&gt;linearWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maximumAgeHours&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;weightedMentions&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibility&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;totalWeight&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;weight&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;totalWeight&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="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;weightedMentions&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalWeight&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;Returning null when total weight is zero is important. A missing current measurement must not silently become a zero visibility result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep recent coverage separate
&lt;/h2&gt;

&lt;p&gt;Coverage asks whether the system collected the runs it expected. Freshness asks how much an existing observation should influence the present score.&lt;/p&gt;

&lt;p&gt;Track both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ScoreEnvelope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;computedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;weightedVisibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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="nl"&gt;recentCoverageRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;medianAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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="nl"&gt;oldestIncludedAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FreshnessState&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;This envelope gives dashboards and APIs enough context to avoid false precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Derive an operational state
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;coverageRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;medianAgeHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&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;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FreshnessPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FreshnessState&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;medianAgeHours&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;||&lt;/span&gt;
    &lt;span class="nx"&gt;coverageRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minimumRatio&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppressed&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;medianAgeHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentMaxMedianAgeHours&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;current&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;medianAgeHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agingMaxMedianAgeHours&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aging&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;medianAgeHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;staleMaxMedianAgeHours&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stale&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;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppressed&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state should travel with the score. Do not rely on a dashboard color that disappears when data is exported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate policy changes
&lt;/h2&gt;

&lt;p&gt;Configuration is only safer when invalid combinations are rejected. Add startup checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;half-life greater than zero;&lt;/li&gt;
&lt;li&gt;maximum age greater than the half-life;&lt;/li&gt;
&lt;li&gt;coverage ratio between zero and one;&lt;/li&gt;
&lt;li&gt;state age thresholds in increasing order;&lt;/li&gt;
&lt;li&gt;an effective date and immutable policy identifier;&lt;/li&gt;
&lt;li&gt;overrides that match a known provider or query class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also store the policy ID in every materialized score. When a policy changes, historical reports can then be reproduced under the original rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test failure scenarios, not only happy paths
&lt;/h2&gt;

&lt;p&gt;A minimal test matrix should include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;every observation is fresh;&lt;/li&gt;
&lt;li&gt;all observations are beyond maximum age;&lt;/li&gt;
&lt;li&gt;collection coverage falls below the minimum;&lt;/li&gt;
&lt;li&gt;one provider continues while another stops;&lt;/li&gt;
&lt;li&gt;a future timestamp arrives because of clock skew;&lt;/li&gt;
&lt;li&gt;a backfill is delivered late but keeps its real observation time;&lt;/li&gt;
&lt;li&gt;an override shortens the half-life for one query class;&lt;/li&gt;
&lt;li&gt;a policy version changes halfway through a reporting period.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The expected behavior should be visible degradation: lower weights, an aging or stale state, or suppression. The system should never manufacture a confident current score from insufficient recent evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Version the policy in source control or a controlled configuration store.&lt;/li&gt;
&lt;li&gt;Validate it before scoring starts.&lt;/li&gt;
&lt;li&gt;Record observedAt and computedAt separately.&lt;/li&gt;
&lt;li&gt;Preserve raw observations so historical scores can be replayed.&lt;/li&gt;
&lt;li&gt;Publish coverage and age statistics beside the rate.&lt;/li&gt;
&lt;li&gt;Suppress the score when guardrails fail.&lt;/li&gt;
&lt;li&gt;Review overrides when providers or query sets change.&lt;/li&gt;
&lt;li&gt;Document the policy where analysts and downstream developers can find it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt; is building measurement workflows for how brands appear across AI search and answer systems. For the conceptual background behind decay shapes and decision horizons, see the companion guide: &lt;a href="https://corankai.substack.com/p/how-to-model-freshness-decay-in-ai" rel="noopener noreferrer"&gt;How to Model Freshness Decay in AI Visibility Monitoring&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The governing principle is simple: an old observation may remain useful, but it should never impersonate a current one. A versioned freshness policy makes that distinction explicit, testable, and reproducible.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>testing</category>
      <category>seo</category>
    </item>
    <item>
      <title>How to Set Collection SLOs for AI Search Visibility Monitoring</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:53:32 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-set-collection-slos-for-ai-search-visibility-monitoring-hl9</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-set-collection-slos-for-ai-search-visibility-monitoring-hl9</guid>
      <description>&lt;p&gt;AI-search visibility reports are only as trustworthy as the collection system behind them.&lt;/p&gt;

&lt;p&gt;If a dashboard shows a zero, the reader needs to know whether the brand was genuinely absent from a completed answer or whether the run timed out, the response stream ended early, authentication failed, or citations could not be resolved. Treating all of those outcomes as the same number makes a polished chart less useful, not more.&lt;/p&gt;

&lt;p&gt;A collection service-level objective (SLO) gives teams a practical way to define, measure, and improve that reliability.&lt;/p&gt;

&lt;p&gt;This guide describes an implementation pattern. The thresholds are illustrative starting points, not universal benchmarks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an explicit run event
&lt;/h2&gt;

&lt;p&gt;Every scheduled observation should create a durable run event before any provider call begins. A useful minimum schema includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;run_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prompt_set_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prompt_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;model&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requested_at&lt;/code&gt;, &lt;code&gt;started_at&lt;/code&gt;, and &lt;code&gt;completed_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;failure_class&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eligibility_state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attempt_count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;answer_preserved&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;citations_resolved&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;raw_artifact_uri&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;collector_version&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important design choice is that an attempted run never disappears. A retry may recover the observation, but it should not erase the evidence that the first attempt failed.&lt;/p&gt;

&lt;p&gt;A companion failure taxonomy should distinguish network and timeout errors, authentication and rate limits, incomplete streams, parser failures, citation-resolution failures, and explicit exclusions. I published a deeper implementation guide here: &lt;a href="https://corank-ai-search.hashnode.dev/how-to-build-a-failure-taxonomy-for-ai-visibility-data-collection" rel="noopener noreferrer"&gt;How to build a failure taxonomy for AI visibility data collection&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define reliability indicators before setting targets
&lt;/h2&gt;

&lt;p&gt;An SLO is only useful when the underlying service-level indicators (SLIs) are measurable from the event data.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Eligible-run completion rate
&lt;/h3&gt;

&lt;p&gt;This answers: of the runs the methodology said should execute, how many ended with a completed, preserved answer?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eligible completion rate =
completed eligible runs with preserved answers
/
all eligible scheduled runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep excluded and limited runs out of the denominator only when the eligibility rule is explicit and versioned. Silently dropping them makes the rate look healthier than the collection system really is.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Evidence capture rate
&lt;/h3&gt;

&lt;p&gt;A completed status is not enough. The answer artifact should be stored so an analyst can reproduce the classification later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;evidence capture rate =
completed runs with preserved answer artifacts
/
all completed runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This indicator catches pipelines that calculate a metric successfully but lose the underlying evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Citation-resolution rate
&lt;/h3&gt;

&lt;p&gt;For answers that expose citations or outbound sources, measure how often the collector stores both the displayed reference and a resolved destination.&lt;/p&gt;

&lt;p&gt;Track this separately from answer completion. A run can produce a perfectly readable answer while the citation resolver fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Freshness attainment
&lt;/h3&gt;

&lt;p&gt;Define a maximum acceptable age for each reporting tier.&lt;/p&gt;

&lt;p&gt;For example, a daily executive report may require that all included observations were collected within the previous 24 hours. A slower research benchmark may use a wider window. The correct limit depends on the reporting promise; the key is to make it visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Recovery without evidence loss
&lt;/h3&gt;

&lt;p&gt;Retries are normal. Hidden retries are dangerous.&lt;/p&gt;

&lt;p&gt;Measure the percentage of recovered runs that retain the original attempt history, including failure class, timestamp, and collector version. This protects the difference between a healthy first-pass system and one that succeeds only after repeated attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the indicators into an SLO
&lt;/h2&gt;

&lt;p&gt;A starting policy might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reliability dimension&lt;/th&gt;
&lt;th&gt;Illustrative target&lt;/th&gt;
&lt;th&gt;Measurement window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eligible-run completion&lt;/td&gt;
&lt;td&gt;98% or better&lt;/td&gt;
&lt;td&gt;Rolling 7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence capture&lt;/td&gt;
&lt;td&gt;99.5% or better&lt;/td&gt;
&lt;td&gt;Rolling 7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citation resolution&lt;/td&gt;
&lt;td&gt;97% or better&lt;/td&gt;
&lt;td&gt;Rolling 7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness attainment&lt;/td&gt;
&lt;td&gt;99% or better&lt;/td&gt;
&lt;td&gt;Per reporting cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovered runs retaining attempt history&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;Rolling 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These values are examples. A team should choose targets based on the decisions the report supports, provider constraints, collection frequency, and the cost of a wrong conclusion.&lt;/p&gt;

&lt;p&gt;The most important rule is semantic: a failed or indeterminate run must not be converted into a brand-absence observation simply to satisfy a completion target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an error budget
&lt;/h2&gt;

&lt;p&gt;Suppose a benchmark schedules 1,000 eligible runs in a week and the completion SLO is 98%.&lt;/p&gt;

&lt;p&gt;That leaves an error budget of 20 runs.&lt;/p&gt;

&lt;p&gt;The budget turns reliability into an operating decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If five runs fail independently across unrelated prompts, investigate but continue normal collection.&lt;/li&gt;
&lt;li&gt;If 15 runs fail for one provider within an hour, treat the pattern as a likely incident.&lt;/li&gt;
&lt;li&gt;If the budget is exhausted, pause methodology changes and prioritize collector reliability.&lt;/li&gt;
&lt;li&gt;If a retry recovers a run, preserve both attempts and report first-pass and final completion separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single aggregate percentage should not hide correlated failures. Break down the budget by provider, model, prompt group, geography, and collector version so one healthy segment cannot mask another that is unusable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build alerts around decision risk
&lt;/h2&gt;

&lt;p&gt;Not every failure deserves a page.&lt;/p&gt;

&lt;p&gt;A practical alert policy can use three levels:&lt;/p&gt;

&lt;h3&gt;
  
  
  Review
&lt;/h3&gt;

&lt;p&gt;Trigger a review when a small number of isolated runs fail but the reporting window remains within budget. Queue samples for replay and inspect the preserved artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incident
&lt;/h3&gt;

&lt;p&gt;Open an incident when failures are correlated by provider, model, collector release, or citation resolver, or when a reporting tier risks missing its freshness promise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement hold
&lt;/h3&gt;

&lt;p&gt;Prevent the affected slice from contributing to visibility scores when the system cannot distinguish genuine absence from collection uncertainty. Mark the slice as limited or under review instead of publishing a misleading zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep reliability separate from the visibility score
&lt;/h2&gt;

&lt;p&gt;Collection health and brand visibility answer different questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visibility asks whether and how a brand appears in eligible AI answers.&lt;/li&gt;
&lt;li&gt;Reliability asks whether the observation pipeline produced enough evidence to trust that conclusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not reward the visibility score because the collector completed more runs, and do not punish the brand because the collector failed. Show the two dimensions side by side.&lt;/p&gt;

&lt;p&gt;For every chart or export, include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;methodology version&lt;/li&gt;
&lt;li&gt;prompt-set version&lt;/li&gt;
&lt;li&gt;collection window&lt;/li&gt;
&lt;li&gt;eligible, limited, excluded, and review counts&lt;/li&gt;
&lt;li&gt;first-pass and final completion rates&lt;/li&gt;
&lt;li&gt;unresolved citation count&lt;/li&gt;
&lt;li&gt;known incident annotations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That context makes changes interpretable when providers, prompts, or collector code evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact implementation checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create the run event before the external request.&lt;/li&gt;
&lt;li&gt;Version prompts, eligibility rules, parsers, and collectors.&lt;/li&gt;
&lt;li&gt;Preserve completed answers before calculating brand presence.&lt;/li&gt;
&lt;li&gt;Store every failed attempt, including recovered attempts.&lt;/li&gt;
&lt;li&gt;Separate answer completion from citation resolution.&lt;/li&gt;
&lt;li&gt;Define SLIs with explicit denominators.&lt;/li&gt;
&lt;li&gt;Set targets by reporting risk, not by aesthetics.&lt;/li&gt;
&lt;li&gt;Break error budgets down by important segments.&lt;/li&gt;
&lt;li&gt;Hold indeterminate slices out of visibility scores.&lt;/li&gt;
&lt;li&gt;Publish reliability metadata beside the measurement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reliable AI-visibility monitoring is not about pretending every provider behaves the same way. It is about making uncertainty explicit enough that a reader can tell the difference between a market signal and a collection problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt; focuses on measurable AI-search visibility. The reliability framework above is an educational implementation pattern for teams designing or evaluating this kind of monitoring.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>monitoring</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Design a Data Dictionary for AI Visibility Analytics</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:18:55 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-design-a-data-dictionary-for-ai-visibility-analytics-1knd</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-design-a-data-dictionary-for-ai-visibility-analytics-1knd</guid>
      <description>&lt;p&gt;AI-visibility programs often start with dashboards. That is understandable: charts make a new discipline feel concrete.&lt;/p&gt;

&lt;p&gt;But the dashboard is not the foundation. The foundation is a shared data dictionary that tells every analyst, crawler, pipeline, and stakeholder what one observation means.&lt;/p&gt;

&lt;p&gt;Without that dictionary, two people can review the same answer and disagree about whether a brand was merely mentioned, actively recommended, or cited as evidence. The chart may still render. The number may still have two decimal places. It just will not be trustworthy.&lt;/p&gt;

&lt;p&gt;This guide describes a practical way to design the data model behind an AI-visibility workflow. It is intended for teams auditing brand mentions, recommendations, citations, and source destinations across AI answer surfaces.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Educational note: this is an implementation framework, not a claim about any particular AI platform's ranking algorithm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Start by declaring the observation grain
&lt;/h2&gt;

&lt;p&gt;Before choosing columns, write one sentence that defines a row.&lt;/p&gt;

&lt;p&gt;A useful default is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One row represents one captured answer for one versioned prompt, on one named AI surface, in one market and locale, at one capture time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence prevents a common mistake: mixing prompt-level facts, answer-level facts, brand-level annotations, and citation-level facts in a single wide table.&lt;/p&gt;

&lt;p&gt;A captured answer may mention several brands and include several citations. Those are one-to-many relationships. Model them as related tables or nested records instead of repeatedly overwriting fields.&lt;/p&gt;

&lt;p&gt;A simple logical model has four layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt&lt;/strong&gt; — the question, its intent, and its version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observation&lt;/strong&gt; — where and when the answer was captured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity assessment&lt;/strong&gt; — how a brand or product appears in that answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citation assessment&lt;/strong&gt; — which URLs were surfaced and what role they played.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Give every mutable thing a version
&lt;/h2&gt;

&lt;p&gt;Prompt text changes. Entity aliases change. URL canonicalization rules change. Analyst rubrics improve.&lt;/p&gt;

&lt;p&gt;If a changing object has only an ID, a historical result can silently acquire a new meaning. Pair stable IDs with explicit versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prompt_id&lt;/strong&gt; + &lt;strong&gt;prompt_version&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;entity_id&lt;/strong&gt; + &lt;strong&gt;entity_dictionary_version&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;methodology_version&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;canonicalization_rule_version&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never edit an old prompt definition in place after collecting observations. Create a new version and preserve the bridge between versions. This keeps trend lines auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separate collection facts from analyst judgments
&lt;/h2&gt;

&lt;p&gt;Collection facts should describe what happened without interpretation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;observation_id&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prompt_id&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prompt_version&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;engine_surface&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;market&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;locale&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;captured_at&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;capture_status&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;raw_answer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;evidence_bundle_uri&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analyst judgments belong in a second layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;entity_id&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mention_state&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;recommendation_strength&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;answer_position&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;claim_alignment&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;confidence_label&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;analyst_id&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reviewed_at&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters operationally. If the rubric changes, the team can re-annotate preserved raw evidence without pretending the source observation changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use enums that can survive review
&lt;/h2&gt;

&lt;p&gt;Free-text labels drift quickly. One analyst writes "strong mention," another writes "top pick," and a third writes "recommended." A dashboard later treats them as different categories.&lt;/p&gt;

&lt;p&gt;Prefer a small controlled vocabulary. For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  Mention state
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;absent&lt;/strong&gt; — the entity is not present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;named&lt;/strong&gt; — the entity is mentioned without evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;described&lt;/strong&gt; — the answer provides meaningful descriptive context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compared&lt;/strong&gt; — the entity is evaluated against alternatives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recommended&lt;/strong&gt; — the answer explicitly suggests choosing or considering it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recommendation strength
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;conditional&lt;/strong&gt; — suitable only for a stated situation or constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;positive&lt;/strong&gt; — clearly favorable, but not the leading choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;primary&lt;/strong&gt; — presented as a leading or first choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Confidence label
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;high&lt;/strong&gt; — the evidence directly matches the definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;medium&lt;/strong&gt; — the label is reasonable but requires interpretation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;low&lt;/strong&gt; — evidence is incomplete or ambiguous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Confidence should describe the annotation, not the analyst's enthusiasm for the brand.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Model citations as first-class records
&lt;/h2&gt;

&lt;p&gt;A URL is not merely a string attached to an answer. It passes through redirects, tracking parameters, fragments, and canonical tags. Preserve both the observed value and the normalized result.&lt;/p&gt;

&lt;p&gt;Useful fields include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;raw_cited_url&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;resolved_url&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;canonical_url&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;registrable_domain&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;source_role&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;citation_position&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;http_status_at_capture&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;last_verified_at&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the raw URL immutable. Canonicalization can then be re-run as rules improve.&lt;/p&gt;

&lt;p&gt;A conservative normalization policy might:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;lowercase the hostname;&lt;/li&gt;
&lt;li&gt;remove a default port;&lt;/li&gt;
&lt;li&gt;strip a fragment;&lt;/li&gt;
&lt;li&gt;remove only known tracking parameters;&lt;/li&gt;
&lt;li&gt;follow redirects with a bounded hop count;&lt;/li&gt;
&lt;li&gt;honor a canonical tag only when it is safe and relevant;&lt;/li&gt;
&lt;li&gt;retain both the resolved and canonical values.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not remove every query parameter. Some parameters identify genuinely different resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Store the evidence needed to reproduce a judgment
&lt;/h2&gt;

&lt;p&gt;A row without evidence is difficult to audit. An evidence bundle can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;captured answer text;&lt;/li&gt;
&lt;li&gt;citation list in displayed order;&lt;/li&gt;
&lt;li&gt;screenshot or render;&lt;/li&gt;
&lt;li&gt;capture timestamp and timezone;&lt;/li&gt;
&lt;li&gt;prompt text and version;&lt;/li&gt;
&lt;li&gt;engine surface and visible mode;&lt;/li&gt;
&lt;li&gt;collection notes and error state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bundle should be addressable with a stable URI or object key. Access controls and retention policies still apply, but the record needs to tell a reviewer where the evidence lives.&lt;/p&gt;

&lt;p&gt;A compact observation can store a generated observation ID, the prompt ID and version, the named surface, market, locale, UTC capture time, capture status, methodology version, and evidence-bundle URI. The specific storage system is less important than immutable identity and traceable provenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Define invariants in plain language and code
&lt;/h2&gt;

&lt;p&gt;An invariant is a rule that must remain true. Good data dictionaries include both a human explanation and a machine-checkable constraint.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;captured_at&lt;/strong&gt; must include a timezone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;raw_answer&lt;/strong&gt; may be empty only when &lt;strong&gt;capture_status&lt;/strong&gt; is not &lt;strong&gt;complete&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recommendation_strength&lt;/strong&gt; cannot be &lt;strong&gt;primary&lt;/strong&gt; when &lt;strong&gt;mention_state&lt;/strong&gt; is &lt;strong&gt;absent&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;every citation record must reference an existing observation.&lt;/li&gt;
&lt;li&gt;one observation cannot contain duplicate citation positions.&lt;/li&gt;
&lt;li&gt;a normalized URL never replaces its raw source value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put these checks close to ingestion. A dashboard should not be the first place anyone discovers an impossible combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Build a small adjudication loop
&lt;/h2&gt;

&lt;p&gt;Before scaling collection, ask two analysts to label the same small sample independently.&lt;/p&gt;

&lt;p&gt;Do not begin by chasing an impressive agreement score. Begin by finding ambiguous definitions.&lt;/p&gt;

&lt;p&gt;For every disagreement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;reveal the evidence;&lt;/li&gt;
&lt;li&gt;identify which phrase in the rubric allowed two interpretations;&lt;/li&gt;
&lt;li&gt;refine the definition or add a boundary example;&lt;/li&gt;
&lt;li&gt;record the decision in a change log;&lt;/li&gt;
&lt;li&gt;re-label the affected sample.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Boundary examples are especially valuable. A dictionary becomes useful when it explains not only what a label is, but also the nearest thing that does &lt;strong&gt;not&lt;/strong&gt; qualify.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Keep methodology changes visible
&lt;/h2&gt;

&lt;p&gt;A methodology change should create a release note. At minimum, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;version number;&lt;/li&gt;
&lt;li&gt;effective date;&lt;/li&gt;
&lt;li&gt;changed fields or definitions;&lt;/li&gt;
&lt;li&gt;reason for the change;&lt;/li&gt;
&lt;li&gt;expected effect on historical comparability;&lt;/li&gt;
&lt;li&gt;migration or re-annotation plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an old and new method cannot be compared directly, show a break in the trend rather than blending them. Honest discontinuity is better than false precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Design the reporting layer last
&lt;/h2&gt;

&lt;p&gt;Once the schema and definitions are stable, metrics become easier to defend.&lt;/p&gt;

&lt;p&gt;For example, "recommendation rate" still needs a denominator. Is it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;all scheduled prompts;&lt;/li&gt;
&lt;li&gt;only successful captures;&lt;/li&gt;
&lt;li&gt;only answers where the category was understood;&lt;/li&gt;
&lt;li&gt;or only observations eligible under a particular rubric version?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write that denominator into the metric definition. Include exclusions, required fields, and version scope. Then the same calculation can be implemented in SQL, a notebook, or a dashboard without changing meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping an AI-visibility data dictionary, confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every table has a declared grain;&lt;/li&gt;
&lt;li&gt;stable IDs and mutable versions are separate;&lt;/li&gt;
&lt;li&gt;raw evidence is preserved;&lt;/li&gt;
&lt;li&gt;collection facts and judgments are not conflated;&lt;/li&gt;
&lt;li&gt;enum values have positive and negative examples;&lt;/li&gt;
&lt;li&gt;URLs retain raw, resolved, and canonical forms;&lt;/li&gt;
&lt;li&gt;invariants are machine-checkable;&lt;/li&gt;
&lt;li&gt;timestamps include timezones;&lt;/li&gt;
&lt;li&gt;analyst and methodology provenance are recorded;&lt;/li&gt;
&lt;li&gt;metric denominators and exclusions are explicit;&lt;/li&gt;
&lt;li&gt;changes have release notes;&lt;/li&gt;
&lt;li&gt;historical observations remain reproducible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;AI-visibility reporting is young enough that teams are still inventing vocabulary while building the measurement systems. That makes definitions a product feature, not administrative cleanup.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt;, we think about visibility as an evidence and measurement problem: preserve the answer, define the observation, make judgments reviewable, and let reporting inherit those disciplined choices.&lt;/p&gt;

&lt;p&gt;For a companion reference of common terms, see the &lt;a href="https://rentry.co/corank-ai-visibility-reporting-glossary" rel="noopener noreferrer"&gt;AI visibility reporting glossary&lt;/a&gt;. Use it as a starting point, then adapt definitions to your own methodology and document every change.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>analytics</category>
      <category>dataengineering</category>
      <category>seo</category>
    </item>
    <item>
      <title>How to Audit Where AI Answers Actually Send Users</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:55:52 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-audit-where-ai-answers-actually-send-users-4p49</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-audit-where-ai-answers-actually-send-users-4p49</guid>
      <description>&lt;p&gt;An AI answer can mention a brand, cite a source, and still send the user somewhere unhelpful.&lt;/p&gt;

&lt;p&gt;The displayed citation may resolve through a redirect. The final page may be an old article, a marketplace listing, a review site, or a URL that no longer works. Some interfaces show a publisher name without exposing the full destination until a click. Others include a brand name but no outbound link at all.&lt;/p&gt;

&lt;p&gt;That is why AI visibility needs a destination-access audit in addition to mention and citation counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the unit of analysis
&lt;/h2&gt;

&lt;p&gt;Use the prompt run as the parent record.&lt;/p&gt;

&lt;p&gt;One answer can contain several citations and links, but it is still one visibility opportunity. Store outbound destinations as child records instead of treating every link as a separate answer-level win.&lt;/p&gt;

&lt;p&gt;A parent run might contain:&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;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run_2026_08_03_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comparison_smb_analytics_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"benchmark_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example_provider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"interface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"collected_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_text"&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;"entity_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brand_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mention_role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shortlisted"&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;Each link record should reference that run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve four URL states
&lt;/h2&gt;

&lt;p&gt;Do not normalize a citation into one URL and discard the rest. Keep at least four states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Displayed destination:&lt;/strong&gt; the label or URL visible in the answer interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw href:&lt;/strong&gt; the exact link attached to the visible element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolved destination:&lt;/strong&gt; the final URL after redirects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical destination:&lt;/strong&gt; the page's declared or analyst-approved canonical URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These fields answer different questions.&lt;/p&gt;

&lt;p&gt;The displayed destination shows what the user sees. The raw href preserves the interface output. The resolved destination reveals where the click actually lands. The canonical destination supports deduplication across tracking parameters and equivalent URLs.&lt;/p&gt;

&lt;p&gt;A link record can look like this:&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;"link_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"link_0042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run_2026_08_03_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example publisher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"raw_href"&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://redirect.example/xYz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resolved_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://publisher.example/reviews/product?utm_source=ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonical_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://publisher.example/reviews/product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"http_status"&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;"captured_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T12:00:03Z"&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;Never overwrite the raw value with the normalized one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify destination ownership
&lt;/h2&gt;

&lt;p&gt;The final URL should receive an ownership class. A conservative taxonomy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owned product:&lt;/strong&gt; a product, service, pricing, or signup page controlled by the entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owned editorial:&lt;/strong&gt; an article, guide, documentation page, or research page controlled by the entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owned profile:&lt;/strong&gt; an official social, marketplace, or directory profile controlled by the entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent editorial:&lt;/strong&gt; a publisher, analyst, or journalist page not controlled by the entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent review:&lt;/strong&gt; a review or comparison platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace:&lt;/strong&gt; an app store, extension store, reseller, or integration marketplace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregator:&lt;/strong&gt; a directory or database that summarizes multiple entities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown:&lt;/strong&gt; ownership cannot be established from the evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid:&lt;/strong&gt; the destination fails, loops, or produces a blocked response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not infer ownership only from the displayed publisher name. Resolve and inspect the destination domain.&lt;/p&gt;

&lt;p&gt;If a corporate group owns several domains, keep both the registrable domain and the normalized owner ID. That prevents a subsidiary or documentation subdomain from being misclassified as independent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record interface accessibility
&lt;/h2&gt;

&lt;p&gt;A link that exists in source data may not be equally accessible to a user.&lt;/p&gt;

&lt;p&gt;For every captured destination, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clickable in the captured interface&lt;/li&gt;
&lt;li&gt;visible without an extra interaction&lt;/li&gt;
&lt;li&gt;exposed only after expanding citations&lt;/li&gt;
&lt;li&gt;displayed as a full URL, publisher label, favicon, or numbered citation&lt;/li&gt;
&lt;li&gt;opens in the same tab or a new context&lt;/li&gt;
&lt;li&gt;requires sign-in&lt;/li&gt;
&lt;li&gt;blocked by a warning or interstitial&lt;/li&gt;
&lt;li&gt;available on mobile, desktop, or both&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not only a technical detail. A brand can have strong citation coverage and weak referral opportunity if users cannot easily reach the owned domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow redirects carefully
&lt;/h2&gt;

&lt;p&gt;Resolve redirects with a bounded, auditable process.&lt;/p&gt;

&lt;p&gt;Recommended fields include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redirect hop number&lt;/li&gt;
&lt;li&gt;source URL&lt;/li&gt;
&lt;li&gt;status code&lt;/li&gt;
&lt;li&gt;destination URL&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;li&gt;method used&lt;/li&gt;
&lt;li&gt;error class&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop on loops, invalid schemes, excessive hops, or destinations outside the allowed protocol set. Do not execute downloaded files or active content just to resolve a destination. A safe resolver should usually limit itself to HTTP and HTTPS requests and apply normal network-security protections.&lt;/p&gt;

&lt;p&gt;Keep temporary tracking parameters in the raw chain. Remove them only in the canonicalization layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize without over-merging
&lt;/h2&gt;

&lt;p&gt;URL normalization is useful, but aggressive rules can hide meaningful differences.&lt;/p&gt;

&lt;p&gt;Safe candidates often include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lowercasing the host&lt;/li&gt;
&lt;li&gt;removing a default port&lt;/li&gt;
&lt;li&gt;stripping a fragment when it does not change server content&lt;/li&gt;
&lt;li&gt;removing explicitly approved tracking parameters&lt;/li&gt;
&lt;li&gt;normalizing a trailing slash according to the site's canonical behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Risky candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deleting every query parameter&lt;/li&gt;
&lt;li&gt;treating HTTP and HTTPS as equivalent without checking&lt;/li&gt;
&lt;li&gt;collapsing product and editorial subdomains&lt;/li&gt;
&lt;li&gt;assuming every locale path is duplicate content&lt;/li&gt;
&lt;li&gt;trusting a canonical tag that points to an unrelated page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the normalization-rule version. When a rule changes, reprocess a sample and compare the old and new destination groups before rewriting historical reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check claim-to-destination fit
&lt;/h2&gt;

&lt;p&gt;A successful link is not automatically a useful link.&lt;/p&gt;

&lt;p&gt;Compare the answer's claim with the final destination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the page support the statement associated with the citation?&lt;/li&gt;
&lt;li&gt;Is the content about the named entity or a different product?&lt;/li&gt;
&lt;li&gt;Is the page current enough for the claim?&lt;/li&gt;
&lt;li&gt;Does the destination contain the feature, price, statistic, or policy referenced in the answer?&lt;/li&gt;
&lt;li&gt;Does the link land on the specific evidence page or only on a homepage?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a small review taxonomy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Supports claim&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partially supports claim&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Related but non-supporting&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contradicts claim&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unavailable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not reviewed&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the evidence sentence and reviewer state. Do not let a classifier silently promote “related” to “supports.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate destination metrics
&lt;/h2&gt;

&lt;p&gt;Once the records are preserved, useful rates include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Owned-link rate
&lt;/h3&gt;

&lt;p&gt;Eligible prompt runs with at least one clickable owned destination divided by eligible prompt runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Owned-product-link rate
&lt;/h3&gt;

&lt;p&gt;Eligible prompt runs with a clickable owned product or service page divided by eligible prompt runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Citation resolution success
&lt;/h3&gt;

&lt;p&gt;Citation links that resolve to a valid final destination divided by citation links tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  Destination mismatch rate
&lt;/h3&gt;

&lt;p&gt;Resolved destinations whose owner or page type differs materially from the displayed label divided by resolved destinations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claim-support rate
&lt;/h3&gt;

&lt;p&gt;Reviewed citation destinations classified as supporting the associated claim divided by reviewed destinations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third-party dependency rate
&lt;/h3&gt;

&lt;p&gt;Eligible prompt runs where every clickable destination is independent or marketplace-owned divided by eligible prompt runs with at least one link.&lt;/p&gt;

&lt;p&gt;Keep the denominator beside every metric. Do not combine answer-level and link-level records in one rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a repeatable audit workflow
&lt;/h2&gt;

&lt;p&gt;A practical sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Freeze the prompt set and eligibility policy.&lt;/li&gt;
&lt;li&gt;Capture the full answer and interface context.&lt;/li&gt;
&lt;li&gt;Extract visible citations and outbound links.&lt;/li&gt;
&lt;li&gt;Store the displayed label and raw href.&lt;/li&gt;
&lt;li&gt;Resolve redirects with bounded safety rules.&lt;/li&gt;
&lt;li&gt;Capture status, final URL, and redirect chain.&lt;/li&gt;
&lt;li&gt;Determine registrable domain and normalized owner.&lt;/li&gt;
&lt;li&gt;Apply versioned canonicalization rules.&lt;/li&gt;
&lt;li&gt;Classify destination type and interface accessibility.&lt;/li&gt;
&lt;li&gt;Review claim-to-destination fit for important links.&lt;/li&gt;
&lt;li&gt;Calculate answer-level and link-level metrics separately.&lt;/li&gt;
&lt;li&gt;Preserve corrections and rule versions in an audit trail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat a subset of prompts to learn whether destination behavior is stable. An answer may continue mentioning the same brand while switching its citations or outbound destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example failure patterns
&lt;/h2&gt;

&lt;p&gt;Several patterns deserve separate alerts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mention without access:&lt;/strong&gt; the brand is recommended but no clickable destination is available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owned source without attribution:&lt;/strong&gt; an owned page is cited while the brand is not named.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party detour:&lt;/strong&gt; the brand is recommended, but the only link points to a review site or marketplace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken evidence:&lt;/strong&gt; the citation fails or redirects to an unrelated page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage fallback:&lt;/strong&gt; a specific claim links only to the company homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical collision:&lt;/strong&gt; two materially different pages collapse into one normalized URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility-only link:&lt;/strong&gt; the destination exists in extracted data but is not visible or clickable in the captured interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These patterns lead to different fixes. A third-party detour is not the same problem as a broken citation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep destination access separate from recommendations
&lt;/h2&gt;

&lt;p&gt;Recommendation is a semantic role. Destination access is an interface and URL outcome.&lt;/p&gt;

&lt;p&gt;A brand can be recommended with no link. It can receive an owned link without being recommended. It can be cited as evidence while a competitor is selected. Preserve each state independently.&lt;/p&gt;

&lt;p&gt;A useful report can therefore show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;named coverage rate&lt;/li&gt;
&lt;li&gt;recommendation rate&lt;/li&gt;
&lt;li&gt;owned-source citation rate&lt;/li&gt;
&lt;li&gt;owned-link rate&lt;/li&gt;
&lt;li&gt;owned-product-link rate&lt;/li&gt;
&lt;li&gt;destination mismatch rate&lt;/li&gt;
&lt;li&gt;claim-support rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The combination is more diagnostic than a single visibility score.&lt;/p&gt;

&lt;p&gt;I’m building &lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt; around the broader problem of understanding how brands appear in search and AI-generated answers. Destination access is a critical part of that measurement because an appearance and a usable path to the company are not the same thing.&lt;/p&gt;

&lt;p&gt;Track what the interface shows, where the click goes, who owns the destination, and whether the page supports the claim. That turns a citation count into evidence you can act on.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Measure Brand Share of Voice in AI Answers Without Double Counting</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:16:37 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-measure-brand-share-of-voice-in-ai-answers-without-double-counting-f0p</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-measure-brand-share-of-voice-in-ai-answers-without-double-counting-f0p</guid>
      <description>&lt;p&gt;Share of voice sounds simple: count how often a brand appears and divide by a total.&lt;/p&gt;

&lt;p&gt;In AI-generated answers, that shortcut breaks quickly. One answer can mention the same brand several times, cite three pages from one domain, recommend two products from the same company, or fail to return at all. A benchmark can also mix providers, intents, locales, and repeated runs.&lt;/p&gt;

&lt;p&gt;If those units are not defined before collection, a polished percentage can hide duplicate counting and a changing denominator.&lt;/p&gt;

&lt;p&gt;This guide describes a practical measurement model for brand share of voice in AI answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with the observation unit
&lt;/h2&gt;

&lt;p&gt;The safest base unit is an &lt;strong&gt;observation&lt;/strong&gt;, not a prompt.&lt;/p&gt;

&lt;p&gt;An observation is one documented run of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one exact prompt;&lt;/li&gt;
&lt;li&gt;one provider or interface;&lt;/li&gt;
&lt;li&gt;one locale and account condition;&lt;/li&gt;
&lt;li&gt;one timestamp;&lt;/li&gt;
&lt;li&gt;one repeat or retry identifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same prompt on two providers creates two observations. The same prompt repeated three times on one provider creates three observations. A retry remains linked to the failed attempt and should not silently become an unrelated new run.&lt;/p&gt;

&lt;p&gt;A compact observation key can combine the prompt version, provider, locale, scheduled window, and repeat number. That makes the denominator auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Resolve entities before counting them
&lt;/h2&gt;

&lt;p&gt;AI answers rarely use one canonical brand string. They may use a company name, product name, abbreviation, former name, domain, or punctuation variant.&lt;/p&gt;

&lt;p&gt;Create an entity registry before calculating share of voice. Each record should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a stable entity ID;&lt;/li&gt;
&lt;li&gt;the canonical name;&lt;/li&gt;
&lt;li&gt;aliases;&lt;/li&gt;
&lt;li&gt;associated domains;&lt;/li&gt;
&lt;li&gt;product names;&lt;/li&gt;
&lt;li&gt;explicit exclusions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entity resolution must also define what does not count. A generic word that happens to match a brand name should not be classified automatically. Ambiguous matches should be reviewed or marked unresolved.&lt;/p&gt;

&lt;p&gt;Store the raw matched string alongside the canonical entity ID so the classification can be reproduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separate mentions, citations, and recommendations
&lt;/h2&gt;

&lt;p&gt;These are different events.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;mention&lt;/strong&gt; means the entity appears in the answer.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;citation&lt;/strong&gt; means a source associated with the entity is linked or referenced as evidence.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;recommendation&lt;/strong&gt; means the answer presents the entity as an option for the user’s stated task.&lt;/p&gt;

&lt;p&gt;One observation can contain all three, but they should not be collapsed into one flag called “visibility.”&lt;/p&gt;

&lt;p&gt;For each entity and observation, store three booleans: mentioned, cited, and recommended. Also keep the evidence span or cited URL that supports each classification.&lt;/p&gt;

&lt;p&gt;This separation prevents a cited source from being misreported as a product recommendation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deduplicate within an answer
&lt;/h2&gt;

&lt;p&gt;For basic entity share of voice, count an entity at most once per observation.&lt;/p&gt;

&lt;p&gt;If an answer says “Acme” four times, that is still one mentioned observation for Acme. If it links to three Acme pages, that is one cited observation for Acme in the entity-level metric.&lt;/p&gt;

&lt;p&gt;Repeated references can be useful as a separate frequency or source-depth metric, but they should not inflate the binary share-of-voice numerator.&lt;/p&gt;

&lt;p&gt;The aggregation rule is effectively:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entity_mentioned_in_observation =
  maximum value of all mention events for that entity and observation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The same pattern applies to citation and recommendation flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Define the eligible denominator
&lt;/h2&gt;

&lt;p&gt;Not every scheduled run belongs in every denominator.&lt;/p&gt;

&lt;p&gt;A run may fail because of provider access, timeout, empty response, parsing error, policy block, unsupported locale, or a collection-system failure.&lt;/p&gt;

&lt;p&gt;A failed observation is not an absent brand mention.&lt;/p&gt;

&lt;p&gt;For a basic mention-share metric:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mention share =
  observations where the entity was mentioned
  divided by successful eligible observations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;“Successful eligible” must be explicit. An observation may be successful for answer-level mention analysis but ineligible for citation analysis if the interface does not expose citations.&lt;/p&gt;

&lt;p&gt;Keep collection status, mention eligibility, citation eligibility, recommendation eligibility, and failure reason separately. Never turn collection failures into zeros just to keep the denominator convenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Choose between presence share and competitive share
&lt;/h2&gt;

&lt;p&gt;Two useful metrics answer different questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Presence share
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entity presence share =
  eligible observations mentioning the entity
  divided by all eligible observations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This asks: “In what percentage of relevant answers did the brand appear?”&lt;/p&gt;

&lt;h3&gt;
  
  
  Competitive mention share
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;competitive mention share =
  entity observation-mentions
  divided by total observation-mentions across the comparison set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This asks: “Of all brand appearances in the defined competitive set, what portion belonged to this entity?”&lt;/p&gt;

&lt;p&gt;An answer that mentions three competitors contributes one observation-mention to each. The competitive numerator and denominator therefore operate on entity-observation pairs, not raw word frequency.&lt;/p&gt;

&lt;p&gt;Report which definition is being used. Both may be valid, but they are not interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Do not let large answers dominate
&lt;/h2&gt;

&lt;p&gt;Suppose one answer lists ten vendors and another recommends one vendor. Raw competitive share treats the ten-vendor answer as ten entity-observation pairs.&lt;/p&gt;

&lt;p&gt;That may be appropriate for coverage, but not for recommendation strength.&lt;/p&gt;

&lt;p&gt;A second view can allocate one unit of weight per observation:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entity weight in observation =
  1 divided by the number of comparison-set entities in that observation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If four brands appear, each receives 0.25 for that observation. If one brand appears, it receives 1.0.&lt;/p&gt;

&lt;p&gt;This normalized view prevents broad list answers from dominating the result. Keep it separate from binary presence share instead of blending the two without explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Handle repeated runs deliberately
&lt;/h2&gt;

&lt;p&gt;Generative outputs vary. Repeats reveal whether visibility is persistent or intermittent.&lt;/p&gt;

&lt;p&gt;Two common aggregation choices are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run-level share:&lt;/strong&gt; every successful repeat remains one observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-cell share:&lt;/strong&gt; repeats are collapsed into a prompt-provider-locale cell before calculating the portfolio metric.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run-level share reflects observed frequency. Prompt-cell share prevents heavily repeated prompts from receiving accidental extra weight.&lt;/p&gt;

&lt;p&gt;For a prompt cell with three repeats, an entity may have persistence of two out of three. The cell can then contribute its persistence value to a higher-level metric.&lt;/p&gt;

&lt;p&gt;Do not mix repeated and non-repeated prompts in one naive denominator. Either weight prompt cells equally or disclose why some cells receive more observations.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Weight prompts only for a documented reason
&lt;/h2&gt;

&lt;p&gt;Equal weighting is the cleanest default. Weighted benchmarks can be useful when intents differ materially in business importance, but weights create governance work.&lt;/p&gt;

&lt;p&gt;If weights are used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;assign them before looking at the current result;&lt;/li&gt;
&lt;li&gt;record the source and owner of each weight;&lt;/li&gt;
&lt;li&gt;keep them stable for the comparison period;&lt;/li&gt;
&lt;li&gt;version every change;&lt;/li&gt;
&lt;li&gt;show both weighted and unweighted results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A transparent weighted presence metric is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;weighted presence =
  sum of prompt-cell weight multiplied by entity persistence
  divided by the sum of eligible prompt-cell weights
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Weights should represent a decision model, not a way to improve a disappointing score.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Keep provider and intent slices visible
&lt;/h2&gt;

&lt;p&gt;A global share can conceal opposing movements.&lt;/p&gt;

&lt;p&gt;For example, one provider may rise while another falls. Branded prompts may improve while non-branded commercial prompts decline. The total may look unchanged.&lt;/p&gt;

&lt;p&gt;Always retain sliceable fields for provider and interface, visible model or experience version, intent class, funnel stage, branded versus unbranded, locale, prompt-set version, and collection window.&lt;/p&gt;

&lt;p&gt;Do not average providers with different sample sizes without showing the component results.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Canonicalize citations without erasing evidence
&lt;/h2&gt;

&lt;p&gt;Citation metrics need two URL forms: the raw URL returned by the interface and the canonical URL used for aggregation.&lt;/p&gt;

&lt;p&gt;Redirects, tracking parameters, fragments, mobile paths, and protocol variants can make one source appear to be several.&lt;/p&gt;

&lt;p&gt;Store the raw citation URL, resolved URL, canonical URL, registrable domain, and canonicalization-rule version.&lt;/p&gt;

&lt;p&gt;For domain share, count one domain at most once per observation. For page-level analysis, preserve individual canonical pages.&lt;/p&gt;

&lt;p&gt;Never overwrite the raw URL. It is part of the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. A worked example
&lt;/h2&gt;

&lt;p&gt;Assume a small benchmark produces 12 scheduled observations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one times out;&lt;/li&gt;
&lt;li&gt;one returns a successful answer but exposes no citation surface;&lt;/li&gt;
&lt;li&gt;Acme is mentioned in five successful observations;&lt;/li&gt;
&lt;li&gt;Acme is cited in three citation-eligible observations;&lt;/li&gt;
&lt;li&gt;the comparison set produces 14 total entity-observation mentions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The denominators differ:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mention-eligible observations = 11
citation-eligible observations = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Acme’s presence share is five divided by 11, or 45.5%.&lt;/p&gt;

&lt;p&gt;Its citation presence is three divided by 10, or 30%.&lt;/p&gt;

&lt;p&gt;If Acme owns five of 14 competitive entity-observation mentions, its competitive mention share is 35.7%.&lt;/p&gt;

&lt;p&gt;These are three valid numbers answering three different questions. Reporting one as “AI share of voice” without its definition would be misleading.&lt;/p&gt;

&lt;p&gt;The example is illustrative; real programs need uncertainty, repeat behavior, and slice sizes shown alongside percentages.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Minimum event schema
&lt;/h2&gt;

&lt;p&gt;A practical long-form event table can contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;observation_id&lt;/li&gt;
&lt;li&gt;prompt_id&lt;/li&gt;
&lt;li&gt;prompt_version&lt;/li&gt;
&lt;li&gt;provider&lt;/li&gt;
&lt;li&gt;interface_version&lt;/li&gt;
&lt;li&gt;locale&lt;/li&gt;
&lt;li&gt;scheduled_window&lt;/li&gt;
&lt;li&gt;repeat_number&lt;/li&gt;
&lt;li&gt;collection_status&lt;/li&gt;
&lt;li&gt;failure_reason&lt;/li&gt;
&lt;li&gt;entity_id&lt;/li&gt;
&lt;li&gt;raw_entity_string&lt;/li&gt;
&lt;li&gt;mentioned&lt;/li&gt;
&lt;li&gt;cited&lt;/li&gt;
&lt;li&gt;recommended&lt;/li&gt;
&lt;li&gt;evidence_span&lt;/li&gt;
&lt;li&gt;raw_citation_url&lt;/li&gt;
&lt;li&gt;canonical_citation_url&lt;/li&gt;
&lt;li&gt;classification_rule_version&lt;/li&gt;
&lt;li&gt;review_status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A separate observation table should exist even when no entity event is found. Otherwise, observations with zero matches disappear and the denominator becomes impossible to reconstruct.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Quality checks before publishing the metric
&lt;/h2&gt;

&lt;p&gt;Run at least these checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every event points to a valid observation;&lt;/li&gt;
&lt;li&gt;failed observations are not classified as absent mentions;&lt;/li&gt;
&lt;li&gt;one entity has at most one binary event per observation per event type;&lt;/li&gt;
&lt;li&gt;aliases map to one canonical entity;&lt;/li&gt;
&lt;li&gt;ambiguous aliases are reviewable;&lt;/li&gt;
&lt;li&gt;raw and canonical citation URLs are both preserved;&lt;/li&gt;
&lt;li&gt;prompt weights sum as expected;&lt;/li&gt;
&lt;li&gt;repeated cells follow the declared aggregation policy;&lt;/li&gt;
&lt;li&gt;provider and locale sample sizes are visible;&lt;/li&gt;
&lt;li&gt;prompt-set or classification changes create a new version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also sample raw answers manually. A metric can pass database checks while the underlying classification rule is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Report the metric as a contract
&lt;/h2&gt;

&lt;p&gt;Every chart should state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the numerator;&lt;/li&gt;
&lt;li&gt;the denominator;&lt;/li&gt;
&lt;li&gt;the eligibility rule;&lt;/li&gt;
&lt;li&gt;the deduplication rule;&lt;/li&gt;
&lt;li&gt;the prompt and provider scope;&lt;/li&gt;
&lt;li&gt;the weighting policy;&lt;/li&gt;
&lt;li&gt;the time window;&lt;/li&gt;
&lt;li&gt;the relevant versions;&lt;/li&gt;
&lt;li&gt;the number of failures and unresolved classifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contract makes trend changes debuggable. It also allows another analyst to reproduce the result from the observation ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final principle
&lt;/h2&gt;

&lt;p&gt;AI-answer share of voice is not a count of brand words. It is an aggregation over documented observations, resolved entities, eligible denominators, and versioned rules.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;Corank&lt;/a&gt;, we are building around this audit-first view of AI visibility: the evidence and measurement contract should be inspectable, not hidden behind a single unexplained score.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Corank is our product. This article is an educational measurement framework, not a claim that one formula fits every provider, market, or decision.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to Build an Entity Consistency Ledger for AI Search</title>
      <dc:creator>Waleed Arshad</dc:creator>
      <pubDate>Mon, 03 Aug 2026 11:26:51 +0000</pubDate>
      <link>https://dev.to/waleed_arshad_ada1184b196/how-to-build-an-entity-consistency-ledger-for-ai-search-49m2</link>
      <guid>https://dev.to/waleed_arshad_ada1184b196/how-to-build-an-entity-consistency-ledger-for-ai-search-49m2</guid>
      <description>&lt;p&gt;AI-search measurement can fail before a prompt is ever sampled.&lt;/p&gt;

&lt;p&gt;If a company appears online under several names, domains, categories, addresses, product labels, or descriptions, an answer engine may treat those records as different entities. A monitoring system can make the same mistake. The result is a mix of false negatives, missed citations, duplicate counts, and explanations nobody can reproduce.&lt;/p&gt;

&lt;p&gt;An entity consistency ledger is a practical control for this problem. It is not a giant knowledge graph. It is a versioned record of the public facts that identify a company, product, or person, plus the evidence showing where each fact was observed.&lt;/p&gt;

&lt;p&gt;This guide describes a schema, collection workflow, conflict policy, and QA process for building one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the ledger should answer
&lt;/h2&gt;

&lt;p&gt;For every monitored entity, an analyst should be able to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the canonical name?&lt;/li&gt;
&lt;li&gt;Which aliases are legitimate?&lt;/li&gt;
&lt;li&gt;Which domain and URLs are authoritative?&lt;/li&gt;
&lt;li&gt;Which products belong to the entity?&lt;/li&gt;
&lt;li&gt;Which categories are claimed publicly?&lt;/li&gt;
&lt;li&gt;Which locations and contact details are current?&lt;/li&gt;
&lt;li&gt;Where was each value observed?&lt;/li&gt;
&lt;li&gt;When was it last verified?&lt;/li&gt;
&lt;li&gt;Is a difference an acceptable variant or a real conflict?&lt;/li&gt;
&lt;li&gt;Who owns the correction?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the system cannot answer those questions, entity matching becomes guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve observations before normalizing them
&lt;/h2&gt;

&lt;p&gt;The most important design rule is to keep raw observations separate from canonical values.&lt;/p&gt;

&lt;p&gt;Suppose four profiles contain these names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Corank&lt;/li&gt;
&lt;li&gt;Corank AI&lt;/li&gt;
&lt;li&gt;CoreRank.ai&lt;/li&gt;
&lt;li&gt;Remap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A destructive cleanup that rewrites every value to "Corank" hides useful history. Some values may be valid aliases, one may be an old brand, and one may be an error.&lt;/p&gt;

&lt;p&gt;Store both layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observed value&lt;/strong&gt; — the exact text found on the source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical interpretation&lt;/strong&gt; — the normalized value chosen under an explicit rule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you an audit trail and makes future corrections possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal data model
&lt;/h2&gt;

&lt;p&gt;A compact implementation can use four tables or collections.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Entities
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;entity_id&lt;/td&gt;
&lt;td&gt;Stable internal identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entity_type&lt;/td&gt;
&lt;td&gt;Organization, product, person, location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canonical_name&lt;/td&gt;
&lt;td&gt;Approved display name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canonical_domain&lt;/td&gt;
&lt;td&gt;Primary domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;td&gt;Active, legacy, acquired, retired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;owner&lt;/td&gt;
&lt;td&gt;Person or team responsible for accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valid_from&lt;/td&gt;
&lt;td&gt;Start of this entity version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valid_to&lt;/td&gt;
&lt;td&gt;End of this entity version, if closed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Aliases
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;entity_id&lt;/td&gt;
&lt;td&gt;Parent entity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alias&lt;/td&gt;
&lt;td&gt;Observed or approved alternate name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alias_type&lt;/td&gt;
&lt;td&gt;Brand, legal, product, abbreviation, legacy, misspelling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;match_policy&lt;/td&gt;
&lt;td&gt;Exact, case-insensitive, token, manual-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valid_from&lt;/td&gt;
&lt;td&gt;When the alias became valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valid_to&lt;/td&gt;
&lt;td&gt;When it stopped being valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;evidence_url&lt;/td&gt;
&lt;td&gt;Source supporting the alias&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Claims
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;claim_id&lt;/td&gt;
&lt;td&gt;Stable claim identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entity_id&lt;/td&gt;
&lt;td&gt;Entity described&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;field&lt;/td&gt;
&lt;td&gt;Name, category, address, phone, pricing, capability, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canonical_value&lt;/td&gt;
&lt;td&gt;Approved interpretation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;confidence&lt;/td&gt;
&lt;td&gt;Confirmed, probable, disputed, unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;effective_date&lt;/td&gt;
&lt;td&gt;Date the claim became true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expiry_date&lt;/td&gt;
&lt;td&gt;Optional review or sunset date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;owner&lt;/td&gt;
&lt;td&gt;Team responsible for the claim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4. Observations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;observation_id&lt;/td&gt;
&lt;td&gt;Stable record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claim_id&lt;/td&gt;
&lt;td&gt;Claim being evaluated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source_url_raw&lt;/td&gt;
&lt;td&gt;Exact retrieved URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source_url_canonical&lt;/td&gt;
&lt;td&gt;Normalized URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;observed_value&lt;/td&gt;
&lt;td&gt;Exact public value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retrieved_at&lt;/td&gt;
&lt;td&gt;Collection timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source_type&lt;/td&gt;
&lt;td&gt;Website, directory, social, review, repository, press&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;classification&lt;/td&gt;
&lt;td&gt;Match, acceptable variant, stale, conflicting, unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;evidence&lt;/td&gt;
&lt;td&gt;Snapshot, response excerpt, or hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reviewer&lt;/td&gt;
&lt;td&gt;Human or rule that classified it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A relational database is helpful but not required. A spreadsheet can work for a small inventory if identifiers, evidence URLs, and dates are treated as real fields rather than comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example record
&lt;/h2&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;"entity_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org_corank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonical_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonical_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corank.ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alias"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remap"&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;"legacy_brand"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"valid_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&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;"observation"&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;"source_url_raw"&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://example.com/company/remap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_url_canonical"&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://example.com/company/remap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"observed_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T10:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stale"&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;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brand-operations"&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 record does not pretend the old name was never used. It explains why the value is now stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: inventory identity-bearing claims
&lt;/h2&gt;

&lt;p&gt;Start with facts that help people and systems decide whether two records describe the same entity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canonical name&lt;/li&gt;
&lt;li&gt;legal name&lt;/li&gt;
&lt;li&gt;former names&lt;/li&gt;
&lt;li&gt;domain&lt;/li&gt;
&lt;li&gt;product names&lt;/li&gt;
&lt;li&gt;logo&lt;/li&gt;
&lt;li&gt;primary category&lt;/li&gt;
&lt;li&gt;short description&lt;/li&gt;
&lt;li&gt;headquarters and service locations&lt;/li&gt;
&lt;li&gt;phone and public email&lt;/li&gt;
&lt;li&gt;founders or key people&lt;/li&gt;
&lt;li&gt;social profiles&lt;/li&gt;
&lt;li&gt;repository and documentation URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not begin with every marketing statement. Identity-bearing facts have the highest matching value and are usually easier to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: declare canonical values and sources
&lt;/h2&gt;

&lt;p&gt;A canonical value needs an owner and a source of truth.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product capabilities may be owned by product documentation&lt;/li&gt;
&lt;li&gt;address and phone may be owned by operations&lt;/li&gt;
&lt;li&gt;legal name may be owned by corporate records&lt;/li&gt;
&lt;li&gt;category and positioning may be owned by marketing&lt;/li&gt;
&lt;li&gt;repository names may be owned by engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source of truth is not necessarily the highest-authority third-party page. It is the record authorized to define the value. Independent sources then corroborate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: collect public observations
&lt;/h2&gt;

&lt;p&gt;Collect from legitimate surfaces where entity facts are likely to appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the website and structured data&lt;/li&gt;
&lt;li&gt;documentation and changelogs&lt;/li&gt;
&lt;li&gt;social profiles&lt;/li&gt;
&lt;li&gt;business and software directories&lt;/li&gt;
&lt;li&gt;review platforms&lt;/li&gt;
&lt;li&gt;public repositories&lt;/li&gt;
&lt;li&gt;app marketplaces&lt;/li&gt;
&lt;li&gt;press coverage&lt;/li&gt;
&lt;li&gt;association listings&lt;/li&gt;
&lt;li&gt;partner pages&lt;/li&gt;
&lt;li&gt;conference and podcast pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Respect access controls and platform rules. The purpose is to audit public facts, not to bypass sign-in or scrape private data.&lt;/p&gt;

&lt;p&gt;Record the exact retrieved URL, not only the domain. Preserve redirects and tracking parameters before canonicalization so transformations remain inspectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: normalize conservatively
&lt;/h2&gt;

&lt;p&gt;Normalization should remove technical noise without erasing semantic differences.&lt;/p&gt;

&lt;p&gt;Usually safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lowercasing domains&lt;/li&gt;
&lt;li&gt;removing default ports&lt;/li&gt;
&lt;li&gt;normalizing protocol where redirects prove equivalence&lt;/li&gt;
&lt;li&gt;sorting or removing known tracking parameters&lt;/li&gt;
&lt;li&gt;resolving canonical URLs&lt;/li&gt;
&lt;li&gt;standardizing whitespace&lt;/li&gt;
&lt;li&gt;case-insensitive comparison for approved fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Potentially unsafe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collapsing two product names because they share tokens&lt;/li&gt;
&lt;li&gt;treating a former brand as a current alias forever&lt;/li&gt;
&lt;li&gt;merging locations with similar street names&lt;/li&gt;
&lt;li&gt;assuming two social handles belong to the same company&lt;/li&gt;
&lt;li&gt;replacing a legal name with a marketing name&lt;/li&gt;
&lt;li&gt;stripping locale or path segments that change meaning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every nontrivial transformation should be named and testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: classify differences
&lt;/h2&gt;

&lt;p&gt;Not every mismatch is an error. Use explicit classes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Match
&lt;/h3&gt;

&lt;p&gt;The observed value is equivalent to the canonical value under an approved rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acceptable variant
&lt;/h3&gt;

&lt;p&gt;The value is legitimate in context, such as an abbreviation or localized form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale
&lt;/h3&gt;

&lt;p&gt;The value was once valid but should now be updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conflicting
&lt;/h3&gt;

&lt;p&gt;The source asserts a different current fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unknown
&lt;/h3&gt;

&lt;p&gt;The system lacks enough evidence to decide.&lt;/p&gt;

&lt;p&gt;This classification prevents teams from wasting time "fixing" valid variants while missing true contradictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: prioritize by identity risk
&lt;/h2&gt;

&lt;p&gt;Fixing every mismatch at once is rarely practical. Score conflicts by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reach&lt;/strong&gt; — how visible is the source?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relevance&lt;/strong&gt; — does it influence important prompts or audiences?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity weight&lt;/strong&gt; — name, domain, and product ownership matter more than punctuation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recency&lt;/strong&gt; — is the record still active?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradiction severity&lt;/strong&gt; — could the value cause a wrong recommendation or lost attribution?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correction control&lt;/strong&gt; — can the team edit it directly, request an update, or only monitor it?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A stale phone number on a prominent profile can be more urgent than a minor description difference across ten low-use pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: connect the ledger to AI-search measurement
&lt;/h2&gt;

&lt;p&gt;An entity ledger should influence both collection and reporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  During detection
&lt;/h3&gt;

&lt;p&gt;Use approved aliases and validity windows when identifying mentions. A legacy brand should not count as a current positive mention unless the analysis explicitly asks for historical visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  During citation normalization
&lt;/h3&gt;

&lt;p&gt;Attach cited URLs to the entity and claim they support. Preserve raw URLs alongside canonical records.&lt;/p&gt;

&lt;h3&gt;
  
  
  During scoring
&lt;/h3&gt;

&lt;p&gt;Report uncertain matches separately. Do not silently convert low-confidence entity matches into visibility wins or losses.&lt;/p&gt;

&lt;h3&gt;
  
  
  During incident review
&lt;/h3&gt;

&lt;p&gt;When a mention disappears, check whether the brand actually lost visibility or whether an alias, URL, or category rule changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  During remediation
&lt;/h3&gt;

&lt;p&gt;Create tasks against the conflicting source, canonical owner, and verification test. A recommendation without an owner or recheck condition is not operational.&lt;/p&gt;

&lt;h2&gt;
  
  
  QA rules worth automating
&lt;/h2&gt;

&lt;p&gt;A useful first ruleset can flag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one active entity with multiple canonical domains&lt;/li&gt;
&lt;li&gt;one domain assigned to multiple active entities&lt;/li&gt;
&lt;li&gt;aliases with overlapping validity windows and conflicting meanings&lt;/li&gt;
&lt;li&gt;public profiles using a legacy name after its validity window&lt;/li&gt;
&lt;li&gt;different current phone or address values&lt;/li&gt;
&lt;li&gt;product pages that attribute a product to the wrong parent&lt;/li&gt;
&lt;li&gt;canonical URLs that redirect to a different entity&lt;/li&gt;
&lt;li&gt;observations without timestamps&lt;/li&gt;
&lt;li&gt;claims without owners&lt;/li&gt;
&lt;li&gt;normalization transformations without a rule identifier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks should create review queues, not automatically overwrite evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure ledger quality
&lt;/h2&gt;

&lt;p&gt;Track operational metrics rather than a vague "consistency score":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;percentage of identity claims with owners&lt;/li&gt;
&lt;li&gt;percentage with authoritative evidence&lt;/li&gt;
&lt;li&gt;unresolved conflicts by severity&lt;/li&gt;
&lt;li&gt;stale observations past their review date&lt;/li&gt;
&lt;li&gt;sources with failed retrieval&lt;/li&gt;
&lt;li&gt;uncertain entity matches in AI-answer samples&lt;/li&gt;
&lt;li&gt;time from conflict discovery to verified correction&lt;/li&gt;
&lt;li&gt;regression rate after correction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single percentage can hide the exact conflict that causes attribution failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Copying one profile everywhere
&lt;/h3&gt;

&lt;p&gt;Consistency is not the same as duplication. Each platform should have content appropriate to its audience while core identity facts remain aligned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treating all backlinks as identity evidence
&lt;/h3&gt;

&lt;p&gt;A link can exist without supporting any claim. Evaluate the surrounding page, not only the href.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overwriting legacy history
&lt;/h3&gt;

&lt;p&gt;Former names and domains are useful when bounded by dates. Deleting them can make historical citations impossible to interpret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trusting structured data alone
&lt;/h3&gt;

&lt;p&gt;Schema helps machines parse information, but visible page content and public profiles still need to agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using normalization to hide conflicts
&lt;/h3&gt;

&lt;p&gt;If two values differ in meaning, a fuzzy-match threshold should not force them together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring failed collection as inconsistency
&lt;/h3&gt;

&lt;p&gt;A timeout is not a conflicting fact. Collection health must be evaluated before entity quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rollout
&lt;/h2&gt;

&lt;p&gt;Week 1: define the entity, claim, alias, and observation schemas.&lt;/p&gt;

&lt;p&gt;Week 2: inventory high-impact identity claims and assign owners.&lt;/p&gt;

&lt;p&gt;Week 3: collect the website, documentation, social, directory, and repository surfaces.&lt;/p&gt;

&lt;p&gt;Week 4: classify conflicts and correct the highest-risk records.&lt;/p&gt;

&lt;p&gt;Then version the ledger, rerun the checks, and monitor new observations continuously.&lt;/p&gt;

&lt;p&gt;The system becomes valuable when another analyst can reproduce why two records were merged, why a value was marked stale, and how a correction was verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing principle
&lt;/h2&gt;

&lt;p&gt;Entity consistency is not cosmetic brand hygiene. It is part of the measurement layer.&lt;/p&gt;

&lt;p&gt;If the identity model is wrong, citation attribution, competitor comparison, and visibility scoring can all be wrong in ways that look statistically convincing.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on Corank, where we are building auditable AI-search measurement and hands-on remediation. You can see the approach at &lt;a href="https://corank.ai" rel="noopener noreferrer"&gt;https://corank.ai&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal is not to make every page say the same thing. It is to make every important claim traceable to a clear entity, a responsible owner, and verifiable public evidence.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
