provenance:programming:crawler:webxray:random_sample
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| provenance:programming:crawler:webxray:random_sample [2026/09/11 16:08] – Reverse the kameleoon.io demotion: the quote is a <script src> line and both citation checks are blind to markup by construction. owner_verify_sources.py gains OK-MARKUP; corrections script and mutation harness updated. Authored by Claude karel.kubicek.claude | provenance:programming:crawler:webxray:random_sample [2026/09/17 07:24] (current) – Markup rendering sweep: wrap escapes, CLI flag typography, and heading quotes. Authored by Claude. karel.kubicek.claude | ||
|---|---|---|---|
| Line 7: | Line 7: | ||
| **inter-rater re-adjudication of 2026-09-11** (sections AA–AK), which measured | **inter-rater re-adjudication of 2026-09-11** (sections AA–AK), which measured | ||
| how far a second model reading the same brief reaches the same verdicts. | how far a second model reading the same brief reaches the same verdicts. | ||
| + | |||
| + | The **second pass over the residue**, run later the same day — which | ||
| + | re-adjudicated the 40 domains this sample could not settle, moved 28 of them | ||
| + | into the rates, and changed the content page's headline table and its Fisher | ||
| + | table — has its own appendix, for exactly the reason given below: | ||
| + | [[provenance: | ||
| + | 343 kB, most of it one probe log, and adding them here would have made this page | ||
| + | 628 kB. That page computes both figures when it is generated, from the same file | ||
| + | lists both manifests use. | ||
| Read [[provenance: | Read [[provenance: | ||
| Line 17: | Line 26: | ||
| AF–AK their outputs. | AF–AK their outputs. | ||
| - | **Why this is a separate page.** These blocks are 274 kB, and the parent | + | **Why this is a separate page.** These blocks are 290 kB, and the parent |
| provenance page is already large without them. Raw size alone would not decide | provenance page is already large without them. Raw size alone would not decide | ||
| it — '' | it — '' | ||
| Line 48: | Line 57: | ||
| | C | '' | | C | '' | ||
| | D | '' | | D | '' | ||
| - | | E | '' | + | | E | '' |
| | F | '' | | F | '' | ||
| - | | G | '' | + | | G | '' |
| | H | '' | | H | '' | ||
| | I | '' | | I | '' | ||
| Line 61: | Line 70: | ||
| | P | '' | | P | '' | ||
| | Q | '' | | Q | '' | ||
| - | | R | '' | + | | R | '' |
| | S | '' | | S | '' | ||
| | T | '' | | T | '' | ||
| Line 71: | Line 80: | ||
| | Z | '' | | Z | '' | ||
| | AA | '' | | AA | '' | ||
| - | | AB | '' | + | | AB | '' |
| | AC | '' | | AC | '' | ||
| - | | AD | '' | + | | AD | '' |
| - | | AE | '' | + | | AE | '' |
| | AF | '' | | AF | '' | ||
| | AG | '' | | AG | '' | ||
| | AH | '' | | AH | '' | ||
| | AI | '' | | AI | '' | ||
| - | | AJ | '' | + | | AJ | '' |
| - | | AK | '' | + | | AK | '' |
| ===== A. scripts/ | ===== A. scripts/ | ||
| Line 603: | Line 612: | ||
| s = re.sub(r" | s = re.sub(r" | ||
| return re.sub(r" | return re.sub(r" | ||
| + | |||
| + | |||
| + | def decode(body): | ||
| + | """ | ||
| + | |||
| + | Decoding as UTF-8 with ``errors=" | ||
| + | windows-1251 or shift_jis page into U+FFFD, so a Cyrillic or Japanese quote | ||
| + | can never match and the row is reported NOTFOUND -- a fabrication verdict on | ||
| + | a correct citation. The normaliser was already fixed once for this class of | ||
| + | bug (it used to delete non-Latin letters); this is the same bug one layer | ||
| + | earlier, found on 2026-09-11 when ``help.i.ua/ | ||
| + | page declares ``charset=windows-1251`` and every Ukrainian word in it | ||
| + | decoded to replacement characters. | ||
| + | |||
| + | Sniffed from the bytes rather than from the HTTP header, because the header | ||
| + | is not cached -- the cache holds the body alone. If nothing is declared, or | ||
| + | the declared codec is unknown, the old behaviour is the fallback. | ||
| + | """ | ||
| + | m = re.search(rb""" | ||
| + | if m: | ||
| + | enc = m.group(1).decode(" | ||
| + | if enc.lower().replace(" | ||
| + | try: | ||
| + | return body.decode(enc) | ||
| + | except (LookupError, | ||
| + | pass | ||
| + | return body.decode(" | ||
| Line 623: | Line 659: | ||
| except Exception as e: | except Exception as e: | ||
| return None, f"tls: {e}" | return None, f"tls: {e}" | ||
| + | os.makedirs(cache, | ||
| + | open(path, " | ||
| + | return out, None | ||
| + | if url.startswith(" | ||
| + | # The 2026-09-11 second pass over the residue cites registry and | ||
| + | # registrar WHOIS records, which are not reachable over HTTP: the | ||
| + | # protocol is a port-43 socket. Without this branch every such row | ||
| + | # FETCHFAILs, which reads as an unverifiable citation when in fact the | ||
| + | # checker had no client. The URL is `whois://< | ||
| + | # query is re-issued against the *named server*, not re-resolved | ||
| + | # through IANA, so the row is checked against the record it cites. | ||
| + | rest = url[len(" | ||
| + | server, _, dom = rest.partition("/" | ||
| + | if not dom: | ||
| + | return None, " | ||
| + | sys.path.insert(0, | ||
| + | import time | ||
| + | import whois43 | ||
| + | out = b"" | ||
| + | # Two attempts, 20 s apart. One retry is enough: the observed limit is a | ||
| + | # burst limit, and both stubs in the first run answered correctly 25 s | ||
| + | # later. Retrying forever would turn a registrar' | ||
| + | err = None | ||
| + | for attempt in range(2): | ||
| + | if attempt: | ||
| + | time.sleep(20) | ||
| + | try: | ||
| + | out = whois43.ask(server, | ||
| + | except Exception as e: | ||
| + | # A raised exception is a transport failure too -- a reset or a | ||
| + | # timeout is exactly what a rate limiter does -- so it must take | ||
| + | # the retry as well. It did not until 2026-09-11, when a review | ||
| + | # pass read the comment above ("two attempts, 20 s apart" | ||
| + | # then read the code, which returned on the first exception. | ||
| + | err, out = e, b"" | ||
| + | continue | ||
| + | err = None | ||
| + | if len(out) >= 400 and dom.encode().lower() in out.lower(): | ||
| + | break | ||
| + | if err is not None: | ||
| + | return None, f" | ||
| + | # Registrar WHOIS servers rate-limit. MarkMonitor answers the fourth | ||
| + | # rapid query with a stub that has no registrant block, and caching that | ||
| + | # stub turns a correct citation into a permanent NOTFOUND: it happened | ||
| + | # to ``sa-as.com`` and ``blogblog.com`` in this script' | ||
| + | # 2026-09-11, and both verified on a retry 25 seconds later. A short or | ||
| + | # domain-less answer is a transport failure, not evidence, so it is | ||
| + | # neither returned nor cached. | ||
| + | if len(out) < 400 or dom.encode().lower() not in out.lower(): | ||
| + | return None, (f" | ||
| + | f" | ||
| os.makedirs(cache, | os.makedirs(cache, | ||
| open(path, " | open(path, " | ||
| Line 674: | Line 761: | ||
| " | " | ||
| continue | continue | ||
| - | hay = norm(body.decode(" | + | hay = norm(decode(body)) |
| nq = norm(quote) | nq = norm(quote) | ||
| # A quote the adjudicator elided ("A ... B") is checked fragment by | # A quote the adjudicator elided ("A ... B") is checked fragment by | ||
| Line 686: | Line 773: | ||
| # strip for every row would let a quote match an HTML comment or an | # strip for every row would let a quote match an HTML comment or an | ||
| # attribute it was never on. | # attribute it was never on. | ||
| - | raw_hay = norm(body.decode(" | + | raw_hay = norm(decode(body), strip_tags=False) |
| if not nq: | if not nq: | ||
| status = " | status = " | ||
| Line 785: | Line 872: | ||
| </ | </ | ||
| - | ===== G. scripts/ | + | ===== G. scripts/ |
| Every row a machine flagged and a human then re-checked, with the action taken and why: '' | Every row a machine flagged and a human then re-checked, with the action taken and why: '' | ||
| Line 803: | Line 890: | ||
| quotation marks" failure, and it happened in 4 of 175 rows despite an | quotation marks" failure, and it happened in 4 of 175 rows despite an | ||
| instruction that said verbatim -- which is the argument for verifying every | instruction that said verbatim -- which is the argument for verifying every | ||
| - | citation by machine rather than trusting the adjudicator. | + | citation by machine rather than trusting the adjudicator. A fifth requote, |
| + | ``compass-fit.jp``, | ||
| + | dropped character. It is counted with them because the remedy is the same, | ||
| + | not because it is the same defect. It was | ||
| + | found on 2026-09-11 and applied on 2026-09-17, by re-running a *fixed* | ||
| + | checker over rows the broken one had passed: ``norm()`` used to delete every non-Latin character, | ||
| + | so ``compass-fit.jp``' | ||
| + | letters ``COMPASS-FIT`` it happens to contain. A checker that cannot see a | ||
| + | script cannot police it, and every row it passed in that script is | ||
| + | unverified until it is re-run. | ||
| * ``resource`` -- the cited URL was the wrong page (a 404 template, a | * ``resource`` -- the cited URL was the wrong page (a 404 template, a | ||
| JS-only route, a blocked host). A different primary source for the same | JS-only route, a blocked host). A different primary source for the same | ||
| Line 873: | Line 969: | ||
| " | " | ||
| "error needs a source naming the real owner and there is none." | "error needs a source naming the real owner and there is none." | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "found 2026-09-11, applied 2026-09-17. The norm() character-class fix " | ||
| + | "of 2026-09-11 was re-run that day over all 175 rows of the first " | ||
| + | " | ||
| + | "Latin letters COMPASS-FIT its Japanese quote happens to contain. The " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "the operating subsidiary together, which is what owner_today = " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| } | } | ||
| Line 925: | Line 1042: | ||
| </ | </ | ||
| - | ===== H. scripts/ | + | ===== H. scripts/ |
| The methodological correction made during the run, kept rather than smoothed away. Each of the 26 re-scores carries the entity string, the adjudicated owner and the reason. | The methodological correction made during the run, kept rather than smoothed away. Each of the 26 re-scores carries the entity string, the adjudicated owner and the reason. | ||
| Line 2156: | Line 2273: | ||
| ===== R. Unedited output: scripts/ | ===== R. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| - | 10 corrections applied to 175 rows, after machine verification of every cited source: demote=1, requote=4, resource=5 | + | 11 corrections applied to 175 rows, after machine verification of every cited source: demote=1, requote=5, resource=5 |
| twiago.com | twiago.com | ||
| Line 2210: | Line 2327: | ||
| was quote : Condé Nast Entertainment | was quote : Condé Nast Entertainment | ||
| now source: None | now source: None | ||
| + | |||
| + | compass-fit.jp | ||
| + | was source: https:// | ||
| + | was quote : 台湾国内でのネイティブ型広告配信サービス「COMPASS-FIT」の提供を開始致しました。 | ||
| + | now source: https:// | ||
| wrote out/ | wrote out/ | ||
| Line 2216: | Line 2338: | ||
| ===== S. Unedited output: scripts/ | ===== S. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| Line 2317: | Line 2439: | ||
| ===== V. Unedited output: scripts/ | ===== V. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| Line 4523: | Line 4645: | ||
| ALL all 35 rows, raw vocabulary. | ALL all 35 rows, raw vocabulary. | ||
| | | ||
| - | NAMED rows where the list file names an owner, i.e. neither rater is | + | NAMED rows where the list file names an owner. |
| - | forced to `absent`. | + | from the list file by `owner_merge_rows.py` rather than judged, so |
| - | asks a rater to judge. | + | the two raters' |
| + | | ||
| + | the equivalence rather than assuming it. | ||
| + | | ||
| + | move between 2026-08-17 and 2026-09-05 -- the sets could diverge, | ||
| + | and an `or` selection would then silently pair a real verdict | ||
| + | | ||
| ADJUDICABLE | ADJUDICABLE | ||
| | | ||
| Line 4548: | Line 4676: | ||
| kappa = (po - pe) / (1 - pe), pe = sum_c p1(c) * p2(c) | kappa = (po - pe) / (1 - pe), pe = sum_c p1(c) * p2(c) | ||
| - | Intervals are bootstrap percentiles over **domains** | + | Intervals are bootstrap percentiles over **domains** |
| + | over **cells** for the pooled rows -- a pooled row has already flattened the | ||
| + | three lists' verdicts into one sequence, so its resample can draw two cells of | ||
| + | one domain independently and its interval is narrower than it should be. That is | ||
| + | stated on the page as well. 10,000 resamples, | ||
| normal approximation: | normal approximation: | ||
| variance is not usable. | variance is not usable. | ||
| Line 4577: | Line 4709: | ||
| po = sum(1 for a, b in pairs if a == b) / n | po = sum(1 for a, b in pairs if a == b) / n | ||
| m1, m2 = Counter(a for a, _ in pairs), Counter(b for _, b in pairs) | m1, m2 = Counter(a for a, _ in pairs), Counter(b for _, b in pairs) | ||
| - | pe = sum(m1[c] / n * m2[c] / n for c in set(m1) | set(m2)) | + | |
| + | # so the float addition order varied between runs and the JSON artefact' | ||
| + | # 15th-16th decimal moved. No published 3-decimal figure ever changed, but a | ||
| + | # " | ||
| + | | ||
| if abs(1 - pe) < 1e-12: | if abs(1 - pe) < 1e-12: | ||
| return {" | return {" | ||
| Line 4615: | Line 4751: | ||
| s += f" | s += f" | ||
| return s | return s | ||
| + | |||
| + | |||
| + | def assert_absent_agrees(doms, | ||
| + | """ | ||
| + | exactly the same cells. Every population below is selected with `or`, which | ||
| + | is only equivalent to the docstring' | ||
| + | while that holds. Checked, not assumed.""" | ||
| + | bad = [(d, lab, R1[d][key], R2[d][key]) | ||
| + | for lab, key in LISTS for d in doms | ||
| + | if (R1[d][key] == " | ||
| + | if bad: | ||
| + | lines = " | ||
| + | raise SystemExit( | ||
| + | f" | ||
| + | "from the list file, not judged, so this means the two raters were " | ||
| + | " | ||
| + | "would be wrong.\n" | ||
| + | print(f" | ||
| + | "list file, not a judgement) -- the NAMED/ | ||
| Line 4635: | Line 4790: | ||
| if missing: | if missing: | ||
| raise SystemExit(f" | raise SystemExit(f" | ||
| + | assert_absent_agrees(doms, | ||
| print(" | print(" | ||
| Line 5086: | Line 5242: | ||
| ap.add_argument(" | ap.add_argument(" | ||
| ap.add_argument(" | ap.add_argument(" | ||
| + | ap.add_argument(" | ||
| + | help=" | ||
| + | " | ||
| + | " | ||
| args = ap.parse_args() | args = ap.parse_args() | ||
| Line 5120: | Line 5280: | ||
| fails.append(f" | fails.append(f" | ||
| " | " | ||
| + | |||
| + | # Figures that live only in the scripts' | ||
| + | # A reviewer mutated each of these on the page and this guard survived every | ||
| + | # one, because it only ever read the JSON. They are parsed out of the two | ||
| + | # committed outputs so they are checked against the tool, not retyped here. | ||
| + | kout = out | ||
| + | vout = open(args.verify_output, | ||
| + | |||
| + | def need(label, value, where): | ||
| + | counter[0] += 1 | ||
| + | if value not in where: | ||
| + | fails.append(f" | ||
| + | |||
| + | # section 0: how many cells each rater scored `self-named` | ||
| + | m = re.search(r" | ||
| + | r"both on the same cell (\d+)", | ||
| + | if not m: | ||
| + | fails.append(" | ||
| + | else: | ||
| + | r1, r2, both = m.groups() | ||
| + | # Bare digits are useless needles -- " | ||
| + | # are deliberately coupled to the sentence' | ||
| + | # sentence breaks the guard instead of silently unchecking the figure. | ||
| + | # Checked wherever the sentence SHAPE occurs, on every page, rather | ||
| + | # than as a needle in the union: both pages carry this sentence, so | ||
| + | # corrupting one copy left the other to satisfy a union check. | ||
| + | counter[0] += 1 | ||
| + | seen = 0 | ||
| + | for role, text in sorted(pages.items()): | ||
| + | for mm in re.finditer(r" | ||
| + | seen += 1 | ||
| + | if mm.group(1) != r2: | ||
| + | fails.append(f" | ||
| + | | ||
| + | if not seen: | ||
| + | fails.append(" | ||
| + | need(" | ||
| + | need(" | ||
| + | |||
| + | # section 2b: the sourcing-vs-reading split, per list and pooled | ||
| + | for line in kout.splitlines(): | ||
| + | m = re.match(r" | ||
| + | | ||
| + | | ||
| + | if m: | ||
| + | lab, n, ag, so, rd = m.groups() | ||
| + | # Whole-row match, not four bare digits: " | ||
| + | # over these pages, so per-cell needles passed a mutated table. | ||
| + | name = " | ||
| + | row = (r" | ||
| + | | ||
| + | | ||
| + | counter[0] += 1 | ||
| + | if not re.search(row, | ||
| + | fails.append(f" | ||
| + | | ||
| + | m = re.search(r" | ||
| + | if not m: | ||
| + | fails.append(" | ||
| + | else: | ||
| + | need(" | ||
| + | |||
| + | # section 5: row-level resolution counts | ||
| + | m = re.search(r" | ||
| + | r" | ||
| + | if not m: | ||
| + | fails.append(" | ||
| + | else: | ||
| + | a, n, b, same = m.groups() | ||
| + | need(" | ||
| + | need(" | ||
| + | need(" | ||
| + | |||
| + | # section 6: the unweighted current-share table, both raters, all three lists | ||
| + | for line in kout.splitlines(): | ||
| + | m = re.match(r" | ||
| + | | ||
| + | if m: | ||
| + | lab, a, b = m.groups() | ||
| + | need(f" | ||
| + | need(f" | ||
| + | |||
| + | # the citation tally, from owner_verify_sources.py' | ||
| + | m = re.search(r" | ||
| + | if not m: | ||
| + | fails.append(" | ||
| + | else: | ||
| + | for part in m.group(1).split(", | ||
| + | k_, v_ = part.split(" | ||
| + | need(f" | ||
| # A three-decimal value in kappa context that neither JSON produces is a | # A three-decimal value in kappa context that neither JSON produces is a | ||
| Line 5170: | Line 5420: | ||
| JC=out/ | JC=out/ | ||
| OUT=out/ | OUT=out/ | ||
| + | VOUT=out/ | ||
| TMP=$(mktemp -d) | TMP=$(mktemp -d) | ||
| fails=0 | fails=0 | ||
| Line 5188: | Line 5439: | ||
| else roles=(--page " | else roles=(--page " | ||
| if python3 scripts/ | if python3 scripts/ | ||
| - | | + | |
| + | | ||
| echo " | echo " | ||
| fails=$((fails+1)) | fails=$((fails+1)) | ||
| Line 5213: | Line 5465: | ||
| mutate "prov: current-vs-rest agreement 87.2% -> 88.2%" | mutate "prov: current-vs-rest agreement 87.2% -> 88.2%" | ||
| mutate "prov: invent an unsourced kappa of 0.842" | mutate "prov: invent an unsourced kappa of 0.842" | ||
| + | # Figures that live only in the scripts' | ||
| + | # one of these and the guard survived, because it only read the kappa JSON. | ||
| + | mutate "prov: self-named rater2 6 -> 9 cells" | ||
| + | mutate "prov: split table sourcing 17 -> 20" | ||
| + | mutate "prov: sourcing headline 63.0% -> 70.0%" | ||
| + | mutate "prov: rater-1 unresolved 10 of 35 -> 13 of 35" | ||
| + | mutate "prov: webXray current-share 70.0% (7\/ | ||
| + | mutate "prov: citation tally OK 30 -> OK 32" | ||
| mutate "page: pooled kappa +0.400 -> +0.410" | mutate "page: pooled kappa +0.400 -> +0.410" | ||
| mutate "page: Disconnect NAMED kappa +0.460 -> +0.461" | mutate "page: Disconnect NAMED kappa +0.460 -> +0.461" | ||
| Line 5227: | Line 5487: | ||
| ===== AF. Unedited output: scripts/ | ===== AF. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| Line 5251: | Line 5511: | ||
| ===== AG. Unedited output: scripts/ | ===== AG. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| Line 5263: | Line 5523: | ||
| ===== AH. Unedited output: scripts/ | ===== AH. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| Line 5303: | Line 5563: | ||
| ===== AI. Unedited output: scripts/ | ===== AI. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| Line 5319: | Line 5579: | ||
| ===== AJ. Unedited output: scripts/ | ===== AJ. Unedited output: scripts/ | ||
| - | Run as '' | + | Run as '' |
| < | < | ||
| + | `absent` agrees on all 105 cells (it is a fact about the list file, not a judgement) -- the NAMED/ | ||
| ============================================================================== | ============================================================================== | ||
| INTER-RATER AGREEMENT, OWNERSHIP ADJUDICATION | INTER-RATER AGREEMENT, OWNERSHIP ADJUDICATION | ||
| Line 5527: | Line 5788: | ||
| caught | caught | ||
| caught | caught | ||
| + | caught | ||
| + | caught | ||
| + | caught | ||
| + | caught | ||
| + | caught | ||
| + | caught | ||
| caught | caught | ||
| caught | caught | ||
| Line 5538: | Line 5805: | ||
| * [[provenance: | * [[provenance: | ||
| * [[programming: | * [[programming: | ||
| + | * [[provenance: | ||
| * [[literature: | * [[literature: | ||
| + | |||
| + | ===== Markup sweep, 2026-09-17 ===== | ||
| + | |||
| + | Mechanical rendering repair only: a fresh live raw/XHTML export of 188 pages was checked with '' | ||
provenance/programming/crawler/webxray/random_sample.1789142894.txt.gz · Last modified: by karel.kubicek.claude
