{
 "changes": [
  {
   "tool": "crawlcheck",
   "fault": "R6",
   "from": "missed",
   "to": "partial",
   "reason": "The relative Sitemap line's direct symptom is shown at the right row: robots.txt sitemap 'none named' on lab2, '1 of 1 ✓ Passed' on the clean twin. That is the same statement graded partial for aicrawlercheck ('Not declared in robots.txt') and for isitagentready ('No Sitemap directives found in robots.txt', a neutral outcome). The CrawlCheck grader also gives partial elsewhere for values shown but not flagged (J1, J4, X5, L4). The contradicting pass row ('robots.txt names a sitemap | 1 Sitemap: line(s) ✓') does not lower it below partial; the same was accepted for hostinger ('Sitemap declared in robots.txt 3/3' next to its partial).",
   "evidence": "'Sitemaps named in robots.txt resolve ... | none named | every named sitemap returns 200 | Reported' (raw/cc_r_wyka6piddc.html, Sitemap coverage); clean: '... | 1 of 1 | every named sitemap returns 200 | ✓ Passed' (raw/cc_r_kcys45p8jf.html)"
  },
  {
   "tool": "hostinger",
   "fault": "S4",
   "from": "missed",
   "to": "wrong",
   "reason": "Hostinger's only crawler-UA request on lab2 is one ClaudeBot GET / (302 to /unavailable, then 200 'This page is unavailable'). On the clean twin the same probe gets 200. The robots rows ('robots.txt is present', 'AI crawlers are not blocked') do not need a fetch, so the probe can only feed 'Not blocked by bot detection', which gives full marks. That states as fact the opposite of S4 (a UA-keyed block). This matches writesonic, where an untrue site-level 'cloaking: false' from a probe that could not see the fault is graded wrong (S6). The editor's doubt was about attribution; the request log settles it. If the editor keeps 'missed', the claim should at least go into other_false_claims_lab2, as writesonic's 'No WAF / network block' does.",
   "evidence": "'Not blocked by bot detection 6/6' (raw/hostinger_lab2.txt); log/per_tool.txt hostinger lab2: '1 302 / Mozilla/5.0 (compatible; ClaudeBot/1.0)' and '1 200 /unavailable Mozilla/5.0 (compatible; ClaudeBot/1.0)'; hostinger clean: '1 200 / Mozilla/5.0 (compatible; ClaudeBot/1.0)'"
  },
  {
   "tool": "aicrawlercheck",
   "fault": "S2",
   "from": "unreached",
   "to": "missed",
   "reason": "GRADING.md fixes reach by class: S1-S6 are 'home' (the homepage response for some agent), and 'a tool that only fetched the homepage and site files reaches home+file'. aicrawlercheck fetched /. geotoolbox, hostinger, swetrix and googlerrt are all graded this way; for example, geotoolbox S2 is missed although it never sent Applebot. ACC says nothing true or false about the Applebot fetch beyond a robots-only 'ALLOWED'. Points unchanged; reached 22->23.",
   "evidence": "GRADING.md: 'home (homepage response for some agent) ... A tool that only fetched the homepage and site files reaches home+file'; log/per_tool.txt aicrawlercheck lab2: '3 200 / Mozilla/5.0 (Windows NT 10.0; ...' (homepage fetched)"
  },
  {
   "tool": "knowatoa",
   "fault": "S6",
   "from": "unreached",
   "to": "missed",
   "reason": "Same class rule: knowatoa fetched / (25 agents), so all home faults count as reached. It sent no Googlebot UA, but geotoolbox S2 and hostinger/swetrix S1-S6 count as reached without the agent. Points unchanged; reached 12->13.",
   "evidence": "log/per_tool.txt knowatoa lab2: 25 GETs of '/'"
  },
  {
   "tool": "isitagentready",
   "fault": "S1,S2,S3,S4,S5,S6",
   "from": "unreached",
   "to": "missed",
   "reason": "Same class rule: it fetched / (5x AgentReadinessScanner, 2x HeadlessChrome). S7 stays unreached (/pricing never fetched). Points unchanged; reached 15->21.",
   "evidence": "log/per_tool.txt isitagentready lab2: '5 200 / Mozilla/5.0 (compatible; AgentReadinessScanner/1.0 ...'"
  },
  {
   "tool": "schemaorg",
   "fault": "S1,S2,S3,S4,S5,S6",
   "from": "unreached",
   "to": "missed",
   "reason": "Same class rule: it fetched / (Schema-Markup-Validator). Its grader already counts the other home faults (I5, J2, D1, L1, P5, T1, T4) as reached; S1-S6 were left out. Points unchanged; reached 7->13.",
   "evidence": "log/per_tool.txt schemaorg lab2: '1 200 / Mozilla/5.0 (compatible; Schema-Markup-Validator ...'"
  },
  {
   "tool": "writesonic",
   "fault": "S3",
   "from": "unreached",
   "to": "missed",
   "reason": "Same class rule: writesonic fetched /, and its grader counts S1, S2, S4, S5 and S6 as reached (graded wrong) without those agents' requests. 'OAI-SearchBot canRead: yes' is not the opposite of S3 (it does get a page, just a reduced one), so missed. Points unchanged.",
   "evidence": "raw/ws_lab2.json report.bots[1] {\"name\":\"OAI-SearchBot\",...,\"robots\":\"allow\",\"canRead\":\"yes\"}"
  },
  {
   "tool": "writesonic",
   "fault": "S7",
   "from": "unreached",
   "to": "missed",
   "reason": "S7 lives on /pricing, which writesonic fetched (as WritesonicBot). CrawlCheck is graded the same way: 'S7 page was reached but never as Bingbot' gives missed, not unreached. Points unchanged; with S3, reached 26->28.",
   "evidence": "log/per_tool.txt writesonic lab2: '1 200 /pricing WritesonicBot/1.0'; grades/crawlcheck.json S7 'Page reached, fault not tested'"
  }
 ],
 "inconsistencies": [
  "REACH RULE (biggest inconsistency; affects points_of_reached for 5 tools). Four graders apply GRADING.md's class rule: if the homepage was fetched, all home faults S1-S6 count as reached (geotoolbox, hostinger, swetrix, googlerrt). Five apply an agent-specific rule: a fault counts only if the tool sent that crawler's UA (aicrawlercheck S2, knowatoa S6, isitagentready S1-S6, schemaorg S1-S6, writesonic S3). Writesonic's grader then counts its wrongs on S1/S2/S4/S5/S6 as reached although it never sent those UAs. Fixed by the class-rule changes above.",
  "WRONG-BUT-UNREACHED: hostinger J1 is graded wrong yet left out of 'reached' (22). writesonic counts its wrongs as reached ('Wrong verdicts are counted in reached because the tool chose to state them'), and so do knowatoa and crawlcheck. Make hostinger reached 23 (see arithmetic).",
  "R6 'Sitemap not declared' for the relative line: aicrawlercheck, isitagentready, hostinger and swetrix are partial; crawlcheck was missed for the same 'none named' statement. Fixed above. geotoolbox, writesonic and googlerrt say nothing about the Sitemap line: missed is correct.",
  "S4 (ClaudeBot 302): crawlcheck is caught ('ClaudeBot | error | sent to /unavailable'). aicrawlercheck is partial ('1 redirect, ending at https://lab2.crawlcheck.io/unavailable' in a gate marked ok:true, status 'HTTP 200'). Both name the destination and neither shows the 302 code. The difference holds only because crawlcheck labels the row 'error' while ACC marks the redirect gate ok. Kept, but the write-up should say so. geotoolbox partial ('200 but only 4 words', no redirect named) and knowatoa partial ('success 302') are consistent with that line.",
  "knowatoa internal: S2 'success 200 OK' for Applebot is graded wrong, but S4 'success 302 Found' for ClaudeBot is graded partial, although 'success' is equally untrue for ClaudeBot. Defensible only because the 302 code makes the symptom visible. Note it in the write-up.",
  "Robots-only vs can-read claims are applied consistently. Graded missed: swetrix 'PerplexityBot | Yes', ACC geo 'AI search bots allowed in robots.txt ... may fetch this URL', ACC main 'effectiveStatus: ALLOWED' (permission wording; Applebot has no truthful counterpart in ACC either, but the wording is still permission), geotoolbox robots counts. Graded wrong: writesonic 'canRead: yes', a separate capability field next to 'robots: allow'. No change.",
  "D1 'JSON-LD absent' is partial everywhere it occurs (geotoolbox 'No JSON-LD schema.org markup found', writesonic 'Structured data (JSON-LD): none'). 'No valid JSON-LD' (hostinger), 'none of them valid JSON' (ACC geo), parse errors (googlerrt, schemaorg) and 'JSON-LD blocks that parse 0 of 1' (crawlcheck) are caught. Swetrix 'STRUCTURED DATA 1' on both hosts is missed. Consistent.",
  "D1 cascade claims are listed unevenly. crawlcheck ('Entity nodes 0 ... declares no local business') and hostinger ('No sameAs links in the structured data') list them as uncounted consequences of D1. aicrawlercheck's equivalents are not listed: acc_geo_lab2.json criteria[schema_entity] 'No Organization, LocalBusiness or Person node.' and acc_main recommendations[1] 'No Schema.org markup detected on your homepage' (only in notes). Add them to aicrawlercheck other_false_claims_lab2 as uncounted cascades. No score effect.",
  "Showing a value without calling it a problem: crawlcheck gets partial for its own rows (J1 /pricing 20% JS-only, J4, X5 36 of 38, L4 'Page not found' at 200). aicrawlercheck S3/S6 get missed for per-agent character counts (877 / 1,229 vs 1,138) that are never compared. The ACC rows need the reader to cross-compare 20 agents; the CC rows state the measure directly (a JS-only share, a count of dated URLs). Acceptable distinction, no change.",
  "Not a scored fault, but two tools contradict each other on lab2: aicrawlercheck 'http_redirect pass | HTTP 301 to https://lab2.crawlcheck.io/' vs crawlcheck 'stays on HTTP: http://lab2.crawlcheck.io (200)'. The log has only CrawlCheck's 4 plain-HTTP requests, all answered 200 by the worker. An edge redirect would not be logged, so ACC's 301 can be neither confirmed nor refuted from here. Neither is counted; leave both uncounted."
 ],
 "false_alarm_changes": [
  {
   "tool": "crawlcheck",
   "host": "lab2-clean",
   "action": "add (or merge)",
   "claim": "Phone numbers written on the page | 0 | 1 to 4 | Reported",
   "where": "raw/cc_r_kcys45p8jf.html 'Local' rows (line next to 'Telephone in the structured data ... ✓ Passed')",
   "truth": "(555) 014-2290 is printed in the homepage body and footer; the same report's 'Schema phone appears on the page | yes' and its claim table ('visible text, line 1 (bytes 1441–1455) | holds') contradict it",
   "note": "Same root mis-detection as the two phone false alarms already listed. Either count all three (the grader's per-claim convention) or merge them into one root cause. Do not count two of three."
  },
  {
   "tool": "crawlcheck",
   "host": "lab2",
   "action": "remove from other_false_claims_lab2",
   "claim": "ClaudeBot turned away by the site's robots rules",
   "reason": "Under the R1 correction ClaudeBot IS disallowed by robots.txt (reference parser, line 11), and the same grade sheet credits R1 as caught. The statement is true. It is incomplete (the server's 302 is not given as the cause) but not false."
  }
 ],
 "arithmetic": [
  {
   "tool": "hostinger",
   "issue": "score.reached = 22, but the verdicts give 23 non-unreached (J1 is 'wrong'); points_of_reached holds points (3.5), not a ratio",
   "fix": "Count J1 as reached (same as writesonic). With the S4 change: caught 3, partial 1, points 3.5, wrong 2, reached 23, points_of_reached 0.152"
  },
  {
   "tool": "geotoolbox",
   "issue": "points_of_reached holds 7.0 (points) instead of a ratio; the _pct field is right",
   "fix": "points_of_reached 0.233 (7/30)"
  },
  {
   "tool": "googlerrt",
   "issue": "points_of_reached holds 1.0 instead of a ratio",
   "fix": "points_of_reached 0.05 (1/20)"
  },
  {
   "tool": "swetrix",
   "issue": "points_of_reached holds 1.5 instead of a ratio",
   "fix": "points_of_reached 0.068 (1.5/22)"
  },
  {
   "tool": "crawlcheck",
   "issue": "after R6 -> partial",
   "fix": "caught 26, partial 14, points 33.0, wrong 1, reached 63, points_of_reached 0.524"
  },
  {
   "tool": "aicrawlercheck",
   "issue": "after S2 -> missed",
   "fix": "points 9.5, reached 23, points_of_reached 0.413"
  },
  {
   "tool": "knowatoa",
   "issue": "after S6 -> missed",
   "fix": "points 2.5, reached 13, points_of_reached 0.192"
  },
  {
   "tool": "isitagentready",
   "issue": "after S1-S6 -> missed",
   "fix": "points 2.0, reached 21, points_of_reached 0.095"
  },
  {
   "tool": "schemaorg",
   "issue": "after S1-S6 -> missed",
   "fix": "points 1.0, reached 13, points_of_reached 0.077"
  },
  {
   "tool": "writesonic",
   "issue": "after S3, S7 -> missed",
   "fix": "points 0.5, wrong 7, reached 28, points_of_reached 0.018"
  },
  {
   "tool": "all others",
   "issue": "none",
   "fix": "points = caught + 0.5*partial holds for every file as submitted"
  }
 ],
 "confirmed_ok": "I searched the raw outputs for every quoted caught/partial/wrong string. All are present: whitespace and table-cell joins differ for CC L2/L3, geotoolbox R1 joins three lines with '·', and IAR R1 omits the 'rules' field. None is a misquote. Each one matches the server code: ClaudeBot/Googlebot-Image group merge at line 11; 403/429/302/challenge by UA; the trailing comma in the homepage JSON-LD (schemaorg offset 1064); nosnippet; llms.txt with no H1 and the /warranty and /financing 404s; the /services/gutter-guards noindex; X-Robots-Tag on /pricing; canonical-elsewhere on downspout-repair and riverbend; the redirect chain and loop; the orphan lakeside; the /careers missing title; duplicate Millbrook titles; two h1 on ice-dam-prevention; three images with no alt; sid cookie without Secure/HttpOnly; no HSTS on lab2. CrawlCheck borderlines checked and kept: I7 wrong (the '/about | self' canonical vs http://), S3/S6/I3/I6/J1/J2/J4/L1/L2/L4/X5/X6/M5 partial, S4 caught. Writesonic's 7 wrongs and knowatoa's S2 wrong stand on the canRead / 'ability to read' wording. Clean-twin false alarms: CrawlCheck's three listed claims are untrue (the phone is printed on the page; every sitemap entry has a lastmod); the HSTS-preload title is correctly left unverifiable (the clean header lacks 'preload'). No other tool makes an untrue claim on lab2-clean in the captured output. No under-credit found in the searched outputs: there is no evidence for P5, T1, T4, I5, L1 or X1 in any non-CC tool that missed them."
}