provenance:design:platforms
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revision | |||
| provenance:design:platforms [2026/09/22 01:32] – Correct the candidate-pool description (97/26/71/48 from the script, not 75) and record both review passes and the two accepted findings. Authored by Claude karel.kubicek.claude | provenance: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 4884: | Line 4884: | ||
| **Two of the six read as contradicting their own object.** //A Large-scale Investigation into Geodifferences in Mobile Apps// (USENIX Security 2022) has a '' | **Two of the six read as contradicting their own object.** //A Large-scale Investigation into Geodifferences in Mobile Apps// (USENIX Security 2022) has a '' | ||
| + | |||
| + | ==== scripts/ | ||
| + | |||
| + | The committed file, verbatim. | ||
| + | |||
| + | <file javascript auth145_audit.mjs> | ||
| + | // Audit of the 145 platform-subject papers labelled crawlConfig.authentication=' | ||
| + | // | ||
| + | // Filed by design: | ||
| + | // 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: | ||
| + | // evidence. It reads the PAPER TEXT. | ||
| + | // | ||
| + | // node scripts/ | ||
| + | // node scripts/ | ||
| + | // node scripts/ | ||
| + | // | ||
| + | // The paper population is re-derived with the same four signals | ||
| + | // platforms_report.mjs uses; the counts are asserted against that script' | ||
| + | // published output (260 with a crawlConfig object, 145 labelled ' | ||
| + | // this script throws if either moves. | ||
| + | |||
| + | import fs from ' | ||
| + | import path from ' | ||
| + | import { loadExtractions, | ||
| + | import { tagsOf, roleOf, familiesOf, ANY_PLATFORM } from ' | ||
| + | |||
| + | 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(' | ||
| + | const pct = (n, d) => `${((100 * n) / d).toFixed(1)}%`; | ||
| + | |||
| + | const rows = loadExtractions(); | ||
| + | const key = (r) => `${r.venue}/ | ||
| + | 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 === ' | ||
| + | .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 === ' | ||
| + | for (const { family, role } of tagsOf(s)) { | ||
| + | if (role !== ' | ||
| + | if (!fams.has(family)) fams.set(family, | ||
| + | fams.get(family).add(sig); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | if (fams.size) perPaper.set(k, | ||
| + | } | ||
| + | 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 === ' | ||
| + | |||
| + | // Guard: this script' | ||
| + | if (platCrawled.length !== 260) | ||
| + | throw new Error(`population drift: platform-subject papers with a crawlConfig object = ${platCrawled.length}, | ||
| + | if (none.length !== 145) | ||
| + | throw new Error(`population drift: authentication=' | ||
| + | |||
| + | // ------------------------------------------------------------------ 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(/ | ||
| + | function textOf(r) { | ||
| + | const dir = path.join(dataRoot(), | ||
| + | const out = []; | ||
| + | for (const f of [' | ||
| + | const p = path.join(dir, | ||
| + | if (!fs.existsSync(p)) continue; | ||
| + | const raw = fs.readFileSync(p, | ||
| + | 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 = [ | ||
| + | [' | ||
| + | / | ||
| + | [' | ||
| + | / | ||
| + | [' | ||
| + | / | ||
| + | [' | ||
| + | / | ||
| + | [' | ||
| + | / | ||
| + | ]; | ||
| + | |||
| + | 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, | ||
| + | const mL = text.match(new RegExp(loose.source, | ||
| + | if (mT.length > mL.length) | ||
| + | throw new Error(`probe ${name} on ${key(r)} in ${'' | ||
| + | nT = Math.max(nT, | ||
| + | nL = Math.max(nL, | ||
| + | if (!sample && mT.length) { | ||
| + | const i = text.search(new RegExp(tight.source, | ||
| + | sample = text.slice(Math.max(0, | ||
| + | } | ||
| + | } | ||
| + | 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(`## | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | if (notext.length) for (const x of notext) console.log(` | ||
| + | |||
| + | console.log(`\n## | ||
| + | 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(` | ||
| + | } | ||
| + | console.log(`\n | ||
| + | console.log(` | ||
| + | |||
| + | if (has(' | ||
| + | // Deterministic sample of the quiet papers, for the false-negative side. | ||
| + | const s = quiet.slice().sort((a, | ||
| + | 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## | ||
| + | for (const x of pick) console.log(` | ||
| + | process.exit(0); | ||
| + | } | ||
| + | |||
| + | console.log(`\n## | ||
| + | for (const x of fired.sort((a, | ||
| + | const names = Object.entries(x.hits).filter(([, | ||
| + | console.log(`\n--- ${key(x.r)}`); | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | for (const [n, h] of Object.entries(x.hits)) if (h.sample) console.log(` | ||
| + | } | ||
| + | |||
| + | // ---------------------------------------------------------- stage 2: --method | ||
| + | // A tight probe fires on any mention of accounts or logins, including the | ||
| + | // paper' | ||
| + | // 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(' | ||
| + | const FIRST = / | ||
| + | const APPARATUS = / | ||
| + | const AUTH = / | ||
| + | let printed = 0; | ||
| + | for (const x of results.sort((a, | ||
| + | const seen = new Set(); | ||
| + | const keep = []; | ||
| + | for (const { text } of textOf(x.r)) { | ||
| + | for (const sent of text.split(/ | ||
| + | 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(/ | ||
| + | if (seen.has(norm)) continue; | ||
| + | seen.add(norm); | ||
| + | keep.push(sent.trim()); | ||
| + | } | ||
| + | } | ||
| + | console.log(`\n=== ${key(x.r)}`); | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | if (!keep.length) console.log(' | ||
| + | for (const s of keep.slice(0, | ||
| + | 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 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(' | ||
| + | // "does not take into account" | ||
| + | // 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/ | ||
| + | const IDIOM = /(take[ns]? |taking |taken )?into account|accounts? | ||
| + | const AFFIRM = [ | ||
| + | / | ||
| + | / | ||
| + | / | ||
| + | ]; | ||
| + | const CONTRA = [ | ||
| + | / | ||
| + | / | ||
| + | / | ||
| + | / | ||
| + | ]; | ||
| + | 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(/ | ||
| + | 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## | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | if (bucket.A.length + bucket.B.length + bucket.C.length !== 145) throw new Error(' | ||
| + | for (const [name, set] of Object.entries(bucket)) { | ||
| + | console.log(`\n######## | ||
| + | for (const x of set.sort((p, | ||
| + | console.log(`\n=== ${key(x.r)}\n | ||
| + | console.log(` | ||
| + | const seen = new Set(); | ||
| + | for (const s of [...x.cHits.map((v) => [' | ||
| + | const n = s[1].toLowerCase().replace(/ | ||
| + | if (seen.has(n)) continue; seen.add(n); | ||
| + | if (seen.size > 8) break; | ||
| + | console.log(` | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ------------------------------------------------------- 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: | ||
| + | // | ||
| + | // 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, | ||
| + | // direction). Papers in neither bucket are SILENT by construction: | ||
| + | // 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. | ||
| + | // | ||
| + | // | ||
| + | // | ||
| + | // collection the crawlConfig object describes | ||
| + | // | ||
| + | // describes plausibly did not; a reasonable person could score | ||
| + | // this either way | ||
| + | // | ||
| + | const VERDICTS = { | ||
| + | // ---- contradicted (6) | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | // ---- affirmed (12) | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | // ---- ambiguous (8) | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | ' | ||
| + | [' | ||
| + | }; | ||
| + | |||
| + | // 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/ | ||
| + | // subset of that list is what was read. Stage 3 (--buckets) adds its A and C | ||
| + | // sets. A figures review caught that WWW/ | ||
| + | // 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 = [ | ||
| + | / | ||
| + | / | ||
| + | / | ||
| + | / | ||
| + | ]; | ||
| + | function stage2Strong(x) { | ||
| + | for (const { text } of textOf(x.r)) | ||
| + | for (const sent of text.split(/ | ||
| + | if (sent.length < 30 || sent.length > 700) continue; | ||
| + | if (STRONG.some((p) => p.test(sent))) return true; | ||
| + | } | ||
| + | return false; | ||
| + | } | ||
| + | |||
| + | if (has(' | ||
| + | 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 = [ | ||
| + | / | ||
| + | / | ||
| + | ]; | ||
| + | const CO = [ | ||
| + | / | ||
| + | / | ||
| + | / | ||
| + | ]; | ||
| + | const IDIOM2 = /(take[ns]? |taking |taken )?into account|accounts? | ||
| + | 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(/ | ||
| + | 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## | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | const counts = { contradicted: | ||
| + | const rowsOut = []; | ||
| + | for (const r of none) { | ||
| + | const v = VERDICTS[key(r)]; | ||
| + | const verdict = v ? v[0] : ' | ||
| + | counts[verdict]++; | ||
| + | if (v) rowsOut.push([verdict, | ||
| + | } | ||
| + | const total = Object.values(counts).reduce((a, | ||
| + | if (total !== 145) throw new Error(`verdicts sum to ${total}, not 145`); | ||
| + | console.log(`\n## | ||
| + | console.log(` | ||
| + | for (const k of [' | ||
| + | console.log(` | ||
| + | console.log(` | ||
| + | console.log(`\n## | ||
| + | for (const k of [' | ||
| + | for (const [v, kk, t, why] of rowsOut.filter((x) => x[0] === k)) | ||
| + | console.log(`\n | ||
| + | } | ||
| + | |||
| + | </ | ||
| ==== The 26 adjudicated papers ==== | ==== The 26 adjudicated papers ==== | ||
provenance/design/platforms.1790040764.txt.gz · Last modified: by karel.kubicek.claude
