User Tools

Site Tools


provenance:design:platforms

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
provenance:design:platforms [2026/09/22 01:22] – Append the 145-paper crawlConfig.authentication audit: 6 contradicted / 12 affirmed / 8 ambiguous / 119 silent, with per-paper deciding sentences. Authored by Claude karel.kubicek.claudeprovenance:design:platforms [2026/09/22 01:33] (current) – Embed the real scripts/auth145_audit.mjs in the provenance log, per the page's convention. Authored by Claude karel.kubicek.claude
Line 4879: Line 4879:
 </code> </code>
  
-**How the candidate set was built, and what its recall rests on.** The 145 were re-derived inside ''%%scripts/auth145_audit.mjs%%'' with the same four signals ''%%platforms_report.mjs%%'' uses, and the script **throws** if the platform-subject-with-crawlConfig population is not 260 or the ''%%none%%'' count is not 145. Five paired probes then ran over the paper text (''%%--ctx%%''), each with a tight and a loose form, and the script **throws** if any tight probe returns more matches than its loose form in the same file — the first version did, on //Cookies That Give You Away//, which is why the guard exists. Stage 3 (''%%--buckets%%'') sorted all 145 into three candidate sets: papers with a first-person register/login sentence (**64**), papers with an explicit no-authentication sentence (**11**), and the residue where neither fires (**70**). Every paper in the first two sets — 75 papers — was read by hand and given a verdict. The 119 **silent** are the papers with no first-person statement about their own authentication posture in either direction; they were **not** individually read, and the claim about them is exactly that: the probes found nothing to read. Recall therefore rests on the C probe being loose (it fires on any ''%%our ... account%%''), which is why it returned 64 candidates of which most were topical noise about the papers' //subjects// rather than their methods.+**How the candidate set was built, and what its recall rests on.** The 145 were re-derived inside ''%%scripts/auth145_audit.mjs%%'' with the same four signals ''%%platforms_report.mjs%%'' uses, and the script **throws** if the platform-subject-with-crawlConfig population is not 260 or the ''%%none%%'' count is not 145. Five paired probes then ran over the paper text (''%%--ctx%%''), each with a tight and a loose form, and the script **throws** if any tight probe returns more matches than its loose form in the same file — the first version did, on //Cookies That Give You Away//, which is why the guard exists. Stage 3 (''%%--buckets%%'') sorted all 145 into three candidate sets: papers with a first-person register/login sentence (**64**), papers with an explicit no-authentication sentence (**11**), and the residue where neither fires (**70**). **This sentence was wrong when first published, and a figures review caught it.** It said the pool read by hand was stage 3's A and C sets, 75 papers. It was not: the pool is the **union** of stage 2's strong first-person list and stage 3's A and C sets, and the review found the proof — //A View into YouTube View Fraud// carries a verdict below while sitting in stage 3's **B residue**, because stage 2 surfaced it and stage 3 did not. The script now computes that union itself and ''%%--verdicts%%'' **throws** if any adjudicated paper falls outside it (mutation-tested: disabling the stage 2 term makes it throw on exactly that paper). Its real output: **97 of 145** papers in the pool, **26** given a verdict, **71** read and left silent, and **48** that never surfaced a sentence to read. So the 119 **silent** are 71 papers read and found to say nothing about their own authentication posture plus 48 that no probe reached — not, as first published, 119 papers none of which were read. Recall therefore rests on the C probe being loose (it fires on any ''%%our ... account%%''), which is why it returned 64 candidates of which most were topical noise about the papers' //subjects// rather than their methods.
  
 **What changed.** [[design:platforms]]'s logged-out-surface bullet now carries the audit result in place of "We did not audit these 145". The schema counts (145 / 22 / 4 / 0, and the 55.8%) are **unchanged** — this is a precision audit published alongside the labels, in the same style as the Meta / Twitter-X / TikTok / Amazon subject-fold audits higher up this page. No number was hand-edited and no label was rewritten in ''%%extractions.jsonl%%''. **What changed.** [[design:platforms]]'s logged-out-surface bullet now carries the audit result in place of "We did not audit these 145". The schema counts (145 / 22 / 4 / 0, and the 55.8%) are **unchanged** — this is a precision audit published alongside the labels, in the same style as the Meta / Twitter-X / TikTok / Amazon subject-fold audits higher up this page. No number was hand-edited and no label was rewritten in ''%%extractions.jsonl%%''.
  
 **Two of the six read as contradicting their own object.** //A Large-scale Investigation into Geodifferences in Mobile Apps// (USENIX Security 2022) has a ''%%crawlConfig%%'' quote about the Selenium privacy-policy scraper while the study's actual download apparatus is ten Google Pixel 2 phones, one Google account per country, with an apk scraper that "requires the device's ID and Google account credentials". //Experimental Analyses of the Physical Surveillance Risks in Client-Side Content Scanning// (NDSS 2024) is scored ''%%none%%'' while its own ''%%crawlConfig.evidence.quote%%'' reads "we scrape user photographs from Instagram using an alias account" — the label contradicts the quote it was stored with. **Two of the six read as contradicting their own object.** //A Large-scale Investigation into Geodifferences in Mobile Apps// (USENIX Security 2022) has a ''%%crawlConfig%%'' quote about the Selenium privacy-policy scraper while the study's actual download apparatus is ten Google Pixel 2 phones, one Google account per country, with an apk scraper that "requires the device's ID and Google account credentials". //Experimental Analyses of the Physical Surveillance Risks in Client-Side Content Scanning// (NDSS 2024) is scored ''%%none%%'' while its own ''%%crawlConfig.evidence.quote%%'' reads "we scrape user photographs from Instagram using an alias account" — the label contradicts the quote it was stored with.
 +
 +==== scripts/auth145_audit.mjs ====
 +
 +The committed file, verbatim.
 +
 +<file javascript auth145_audit.mjs>
 +// Audit of the 145 platform-subject papers labelled crawlConfig.authentication='none'.
 +//
 +// Filed by design:platforms itself: "We did not audit these 145. Treat the
 +// authentication shape as real and the precise share as unverified."
 +//
 +// The label cannot be checked against its own evidence quote, because
 +// crawlConfig carries ONE evidence quote for the whole object (see
 +// programming:registration). So this script does not read the quote as
 +// evidence. It reads the PAPER TEXT.
 +//
 +//   node scripts/auth145_audit.mjs            # probe + candidate list
 +//   node scripts/auth145_audit.mjs --ctx <n>  # printed context window (default 260)
 +//   node scripts/auth145_audit.mjs --control  # random sample of non-firing papers
 +//
 +// The paper population is re-derived with the same four signals
 +// platforms_report.mjs uses; the counts are asserted against that script's
 +// published output (260 with a crawlConfig object, 145 labelled 'none') and
 +// this script throws if either moves.
 +
 +import fs from 'node:fs';
 +import path from 'node:path';
 +import { loadExtractions, isSentinel, dataRoot } from './lib.mjs';
 +import { tagsOf, roleOf, familiesOf, ANY_PLATFORM } from './plat_fold.mjs';
 +
 +const argv = process.argv.slice(2);
 +const has = (f) => argv.includes(f);
 +const val = (f, d) => (argv.indexOf(f) >= 0 ? argv[argv.indexOf(f) + 1] : d);
 +const CTX = Number(val('--ctx', 260));
 +const pct = (n, d) => `${((100 * n) / d).toFixed(1)}%`;
 +
 +const rows = loadExtractions();
 +const key = (r) => `${r.venue}/${r.year}/${r.slug}`;
 +const SIGNALS = {
 +  title: (r) => [r.title],
 +  sourceList: (r) => (r.population || []).map((p) => p.sourceList),
 +  phenomenon: (r) => (r.detection || []).map((d) => d.phenomenon),
 +  tool: (r) =>
 +    (r.tools || [])
 +      .filter((t) => t.usedOrMentioned === 'used' || t.usedOrMentioned === 'produced')
 +      .map((t) => t.name),
 +};
 +
 +const perPaper = new Map();
 +for (const r of rows) {
 +  const k = key(r);
 +  const fams = new Map();
 +  for (const [sig, get] of Object.entries(SIGNALS)) {
 +    for (const raw of get(r)) {
 +      if (!raw || isSentinel(raw)) continue;
 +      const s = String(raw);
 +      if (sig === 'sourceList' && (r.participants || []).length > 0) continue;
 +      for (const { family, role } of tagsOf(s)) {
 +        if (role !== 'subject') continue;
 +        if (!fams.has(family)) fams.set(family, new Set());
 +        fams.get(family).add(sig);
 +      }
 +    }
 +  }
 +  if (fams.size) perPaper.set(k, fams);
 +}
 +const byKey = new Map(rows.map((r) => [key(r), r]));
 +const anySubject = [...perPaper.keys()].map((k) => byKey.get(k));
 +const platCrawled = anySubject.filter((r) => r.crawlConfig !== null);
 +const none = platCrawled.filter((r) => r.crawlConfig.authentication === 'none');
 +
 +// Guard: this script's population must be platforms_report.mjs's population.
 +if (platCrawled.length !== 260)
 +  throw new Error(`population drift: platform-subject papers with a crawlConfig object = ${platCrawled.length}, platforms_report.mjs publishes 260`);
 +if (none.length !== 145)
 +  throw new Error(`population drift: authentication='none' = ${none.length}, platforms_report.mjs publishes 145`);
 +
 +// ------------------------------------------------------------------ full text
 +// paper.cols.txt de-columns but leaves ~a quarter of pages spliced; paper.txt
 +// is the raw pypdf render. Read BOTH and probe the union, whitespace collapsed,
 +// because a PDF line break inside a phrase silently undercounts.
 +const collapse = (s) => s.replace(/­/g, '').replace(/-\s*\n\s*/g, '').replace(/\s+/g, ' ');
 +function textOf(r) {
 +  const dir = path.join(dataRoot(), 'fulltext', String(r.year), r.venue, r.slug);
 +  const out = [];
 +  for (const f of ['paper.cols.txt', 'paper.txt', 'paper.norm.txt']) {
 +    const p = path.join(dir, f);
 +    if (!fs.existsSync(p)) continue;
 +    const raw = fs.readFileSync(p, 'latin1');
 +    if (raw.length === 0) continue;
 +    out.push({ file: f, text: collapse(raw) });
 +  }
 +  return out;
 +}
 +
 +// ------------------------------------------------------------------- probes
 +// Two widths per concern. The tight probe must return a subset of the loose
 +// one; the script asserts tight <= loose per paper and prints both.
 +const PROBES = [
 +  ['created-account', /\b(creat(?:e|ed|ing)|register(?:ed|ing)?|set up|made)\s+(?:\w+\s+){0,4}(accounts?|profiles?|personas?|sock[- ]?puppets?|bot accounts?)\b/i,
 +                      /(account|profile|persona|sock)/i],
 +  ['logged-in',       /\b(logged[- ]?in|log(?:ged)?\s+into|sign(?:ed|ing)?[- ]in(?:to)?|authenticated (?:session|crawl|user|request)|login(?:s|ed)? (?:to|into)|after (?:the )?login)\b/i,
 +                      /(log|sign|authenticat)/i],
 +  ['credentials',     /\b(credentials?|username and password|password(?:ed)? account|session cookies?|auth(?:entication)? tokens?|access tokens?|OAuth)\b/i,
 +                      /(credential|password|cookie|token|oauth|username)/i],
 +  ['login-wall',      /\b(login[- ]wall|behind a login|requires? (?:a )?(?:login|an account|sign[- ]?up)|logged[- ]?out (?:view|surface|user))\b/i,
 +                      /(log|account|sign)/i],
 +  ['sso',             /\b(single sign[- ]on|SSO|sign in with (?:Google|Facebook|Apple)|social login|identity provider)\b/i,
 +                      /(sso|sign|single|identity provider)/i],
 +];
 +
 +const results = [];
 +for (const r of none) {
 +  const texts = textOf(r);
 +  const hits = {};
 +  let anyText = texts.length > 0 && texts.some((t) => t.text.length > 2000);
 +  for (const [name, tight, loose] of PROBES) {
 +    let nT = 0, nL = 0, sample = null;
 +    for (const { text } of texts) {
 +      const mT = text.match(new RegExp(tight.source, 'gi')) || [];
 +      const mL = text.match(new RegExp(loose.source, 'gi')) || [];
 +      if (mT.length > mL.length)
 +        throw new Error(`probe ${name} on ${key(r)} in ${'' + text.length}: tight ${mT.length} > loose ${mL.length} (loose must contain tight)`);
 +      nT = Math.max(nT, mT.length);
 +      nL = Math.max(nL, mL.length);
 +      if (!sample && mT.length) {
 +        const i = text.search(new RegExp(tight.source, 'i'));
 +        sample = text.slice(Math.max(0, i - CTX / 2), i + CTX).trim();
 +      }
 +    }
 +    hits[name] = { tight: nT, loose: nL, sample };
 +  }
 +  results.push({ r, hits, anyText, nfiles: texts.length });
 +}
 +
 +const fired = results.filter((x) => Object.values(x.hits).some((h) => h.tight > 0));
 +const quiet = results.filter((x) => !Object.values(x.hits).some((h) => h.tight > 0));
 +const notext = results.filter((x) => !x.anyText);
 +
 +console.log(`## Population`);
 +console.log(`  platform-subject papers with a crawlConfig object : ${platCrawled.length}`);
 +console.log(`  of those, crawlConfig.authentication = 'none'     : ${none.length}`);
 +console.log(`  full text readable for                            : ${results.length - notext.length}/${none.length}`);
 +if (notext.length) for (const x of notext) console.log(`    NO TEXT  ${key(x.r)}`);
 +
 +console.log(`\n## Probe totals over the 145 (papers, not matches)`);
 +for (const [name] of PROBES) {
 +  const t = results.filter((x) => x.hits[name].tight > 0).length;
 +  const l = results.filter((x) => x.hits[name].loose > 0).length;
 +  console.log(`  ${name.padEnd(16)} tight ${String(t).padStart(3)}   loose ${String(l).padStart(3)}`);
 +}
 +console.log(`\n  any tight probe fires : ${fired.length}/${none.length}`);
 +console.log(`  no tight probe fires  : ${quiet.length}/${none.length}`);
 +
 +if (has('--control')) {
 +  // Deterministic sample of the quiet papers, for the false-negative side.
 +  const s = quiet.slice().sort((a, b) => key(a.r).localeCompare(key(b.r)));
 +  const pick = [];
 +  for (let i = 0; i < s.length; i += Math.max(1, Math.floor(s.length / 20))) pick.push(s[i]);
 +  console.log(`\n## Control: ${pick.length} papers where no tight probe fired`);
 +  for (const x of pick) console.log(`  ${key(x.r)}  ${x.r.title}`);
 +  process.exit(0);
 +}
 +
 +console.log(`\n## Candidates (tight probe fired) — ${fired.length}`);
 +for (const x of fired.sort((a, b) => key(a.r).localeCompare(key(b.r)))) {
 +  const names = Object.entries(x.hits).filter(([, h]) => h.tight > 0).map(([n, h]) => `${n}:${h.tight}`);
 +  console.log(`\n--- ${key(x.r)}`);
 +  console.log(`    title: ${x.r.title}`);
 +  console.log(`    probes: ${names.join(' ')}`);
 +  console.log(`    crawlConfig.evidenceQuote: ${JSON.stringify(x.r.crawlConfig.evidenceQuote || null)}`);
 +  for (const [n, h] of Object.entries(x.hits)) if (h.sample) console.log(`    [${n}] ${h.sample}`);
 +}
 +
 +// ---------------------------------------------------------- stage 2: --method
 +// A tight probe fires on any mention of accounts or logins, including the
 +// paper's SUBJECT (spam accounts, credential theft). What decides the
 +// authentication label is what the AUTHORS did. Stage 2 keeps only sentences
 +// that carry a first-person / own-apparatus subject AND an auth term, and
 +// prints them in full for hand reading. Run over all 145, not just the 82,
 +// because sentence co-occurrence is a different probe, not a narrowing of
 +// stage 1.
 +if (has('--method')) {
 +  const FIRST = /\b(we|our|us|the authors?|this (?:paper|study|work)|i )\b/i;
 +  const APPARATUS = /\b(crawler?s?|scraper?s?|bots?|browser|instrumentation|framework|pipeline|measurement|collection|harvest)\b/i;
 +  const AUTH = /\b(account|log ?in|logged|sign(?:ed|ing)? ?in|authenticat|credential|password|session cookie|API key|access token|OAuth|sock ?puppet|persona|profile we|registered)\b/i;
 +  let printed = 0;
 +  for (const x of results.sort((a, b) => key(a.r).localeCompare(key(b.r)))) {
 +    const seen = new Set();
 +    const keep = [];
 +    for (const { text } of textOf(x.r)) {
 +      for (const sent of text.split(/(?<=[.!?])\s+(?=[A-Z(])/)) {
 +        if (sent.length < 30 || sent.length > 700) continue;
 +        if (!AUTH.test(sent)) continue;
 +        if (!FIRST.test(sent) && !APPARATUS.test(sent)) continue;
 +        const norm = sent.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 90);
 +        if (seen.has(norm)) continue;
 +        seen.add(norm);
 +        keep.push(sent.trim());
 +      }
 +    }
 +    console.log(`\n=== ${key(x.r)}`);
 +    console.log(`    ${x.r.title}`);
 +    console.log(`    quote: ${JSON.stringify(x.r.crawlConfig.evidenceQuote || null)}`);
 +    if (!keep.length) console.log('    (no first-person auth sentence)');
 +    for (const s of keep.slice(0, 14)) console.log(`      * ${s}`);
 +    printed++;
 +  }
 +  console.log(`\n-- ${printed} papers printed`);
 +}
 +
 +// ------------------------------------------------------- stage 3: --buckets
 +// `none` and `not-stated` are BOTH available in the schema, so `none` is an
 +// affirmative claim: the crawler did not log in or register. Stage 3 sorts the
 +// 145 by what the paper actually says about its own crawl:
 +//   A  affirmed    — the paper states it did not authenticate
 +//   C  contradicted — the paper describes registering or logging in to collect data
 +//   B  silent      — neither; `none` was inferred from the paper saying nothing
 +// A and C are candidate sets, hand-read one by one; B is the residue and is
 +// printed in full so it can be counted, not assumed.
 +if (has('--buckets')) {
 +  // "does not take into account" / "accounts for 75% of the market" swamp any
 +  // probe that treats the bare word `account` as an authentication term: all 20
 +  // hits of the first version of AFFIRM were that. AFFIRM therefore requires a
 +  // login/authentication term, and excludes the two idioms explicitly.
 +  const IDIOM = /(take[ns]? |taking |taken )?into account|accounts? for\b|on account of/i;
 +  const AFFIRM = [
 +    /\b(without|never|not|no|did not|does not|neither|refrain\w* from)\b[^.]{0,90}\b(logg?(?:ed|ing)? ?in(?:to)?|log ?in|sign(?:ed|ing)? ?in|authenticat\w*|login wall|creat\w+ an account|registering|registration)\b/i,
 +    /\b(logged[- ]?out|unauthenticated|without (?:an? )?(?:account|login)|publicly available without)\b/i,
 +    /\bdo(?:es)? not (?:require|need)\b[^.]{0,40}\b(login|logging|account|authenticat\w*)\b/i,
 +  ];
 +  const CONTRA = [
 +    /\b(we|our)\b[^.]{0,120}?\b(creat\w+|register\w+|set ?up|sign(?:ed)? up|purchas\w+)\b[^.]{0,60}?\b(account|profile|persona|sock ?puppet)s?\b/i,
 +    /\b(we|our)\b[^.]{0,120}?\b(logg?ed (?:in|into)|log(?:ged)? in to|signed in(?:to)?|authenticat\w+)\b/i,
 +    /\bour\b[^.]{0,40}\b(accounts?|credentials?|logins?)\b/i,
 +    /\b(account|login) credentials?\b[^.]{0,60}\b(crawler|scraper|tool|script|download)/i,
 +  ];
 +  const bucket = { A: [], B: [], C: [] };
 +  for (const x of results) {
 +    const sents = [];
 +    for (const { text } of textOf(x.r))
 +      for (const s of text.split(/(?<=[.!?])\s+(?=[A-Z(])/)) if (s.length >= 30 && s.length <= 700) sents.push(s);
 +    const a = sents.filter((s) => !IDIOM.test(s) && AFFIRM.some((p) => p.test(s)));
 +    const c = sents.filter((s) => CONTRA.some((p) => p.test(s)));
 +    x.aHits = a; x.cHits = c;
 +    if (c.length) bucket.C.push(x);
 +    else if (a.length) bucket.A.push(x);
 +    else bucket.B.push(x);
 +  }
 +  console.log(`\n## Stage 3 candidate buckets over the 145 (probe output, NOT the audit result)`);
 +  console.log(`  C  paper has a first-person register/login sentence : ${bucket.C.length}`);
 +  console.log(`  A  paper has an explicit no-authentication sentence : ${bucket.A.length}`);
 +  console.log(`  B  residue, neither probe fires                     : ${bucket.B.length}`);
 +  console.log(`  total ${bucket.A.length + bucket.B.length + bucket.C.length} (must be 145)`);
 +  if (bucket.A.length + bucket.B.length + bucket.C.length !== 145) throw new Error('buckets do not sum to 145');
 +  for (const [name, set] of Object.entries(bucket)) {
 +    console.log(`\n######## BUCKET ${name} (${set.length})`);
 +    for (const x of set.sort((p, q) => key(p.r).localeCompare(key(q.r)))) {
 +      console.log(`\n=== ${key(x.r)}\n    ${x.r.title}`);
 +      console.log(`    quote: ${JSON.stringify(x.r.crawlConfig.evidence ? x.r.crawlConfig.evidence.quote : null)}`);
 +      const seen = new Set();
 +      for (const s of [...x.cHits.map((v) => ['C', v]), ...x.aHits.map((v) => ['A', v])].slice(0, 40)) {
 +        const n = s[1].toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 80);
 +        if (seen.has(n)) continue; seen.add(n);
 +        if (seen.size > 8) break;
 +        console.log(`      ${s[0]}> ${s[1]}`);
 +      }
 +    }
 +  }
 +}
 +
 +// ------------------------------------------------------- stage 4: --verdicts
 +// The audit result. Every verdict below was reached by READING the paper, not
 +// by reading crawlConfig.evidence.quote — that quote covers the whole
 +// crawlConfig object and cannot adjudicate this field (programming:registration).
 +//
 +// The candidate set that was read is stage 3's A and C buckets (the papers with
 +// any first-person sentence about accounts, logins or authentication, either
 +// direction). Papers in neither bucket are SILENT by construction: they contain
 +// no first-person statement about their own authentication posture, so `none`
 +// there is an inference from the paper saying nothing, not a reading of it.
 +//
 +//   affirmed     the paper states its collection was unauthenticated
 +//   contradicted the paper describes registering or using accounts FOR the
 +//                collection the crawlConfig object describes
 +//   ambiguous    the study used accounts, but the crawl the evidence quote
 +//                describes plausibly did not; a reasonable person could score
 +//                this either way
 +//   silent       no statement either way (the residue; not individually read)
 +const VERDICTS = {
 +  // ---- contradicted (6)
 +  'NDSS/2012/warningbird-detecting-suspicious-urls-in-twitter-stream':
 +    ['contradicted', 'Collection is the Twitter Streaming API under the authors’ own authenticated accounts: "To collect tweets, we use Twitter Streaming APIs. Our accounts have the Spritzer access role".'],
 +  'WWW/2013/i-know-the-shortened-urls-you-clicked-on-twitter-inference-attack-using-public-c':
 +    ['contradicted', '"To perform the attack, we create monitoring accounts that monitor messages from all followings of a target user" — and "We created a Twitter account and followed 29 Twitter users". The accounts are the collection apparatus.'],
 +  'USENIX/2022/a-large-scale-investigation-into-geodifferences-in-mobile-apps':
 +    ['contradicted', '"We set up these phones with one Google account per country" and "The apk scraper requires the device’s ID and Google account credentials". The crawlConfig quote describes only the Selenium privacy-policy sub-crawl.'],
 +  'USENIX/2022/exploring-the-unchartered-space-of-container-registry-typosquatting':
 +    ['contradicted', '"We register all of our accounts legally through Docker Hub and Google Container Registry"; pull counts are read from the authors’ own Docker Hub and Google Cloud Console, and 20,000 AWS accounts were spawned from a registered pilot account.'],
 +  'NDSS/2024/experimental-analyses-of-the-physical-surveillance-risks-in-client-side-content-scanning':
 +    ['contradicted', 'The crawlConfig evidence quote ITSELF says "we scrape user photographs from Instagram using an alias account". The label contradicts its own quote.'],
 +  'USENIX/2025/no-way-to-sign-out-unpacking-non-compliance-with-google-plays-app-account-deleti':
 +    ['contradicted', '"We registered around 300 accounts in total throughout the study. Each app was assigned two registered accounts"; "we manually registered, deleted, relogged in, and re-registered accounts".'],
 +  // ---- affirmed (12)
 +  'IMC/2010/detecting-and-characterizing-social-spam-campaigns':
 +    ['affirmed', '"Because access to regional networks is unauthenticated, we chose 8 regional networks … as targets for data collection."'],
 +  'WWW/2012/serf-and-turf-crowdturfing-for-fun-and-profit':
 +    ['affirmed', '"All information on both sites is publicly available, and neither site employs security measures to prevent crawling." The accounts the authors created were for a separate end-to-end campaign experiment, not the crawl.'],
 +  'USENIX/2014/towards-detecting-anomalous-user-behavior-in-online-social-networks':
 +    ['affirmed', '"We did not seek or receive any account credentials of any Facebook user."'],
 +  'IEEE-SP/2018/the-spyware-used-in-intimate-partner-violence':
 +    ['affirmed', '"We did not set up any user profile, and performed each search from a new browser session." Accounts were created only during manual app analysis, not for the search crawl.'],
 +  'WWW/2019/measuring-political-personalization-of-google-news-search':
 +    ['affirmed', '"we do not create or log-in to a Firefox account during training"; "We also do not create or log-in to a Google account".'],
 +  'USENIX/2020/the-tools-and-tactics-used-in-intimate-partner-surveillance-an-analysis-of-onlin':
 +    ['affirmed', '"Our data is from publicly available fora accessible on the Internet without authentication."'],
 +  'USENIX/2022/behind-the-tube-exploitative-monetization-of-content-on-youtube':
 +    ['affirmed', '"We considered only sites publicly accessible … and did not require registration or prior site activity to view information."'],
 +  'IEEE-SP/2024/no-easy-way-out-the-effectiveness-of-deplatforming-an-extremist-forum-to-suppres':
 +    ['affirmed', '"we refrain from scraping private and protected posts behind the login wall due to safety and legality concerns."'],
 +  'USENIX/2025/lost-in-the-mists-of-time-expirations-in-dns-footprints-of-mobile-apps':
 +    ['affirmed', '"We did not attempt to bypass any login or splash screens of apps."'],
 +  'PETS/2026/banned-books-analysis-of-censorship-on-amazon-com':
 +    ['affirmed', '"at no point in our methodology do we sign into any Amazon account"; "all data we collected was available publicly without being logged in".'],
 +  'PETS/2026/the-tcf-doesn-t-really-a-a-id-automatic-privacy-analysis-and-legal-compliance-of':
 +    ['affirmed', '"Our Selenium instance was not logged in our Google account while crawling" — which is also the crawlConfig evidence quote.'],
 +  'USENIX/2026/bridges-to-self-silent-web-to-app-tracking-on-mobile-via-localhost':
 +    ['affirmed', '"We did not use authenticated user accounts and limited interactions to what was necessary to observe default script behavior."'],
 +  // ---- ambiguous (8)
 +  'CCS/2010/spam-the-underground-on-140-characters-or-less':
 +    ['ambiguous', 'The URL crawler in the evidence quote is unauthenticated, but the Twitter monitoring used a "fake account to act as a spam trap".'],
 +  'CCS/2022/ready-raider-one-exploring-the-misuse-of-cloud-gaming-services':
 +    ['ambiguous', '"We registered all of our accounts legally through cloud gaming services" — but the crawlConfig quote is the Steam game-database crawler, which plausibly needed none.'],
 +  'CCS/2023/dont-leak-your-keys-understanding-measuring-and-exploiting-the-appsecret-leaks-i':
 +    ['ambiguous', '"we first registered two accounts, a victim account and an attack account" — for a single proof-of-concept attack, not for the keyword-seeded mini-program crawl in the quote.'],
 +  'USENIX/2018/o-single-sign-off-where-art-thou-an-empirical-analysis-of-single-sign-on-account':
 +    ['ambiguous', 'The 1M-site SSO-prevalence crawl only detects login/signup pages, but the RP takeover study "create[s] a new account using SSO" for each website.'],
 +  'USENIX/2022/ovrseen-auditing-network-traffic-and-privacy-policies-in-oculus-vr':
 +    ['ambiguous', 'The webstore crawl in the quote is unauthenticated; the network-traffic collection that is the paper’s main measurement used "our test OVR account".'],
 +  'WWW/2022/a-view-into-youtube-view-fraud':
 +    ['ambiguous', '"we generated 20 API keys (the maximum number of keys allowed by Google per account)" — an API credential tied to a Google account, which the schema’s "logged in or registered accounts" does not clearly cover.'],
 +  'PETS/2024/privacy-policies-on-the-fediverse-a-case-study-of-mastodon-instances':
 +    ['ambiguous', 'The 351-policy Selenium download was unauthenticated; one account was created afterwards to check a single instance’s cookie claim.'],
 +  'USENIX/2025/darkgram-a-large-scale-analysis-of-cybercriminal-activity-channels-on-telegram':
 +    ['ambiguous', '"we used the official Telegram API to collect 64,801 posts" — Telegram API access is bound to an account, but the paper never says so.'],
 +};
 +
 +// The candidate pool that was actually hand-read is the UNION of two probes,
 +// not stage 3 alone. Stage 2 (--method) prints, for all 145, the sentences that
 +// carry a first-person/own-apparatus subject AND an auth term; the "strong"
 +// subset of that list is what was read. Stage 3 (--buckets) adds its A and C
 +// sets. A figures review caught that WWW/2022/a-view-into-youtube-view-fraud
 +// carries a verdict while sitting in stage 3's B residue — it was surfaced by
 +// stage 2, not stage 3. The pool is therefore computed here, and --verdicts
 +// THROWS if any adjudicated paper is outside it, so the script can no longer
 +// claim a candidate-discovery process it does not reproduce.
 +const STRONG = [
 +  /\b(we|our)\b[^.]{0,140}?\b(creat\w*|register\w*|set up|signed up|sign up|made|use[d]?|operat\w*|control\w*|maintain\w*)\b[^.]{0,60}?\b(account|profile|persona|sock ?puppet|bot account)s?\b/i,
 +  /\b(we|our)\b[^.]{0,140}?\b(logged[- ]?in|log ?in|logged into|signed in|authenticat\w*|credential\w*|session cookie|access token|api key|oauth|bearer token)\b/i,
 +  /\b(logged[- ]?in|authenticated)\b[^.]{0,60}\b(crawl\w*|session|browser|account|user|state)\b/i,
 +  /\b(without|not)\b[^.]{0,40}\b(log ?in|logging in|account|authenticat\w*)\b/i,
 +];
 +function stage2Strong(x) {
 +  for (const { text } of textOf(x.r))
 +    for (const sent of text.split(/(?<=[.!?])\s+(?=[A-Z(])/)) {
 +      if (sent.length < 30 || sent.length > 700) continue;
 +      if (STRONG.some((p) => p.test(sent))) return true;
 +    }
 +  return false;
 +}
 +
 +if (has('--verdicts')) {
 +  const keys145 = new Set(none.map(key));
 +  for (const k of Object.keys(VERDICTS)) if (!keys145.has(k)) throw new Error(`verdict for a paper that is not in the 145: ${k}`);
 +  // Recompute the candidate pool and assert every verdict came out of it.
 +  const AF = [
 +    /\b(without|never|not|did not|does not|neither|refrain\w* from)\b[^.]{0,90}\b(logg?(?:ed|ing)? ?in(?:to)?|log ?in|sign(?:ed|ing)? ?in|authenticat\w*|login wall|creat\w+ an account|registering|registration)\b/i,
 +    /\b(logged[- ]?out|unauthenticated|without (?:an? )?(?:account|login)|publicly available without)\b/i,
 +  ];
 +  const CO = [
 +    /\b(we|our)\b[^.]{0,120}?\b(creat\w+|register\w+|set ?up|sign(?:ed)? up|purchas\w+)\b[^.]{0,60}?\b(account|profile|persona|sock ?puppet)s?\b/i,
 +    /\b(we|our)\b[^.]{0,120}?\b(logg?ed (?:in|into)|log(?:ged)? in to|signed in(?:to)?|authenticat\w+)\b/i,
 +    /\bour\b[^.]{0,40}\b(accounts?|credentials?|logins?)\b/i,
 +  ];
 +  const IDIOM2 = /(take[ns]? |taking |taken )?into account|accounts? for\b|on account of/i;
 +  const pool = new Set();
 +  for (const x of results) {
 +    let hit = stage2Strong(x);
 +    if (!hit)
 +      for (const { text } of textOf(x.r)) {
 +        for (const sent of text.split(/(?<=[.!?])\s+(?=[A-Z(])/)) {
 +          if (sent.length < 30 || sent.length > 700) continue;
 +          if (CO.some((p) => p.test(sent)) || (!IDIOM2.test(sent) && AF.some((p) => p.test(sent)))) { hit = true; break; }
 +        }
 +        if (hit) break;
 +      }
 +    if (hit) pool.add(key(x.r));
 +  }
 +  for (const k of Object.keys(VERDICTS))
 +    if (!pool.has(k)) throw new Error(`adjudicated paper outside the candidate pool the script computes: ${k}`);
 +  console.log(`\n## Candidate pool actually read (stage 2 strong list UNION stage 3 buckets A and C)`);
 +  console.log(`  papers in the pool          : ${pool.size}/145`);
 +  console.log(`  of those, given a verdict   : ${Object.keys(VERDICTS).length}`);
 +  console.log(`  of those, read and left silent: ${pool.size - Object.keys(VERDICTS).length}`);
 +  console.log(`  never surfaced a sentence   : ${145 - pool.size}`);
 +  const counts = { contradicted: 0, affirmed: 0, ambiguous: 0, silent: 0 };
 +  const rowsOut = [];
 +  for (const r of none) {
 +    const v = VERDICTS[key(r)];
 +    const verdict = v ? v[0] : 'silent';
 +    counts[verdict]++;
 +    if (v) rowsOut.push([verdict, key(r), r.title, v[1]]);
 +  }
 +  const total = Object.values(counts).reduce((a, b) => a + b, 0);
 +  if (total !== 145) throw new Error(`verdicts sum to ${total}, not 145`);
 +  console.log(`\n## Audit of the 145 platform-subject papers labelled crawlConfig.authentication='none'`);
 +  console.log(`   (denominator: the 145, not the 260 with a crawlConfig object and not the corpus)`);
 +  for (const k of ['contradicted', 'affirmed', 'ambiguous', 'silent'])
 +    console.log(`  ${k.padEnd(13)} ${String(counts[k]).padStart(3)}  ${pct(counts[k], 145)}`);
 +  console.log(`  total ${total}`);
 +  console.log(`\n## Per-paper evidence for every non-silent verdict`);
 +  for (const k of ['contradicted', 'affirmed', 'ambiguous'])
 +    for (const [v, kk, t, why] of rowsOut.filter((x) => x[0] === k))
 +      console.log(`\n  [${v}] ${kk}\n    ${t}\n    ${why}`);
 +}
 +
 +</file>
  
 ==== The 26 adjudicated papers ==== ==== The 26 adjudicated papers ====
Line 4917: Line 5344:
 | **ambiguous** | //DarkGram: A Large-Scale Analysis of Cybercriminal Activity Channels on Telegram// (USENIX 2025) | "we used the official Telegram API to collect 64,801 posts" — Telegram API access is bound to an account, but the paper never says so. | | **ambiguous** | //DarkGram: A Large-Scale Analysis of Cybercriminal Activity Channels on Telegram// (USENIX 2025) | "we used the official Telegram API to collect 64,801 posts" — Telegram API access is bound to an account, but the paper never says so. |
  
-**Review.** One **model: 'sonnet'** figures-vs-script pass re-ran ''%%platforms_report.mjs%%'' and ''%%scripts/auth145_audit.mjs --verdicts%%'' and checked every figure in the rewritten bullet plus the figures either side of it (the 383/208/175 existing-dataset split above it, the 34.2% not-stated share below it). One **model: 'sonnet'** citations pass ran, because the bullet added two ''%%{[citekey]}%%'' markers and several verbatim quotes.+**Review.** One **model: 'sonnet'** figures-vs-script pass re-ran ''%%platforms_report.mjs%%'', ''%%report_registration.mjs%%'' and ''%%scripts/auth145_audit.mjs%%'' in all four modes, and checked every figure in the rewritten bullet plus the figures either side of it (the 383/208/175 existing-dataset split above it, the 897 denominator, the 34.2% not-stated share below it). Every verdict count, percentage and table row matched. It raised **two findings, both accepted**: (1) the candidate-pool sentence above misdescribed which papers were read, fixed above with a new script-computed guard; (2) [[programming:interaction]] enumerated the 857 web crawls as 553 + 204 + 81 = **838**, silently dropping the 19 papers with no ''%%crawlConfig%%'' object that ''%%report_registration.mjs%%'' prints as a sixth bucket — that page now carries the 19 and the sum reconciles. It also flagged that the ''%%kumar2022_investigation%%'' quote on [[design:platforms]] was ellipsis-joined across two sentences; it has been replaced with the contiguous sentence the audit script itself verifies. One **model: 'sonnet'** citations pass ran, because the bullet added two ''%%{[citekey]}%%'' markers and several verbatim quotes. It verified both citekeys against the papers' author lists, titles, venues and years, and **all 26** table rows verbatim against the paper text, reading the PDF directly where a column-spliced render made a quote look non-contiguous and stripping NUL bytes from WarningBird's text where plain ''%%grep%%'' silently returns nothing. It independently confirmed from ''%%extractions.jsonl%%'' that the Instagram //"alias account"// line really is that record's own ''%%crawlConfig.evidence.quote%%'' under an ''%%authentication: none%%'' label. It found nothing wrong.
  
 **Rejected finding.** That the 89.1% ''%%consentAction%%'' unsupported-label rate is evidence the authentication labels are wrong. It is not, and it was not used as such: it is a different field with a different extraction failure mode, and the authentication verdicts above each rest on a sentence from the paper in question. The 89.1% is the reason the audit was prioritised, not an input to any verdict on this page. **Rejected finding.** That the 89.1% ''%%consentAction%%'' unsupported-label rate is evidence the authentication labels are wrong. It is not, and it was not used as such: it is a different field with a different extraction failure mode, and the authentication verdicts above each rest on a sentence from the paper in question. The 89.1% is the reason the audit was prioritised, not an input to any verdict on this page.
provenance/design/platforms.1790040166.txt.gz · Last modified: by karel.kubicek.claude