<?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: VisibilityAtlas</title>
    <description>The latest articles on DEV Community by VisibilityAtlas (@visibilityatlas).</description>
    <link>https://dev.to/visibilityatlas</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%2F4053258%2F0fb668b2-462c-4218-9a72-3dec6f1caa0e.png</url>
      <title>DEV Community: VisibilityAtlas</title>
      <link>https://dev.to/visibilityatlas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/visibilityatlas"/>
    <language>en</language>
    <item>
      <title>Found Is Not Chosen: What a 12-Answer China AI Test Taught Me About Luxury Jewelry Visibility</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Sat, 15 Aug 2026 03:44:36 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/found-is-not-chosen-what-a-12-answer-china-ai-test-taught-me-about-luxury-jewelry-visibility-3be8</link>
      <guid>https://dev.to/visibilityatlas/found-is-not-chosen-what-a-12-answer-china-ai-test-taught-me-about-luxury-jewelry-visibility-3be8</guid>
      <description>&lt;p&gt;I started this test with what I thought would be the boring part of a luxury-brand audit: can an AI answer point a buyer to the right official China channel?&lt;/p&gt;

&lt;p&gt;The more interesting result appeared when I compared that task with a buying decision.&lt;/p&gt;

&lt;p&gt;In a small exploratory pre-wave, Piaget appeared in all four answers about jewelry brands with verifiable official China channels. It appeared in none of the four answers recommending high-end brands for wedding jewelry.&lt;/p&gt;

&lt;p&gt;That does not make Piaget “invisible,” and it certainly does not establish a market ranking. Four answer opportunities per task are too small for either claim.&lt;/p&gt;

&lt;p&gt;It does expose a measurement problem that matters well beyond jewelry: finding an entity, recommending a brand and routing a buyer to a correct official channel are different jobs. If we report them as one visibility score, the number may be tidy while the diagnosis is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deliberately small test
&lt;/h2&gt;

&lt;p&gt;I fixed eight target brands before collection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cartier&lt;/li&gt;
&lt;li&gt;Tiffany &amp;amp; Co.&lt;/li&gt;
&lt;li&gt;Bvlgari&lt;/li&gt;
&lt;li&gt;Van Cleef &amp;amp; Arpels&lt;/li&gt;
&lt;li&gt;Chaumet&lt;/li&gt;
&lt;li&gt;Boucheron&lt;/li&gt;
&lt;li&gt;Piaget&lt;/li&gt;
&lt;li&gt;De Beers Jewellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used three neutral Chinese buyer questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which high-end brands are worth considering for wedding jewelry, and what should a buyer compare?&lt;/li&gt;
&lt;li&gt;What is the difference between a brand boutique and a daigou for a high-value jewelry purchase?&lt;/li&gt;
&lt;li&gt;Which international jewelry brands have verifiable official websites or boutique channels in mainland China?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The valid collection contained two retrieval-off API surfaces, DeepSeek and Doubao, with two answers per surface and question. That produced 12 raw answers.&lt;/p&gt;

&lt;p&gt;The important implementation choice was to collect each answer once. I did not repeat the same unbranded question eight times, once per target brand. After collection, each stored answer was evaluated against the frozen eight-brand registry, producing 96 answer-brand cells.&lt;/p&gt;

&lt;p&gt;The distinction matters because provider calls and scoring units are not the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12 provider answers
× 8 predeclared target brands
= 96 answer-brand evaluation cells
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reporting “96 AI answers” would overstate the collection by a factor of eight. Reporting only 12 rows without describing the derived brand cells would hide the scoring denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the answers did
&lt;/h2&gt;

&lt;p&gt;For the wedding-selection question, the eight brands received 21 positive shortlist recommendations across 32 eligible answer-brand opportunities.&lt;/p&gt;

&lt;p&gt;For the official-channel question, the models affirmatively listed a target brand in 25 of 32 opportunities.&lt;/p&gt;

&lt;p&gt;Those two rates are not a funnel. They come from different prompt intents. The useful comparison is at brand level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brand&lt;/th&gt;
&lt;th&gt;Wedding shortlist&lt;/th&gt;
&lt;th&gt;Official China channel listed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cartier&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiffany &amp;amp; Co.&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bvlgari&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Van Cleef &amp;amp; Arpels&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chaumet&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;2/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boucheron&lt;/td&gt;
&lt;td&gt;1/4&lt;/td&gt;
&lt;td&gt;2/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Piaget&lt;/td&gt;
&lt;td&gt;0/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;De Beers Jewellers&lt;/td&gt;
&lt;td&gt;1/4&lt;/td&gt;
&lt;td&gt;1/4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Piaget row is the clearest example of why entity recognition is not recommendation. The APIs could associate the brand with a current China-facing channel, but the brand did not enter their wedding shortlist in this tiny sample.&lt;/p&gt;

&lt;p&gt;Chaumet shows the other direction. It appeared in three of four wedding shortlists but only two of four official-channel answers. A brand can enter consideration while the route to verification remains less consistently reproduced.&lt;/p&gt;

&lt;p&gt;Neither pattern tells us why it happened. It tells us what to inspect next.&lt;/p&gt;

&lt;h2&gt;
  
  
  A buyer journey needs more than one metric
&lt;/h2&gt;

&lt;p&gt;For a high-value product, I would split the audit into at least three layers.&lt;/p&gt;

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

&lt;p&gt;Does the brand enter a shortlist for a real decision: wedding jewelry, an anniversary gift, a diamond purchase, a high-jewelry commission or a particular budget?&lt;/p&gt;

&lt;p&gt;This is where associations, product authority, cultural relevance and brand familiarity may matter. The current pre-wave does not identify which factor caused a recommendation.&lt;/p&gt;

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

&lt;p&gt;Can a buyer find a current route that the brand itself controls or authorizes?&lt;/p&gt;

&lt;p&gt;That may include a China website, boutique directory, official customer-service route, mini-program or disclosed marketplace store. A brand-global domain may be legitimate, but it is not always the most precise answer to a mainland-China channel question.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Purchase confidence
&lt;/h3&gt;

&lt;p&gt;Can the buyer verify what the channel is allowed to sell and what the maison will actually support after purchase?&lt;/p&gt;

&lt;p&gt;This is where generic AI language becomes risky. “Official authentication,” “global warranty,” “free lifetime resizing” and “seven-day returns” are not universal benefits. They can vary by product, market, purchase route, documentation and damage.&lt;/p&gt;

&lt;p&gt;A buyer does not need a plausible paragraph. They need the current policy that applies to their item.&lt;/p&gt;

&lt;h2&gt;
  
  
  The truth table was harder than the prompt panel
&lt;/h2&gt;

&lt;p&gt;Checking whether an answer printed a domain was easy. Deciding what the domain meant was not.&lt;/p&gt;

&lt;p&gt;For every asserted official route, I needed to keep separate fields for:&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;"brand_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;"stable entity identifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel_locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"domain, account, store or service route"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operator_entity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the entity actually operating it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ownership_or_authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verified, unresolved or contradicted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"live_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dated observation"&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_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mainland China, global or another market"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sales, boutique lookup, support or other"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"truth_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dated brand-controlled or authoritative 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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not use &lt;code&gt;.cn&lt;/code&gt; ownership, an ICP record or a company registration as interchangeable proof of officiality. They answer different questions. I also did not convert “no public evidence found” into “verified absent.”&lt;/p&gt;

&lt;p&gt;Across the four official-channel answers, there were 19 exact target-brand domain assertions. Fifteen matched the dated China-local route in the truth table. Three were brand-global alternatives. One route remained unresolved.&lt;/p&gt;

&lt;p&gt;That is why I would not publish a simple “channel accuracy rate” without the categories beside it. A global route can be less locally precise without being false. An unresolved route is not evidence of impersonation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalid calls are not negative brand evidence
&lt;/h2&gt;

&lt;p&gt;An ERNIE API surface was planned for the same panel, but all six requests returned an account-state &lt;code&gt;403&lt;/code&gt;. Those rows were retained as attempted measurements with an invalid reason. They did not enter the answer denominator.&lt;/p&gt;

&lt;p&gt;This sounds obvious until an automated report turns a missing response into eight brand absences.&lt;/p&gt;

&lt;p&gt;The minimum record I want for a failed call is still substantial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observation_id
question_id and version
platform and surface
model/configuration
retrieval status
attempt timestamp
instrument versions
error class and raw provider status
validity = invalid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error tells me something about the collection instrument. It tells me nothing about whether the absent answer would have mentioned Cartier or Piaget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a brand team can do with the result
&lt;/h2&gt;

&lt;p&gt;The first action is not “publish more GEO content.” It is to identify which layer is failing.&lt;/p&gt;

&lt;p&gt;If the brand is missing from consideration prompts but its channel is correctly reproduced, audit the evidence around the buying decision: occasion, design language, category authority, local editorial presence and comparison contexts.&lt;/p&gt;

&lt;p&gt;If the brand enters shortlists but channel answers are weak, give digital operations and legal a concrete route inventory to maintain. The official website, store locator, customer-service route, marketplace disclosures and operating entity should not contradict one another.&lt;/p&gt;

&lt;p&gt;If the route is correct but service claims are wrong, the problem belongs partly to customer service and policy publishing. Put current, scoped answers where buyers and reviewers can verify them. Do not promise that publishing the page will change an AI response; retest the same prompt panel.&lt;/p&gt;

&lt;p&gt;This gives the organization a practical ownership map:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Likely owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing from occasion shortlist&lt;/td&gt;
&lt;td&gt;Brand, editorial, PR, category marketing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong or vague official route&lt;/td&gt;
&lt;td&gt;Digital operations, e-commerce, legal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incorrect authorization claim&lt;/td&gt;
&lt;td&gt;Legal, retail operations, channel management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incorrect warranty or service claim&lt;/td&gt;
&lt;td&gt;Customer service, legal, after-sales&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measurement cannot reproduce&lt;/td&gt;
&lt;td&gt;Research, data and analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“Improve AI visibility” is too broad to assign. These failures are specific enough for someone to own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this test does not establish
&lt;/h2&gt;

&lt;p&gt;The APIs ran without live retrieval. This study cannot tell us which sources influenced an answer, whether a displayed domain was retrieved, or what a signed-in consumer sees in the current product interface. A target-supported Citation Rate is not observable here.&lt;/p&gt;

&lt;p&gt;It is also a 12-answer exploratory pre-wave, not the first longitudinal benchmark. It cannot establish a stable brand position, a platform winner, a trend or a causal link between a page and a recommendation.&lt;/p&gt;

&lt;p&gt;The next formal wave needs declared and repeatable consumer or search surfaces, a larger fixed buyer-question panel, a frozen truth table and the same metric membership rules in every period. Qwen, Kimi and ERNIE should be included only after their exact surfaces and account states pass preflight; adding a platform halfway through would create an apparent trend by changing the instrument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful question changed
&lt;/h2&gt;

&lt;p&gt;Before the test, I would have asked whether each jewelry house was visible.&lt;/p&gt;

&lt;p&gt;Now I would ask three narrower questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the brand enter the buyer’s shortlist?&lt;/li&gt;
&lt;li&gt;Can the buyer verify a current official China route?&lt;/li&gt;
&lt;li&gt;Are the purchase and service claims attached to that route accurate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers lead to different work. That is the value of the small pre-wave: not a league table, but a better diagnosis to carry into the larger benchmark.&lt;/p&gt;

&lt;p&gt;The complete method, evidence boundaries and brand-level table are available in the canonical study on Visibility Atlas.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>marketing</category>
      <category>deepseek</category>
    </item>
    <item>
      <title>One AI Answer, Eight Brands: Designing a Benchmark Without Multiplying the Evidence</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:30:05 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/one-ai-answer-eight-brands-designing-a-benchmark-without-multiplying-the-evidence-1o45</link>
      <guid>https://dev.to/visibilityatlas/one-ai-answer-eight-brands-designing-a-benchmark-without-multiplying-the-evidence-1o45</guid>
      <description>&lt;p&gt;I recently ran a small China AI benchmark for eight luxury-jewelry brands.&lt;/p&gt;

&lt;p&gt;The most interesting result was not a platform ranking.&lt;/p&gt;

&lt;p&gt;It was a disagreement between two kinds of visibility.&lt;/p&gt;

&lt;p&gt;Piaget appeared in all four answers about brands with verifiable official China channels. It appeared in none of the four answers recommending brands for wedding jewelry.&lt;/p&gt;

&lt;p&gt;That is a tiny sample, so it is not evidence that Piaget has a stable visibility problem.&lt;/p&gt;

&lt;p&gt;It is evidence that I needed more than one metric.&lt;/p&gt;

&lt;p&gt;It also forced me to separate two units that are often quietly conflated in AI visibility datasets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the answer collected from the model;&lt;/li&gt;
&lt;li&gt;the brand-level judgment derived from that answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That distinction changed the collector, the denominator and what I was willing to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tempting but wrong collection design
&lt;/h2&gt;

&lt;p&gt;The cohort contained eight brands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cartier&lt;/li&gt;
&lt;li&gt;Tiffany &amp;amp; Co.&lt;/li&gt;
&lt;li&gt;Bvlgari&lt;/li&gt;
&lt;li&gt;Van Cleef &amp;amp; Arpels&lt;/li&gt;
&lt;li&gt;Chaumet&lt;/li&gt;
&lt;li&gt;Boucheron&lt;/li&gt;
&lt;li&gt;Piaget&lt;/li&gt;
&lt;li&gt;De Beers Jewellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had three neutral buyer questions, two API surfaces and two replicates.&lt;/p&gt;

&lt;p&gt;A naive brand-expanded collector could have produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8 brands
× 3 questions
× 2 surfaces
× 2 replicates
= 96 provider calls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would look like a larger dataset.&lt;/p&gt;

&lt;p&gt;It would also change the experiment.&lt;/p&gt;

&lt;p&gt;If I asked the same neutral question eight times—once for every target brand—I would collect eight different generated answers and then compare brands across different evidence.&lt;/p&gt;

&lt;p&gt;Randomness, provider conditions and answer variation would be mixed with brand treatment.&lt;/p&gt;

&lt;p&gt;The larger row count would not mean I had more independent evidence about the same answer. It would mean I had asked for more answers.&lt;/p&gt;

&lt;p&gt;So I used an answer-once design.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 questions
× 2 surfaces
× 2 replicates
= 12 valid raw answers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each raw answer was then evaluated against the eight brands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12 answers
× 8 brands
= 96 answer-brand cells
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataset still contains 96 brand-level observations, but it does not pretend that 96 model responses were collected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collection unit and analysis unit
&lt;/h2&gt;

&lt;p&gt;The collection unit was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform_surface_id
× question_id
× replicate_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The derived analysis unit was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;answer_id
× target_brand_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified record relationship looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer
├── answer_id
├── platform_surface_id
├── question_id
├── replicate_id
├── raw_answer
├── validity
└── AnswerBrandCell[8]
    ├── target_brand_id
    ├── mentioned
    ├── recommended
    ├── official_channel_asserted
    └── human_review_state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because Mention, Recommendation and channel accuracy are not properties of the API request alone.&lt;/p&gt;

&lt;p&gt;They are judgments about a particular brand inside a particular answer.&lt;/p&gt;

&lt;p&gt;The raw answer should remain immutable. Brand-level labels can be reviewed, corrected and replayed without recollecting the model output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalid responses are attempts, not negative answers
&lt;/h2&gt;

&lt;p&gt;A third API surface was planned for the pre-wave.&lt;/p&gt;

&lt;p&gt;All six requests returned an account-state &lt;code&gt;403&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those rows belong in the collection record because the attempts happened under a declared instrument. They do not belong in any answer-content denominator.&lt;/p&gt;

&lt;p&gt;The correct accounting was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;planned answer cells = 18
valid answers        = 12
invalid by reason    = 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;valid answers        = 12
brand absent         = 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An error row proves that a collection attempt occurred. It contains no evidence about whether Cartier, Piaget or any other brand was mentioned or recommended.&lt;/p&gt;

&lt;p&gt;I also retained two earlier Doubao timeouts from an instrument-configuration run. They remain in the audit trail but sit outside the final planned set because the collector had not carried forward the reasoning-mode configuration used during access testing.&lt;/p&gt;

&lt;p&gt;Keeping failures does not require pretending every failure belongs to the published denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  One pooled visibility score would have hidden the result
&lt;/h2&gt;

&lt;p&gt;The three questions represented different buyer decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which high-end brands are worth considering for wedding jewelry?&lt;/li&gt;
&lt;li&gt;How should a buyer compare a brand boutique with a daigou purchase?&lt;/li&gt;
&lt;li&gt;Which international jewelry brands have verifiable official China channels?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The aggregate results were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Numerator&lt;/th&gt;
&lt;th&gt;Denominator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wedding shortlist recommendation&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target-brand mention in the daigou-risk answer&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official-channel assertion&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact current China-local route match&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those denominators are not interchangeable.&lt;/p&gt;

&lt;p&gt;The recommendation denominator contains the eight brand cells from the four wedding answers.&lt;/p&gt;

&lt;p&gt;The risk-question denominator contains the eight brand cells from a different four answers.&lt;/p&gt;

&lt;p&gt;The route-match denominator contains only exact domain assertions that were eligible for truth review.&lt;/p&gt;

&lt;p&gt;Combining them into one “AI visibility score” would make the output simpler and the diagnosis worse.&lt;/p&gt;

&lt;p&gt;The Piaget row demonstrates why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brand&lt;/th&gt;
&lt;th&gt;Wedding recommendation&lt;/th&gt;
&lt;th&gt;Official-channel appearance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Piaget&lt;/td&gt;
&lt;td&gt;0/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The brand was recognized when the task was channel verification. It was absent when the task was open-category recommendation.&lt;/p&gt;

&lt;p&gt;A domain or entity project might improve verification while doing nothing to answer why the brand did not enter the shortlist.&lt;/p&gt;

&lt;p&gt;The inverse problem also appeared:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brand&lt;/th&gt;
&lt;th&gt;Wedding recommendation&lt;/th&gt;
&lt;th&gt;Official-channel appearance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chaumet&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;2/4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A brand can enter consideration while its verification route is reproduced less consistently.&lt;/p&gt;

&lt;p&gt;These are different business problems, with different owners and different retest questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store membership, not only the percentage
&lt;/h2&gt;

&lt;p&gt;A count can be reproducible while the population is wrong.&lt;/p&gt;

&lt;p&gt;Two filters can both return 32 rows and still return different sets of 32 rows.&lt;/p&gt;

&lt;p&gt;For every published metric, I therefore stored the exact denominator and numerator memberships.&lt;/p&gt;

&lt;p&gt;A simplified manifest looks 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;"metric_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;"wedding_recommendation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer_brand_cell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eligible_predicate"&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;"question_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;"JW-CORE-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;"answer_validity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VALID"&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;"denominator"&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;"member_ids"&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="s2"&gt;"answer-001::cartier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"answer-001::tiffany"&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;"sha256"&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="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;"numerator"&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;"predicate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"human_recommendation_label == POSITIVE_SHORTLIST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"member_ids"&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="s2"&gt;"answer-001::cartier"&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;"sha256"&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="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;"excluded_by_reason"&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;"invalid_answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"not_eligible_question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real manifest contains the full member sets rather than the abbreviated example above.&lt;/p&gt;

&lt;p&gt;A separate verifier rebuilds those sets from the stored row states and checks invariants such as:&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="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSubset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numeratorIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;denominatorIds&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plannedCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;validCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;invalidByReasonCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorRowIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;denominatorIds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;size&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="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rebuiltDenominatorIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;storedDenominatorHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rebuiltNumeratorIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;storedNumeratorHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important guarantee is not merely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;21 / 32 can be recalculated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the same 21 cells and the same 32 cells can be reconstructed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That catches two implementations producing the same percentage from different observations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendation required human context
&lt;/h2&gt;

&lt;p&gt;A string match was sufficient for a first-pass entity mention.&lt;/p&gt;

&lt;p&gt;It was not sufficient for Recommendation.&lt;/p&gt;

&lt;p&gt;A brand name can appear because the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recommends it;&lt;/li&gt;
&lt;li&gt;rejects it;&lt;/li&gt;
&lt;li&gt;compares it;&lt;/li&gt;
&lt;li&gt;repeats the prompt;&lt;/li&gt;
&lt;li&gt;cites a warning;&lt;/li&gt;
&lt;li&gt;says it is unsuitable;&lt;/li&gt;
&lt;li&gt;lists it as an example without putting it on the shortlist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every counted recommendation was therefore reviewed in the full answer and assigned to the target brand cell.&lt;/p&gt;

&lt;p&gt;The rule was deliberately narrower than positive sentiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Count the brand only when the answer explicitly recommends it
or places it on a positive shortlist for the buyer decision.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparison, exclusion and incidental mention did not qualify.&lt;/p&gt;

&lt;p&gt;This is slower than a regex. For a 96-cell exploratory study, it was still practical and materially safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval off means Citation is not observable
&lt;/h2&gt;

&lt;p&gt;The two valid surfaces were retrieval-off APIs.&lt;/p&gt;

&lt;p&gt;The answers can be studied as dated model outputs. They cannot establish which sources influenced the result.&lt;/p&gt;

&lt;p&gt;Even if an answer prints a domain, that does not prove that the model retrieved or used the page during the request.&lt;/p&gt;

&lt;p&gt;So the study reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Supported target Citation Rate: NOT_OBSERVABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not convert missing citation evidence into zero.&lt;/p&gt;

&lt;p&gt;That distinction is important for longitudinal work. If a later wave uses a consumer search surface with observable retrieval and attributable citations, it should become a separate series rather than silently extending the retrieval-off API series.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical design changed the business question
&lt;/h2&gt;

&lt;p&gt;The implementation lesson is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collect each neutral answer once;&lt;/li&gt;
&lt;li&gt;preserve the raw response;&lt;/li&gt;
&lt;li&gt;derive brand-level cells separately;&lt;/li&gt;
&lt;li&gt;retain invalid attempts by reason;&lt;/li&gt;
&lt;li&gt;freeze eligible populations by metric;&lt;/li&gt;
&lt;li&gt;store exact numerator and denominator memberships;&lt;/li&gt;
&lt;li&gt;make unsupported measurements &lt;code&gt;NOT_OBSERVABLE&lt;/code&gt;, not zero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more interesting consequence is that a visibility audit should not end with one score.&lt;/p&gt;

&lt;p&gt;For a luxury brand, I now want at least these separate layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Entity recognition
→ Open-category consideration
→ Positive recommendation
→ Official-channel accuracy
→ Supported source, when observable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A brand can fail at any transition.&lt;/p&gt;

&lt;p&gt;Knowing which transition failed is more useful than knowing that an overall score moved from 42 to 47.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the next wave needs
&lt;/h2&gt;

&lt;p&gt;This pre-wave contains only four answer opportunities per brand for each task. It is enough to find a measurement problem, not enough to establish a stable brand position.&lt;/p&gt;

&lt;p&gt;A formal next wave would need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed consumer and API surface series;&lt;/li&gt;
&lt;li&gt;a larger but stable buyer-question panel;&lt;/li&gt;
&lt;li&gt;unchanged brand and alias registries;&lt;/li&gt;
&lt;li&gt;a dated channel and policy truth table;&lt;/li&gt;
&lt;li&gt;repeated comparable cells;&lt;/li&gt;
&lt;li&gt;the same metric membership contract;&lt;/li&gt;
&lt;li&gt;separate reporting by buyer decision;&lt;/li&gt;
&lt;li&gt;no platform winner unless each platform cell is complete and comparable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would deepen the buyer journey before adding dozens of brands.&lt;/p&gt;

&lt;p&gt;For jewelry, that means questions around wedding purchases, anniversary gifts, diamond selection, high-jewelry commissions, boutique-versus-daigou risk and mainland after-sales confidence.&lt;/p&gt;

&lt;p&gt;The goal is not to manufacture a larger dataset.&lt;/p&gt;

&lt;p&gt;It is to find out where a brand moves from being known, to being considered, to being recommended, to being correctly verified.&lt;/p&gt;

&lt;p&gt;In this small pre-wave, Piaget made the final distinction visible: the models could place the brand on the official China channel map, but they did not place it on the wedding shortlist.&lt;/p&gt;

&lt;p&gt;That is not a conclusion about the brand.&lt;/p&gt;

&lt;p&gt;It is a much better next question than “What is its AI visibility score?”&lt;/p&gt;

</description>
      <category>ai</category>
      <category>data</category>
      <category>testing</category>
      <category>seo</category>
    </item>
    <item>
      <title>I Added has_china_official_site to My Benchmark. It Failed Before the First Run</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:42:55 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/i-added-haschinaofficialsite-to-my-benchmark-it-failed-before-the-first-run-2cg</link>
      <guid>https://dev.to/visibilityatlas/i-added-haschinaofficialsite-to-my-benchmark-it-failed-before-the-first-run-2cg</guid>
      <description>&lt;p&gt;I was designing the next wave of an AI visibility benchmark when I added what looked like a harmless field:&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="nx"&gt;has_china_official_site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lasted about ten minutes.&lt;/p&gt;

&lt;p&gt;The problem was not TypeScript. I could not explain what &lt;code&gt;true&lt;/code&gt; meant without contradicting my own data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit that created the field
&lt;/h2&gt;

&lt;p&gt;The benchmark started with eight international B2B work-management brands.&lt;/p&gt;

&lt;p&gt;On August 7, I reviewed the public registration records and live status of their exact-match &lt;code&gt;.cn&lt;/code&gt; domains.&lt;/p&gt;

&lt;p&gt;The records identified the benchmark brand as the registrant of one of the eight domains. The other seven were registered to other parties.&lt;/p&gt;

&lt;p&gt;Across the eight domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three returned empty 502 responses.&lt;/li&gt;
&lt;li&gt;Four were parked or listed for sale.&lt;/li&gt;
&lt;li&gt;One served an unrelated operating business.&lt;/li&gt;
&lt;li&gt;None was an active brand-impersonation website.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only &lt;code&gt;.cn&lt;/code&gt; held by the benchmark brand was one of the domains returning 502.&lt;/p&gt;

&lt;p&gt;That row broke the boolean.&lt;/p&gt;

&lt;p&gt;If the brand controls the domain but the site does not work, does it have an official China site?&lt;/p&gt;

&lt;p&gt;If a brand does not control its &lt;code&gt;.cn&lt;/code&gt; but operates a Chinese section on its global domain, should the answer be false?&lt;/p&gt;

&lt;p&gt;What about a verified WeChat account?&lt;/p&gt;

&lt;p&gt;What about an authorized distributor?&lt;/p&gt;

&lt;p&gt;What about a real Chinese website with a valid ICP filing that the international brand has never authorized?&lt;/p&gt;

&lt;p&gt;My schema was about to convert all of those situations into the same value.&lt;/p&gt;

&lt;h2&gt;
  
  
  A plausible explanation arrived too early
&lt;/h2&gt;

&lt;p&gt;I shared the domain result, and a commenter offered a structural explanation.&lt;/p&gt;

&lt;p&gt;Perhaps software brands are worse at maintaining official China channels than consumer brands. Many B2B SaaS companies never establish a local operating presence, while consumer brands may already have retail, manufacturing, or distribution operations in China.&lt;/p&gt;

&lt;p&gt;It is plausible.&lt;/p&gt;

&lt;p&gt;My dataset cannot test it.&lt;/p&gt;

&lt;p&gt;All eight brands are software companies. There is no consumer comparison group. I had not recorded operating-presence evidence before collection. The sample also cannot show whether a missing channel caused an AI attribution error.&lt;/p&gt;

&lt;p&gt;The comment was useful because it exposed the next question. It did not answer it.&lt;/p&gt;

&lt;p&gt;Before comparing industries, I needed to stop treating several different facts as one field.&lt;/p&gt;

&lt;h2&gt;
  
  
  First split: the brand and the channel are separate records
&lt;/h2&gt;

&lt;p&gt;My first replacement looked like this:&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;EvidenceStatus&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;verified_present&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;no_public_evidence_found&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;unresolved&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;BrandSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;brand_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;industry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b2b_software&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;premium_consumer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mainland_operating_presence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EvidenceStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;evidence_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;checked_at&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ChannelSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;channel_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;brand_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;type&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;cn_domain&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;global_domain_chinese_section&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;wechat_official_account&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;marketplace_store&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;authorized_partner&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;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;locator&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;control&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;brand_controlled&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;authorized_third_party&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;unrelated_third_party&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;unresolved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;live_status&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;live&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;parked&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;empty_response&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;unreachable&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;unresolved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;evidence_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;checked_at&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;This is more verbose than a boolean. It is also much closer to what I actually know.&lt;/p&gt;

&lt;p&gt;A brand can control a domain that does not work:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cn_domain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;locator&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.cn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;control&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_controlled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;live_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;empty_response&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;Another domain can serve a real website without being an official channel for the benchmark brand:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cn_domain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;locator&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.cn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unrelated_third_party&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;live_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;live&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;Both are valid observations. They do not support the same conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first validator was still wrong
&lt;/h2&gt;

&lt;p&gt;I then wrote a rule that counted a channel as official only when:&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="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;control&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brand_controlled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That failed too.&lt;/p&gt;

&lt;p&gt;An authorized local partner may operate a legitimate official channel. A verified marketplace store may be run by a licensed operator. An official WeChat account may not map cleanly to a domain the brand owns.&lt;/p&gt;

&lt;p&gt;The opposite mistake is just as easy.&lt;/p&gt;

&lt;p&gt;A real company, a valid ICP filing, or a professional-looking website does not prove that an international brand authorized the channel.&lt;/p&gt;

&lt;p&gt;So I separated control from authorization:&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;AuthorizationStatus&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;brand_operated&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;verified_authorized_operator&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;not_authorized&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;not_verified&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;AuthorizationEvidence&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="nx"&gt;AuthorizationStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;source_type&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;brand_official_source&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;operator_disclosure&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;public_registry&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;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;source_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;checked_at&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;The important value here is &lt;code&gt;not_verified&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A missing public record is not proof that something does not exist.&lt;/p&gt;

&lt;p&gt;If I convert “I did not find evidence” into &lt;code&gt;false&lt;/code&gt;, the benchmark starts manufacturing certainty before the AI answer is even evaluated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI claim needs its own record
&lt;/h2&gt;

&lt;p&gt;The domain audit and the AI-output audit also need separate denominators.&lt;/p&gt;

&lt;p&gt;Seven domains being registered to other parties does not mean seven AI answers falsely described those domains as official.&lt;/p&gt;

&lt;p&gt;Those are different observations.&lt;/p&gt;

&lt;p&gt;For each answer, I need something closer to this:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AIChannelClaim&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;observation_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;platform_surface_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;retrieval_status&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;on&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;off&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;unverified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;brand_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;claimed_channel&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;claim_present&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;truth_status&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;supported&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;contradicted&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;unresolved&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;not_observable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;error_type&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;false_official_attribution&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;fabricated_channel_content&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;fabricated_operator&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_channel&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;false_absence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;evidence_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;evidence_checked_at&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;reviewer&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;An official-channel attribution error should count only when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The answer affirmatively presents a channel as official.&lt;/li&gt;
&lt;li&gt;The claim refers to an identifiable channel.&lt;/li&gt;
&lt;li&gt;Dated evidence contradicts the attribution.&lt;/li&gt;
&lt;li&gt;A reviewer confirms the mismatch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A parked domain is not automatically an AI error.&lt;/p&gt;

&lt;p&gt;A third-party domain is not automatically an AI error.&lt;/p&gt;

&lt;p&gt;A valid ICP filing is not proof that a channel is official.&lt;/p&gt;

&lt;p&gt;A strange-looking answer is not enough either.&lt;/p&gt;

&lt;p&gt;If the necessary evidence cannot be obtained, the result remains &lt;code&gt;unresolved&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legal-looking fields also had to split
&lt;/h2&gt;

&lt;p&gt;The original discussion collapsed several China-related concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registering or holding a &lt;code&gt;.cn&lt;/code&gt; domain&lt;/li&gt;
&lt;li&gt;Hosting a service in mainland China&lt;/li&gt;
&lt;li&gt;Completing an ICP filing&lt;/li&gt;
&lt;li&gt;Holding a commercial internet-information-service licence&lt;/li&gt;
&lt;li&gt;Having a registered operating entity&lt;/li&gt;
&lt;li&gt;Being authorized by the international brand&lt;/li&gt;
&lt;li&gt;Operating a channel that buyers can actually use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not interchangeable.&lt;/p&gt;

&lt;p&gt;CNNIC's registration rules state that natural persons, legal persons, and unincorporated organizations may apply to register a national top-level domain.&lt;/p&gt;

&lt;p&gt;A mainland operating entity is therefore not a universal requirement for simply holding a &lt;code&gt;.cn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;China's internet-information-service rules separately distinguish non-commercial services, which use a filing system, from commercial services, which use a licensing system.&lt;/p&gt;

&lt;p&gt;I do not want the benchmark to make legal determinations. I do want it to stop using one regulatory-looking field as evidence for another.&lt;/p&gt;

&lt;p&gt;The data model now follows four rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.cn ownership != mainland hosting
ICP filing != brand authorization
brand ownership != channel usability
no public evidence found != verified absence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks obvious when written out.&lt;/p&gt;

&lt;p&gt;It was not obvious in the first schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  I needed fixtures before I needed more brands
&lt;/h2&gt;

&lt;p&gt;My initial reaction was to expand the sample.&lt;/p&gt;

&lt;p&gt;That would have produced more rows under an unstable definition.&lt;/p&gt;

&lt;p&gt;Instead, I wrote the cases the measurement contract has to survive:&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;fixtures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brand controls .cn, domain returns 502&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;third party controls .cn, domain is parked&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;third party controls .cn, unrelated business is live&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;third party site has valid ICP filing, no brand authorization found&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 uses a Chinese section on its global .com&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 has an official WeChat account but no local web domain&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;authorized distributor operates the local channel&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;AI presents an affiliate site as the official brand channel&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;public entity evidence cannot be resolved&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;The audit should not pass until every fixture produces a defensible classification.&lt;/p&gt;

&lt;p&gt;This is the same lesson I learned while building answer extractors. A clean test set often means the uncomfortable cases were never written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next benchmark question is now testable
&lt;/h2&gt;

&lt;p&gt;The proposed hypothesis is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After matching for China relevance and brand familiarity, B2B software brands may have lower coverage of usable, verifiable China-facing channels than premium consumer brands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not the result.&lt;/p&gt;

&lt;p&gt;The next wave needs a separate expansion group to test it.&lt;/p&gt;

&lt;p&gt;The rough design is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select comparable B2B software and premium consumer brands.&lt;/li&gt;
&lt;li&gt;Record channel and operating-presence evidence before collecting AI answers.&lt;/li&gt;
&lt;li&gt;Use the same declared platform surfaces, prompt set, locale, and retrieval conditions across groups.&lt;/li&gt;
&lt;li&gt;Have reviewers verify affirmative official-channel claims against dated sources.&lt;/li&gt;
&lt;li&gt;Report the raw numerator and denominator for every industry and evidence group.&lt;/li&gt;
&lt;li&gt;Keep unresolved cases visible instead of converting them into failures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI outcome will be an official-channel attribution error rate, not a general hallucination score.&lt;/p&gt;

&lt;p&gt;The channel outcome also needs two versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usable official web channel:&lt;/strong&gt; a verifiable, live web destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usable official China-facing channel:&lt;/strong&gt; a broader set that may include verified social, marketplace, app, or authorized-partner channels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I initially wanted one headline metric. I now think reporting both is safer.&lt;/p&gt;

&lt;p&gt;The web-only measure is easier to reproduce. The broader channel measure represents the market more accurately. Combining them would hide the distinction I am trying to measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would make the hypothesis fail?
&lt;/h2&gt;

&lt;p&gt;I am writing the failure conditions before collecting the next wave.&lt;/p&gt;

&lt;p&gt;The hypothesis is not supported if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Matched premium consumer brands do not show higher usable-channel coverage than B2B software brands.&lt;/li&gt;
&lt;li&gt;The difference disappears after accounting for brand familiarity or China market presence.&lt;/li&gt;
&lt;li&gt;Brands without a usable official channel do not show a higher verified attribution-error rate.&lt;/li&gt;
&lt;li&gt;The sample becomes too small or unbalanced to separate industry effects from operating-presence effects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final case should be reported as &lt;code&gt;inconclusive&lt;/code&gt;, not quietly converted into support.&lt;/p&gt;

&lt;p&gt;That may leave me with a less interesting article later. It will leave me with a more useful benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the implementation
&lt;/h2&gt;

&lt;p&gt;The first version asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does this brand have an official China site?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current version asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which channels can be identified?
Who controls each channel?
Who is authorized to operate it?
Is it currently usable?
What dated evidence supports those decisions?
What did the AI claim about it?
Can that claim be verified, contradicted, or not resolved?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a much less convenient record.&lt;/p&gt;

&lt;p&gt;It is also a record I can defend six months later when the domain, operator, model, or channel has changed.&lt;/p&gt;

&lt;p&gt;The original eight-brand review is documented in &lt;a href="https://visibilityatlas.com/blog/who-owns-your-cn-domain-ai-official-channel/" rel="noopener noreferrer"&gt;Who Owns Your .cn Domain? We Checked Eight Brands and Found One&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That audit is useful as hypothesis-generating evidence. It is not a software-versus-consumer comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three checks I am keeping
&lt;/h2&gt;

&lt;p&gt;Before publishing any metric based on the new channel layer, I want three checks to pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Every “official” label has dated evidence
&lt;/h3&gt;

&lt;p&gt;A domain suffix, logo, ICP number, or polished page is not enough by itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Unknown states remain unknown
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;no_public_evidence_found&lt;/code&gt; must never be transformed into &lt;code&gt;verified_absent&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Derived metrics expose their denominator
&lt;/h3&gt;

&lt;p&gt;Channel coverage is measured per brand.&lt;/p&gt;

&lt;p&gt;AI attribution errors are measured over eligible, valid answer cells.&lt;/p&gt;

&lt;p&gt;Domain counts are not interchangeable with answer counts.&lt;/p&gt;

&lt;p&gt;If those denominators cannot be explained in one sentence, the number is not ready for a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cnnic.cn/n4/2022/0817/c93-335.html" rel="noopener noreferrer"&gt;CNNIC: National Top-Level Domain Registration Rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sdca.miit.gov.cn/zwgk/fgbz/art/2026/art_fea940f81f1d423e87101adf147ab979.html" rel="noopener noreferrer"&gt;MIIT: Measures for the Administration of Internet Information Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.miit.gov.cn/gyhxxhb/jgsj/cyzcyfgs/bmgz/xxtxl/art/2024/art_84a0cfa0ebd049bbbe751dca9a008e56.html" rel="noopener noreferrer"&gt;MIIT: Measures for the Administration of Non-Commercial Internet Information Service Filings&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am still working through one design question.&lt;/p&gt;

&lt;p&gt;If an official web channel is absent but a verified WeChat or authorized marketplace channel is usable, should the benchmark treat that as channel coverage, or should web and non-web coverage remain separate series?&lt;/p&gt;

&lt;p&gt;If you have modelled this kind of channel evidence before, which edge case would you add to the fixture set?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>data</category>
      <category>testing</category>
      <category>seo</category>
    </item>
    <item>
      <title>Last week I checked the exact-match .cn domains for eight international B2B work-management brands.</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Mon, 10 Aug 2026 06:59:59 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/last-week-i-checked-the-exact-match-cn-domains-for-eight-international-b2b-work-management-brands-4p9e</link>
      <guid>https://dev.to/visibilityatlas/last-week-i-checked-the-exact-match-cn-domains-for-eight-international-b2b-work-management-brands-4p9e</guid>
      <description>&lt;p&gt;In the public registration records reviewed on August 7:&lt;/p&gt;

&lt;p&gt;the benchmark brand was identified as the registrant for one of the eight domains;&lt;/p&gt;

&lt;p&gt;the other seven were registered to other parties;&lt;/p&gt;

&lt;p&gt;three domains returned empty 502 responses;&lt;/p&gt;

&lt;p&gt;four were parked or listed for sale;&lt;/p&gt;

&lt;p&gt;one served an unrelated business.&lt;/p&gt;

&lt;p&gt;None was an active brand-impersonation site.&lt;/p&gt;

&lt;p&gt;The only brand-held .cn was also one of the domains returning 502.&lt;/p&gt;

&lt;p&gt;That last detail matters. Domain ownership and an operating China-facing channel are not the same outcome.&lt;/p&gt;

&lt;p&gt;The explanation I could not accept as a finding&lt;/p&gt;

&lt;p&gt;A commenter proposed a structural explanation.&lt;/p&gt;

&lt;p&gt;Perhaps B2B software brands are more exposed than consumer brands because many SaaS companies never establish the local operating presence needed for a full China web layer. Premium consumer brands may already have entities, retail operations, manufacturing relationships or local paperwork.&lt;/p&gt;

&lt;p&gt;It is plausible.&lt;/p&gt;

&lt;p&gt;My dataset cannot test it.&lt;/p&gt;

&lt;p&gt;All eight brands belong to one B2B software category. There is no consumer comparison group. I did not freeze a mainland-entity field before collecting the data. Brand size, time in China, distribution model and local market presence were not matched.&lt;/p&gt;

&lt;p&gt;Publishing “software brands are worse” from that sample would turn an interesting comment into an unsupported industry conclusion.&lt;/p&gt;

&lt;p&gt;So I registered it as a proposed hypothesis for a future benchmark expansion arm instead.&lt;/p&gt;

&lt;p&gt;First, separate the variables&lt;/p&gt;

&lt;p&gt;The original discussion also collapsed several different facts:&lt;/p&gt;

&lt;p&gt;Who owns a .cn domain?&lt;/p&gt;

&lt;p&gt;Does the domain resolve and serve a usable website?&lt;/p&gt;

&lt;p&gt;Where is the service operated or hosted?&lt;/p&gt;

&lt;p&gt;Is there an ICP filing?&lt;/p&gt;

&lt;p&gt;Is a commercial internet-information-service licence relevant?&lt;/p&gt;

&lt;p&gt;Is there evidence of a mainland operating entity?&lt;/p&gt;

&lt;p&gt;Is the channel controlled or authorized by the brand?&lt;/p&gt;

&lt;p&gt;Does an AI answer describe that channel correctly?&lt;/p&gt;

&lt;p&gt;These cannot be represented by one china_presence: yes/no field.&lt;/p&gt;

&lt;p&gt;CNNIC’s .cn registration rules state that natural persons, legal persons and unincorporated organizations may apply, subject to identity verification. Merely holding a .cn should therefore not be treated as proof of a mainland operating entity.&lt;/p&gt;

&lt;p&gt;China’s internet-information-service rules separately distinguish commercial services requiring a licence from non-commercial services requiring a filing. That regulatory question is not created by the domain suffix alone.&lt;/p&gt;

&lt;p&gt;This is an operational research distinction, not legal advice. Individual cases still require current jurisdiction-specific verification.&lt;/p&gt;

&lt;p&gt;The proposed test&lt;/p&gt;

&lt;p&gt;The expansion arm would start with two matched industry groups:&lt;/p&gt;

&lt;p&gt;B2B SaaS / technology&lt;/p&gt;

&lt;p&gt;Luxury or premium consumer brands&lt;/p&gt;

&lt;p&gt;The directional hypothesis is:&lt;/p&gt;

&lt;p&gt;After matching for China relevance and brand familiarity, B2B SaaS brands may have lower coverage of live, verified China-facing official web channels than premium consumer brands.&lt;/p&gt;

&lt;p&gt;Mainland operating-presence evidence would be a stratifier, not an assumed cause.&lt;/p&gt;

&lt;p&gt;I would record it as:&lt;/p&gt;

&lt;p&gt;VERIFIED_PRESENT&lt;/p&gt;

&lt;p&gt;NO_PUBLIC_EVIDENCE_FOUND&lt;/p&gt;

&lt;p&gt;UNRESOLVED&lt;/p&gt;

&lt;p&gt;“No public evidence found” must not quietly become “absent.”&lt;/p&gt;

&lt;p&gt;Define the outcomes before collecting answers&lt;/p&gt;

&lt;p&gt;A usable official China-facing channel would need more than an available domain.&lt;/p&gt;

&lt;p&gt;The proposed rubric requires:&lt;/p&gt;

&lt;p&gt;brand control or verifiable authorization;&lt;/p&gt;

&lt;p&gt;a live response;&lt;/p&gt;

&lt;p&gt;usable China-facing or Chinese-language information;&lt;/p&gt;

&lt;p&gt;clear operator identity;&lt;/p&gt;

&lt;p&gt;current evidence of official status.&lt;/p&gt;

&lt;p&gt;The AI outcome would not be a generic “hallucination count.”&lt;/p&gt;

&lt;p&gt;A case would count as an official-channel attribution error only when:&lt;/p&gt;

&lt;p&gt;an answer affirmatively presents a channel as official; and&lt;/p&gt;

&lt;p&gt;dated, authoritative evidence contradicts that attribution.&lt;/p&gt;

&lt;p&gt;Unresolved cases remain unresolved.&lt;/p&gt;

&lt;p&gt;The analysis would also keep ownership errors, authorization errors, stale channels and fabricated channel content as separate subtypes.&lt;/p&gt;

&lt;p&gt;Freeze the comparison before running it&lt;/p&gt;

&lt;p&gt;The two industry groups need the same:&lt;/p&gt;

&lt;p&gt;neutral prompts;&lt;/p&gt;

&lt;p&gt;declared platform surfaces;&lt;/p&gt;

&lt;p&gt;retrieval status;&lt;/p&gt;

&lt;p&gt;model and run window;&lt;/p&gt;

&lt;p&gt;replicate count;&lt;/p&gt;

&lt;p&gt;truth-source protocol;&lt;/p&gt;

&lt;p&gt;human-review rules.&lt;/p&gt;

&lt;p&gt;The brands should be matched or at least stratified by China market presence, brand familiarity, company scale, time in market, and retail, manufacturing or distribution footprint.&lt;/p&gt;

&lt;p&gt;The unit of inference is still the brand. Thousands of brand-by-prompt-by-platform answers do not magically create thousands of independent brands.&lt;/p&gt;

&lt;p&gt;If the matched cells are too small or imbalanced, the result should be INCONCLUSIVE.&lt;/p&gt;

&lt;p&gt;What would falsify it?&lt;/p&gt;

&lt;p&gt;The hypothesis would not be supported if:&lt;/p&gt;

&lt;p&gt;matched premium consumer brands do not have higher usable-channel coverage than B2B SaaS brands;&lt;/p&gt;

&lt;p&gt;the apparent industry difference disappears after accounting for operating-presence evidence or brand familiarity;&lt;/p&gt;

&lt;p&gt;brands without a usable official channel do not show a higher rate of verified official-channel attribution errors.&lt;/p&gt;

&lt;p&gt;A wide confidence interval is not supporting evidence. It is an inconclusive study.&lt;/p&gt;

&lt;p&gt;This is not a new headline metric&lt;/p&gt;

&lt;p&gt;The main longitudinal benchmark remains:&lt;/p&gt;

&lt;p&gt;Mention → Recommendation → Supported Citation&lt;/p&gt;

&lt;p&gt;The channel study is a diagnostic arm under factual accuracy and brand misunderstanding. It should not be pooled into the main funnel or used to rewrite the historical baseline.&lt;/p&gt;

&lt;p&gt;The eight-brand audit generated the question. It did not answer it.&lt;/p&gt;

&lt;p&gt;That is the workflow I want to keep:&lt;/p&gt;

&lt;p&gt;community observation → registered hypothesis → frozen comparison → result or falsification.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;interesting explanation → immediate content claim.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A 200 From the Wrong System: How Two Pages Stayed Invisible for 17 Days</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Sat, 08 Aug 2026 09:59:35 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/a-200-from-the-wrong-system-how-two-pages-stayed-invisible-for-17-days-4m8o</link>
      <guid>https://dev.to/visibilityatlas/a-200-from-the-wrong-system-how-two-pages-stayed-invisible-for-17-days-4m8o</guid>
      <description>&lt;p&gt;Two pages on my site went live on July 22. On August 8 they had zero impressions in Google. Not low. Zero, across three weekly exports.&lt;/p&gt;

&lt;p&gt;URL Inspection didn't say "crawled, not indexed." It said Google could not recognise the URL. Referring sitemap: none detected. Referring pages: none detected. Last crawl: not applicable.&lt;/p&gt;

&lt;p&gt;Never discovered. Seventeen days.&lt;/p&gt;

&lt;p&gt;The pipeline was green the entire time&lt;/p&gt;

&lt;p&gt;My deploy is a small chain: rsync the file, import it into MySQL, restart the service, ping IndexNow. Every step returned success. The last step returned 200 on every URL, every deploy, for three weeks.&lt;/p&gt;

&lt;p&gt;Here's what I'd never examined: IndexNow doesn't feed Google. It's Bing, Yandex, Seznam, Naver. My green light was real — it was just about a different search engine than the one whose console I was reading.&lt;/p&gt;

&lt;p&gt;That's the whole bug, and it isn't an SEO bug. It's the generic one:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system A returns 200
  → I conclude something about system B
    → nothing in the response object ever objected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you've ever read a webhook 202 as "the downstream processed it," or a CDN purge 200 as "the edge is cold," it's the same shape.&lt;/p&gt;

&lt;p&gt;What actually broke&lt;/p&gt;

&lt;p&gt;Search Console's Sitemaps report:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Submitted:  2026-07-22
Last read:  2026-07-22     ← seventeen days ago
Discovered: 101 URLs       ← the file has had 117 for weeks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The two pages went live on July 22 — the same day as the only read. Google fetched the sitemap and moved on, within hours of the file changing. Then nothing brought it back, because a sitemap changing on your server notifies nobody. There is no push. It's a pull-only resource with no cache invalidation, and if the consumer doesn't happen to return, your new URLs live in a document no one is reading.&lt;/p&gt;

&lt;p&gt;Resubmitting took two minutes. Read immediately, 117 URLs.&lt;/p&gt;

&lt;p&gt;So I wrote the check. It doesn't catch the bug.&lt;/p&gt;

&lt;p&gt;This is the part worth more than the fix.&lt;/p&gt;

&lt;p&gt;I wrote a post-deploy verifier. It does two things: it asserts that every published, non-redirected page appears in the live sitemap, and that every sitemap URL is fetchable and indexable — no 404, no redirect, no noindex.&lt;/p&gt;

&lt;p&gt;Current output:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check 1 — published content present in sitemap:  35/35
Check 2 — sitemap URLs fetchable and indexable: 117/117
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Both of those would have passed on July 23, and every day after. The sitemap was correct. The pages were correct. Everything on my side of the network boundary was fine. What failed was the consumer's decision to come back, and there is no assertion I can write locally that observes it.&lt;/p&gt;

&lt;p&gt;So the script prints its own limit rather than implying coverage it lacks:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check 3 — was the sitemap actually READ by the search engine?
   NOT CHECKABLE FROM HERE. Both checks above passed on the day two
   published pages had never been discovered, because the file was
   correct and the consumer simply never came back for it. Verify in
   Search Console &amp;gt; Sitemaps that "Last read" is more recent than your
   latest publish date. IndexNow returning 200 does not cover this:
   it does not feed Google.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The assertion you need is on the other side of the boundary. Local checks verify your artifact. Only the consumer can tell you it consumed it. That generalises past sitemaps: you can validate the message you published, but "was it delivered, and did anyone read it" lives in the broker's API, not yours.&lt;/p&gt;

&lt;p&gt;The checker's first run was wrong&lt;/p&gt;

&lt;p&gt;One more, because it's the third time this week I've shipped a verifier that lied on its first run.&lt;/p&gt;

&lt;p&gt;Check 1 initially reported 11 missing URLs. All 11 were pages I had deliberately consolidated — their markdown files still exist locally, but the URLs 301 elsewhere. Absent from the sitemap is exactly correct for them. My check was flagging correct behaviour as failure. A checker that cries wolf on correct behaviour gets muted, and a muted checker is worse than none.&lt;/p&gt;

&lt;p&gt;This week I also had a data audit report "three datasets don't store raw responses" — they had one API timeout each, which correctly stores an error and no response. And a citation parser that counted a brand's name as a source, because the brand is literally called Monday.com.&lt;/p&gt;

&lt;p&gt;Every one of those was a first run. Every one produced a plausible, alarming, false number. So the habit I'd recommend over any specific check: before you believe your new verifier, make it fail on purpose, and read every row it flags. If you can't explain each flag, the tool is measuring something other than what you named it.&lt;/p&gt;

&lt;p&gt;Three things to check today&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In Search Console → Sitemaps, is Last read more recent than your most recent publish? One click. In three months of running this site I had never once looked.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does your deploy pipeline's success signal come from the system you're actually measuring? Mine didn't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When your verifier passes, do you know which failures it's structurally incapable of seeing? Write that in the output, not the README. Mine now prints it every run.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>seo</category>
      <category>testing</category>
    </item>
    <item>
      <title>I rewrote the same detector three times in one day. Each fix introduced a new way of being wrong.</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Thu, 06 Aug 2026 02:30:03 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/i-rewrote-the-same-detector-three-times-in-one-day-each-fix-introduced-a-new-way-of-being-wrong-378e</link>
      <guid>https://dev.to/visibilityatlas/i-rewrote-the-same-detector-three-times-in-one-day-each-fix-introduced-a-new-way-of-being-wrong-378e</guid>
      <description>&lt;p&gt;I wrote a post last week about an LLM fabricating a company's official website. Then I built a detector to find more cases at scale.&lt;/p&gt;

&lt;p&gt;I have now rewritten that detector three times in one day. Each version fixed the previous version's bug and introduced a new way of mislabelling the same data. The counts went 1 → 134 → 41 → 12.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the final number. It's that all three bugs were the same bug wearing different clothes, and I had published an article about that exact bug in between versions two and three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: blind to agreement
&lt;/h2&gt;

&lt;p&gt;The detector's job: find domains an LLM calls a brand's "official website" when the brand doesn't own that domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v1&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;engineA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saysOfficial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;engineB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saysNoSuchSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&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;It found one real case, which I published. A commenter pointed out the hole:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The disagreement is confirmation rather than the trigger. Any URL an engine calls official can be checked against the brand's owned domain list on its own, single engine, no cross run needed. And you'd catch the case where every engine names the same wrong domain, which is the worse version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He was right on all counts, and the third one is the serious one. &lt;strong&gt;A rule that fires on inconsistency is structurally blind to correlated error&lt;/strong&gt; — which is the dangerous kind, because models trained on overlapping corpora fail together, and that's exactly when a wrong answer reaches every user at once.&lt;/p&gt;

&lt;p&gt;My one published finding had survived only because one engine out of six happened to dissent. Drop that engine from the panel and v1 returns nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: 134 findings, mostly wrong
&lt;/h2&gt;

&lt;p&gt;The fix seemed obvious. I had ground truth sitting unused — every brand has a list of domains it owns. Check the claim directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v2&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;urlsIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ownedDomains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&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;o&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;continue&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="nf"&gt;officialClaimNear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&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;index&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&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;134 domains flagged, up from 1. I started drafting this post with a table of the top nine.&lt;/p&gt;

&lt;p&gt;Then I looked at the rest of the output instead of the head of it. Domains like &lt;code&gt;airtable-official.com&lt;/code&gt;, &lt;code&gt;asana-vip.net&lt;/code&gt;, &lt;code&gt;wrike-sales.net&lt;/code&gt;, &lt;code&gt;basecampchina.net&lt;/code&gt;. Names that pattern-match to &lt;em&gt;fabricated examples&lt;/em&gt;, not to real channels.&lt;/p&gt;

&lt;p&gt;They were. Here's a representative flagged passage:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;检查域名：官方主站是 &lt;code&gt;wrike.com&lt;/code&gt;。任何其他域名（如 &lt;code&gt;wrike-official.com&lt;/code&gt;、&lt;code&gt;wrike-sales.net&lt;/code&gt;）都需要警惕。&lt;br&gt;
&lt;em&gt;(Check the domain: the official site is wrike.com. Be wary of any other domain, such as wrike-official.com, wrike-sales.net.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The engine is &lt;strong&gt;warning users about fake sites&lt;/strong&gt;. My detector saw a domain near the words "official website" and recorded it as a claim. I was scoring correct model behaviour as a hallucination.&lt;/p&gt;

&lt;p&gt;Sampling the flagged set: &lt;code&gt;notion-cn.com&lt;/code&gt;, which I had been about to publish as my most alarming new discovery, appeared in a warning context 86% of the time.&lt;/p&gt;

&lt;p&gt;Assertion, warning, and denial are three states. I had two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 3: still attributing to the wrong domain
&lt;/h2&gt;

&lt;p&gt;So I added the third state, reusing the polarity logic I'd built three days earlier for brand mentions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;negationCues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tightClause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;denied&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;warningCues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;260&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;warned&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;assertCues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;asserted&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="s2"&gt;referenced&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;41 flagged. Better. Still wrong, and wrong in a way I should have predicted, because I had written an article about it that week.&lt;/p&gt;

&lt;p&gt;Look at that Wrike passage again. The phrase 官方主站 ("official main site") appears roughly thirty characters before &lt;code&gt;wrike-sales.net&lt;/code&gt;. Any proximity-based rule attributes it to the nearest domain. But the cue belongs to &lt;code&gt;wrike.com&lt;/code&gt; — the domain named &lt;em&gt;before&lt;/em&gt; it. The sentence structure is: [real domain, asserted] then [fake domains, warned].&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proximity is not attribution.&lt;/strong&gt; This is the identical failure I'd published about days earlier, in which a keyword window around a brand name picked up sentiment belonging to a neighbouring brand. Same bug, different entity type, and I walked straight into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 4: attribution, finally
&lt;/h2&gt;

&lt;p&gt;The fix is to require that no other domain sits between the assertion cue and the domain being classified:&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;clause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tightClauseAround&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;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ASSERT_CUES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clause&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;m&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;referenced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Attribution, not proximity: if another domain sits between the cue and this one,&lt;/span&gt;
&lt;span class="c1"&gt;// the cue belongs to that one.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clause&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&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;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hereIdx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hereIdx&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;domainsBetween&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DOMAIN_RE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&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;domainsBetween&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;referenced&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;asserted&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;Final counts over the same 4,023 stored answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12 domains asserted as official and not owned
173 domains appearing ONLY in warning contexts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second number is the one worth sitting with. &lt;strong&gt;173 cases where the engines were doing exactly the right thing&lt;/strong&gt; — telling buyers to be careful about lookalike domains — and version 2 of my detector counted every one of them as a hallucinated official channel.&lt;/p&gt;

&lt;p&gt;If I'd shipped v2, I'd have published a story about LLMs inventing 134 fake official websites. The true story is that they invented a handful and warned about many more, which is close to the opposite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Three bugs, one shape.&lt;/strong&gt; Each version failed because I treated &lt;em&gt;nearby text&lt;/em&gt; as &lt;em&gt;text about this thing&lt;/em&gt;. Windows around brand names, clauses around URLs, blocks around domains — same mistake at three granularities. Entity-level attribution is the actual problem, and no amount of tuning window size solves it. I now assume any new extractor I write has this bug until I've specifically checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading the head of the output is not reading the output.&lt;/strong&gt; I drafted a version of this post based on the first fifty lines. The tail was where the story fell apart. Sorting by count and reading the top is how you confirm what you expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A detection rule needs a false-positive class, not just a true-positive class.&lt;/strong&gt; v2 had no concept of "correct behaviour that resembles the thing I'm looking for." Once I named that class, it turned out to be 93% of what I'd flagged.&lt;/p&gt;

&lt;p&gt;Three questions I'd now ask of any extractor before trusting its counts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does it distinguish the model asserting X, warning about X, and denying X? Those look nearly identical in text and mean opposite things.&lt;/li&gt;
&lt;li&gt;When it finds a cue near an entity, what guarantees the cue is &lt;em&gt;about&lt;/em&gt; that entity rather than the one next to it?&lt;/li&gt;
&lt;li&gt;If the thing you're detecting has a legitimate lookalike — correct behaviour that pattern-matches to the failure — is that lookalike represented in your test cases?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I got all three wrong in a single day, on a codebase whose entire purpose is catching this category of error. The one thing that worked was a stranger asking whether my rule handled a case I hadn't considered, and then me reading the raw output instead of the summary.&lt;/p&gt;




&lt;p&gt;Harness, scoring module, and labelled validation sets are public under CC BY 4.0: &lt;a href="https://github.com/David88666/china-ai-visibility-benchmark" rel="noopener noreferrer"&gt;github.com/David88666/china-ai-visibility-benchmark&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>testing</category>
      <category>debugging</category>
      <category>ai</category>
    </item>
    <item>
      <title>An LLM described a website in detail. The website doesn't exist.</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Wed, 05 Aug 2026 03:13:21 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/an-llm-described-a-website-in-detail-the-website-doesnt-exist-3ldp</link>
      <guid>https://dev.to/visibilityatlas/an-llm-described-a-website-in-detail-the-website-doesnt-exist-3ldp</guid>
      <description>&lt;p&gt;An LLM described a company's official website to me in detail: complete Simplified Chinese interface, pricing displayed in RMB marked "tax not included", China-specific terms of service, a localized privacy policy. It told me this site was "the most important source" for verifying the company's credibility.&lt;/p&gt;

&lt;p&gt;The domain has been registered to a private individual since 2016. It returns a 502. None of those pages have ever existed.&lt;/p&gt;

&lt;p&gt;This is the hallucination shape that worries me most — not "the model made something up," which everyone expects, but &lt;strong&gt;the model made up specific, checkable, mundane details that a human would never think to check&lt;/strong&gt;. Nobody verifies a privacy policy's existence. You verify the big claim and assume the supporting texture came from somewhere.&lt;/p&gt;

&lt;p&gt;Here's how I caught it, why my own pipeline sat on it for a week, and the check that generalizes.&lt;/p&gt;

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

&lt;p&gt;I run a measurement harness against six Chinese LLM APIs — DeepSeek, Doubao, Qwen, Kimi, ERNIE, GLM — asking buyer-style questions about international software brands and logging every answer. 4,023 valid responses, retrieval off, everything stored as JSONL.&lt;/p&gt;

&lt;p&gt;One question type asks, in Chinese and English, some version of &lt;em&gt;"what are this brand's official channels, and how would you verify them?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GLM's answer for one brand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Airtable China Official Website (Airtable中国官网)&lt;/strong&gt;&lt;br&gt;
URL: &lt;code&gt;https://www.airtable.cn/&lt;/code&gt;&lt;br&gt;
What to look for: This is the most important source. Its existence signals a formal commitment to the Chinese market.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in a separate answer, in Chinese:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;域名 &lt;code&gt;.cn&lt;/code&gt; 是中国的国家顶级域名，由 Airtable 官方运营，这本身就是一种官方身份的声明&lt;br&gt;
&lt;em&gt;(The .cn domain is China's country-code TLD, operated officially by Airtable — this is itself a declaration of official identity.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Confident, structured, and it reasons about &lt;em&gt;why&lt;/em&gt; the evidence counts. That last part is what makes it dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why my pipeline missed it
&lt;/h2&gt;

&lt;p&gt;My extractor pulled URLs with a regex and recorded the domains. &lt;code&gt;airtable.cn&lt;/code&gt; went into the citation column as a cited source, indistinguishable from a real one.&lt;/p&gt;

&lt;p&gt;Every quality check I had was a &lt;strong&gt;rate&lt;/strong&gt;: error count, empty-answer rate, answer-length distribution, language distribution. All of them were green, because nothing about this row was anomalous. One URL among 1,416, in a well-formed answer of normal length in the expected language.&lt;/p&gt;

&lt;p&gt;Rates catch a class of rows that changes size between runs. They cannot catch a class that was wrong from the first run and stayed wrong at a stable size. A reviewer put it better than I did: &lt;em&gt;rates catch a class that shrinks, asserts catch a class that was never right.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The check
&lt;/h2&gt;

&lt;p&gt;Three lookups per domain. No tools, no API, about ten minutes for eight brands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Does anything answer for it?&lt;/span&gt;
dig +short airtable.cn A
&lt;span class="c"&gt;# → 223.26.56.104   (someone registered it and pointed it somewhere)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Who holds it?&lt;/span&gt;
whois airtable.cn | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^(Registrant|Registration Time|Sponsoring)"&lt;/span&gt;
&lt;span class="c"&gt;# → Registrant: (a private individual)&lt;/span&gt;
&lt;span class="c"&gt;# → Registrant Contact Email: (a free QQ mail address)&lt;/span&gt;
&lt;span class="c"&gt;# → Registration Time: 2016-02-08&lt;/span&gt;

&lt;span class="c"&gt;# 3. What does it actually serve?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://www.airtable.cn/
&lt;span class="c"&gt;# → 502&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For contrast, the one brand in my sample that does own its &lt;code&gt;.cn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Registrant: BRIAN TYLER EVANS
Registrant Contact Email: help@clickup.com
Sponsoring Registrar: GoDaddy.com, LLC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what ownership looks like in a registration record: a company contact, at the company's own domain. It takes one line to tell the two cases apart, and my pipeline had never looked.&lt;/p&gt;

&lt;p&gt;Running all eight brands from the study:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Held by&lt;/th&gt;
&lt;th&gt;Serves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clickup.cn&lt;/td&gt;
&lt;td&gt;the brand&lt;/td&gt;
&lt;td&gt;nothing (parked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;airtable.cn&lt;/td&gt;
&lt;td&gt;private individual&lt;/td&gt;
&lt;td&gt;502&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wrike.cn&lt;/td&gt;
&lt;td&gt;private individual&lt;/td&gt;
&lt;td&gt;502&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;asana.cn&lt;/td&gt;
&lt;td&gt;private individual&lt;/td&gt;
&lt;td&gt;"domain for sale"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smartsheet.cn&lt;/td&gt;
&lt;td&gt;private individual, &lt;strong&gt;registered 2025&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;"域名转让 — The domain is on sale!"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notion.cn&lt;/td&gt;
&lt;td&gt;a domain-holding company&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;monday.cn&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;the same&lt;/strong&gt; domain-holding company&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;basecamp.cn&lt;/td&gt;
&lt;td&gt;private individual, registered through 2034&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;a bicycle apparel manufacturer in Dongguan&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven of eight belong to someone other than the brand. I'm not publishing registrant names — registering an available domain is legal and these are private individuals. The interesting part is on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell: the model contradicted itself
&lt;/h2&gt;

&lt;p&gt;Same engine, same collection window, a differently-worded question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You do not access a separate &lt;code&gt;airtable.cn&lt;/code&gt; website. Instead, your Airtable China account is configured to use the China-hosted infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two incompatible accounts of the same fact, days apart, neither hedged.&lt;/p&gt;

&lt;p&gt;That's the generalizable detection signal, and it's cheap: &lt;strong&gt;ask the same factual question several ways and diff the answers.&lt;/strong&gt; A model that knows something answers consistently. A model that is constructing something plausible constructs differently each time, because there's no underlying fact constraining it.&lt;/p&gt;

&lt;p&gt;In my open-question data, 18.8% of question-pairs changed outcome between two runs on the same day. If you're evaluating an LLM's factual output and you only ask once, you have no way to distinguish knowledge from confabulation.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;A predicate check on extracted URLs.&lt;/strong&gt; A URL inside a clause that denies its existence is not a citation. My extractor was matching tokens without reading the sentence around them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;URL_NEGATION_CUES&lt;/span&gt; &lt;span class="o"&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="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|无&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;独立|专门|官方&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|不提供|尚未|&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;does not|doesn't|no&lt;/span&gt;&lt;span class="se"&gt;)\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;have|exist|operate&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&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;urlIsNegated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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;URL_NEGATION_CUES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clauseAround&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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;One implementation note that cost me a wrong result: URL predicates need &lt;strong&gt;tighter clause boundaries than entity mentions do&lt;/strong&gt;. Split on sentence punctuation only, and &lt;code&gt;"并没有推出中文官网，其主要官网是 https://basecamp.com"&lt;/code&gt; flags that URL as negated — but the negation targets the Chinese site and the URL is being &lt;em&gt;affirmed&lt;/em&gt;, one comma later. Splitting on commas as well fixed it: zero false flags across 1,416 URLs, and 74 genuine anti-citations in the bare-domain form my original regex never captured at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertions at the joins.&lt;/strong&gt; Anywhere two vocabularies meet, assert a hit that must be there or refuse to run. In my harness there were three such seams and all three were quietly broken:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Competitor list vs answer text — my list was romanized, the answers name competitors in Chinese. The join found almost nothing and reported it as "no substitution." Fixed by refusing to start if a Chinese-language panel has no Chinese-script competitor names.&lt;/li&gt;
&lt;li&gt;Category label vs prompt template — an English label injected into a Chinese question changed what was being asked. Fixed by round-tripping the rendered prompt through a model: &lt;em&gt;"what category does this question ask about?"&lt;/em&gt; Compare to what you meant. One call per template.&lt;/li&gt;
&lt;li&gt;URL vs surrounding clause — the predicate check above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Provenance on every row&lt;/strong&gt;, so a number can be reconstructed later rather than silently changing when the scorer improves: &lt;code&gt;scoring_version&lt;/code&gt;, &lt;code&gt;finish_reason&lt;/code&gt;, completion and reasoning token counts, a response hash, and a validity enum decided &lt;em&gt;before&lt;/em&gt; any content scoring runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'd want you to take away
&lt;/h2&gt;

&lt;p&gt;The failure wasn't that a model hallucinated. It's that the hallucination was &lt;strong&gt;operationally indistinguishable from a fact&lt;/strong&gt; at every layer of my pipeline, and every quality metric I had was green while it sat there.&lt;/p&gt;

&lt;p&gt;If you're building anything that treats LLM output as evidence — extraction, enrichment, research automation, RAG evaluation — the questions worth asking are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If a whole class of your rows were wrong from run one, which metric would move? (If the answer is "none," you're where I was.)&lt;/li&gt;
&lt;li&gt;When your extractor pulls an entity, does anything check the predicate of the sentence it came from?&lt;/li&gt;
&lt;li&gt;Do you ask the same factual question more than once, in more than one phrasing?&lt;/li&gt;
&lt;li&gt;Can you reconstruct last month's number, or would re-running today silently produce a different one?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've published corrections to my own numbers four times in three weeks doing this. Every single one was found either by reading raw output by hand or by a stranger asking a question I couldn't answer. Neither is a metric you can add to a dashboard, which I think is the actual lesson.&lt;/p&gt;




&lt;p&gt;Harness, labelled validation samples, and the re-scoring scripts are public under CC BY 4.0: &lt;a href="https://github.com/David88666/china-ai-visibility-benchmark" rel="noopener noreferrer"&gt;github.com/David88666/china-ai-visibility-benchmark&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>debugging</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Six checks before you trust any number your LLM pipeline produces</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Mon, 03 Aug 2026 04:00:25 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/six-checks-before-you-trust-any-number-your-llm-pipeline-produces-2do1</link>
      <guid>https://dev.to/visibilityatlas/six-checks-before-you-trust-any-number-your-llm-pipeline-produces-2do1</guid>
      <description>&lt;p&gt;Last week the same 96 recorded LLM conversations gave me three different headline numbers: 15%, then 66.7%, then ~31%. The pipeline reported &lt;strong&gt;zero errors&lt;/strong&gt; every time. When I finally hand-read all the answers, the truth wasn't any of them.&lt;/p&gt;

&lt;p&gt;I run a measurement harness against six Chinese AI engines (DeepSeek, Doubao, Qwen, Kimi, ERNIE, GLM) — ask buyer-style questions, log every answer, score which brands appear. I published the first number, had to retract it publicly, and rebuilt the measurement layer with the help of strangers in comment threads who kept predicting my next bug before I found it.&lt;/p&gt;

&lt;p&gt;This is the post-mortem, generalized: six checks I now run before believing any number my own tooling produces, and the statistical trick that saved a classifier I was about to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure shape: classes, not rows
&lt;/h2&gt;

&lt;p&gt;The two worst bugs had an identical signature, and it's worth naming because you probably have a variant of it in your pipeline right now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A whole class of rows gets mislabeled in one direction, silently, and no aggregate will ever show you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: reasoning tokens starve the answer.&lt;/strong&gt; Reasoning models bill their chain of thought against the same &lt;code&gt;max_tokens&lt;/code&gt; as the visible answer. Long reasoning → the API returns HTTP 200, &lt;code&gt;finish_reason: "length"&lt;/code&gt;, and &lt;code&gt;content: ""&lt;/code&gt;. My extractor searched the empty string for brand names, found none, and recorded "brand absent." 10 of 32 first answers and 46 of 96 follow-up turns were empty. Zero errors reported.&lt;/p&gt;

&lt;p&gt;The fix is to make emptiness unrepresentable as a score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;content&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reasoning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;reasoning_content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;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;`empty-answer finish=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finish_reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; reasoning_chars=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better: return a validity enum (&lt;code&gt;valid_answer | empty_due_to_budget | truncated | provider_error&lt;/code&gt;) and decide it &lt;strong&gt;before&lt;/strong&gt; any scoring runs. An invalid row keeps its reason and carries no verdict. Missing output is a fact about the request, never evidence about the brand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: the scorer only speaks one language.&lt;/strong&gt; My polarity cues — the phrases that separate "recommended" from "ruled out" — were Chinese-only. A commenter asked: what's the language distribution of your answers? Answer: 25.4% English (1,053 of 4,151). Every English mention was scoring "neutral" by construction. Same shape as bug 1: an entire class of rows quietly padding one label.&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;answerLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;han&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;4e00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;9fff&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;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;latin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;han&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;han&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;latin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&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;mixed&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;If the language isn't one your scorer covers, return &lt;code&gt;unscorable_language&lt;/code&gt;. Refusing to score is the honest move — it keeps those rows out of the numerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six pre-flight checks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Error count vs row count.&lt;/strong&gt; A full-looking file proves the harness ran, not that the run succeeded. When an API account ran out of credit mid-run, every call returned 403 — and the file still gained a row per attempt. 672 rows, 500 of them errors. Report the ratio next to every headline number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Finish reason + token accounting.&lt;/strong&gt; Store &lt;code&gt;finish_reason&lt;/code&gt;, &lt;code&gt;completion_tokens&lt;/code&gt;, &lt;code&gt;reasoning_tokens&lt;/code&gt; per row. &lt;code&gt;finish_reason: "length"&lt;/code&gt; with high reasoning tokens = starved answer, not short answer. This metadata was in every response the whole time. I just never looked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Answer length distribution.&lt;/strong&gt; Sort by length once per run. Healthy runs have a bell curve; a spike at zero is a fire alarm; a cluster of very short answers is usually truncation, refusals, or the model answering "see above" instead of answering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Language distribution.&lt;/strong&gt; Before anything downstream scores. See bug 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Hand-read 20 scored rows against their labels.&lt;/strong&gt; This hour caught my worst bug, which no automation would ever flag. My harness scored a brand as "surviving" a budget constraint on this answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;之前推荐的一些"顶配"品牌（如 Asana、Monday.com 的付费版）基本都超了，不建议强行上付费版&lt;br&gt;
&lt;em&gt;(The premium brands I recommended earlier — Asana, Monday.com's paid tiers — are over budget. I would not force it.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Name present. Brand being ruled out. &lt;strong&gt;47% of my "survivals" were rejections wearing a mention.&lt;/strong&gt; Constraint-style prompts are exactly where models name things in order to exclude them, so name-matching fails hardest precisely where the metric matters most.&lt;/p&gt;

&lt;p&gt;Bonus trap inside this trap: negated phrases contain their positive form as a substring. 不建议 ("not recommended") contains 建议 ("recommended"). My reject-cue and recommend-cue regexes both fired, cancelled out, and the mention scored neutral. Mask negations before testing positives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. A scoring version stamped on every row.&lt;/strong&gt; My 15% / 66.7% / 31% came from the same data under three scoring rules — and nothing in the stored rows said which rule produced which number. Now every row carries &lt;code&gt;scoring_version&lt;/code&gt;, a response hash, and the validity verdict. If you can't say which rules produced a number, the number isn't evidence; it's a screenshot of your pipeline's current mood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't know: you don't need a good classifier
&lt;/h2&gt;

&lt;p&gt;After all the fixes, my polarity classifier was still bad — it caught only a third of genuine recommendations (sensitivity 33-45% depending on class). I was going to delete it.&lt;/p&gt;

&lt;p&gt;A commenter reframed it: &lt;em&gt;you're never claiming the judge is right — you're quantifying how wrong it is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hand-label a sample. Now you have a confusion matrix, which gives you sensitivity and specificity. With those, the rate your classifier reports over the whole corpus can be &lt;strong&gt;corrected&lt;/strong&gt; — this is the Rogan–Gladen estimator, from 1978 epidemiology, built for exactly this problem: imperfect diagnostic tests that still need to produce usable prevalence numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// observed = p·sens + (1-p)·(1-spec)  →  solve for p&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;corrected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;observed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;specificity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sensitivity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;specificity&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real numbers from my labelled sample (n=36):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Scorer reported&lt;/th&gt;
&lt;th&gt;Corrected&lt;/th&gt;
&lt;th&gt;Hand-labelled truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recommended&lt;/td&gt;
&lt;td&gt;5.6%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rejected&lt;/td&gt;
&lt;td&gt;36.1%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A classifier with 45% sensitivity produced corrected rates that hit ground truth on both classes. I would have thrown it away the week before.&lt;/p&gt;

&lt;p&gt;Two honest caveats. First, intervals: with n=36 and low sensitivity, my corrected 8.3% has a 95% interval of 0–50%. Usable internally, unpublishable alone. Second — and this is the part that changes the economics — &lt;strong&gt;the labelled sample sizes the error estimate, not the corpus.&lt;/strong&gt; 200 labels can carry 20,000 rows, because you're estimating the scorer's error rate, which doesn't grow with the data. Label once, correct everywhere, re-validate when the scorer or the model under test changes.&lt;/p&gt;

&lt;p&gt;So the real choice was never "human reading doesn't scale" vs "automation can't be trusted." It's: automation whose untrustworthiness has been measured on a fixed-cost human sample and adjusted for, interval published alongside. Either half alone is a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Judge-design notes I took from people who run this bigger than me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Different model family for the judge than the model under test.&lt;/strong&gt; Shared blind spots double-count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the judge the clause containing the entity, not the full answer.&lt;/strong&gt; I tested this at the regex level: a ±120-char window picked up sentiment belonging to &lt;em&gt;neighbouring&lt;/em&gt; brands; clause-splitting fixed precision (25%→50%) but then missed rejections living in a heading two lines above the brand list. Wide leaks, narrow misses. Attribution is the actual task — which is why this ends at a validated judge, not a better window size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured elicitation as a second arm, not a replacement.&lt;/strong&gt; You can force the model to end with an explicit "still fits / ruled out" list — then polarity is parsing, not inference. But asking for a list changes the task, so run it alongside the natural-language arm. If the two arms disagree a lot, that disagreement is itself a finding about how much your format drives your numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The re-validation triggers
&lt;/h2&gt;

&lt;p&gt;Re-label a fresh sample when any of these change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The scorer (rules, prompts, cue lists)&lt;/li&gt;
&lt;li&gt;The model under test (version bumps count — DeepSeek deprecated a model name mid-study and the replacement behaved differently)&lt;/li&gt;
&lt;li&gt;The language mix of the answers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each costs an afternoon. Each of mine, skipped, cost a public retraction.&lt;/p&gt;




&lt;p&gt;Everything here — harness, labelled validation sample with the misclassified examples, and the correction script — is public: &lt;a href="https://github.com/David88666/china-ai-visibility-benchmark" rel="noopener noreferrer"&gt;github.com/David88666/china-ai-visibility-benchmark&lt;/a&gt; (CC BY 4.0). The buyer-facing version of this article, with the vendor questions, is on my site.&lt;/p&gt;

&lt;p&gt;I used to think publishing corrections would cost credibility. It's been the single best source of methodology this project has had. The numbers got worse twice, the measurement got better four times, and I'll take that trade every week.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>testing</category>
      <category>datascience</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I asked six Chinese AI engines to recommend CRM software. HubSpot never appeared.</title>
      <dc:creator>VisibilityAtlas</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:58:44 +0000</pubDate>
      <link>https://dev.to/visibilityatlas/i-asked-six-chinese-ai-engines-to-recommend-crm-software-hubspot-never-appeared-pj4</link>
      <guid>https://dev.to/visibilityatlas/i-asked-six-chinese-ai-engines-to-recommend-crm-software-hubspot-never-appeared-pj4</guid>
      <description>&lt;p&gt;I spend my working life on how Chinese search and AI systems describe international brands, and I got tired of arguing about it from screenshots. So I built a harness and recorded 4,704 answers across six Chinese AI engines.&lt;/p&gt;

&lt;p&gt;This post is about one slice of it: the CRM category, where the result was more extreme than I expected.&lt;/p&gt;

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

&lt;p&gt;8 international CRM brands (Salesforce, HubSpot, Zoho CRM, Pipedrive, Microsoft Dynamics 365, Freshsales, SugarCRM, Monday CRM) × 42 Chinese-language buyer questions × 2 runs = &lt;strong&gt;672 recorded answers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The questions come in five groups: branded checks, open category discovery, comparison, decision intent, and risk. That grouping turned out to matter more than anything else in the study.&lt;/p&gt;

&lt;p&gt;Every Chinese engine I tested speaks the OpenAI chat-completions dialect, so the collector is boring on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&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;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// Ark reasoning models bury the answer in reasoning_content unless you do this&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BENCH_THINKING&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thinking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disabled&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/chat/completions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BENCH_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;90000&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="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;200&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One env-var swap per engine — base URL, model ID, platform label — and the same panel runs everywhere. Total API spend for the whole study was under $5.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Mention rate on &lt;strong&gt;open&lt;/strong&gt; category questions ("which CRM should I consider in China?", brand name never mentioned in the prompt):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brand&lt;/th&gt;
&lt;th&gt;Open-question mention&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Salesforce&lt;/td&gt;
&lt;td&gt;87.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Dynamics 365&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoho CRM&lt;/td&gt;
&lt;td&gt;12.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HubSpot&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipedrive&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshsales&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SugarCRM&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monday CRM&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five of eight global brands never surfaced. Not once, across every phrasing and both runs.&lt;/p&gt;

&lt;p&gt;The engine's own framing, repeated almost verbatim across answers: &lt;em&gt;"若企业有全球化需求，优先考虑 Salesforce 或 Microsoft Dynamics"&lt;/em&gt; — if you need global reach, consider Salesforce or Dynamics. The "international option" isn't a shelf with several slots. It's one seat, and Salesforce is in it. Everything else in the list goes to domestic vendors (销售易, 纷享销客, Kingdee).&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that fooled me first
&lt;/h2&gt;

&lt;p&gt;Ask the same engines about HubSpot &lt;strong&gt;by name&lt;/strong&gt; and you get a competent, fair, well-structured paragraph. Branded-question mention rate was ~100% for every brand, on every engine.&lt;/p&gt;

&lt;p&gt;So the check most teams run — "I asked DeepSeek about us and it looked fine" — returns a false positive by construction. The prompt hands the model the answer. The number that predicts whether a buyer ever meets you is the open one, and for this sample it was 23% across all categories.&lt;/p&gt;

&lt;p&gt;Reading HubSpot's 84 branded answers, three things stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No stable Chinese name.&lt;/strong&gt; The model says so directly: "HubSpot 没有正式的中文名称." Every zero-mention brand in my data shared this marker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A self-contradicting story.&lt;/strong&gt; One answer claims HubSpot has China branches; three others reference an exit. The model holds both and serves them interchangeably. Unmanaged narrative → the model assembles one from conflicting fragments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance filtering.&lt;/strong&gt; In this category, compliance caveats attached to 61 of 128 branded/risk answers — the densest I measured. Salesforce clears the filter because the model has one concrete fact to cite (its Alibaba Cloud arrangement). No checkable fact → generic warning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproducibility notes, including what went wrong
&lt;/h2&gt;

&lt;p&gt;Things I'd want to know if I were reading someone else's version of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask everything twice.&lt;/strong&gt; 18.8% of open question-pairs flipped outcome between two same-day runs. A single query is a coin flipped once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning models hide the answer.&lt;/strong&gt; Doubao's flagship and several Qwen/GLM variants returned &lt;code&gt;content: ""&lt;/code&gt; with 2,000 characters in &lt;code&gt;reasoning_content&lt;/code&gt; until I forced answer-only mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts produce fake failures.&lt;/strong&gt; One engine needed the timeout tripled; at 90s it looked like an error rate, at 200s it looked fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count your errors before trusting a file.&lt;/strong&gt; An aggregator ran out of credit mid-run and silently failed 500 of 672 calls with HTTP 403. The file still had 672 lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date-stamp the exact model.&lt;/strong&gt; Mid-study, DeepSeek deprecated &lt;code&gt;deepseek-chat&lt;/code&gt; entirely and forced a rename. These products move faster than the analysis does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode matters.&lt;/strong&gt; This is API model-knowledge mode, not the consumer apps with live retrieval. Different instrument, different question — I record which per engine rather than blending them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;p&gt;Aggregate results and the reusable 42-question bilingual panel are CC BY 4.0:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/David88666/china-ai-visibility-benchmark" rel="noopener noreferrer"&gt;https://github.com/David88666/china-ai-visibility-benchmark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Swap in your own brands and category — the panel has &lt;code&gt;{brand}&lt;/code&gt; / &lt;code&gt;{category}&lt;/code&gt; / &lt;code&gt;{competitor}&lt;/code&gt; slots — and you can reproduce the whole thing in an afternoon.&lt;/p&gt;

&lt;p&gt;If you work on something with international reach, the cheap version of this test is worth running once: three engines, five open category questions in the local language, twice each. It takes an hour, and it's a very different picture from asking the model about yourself by name.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>datascience</category>
      <category>opensource</category>
      <category>api</category>
    </item>
  </channel>
</rss>
