Table of Contents
Provenance: design:longitudinal
Working notes behind longitudinal — every query with its population and denominator, the report script and its unedited output, the fold and its full residue, the quotes that were checked, the probes that were hand-audited, the external sources that were verified or rejected, and what could not be established. Corpus-level caveats that apply to every page on this site are on corpus and are not restated here.
Contemporaneous. Written during the run that produced the content page, 2026-08-27, not reconstructed afterwards.
1. What this page is backing
| Item | Value |
|---|---|
| Content page | longitudinal — new page, created 2026-08-27 |
| Report script | scripts/report_longitudinal.mjs (new) |
| Fold it depends on | scripts/cadence_fold.mjs (new) — temporal.cadence |
| Quote check | scripts/quotecheck_longitudinal.mjs (new) |
| Data | data/extract/run1/extractions.jsonl, 5,859 papers, 7 venues, 2010–2026 |
| Bibliography additions | 6 new entries, scripts/bib_additions_longitudinal.bib |
| Previous figures | none. No earlier version of this page exists and no figure was carried over from any dossier, METHOD.md or other page. |
2. Scope: why a new page rather than extending a neighbour
Four existing pages own a piece of this subject, and the judgement was that none of them owns the whole:
| Page | What it owns | What it leaves open |
|---|---|---|
| archives | the retrospective arm — what an archive preserves, escapes, coverage, the anachronism trap | says nothing about a prospective panel you run yourself |
| sampling | versioning a draw so it is redrawable, including per-wave Tranco ids | one wave at a time; the frame's own composition changing between waves is not its subject |
| biases | survivorship and attrition in repeated crawls, measured | the bias, not the design that avoids it |
| regression | clustering, difference-in-differences, what non-independence does to a p-value | the analysis, once you already have comparable waves |
The gap none of them fills is the one between “I know how to draw a sample” and “wave two is comparable to wave one”: what has to be pinned, what cannot be pinned however hard you try, and what the literature actually reports. That is the page. Every overlapping claim links out rather than restating — the four papers whose variation figures appear in the noise-floor table are all already cited on statistics:biases, and the EasyList-anachronism figure is already on design:archives; both are re-used with a pointer because the longitudinal reading of them is different from the bias reading.
One consequence accepted deliberately: the content page's noise-floor table duplicates rows that appear on statistics:biases. A reader arriving from a search engine at either page should not have to visit the other to learn the number, and the framing differs (there: “your sample is not the population”; here: “this is your wave-to-wave error bar”).
3. Populations and denominators
Every figure on the content page is a share of one of these. crawled uses the same definition as data/extract/OVERVIEW.md so the numbers line up with the rest of the site.
| Name | Definition | N |
|---|---|---|
all | every extracted paper | 5,859 |
empirical | isEmpirical | 5,118 |
crawled | crawlConfig != null OR studyTypes contains automated-web-crawl | 1,120 |
repeated | crawled AND some temporal tuple has snapshots > 1 | 250 |
single-shot | crawled AND not repeated (includes “did not say”) | 870 |
multi-year | repeated AND longest parsable stated span ≥ 12 months | 73 |
sub-year | repeated AND longest parsable stated span < 12 months | 132 |
| Tranco users | any population.sourceList matching /tranco/i | 266 |
| cadence-stating | crawled AND some temporal.cadence is non-null | 389 |
The repeated rule is deliberately identical to the one biases uses, so the two pages agree on 250 rather than publishing two different counts of the same thing.
Sentinels. not-stated, not-applicable, none-mentioned, unknown are never counted as a stated value. This matters most for two rows: counting not-applicable as an answer would move consent action stated from 105 to 148 on the repeated population and statefulness stated from 149 to 166 on the single-shot one, which would have put this page out of step with OVERVIEW.md (219 and 349 over crawled respectively — reproduced exactly here as 70+149 and 105+244).
“Stated” is not “held fixed.” Every pin figure counts papers that mentioned the thing once. The extraction cannot see whether a paper used the same browser in wave one and wave two. Every pin figure is therefore a ceiling on comparability, and the content page says so in the same table.
4. Running it
cd /workspace/artifacts/wiki node scripts/report_longitudinal.mjs > scripts/report_longitudinal-output.txt node scripts/quotecheck_longitudinal.mjs > scripts/quotecheck_longitudinal-output.txt
Both default to /workspace/publications_dataset/data/extract/run1 and …/data/fulltext; –run and –fulltext override.
5. The fold, and its residue in full
temporal.cadence is the only free-text field this page aggregates. 334 distinct strings fire on the 1,120 crawling papers. The fold is ordered fastest-interval-first, so “hourly and daily” folds to hourly — the fastest repeat a paper states is the one that bounds its time resolution.
The first rule is the interesting one. A large minority of the values are durations, not cadences: “one week”, “5-day experiment”, “month-long”, “single crawl lasting 12 days”. Those papers have not stated how often they repeated, and folding them into weekly or monthly would have converted a reporting gap into a finding about cadence. They get their own family and the content page reports them as silence.
After folding, 16 distinct strings (16 papers, 4.1% of the 389) remain unmapped. They are printed in full in the report output below, under UNMAPPED RESIDUE, and reproduced here so they are readable without running anything:
four increasing cumulative periods for threshold selection four APKs per year per browser family July and December 15-minute trend monitoring one hour per website two six-month intervals per year for 2009–2019 longitudinal measurement eleven-minute time slots Crawler B ran at the middle of each month irregular confirmation crawls collection cycles over six months during the day, paused overnight December 2023, May 2024, and December 2024 sequential experimental windows four-month interval before and after the second rollout
Most of those are genuinely ambiguous rather than mis-folded: “four-month interval” could be a cadence or a span, and the paper has to be read to tell. They are left unmapped rather than guessed at.
One value in the field is a sentinel. A single paper has cadence: “not-stated”. It is folded to sentinel and excluded from every cadence figure.
The double-counting trap, and how the published figure avoids it. The families are multi-valued: a paper stating both “daily” and “for three weeks” lands in two. Summing the non-cadence families (58 + 29 + 31 + 1 = 119) therefore over-counts. The figure published on the content page instead counts papers whose cadence values are all non-cadences: 88 of 389 (22.6%), with unmapped generously treated as a possible real cadence. Counting unmapped as a non-cadence would give a larger number; the smaller one is published.
6. Quotes and figures spot-checked
Every quotation and every figure the content page takes from a cited paper is in scripts/quotecheck_longitudinal.mjs, whose unedited output is in §14. The checker reads four renderings of each paper: paper.cols.txt (two-column reading order repaired), paper.norm.txt, paper.txt, and paper.cols.txt with end-of-line hyphenation joined.
The verdicts mean:
| Verdict | Meaning |
|---|---|
EXACT | the quote appears verbatim, whitespace collapsed |
SPLICED | every word is covered by contiguous runs of ≥ 4 words; only the column reading order was broken |
PARTIAL | some words matched only in runs shorter than 4 — read it by hand |
MISSING | a word of the quote does not appear at all. A real failure. |
No quote came back MISSING. Seven came back PARTIAL, every one for the same mechanical reason — a two-column interleave, a hyphen-split word, or both — and every one was read by hand against the source:
| Paper | Quote or figure | Verdict | Hand check |
|---|---|---|---|
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “Over twelve days, we saw a variation of up to 27% (max on day 3–80,274; min on day 9–58,951) in observed tracking requests.” | PARTIAL | Confirmed. variation is hyphen-split as varia- tion and the neighbouring column interleaves every ~12 words. |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “the number of distinct tracking domains remains almost stable during the experiment (variation of 3.5%)” | EXACT | — |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “studies that analyze the ecosystem will find similar results, while studies that aim to analyze the extent of a tracking phenomenon might see different results based on the measurement day” | PARTIAL | Confirmed, same interleave. |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “we find that the identified trackers on pages can vary by 25% based on the used browser configuration” | EXACT | — |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “even the same setup measures different levels of tracking on different days, which might lead to different conclusions of a study” | SPLICED | — |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “We use the popular Open-WPM Framework [21] (v0.15.0 - Firefox version 88)” | EXACT | — |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “The standard deviation of such requests is 8,203.” | PARTIAL | Confirmed; the sentence is split across the column boundary. |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “For our continuous measurement, we visit the top 1k sites from our website corpus (18,377 distinct pages) daily” | EXACT | Added specifically to check the content page's description of the twelve-day experiment — see the correction below. |
| [1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)] | “privacy measurements and analyses can vary up to 65% depending on the region” | PARTIAL | Confirmed. |
| [2Demir, Nurullah; Hörnemann, Jan; Große-Kampmann, Matteo; Urban, Tobias; Pohlmann, Norbert; Holz, Thorsten; Wressnegger, Christian (2023): "On the Similarity of Web Measurements Under Different Experimental Setups", in: Proceedings of the ACM Internet Measurement Conference, pp. 356-369. ACM DOI 10.1145/3618257.3624795 is listed by DBLP but was not registered with the DOI resolver as of 2026-08-12; the DOI above resolves to the authors' institutional record of the same paper (DOI)] | “when comparing two different profiles, 48% of the underlying data varies” | PARTIAL | Confirmed. The full source sentence is “The results show that when comparing two different profiles, 48% of the underlying data varies.” |
| [2Demir, Nurullah; Hörnemann, Jan; Große-Kampmann, Matteo; Urban, Tobias; Pohlmann, Norbert; Holz, Thorsten; Wressnegger, Christian (2023): "On the Similarity of Web Measurements Under Different Experimental Setups", in: Proceedings of the ACM Internet Measurement Conference, pp. 356-369. ACM DOI 10.1145/3618257.3624795 is listed by DBLP but was not registered with the DOI resolver as of 2026-08-12; the DOI above resolves to the authors' institutional record of the same paper (DOI)] | “only 32% of the cookies appear in all profiles and 42% only in one profile” | EXACT | — |
| [3Jueckstock, Jordan; Sarker, Shaown; Snyder, Peter; Beggs, Aidan; Papadopoulos, Panagiotis; Varvello, Matteo; Livshits, Benjamin; Kapravelos, Alexandros (2021): "Towards Realistic and Reproducible Web Crawl Measurements", in: Proceedings of the ACM Web Conference. (DOI)] | “Around 5% of content-providing domains show significant measurement bias across VP” | SPLICED | — |
| [4Ahmad, Syed Suleman; Dar, Muhammad Daniyal; Zaffar, Muhammad Fareed; Vallina-Rodriguez, Narseo; Nithyanand, Rishab (2020): "Apophanies or Epiphanies? How Crawlers Impact Our Understanding of the Web", in: Proceedings of The Web Conference, pp. 271-280. (DOI)] | “variation of over 16% in the number of successful page loads” | EXACT | — |
| [5Nenadić, Luka; Rodriguez, David; Calandrino, Joseph A. (2026): "Overcoming Language Barriers: Multilingual Analysis of the 2023 Swiss Privacy Law's Impact", Proceedings on Privacy Enhancing Technologies 2026(4):703-723. (DOI)] | “we restrict the analysis to the balanced panel of websites with an observed policy in both snapshots” | PARTIAL | Confirmed; snapshots is split as snap- shots. |
| [5Nenadić, Luka; Rodriguez, David; Calandrino, Joseph A. (2026): "Overcoming Language Barriers: Multilingual Analysis of the 2023 Swiss Privacy Law's Impact", Proceedings on Privacy Enhancing Technologies 2026(4):703-723. (DOI)] | “The balanced panel comprises N = 11,800 websites observed in both August and October 2023” | PARTIAL | Confirmed. |
| [5Nenadić, Luka; Rodriguez, David; Calandrino, Joseph A. (2026): "Overcoming Language Barriers: Multilingual Analysis of the 2023 Swiss Privacy Law's Impact", Proceedings on Privacy Enhancing Technologies 2026(4):703-723. (DOI)] | “Standard errors are clustered at the website level to account for repeated observations of the same policy across snapshots.” | SPLICED | — |
| [6Hantke, Florian; Snyder, Peter; Haddadi, Hamed; Stock, Ben (2025): "Web Execution Bundles: Reproducible, Accurate, and Archivable Web Measurements", in: Proceedings of the USENIX Security Symposium. (Link)] | “could be conducted using WebREC as is” | EXACT | — |
| [7Singh, Sachin Kumar; Mahmud, Faisal; Ricci, Robert; Siby, Sandra (2026): "The Empire Strikes Back (at Your Privacy): An Archaeology of Tracking on Government Websites", Proceedings on Privacy Enhancing Technologies 2026(2):108-126. (DOI)] | “We therefore label trackers using the June 2025 EasyList and EasyPrivacy snapshot for all years, keeping the labeling rule fixed” | EXACT | — |
A correction the checker forced. An earlier draft of the lead said Demir et al. ran the twelve-day experiment “from the same place”. The paper's C4 paragraph says only “we visit the top 1k sites from our website corpus (18,377 distinct pages) daily (starting at midnight) throughout our experiment (from 07/08/2021 to 07/19/2021)”, and the study as a whole crawls from three geolocations. It does not say the temporal sub-analysis holds the vantage point fixed, so the claim was removed rather than assumed.
A figure the extraction offered and the page does not use. temporal.snapshots has nine papers reporting more than 1,000 snapshots — the largest is 550,000. Spot-reading them shows page counts and record counts mis-extracted into the field (an NDSS 2019 longitudinal browser study, a CCS 2025 robots.txt study). No maximum, mean or median of snapshots is published anywhere; only bands, and the band table carries the warning.
7. Full-text probes, and the two that were hand-audited
All probes collapse whitespace before matching, because a PDF line break inside a phrase would otherwise hide it. Every probe is a mention count and therefore an upper bound. Two probes carry a load-bearing claim on the content page and were therefore read in full.
Audit A — "does anyone pin their browser version?"
The structured field crawlConfig.browsers contains a digit for 17 of 250 repeated-crawl papers (6.8%). That undercounts, because a paper can write “Chrome” in its setup table and “Chrome 88.0.4298.0” in a footnote. So a deliberately wide probe was run for any <browser> <number> string anywhere in the text: 58 hits (23.2%).
All 58 were read. 26 state the version of a browser the study's own measurement drove. The 32 rejected split into:
- bibliography entries — “Firefox 85 Cracks Down on Supercookies”, “About the security content of Safari 11.1”;
- ecosystem history — “Full support for this CSP feature first landed in browsers around March 2015 (with Chrome 43)”;
- the version of a browser being measured rather than driven — the NDSS 2019 mobile-browser study reports “Firefox v59.0 on Android 7.1” because those browsers are the subject;
- outright false positives — footnote markers (
Chromium 1 and Firefox 2), citation markers (Firefox22), table cells whose numbers are accuracies or counts, and one “visited from a Chrome browser 100 times”.
So the honest range published on the content page is 17 (structured field) to 26 (text), of 250.
Audit B — "has anyone noticed the browser updates itself?"
A wide probe for auto-update or browser-version drift returns 14 of 250. All 14 were read. Not one discusses pinning or reporting the crawler's own browser version across waves. The hits are: Windows Update traffic, phishing blacklist auto-updating, a cited paper title, mobile users auto-updating apps, WordPress auto-update, an ad-blocker list auto-update proposal, and Chromium's internal frame-process mapping being “updated”. The closest to the topic is a limitations note in a PETS 2020 paper: “Our measurement tools require constant maintenance to keep up with browser updates and interface changes.” That is about tool maintenance, not about wave comparability.
The content page states this as “not one of the 14 hits”, never as “nobody”, because a probe cannot support “nobody”.
Audit C — Chrome for Testing, Docker and WebREC
These three are corpus-wide probes (all 5,855 readable papers), not restricted to repeated, because the question is whether the mechanism has any footprint in these seven venues at all. They live in §11 of the report script, and the script prints each regex beside its count — a different regex gives a different number, and hiding the regex would hide that.
- Chrome for Testing. The exact phrase returns 0. Widened to
googlechromelabs,chrome labs,known-good-versionsand the bare tokenCfT, it returns 10 — and reading all 10 shows every one is a false positive: Combating the Financing of Terrorism (three papers), Control Flow Trimming, Call Flow Tree, Crash-Fault Tolerant, a certificate organisation nameO=CFT, aCFtvariable in an algorithm listing,CFT-IDF, and a fitness-test app namedCFT & PFT. - Docker.
/\bdocker\b/igives 293 (5.0%); addingcontainerised/containerizedgives 308 (5.3%). Both are printed, and the content page names which is which — an earlier draft published 308 as “mention Docker”, which is the wider figure under the narrower label. - Image digest.
/\bimage digest\b|\bsha256:[0-9a-f]{12}/ireturns 0. - WebREC.
/\bWebREC\b|\bweb execution bundle/ireturns 2, and reading both shows neither is a use: one is the WebREC paper, and the other cites it in its reference list only. An earlier draft said “two papers mention it” without that distinction, which reads as two adopters.
Every one of these is a mention count. A paper can pin a container digest in its repository and never say so in the text; these are reporting figures, not practice figures, and the content page says so.
Audit D — the Tranco list ID
Two wide probes were run over the 266 Tranco papers; their union was 37 candidates, and all 37 were read. 23 genuinely cite a Tranco list ID. The other 14 matched on a legal section number near the word Tranco (CAL. CIV. Code § 1798.120©), a reference-list code, or nothing identifiable. Two of the 23 were found only by the looser probe and would have been missed by the tighter one — mobileatlas writes “Tranco [25] list V78N” with no “ID” token, and autofr cites it only in the reference list. That is why the report script carries a written-out allowlist rather than recomputing the figure from a regex: no regex found all 23, and the script warns loudly if an allowlist entry stops matching a Tranco paper.
The 23, by year:
| Year | Papers |
|---|---|
| 2020 | beyond-the-front-page (WWW) |
| 2021 | reining-in-the-webs-inconsistencies (NDSS), privacy-preference-signals (PETS) |
| 2022 | helping-or-hindering (CCS), setting-the-bar-low (PETS) |
| 2023 | you-call-this-archaeology (CCS), its-dom-clobbering-time (IEEE S&P), autofr (USENIX), mobileatlas (USENIX), the-hitchhikers-guide (WWW) |
| 2024 | to-auth-or-not-to-auth (IEEE S&P), where-are-the-red-lines (IEEE S&P), a-bilingual-longitudinal-analysis (PETS), johnny-still-cant-opt-out (PETS) |
| 2025 | in-the-dom-we-trust (CCS), a-wall-behind-a-wall (IEEE S&P), chaos-in-the-chain (IMC), towards-a-non-binary-view-of-ipv6-adoption (IMC), johnny-cant-revoke-consent-either (PETS), understanding-privacy-norms-through-web-forms (PETS), exposing-and-circumventing-sni-based-quic-censorship (USENIX), navigating-cookie-consent-violations (USENIX) |
| 2026 | inconsistent-incomplete-and-insecure (USENIX) |
8. External sources
Everything below was fetched on 2026-08-27, from a primary source, not recalled.
| Claim on the page | How it was verified |
|---|---|
| Tranco list ids are permanent and still resolve | GET https://tranco-list.eu/api/lists/id/GVWK returns "available": true with a download URL, for a list generated 2019-11-05 |
| Tranco's default provider set changed four times | GET https://tranco-list.eu/api/lists/date/{20191105,20220101,20230101,20240101,20260101} and /latest; the configuration.providers array in each response is the table on the page verbatim |
| The provider set is settable on a custom list | https://tranco-list.eu/api_documentation documents providers as a field of Configuration on PUT /lists/create (authentication required) |
| Chrome for Testing lists 2,484 versions, 113.0.5672.0 to 154.0.8026.0 | https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json, counted; a spot-check confirms 116.0.5845.96 has linux64, mac-arm64, mac-x64, win32 and win64 downloads |
| Chrome stable was 152.0.7977.64 | https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json |
| OpenWPM pins an exact Firefox build | https://raw.githubusercontent.com/openwpm/OpenWPM/master/scripts/install-firefox.sh — TAG='9ce1ee6baeb9a3c326dbd180bdece65d8fc2eadc' # FIREFOX_154_0_RELEASE |
| OpenWPM v0.36.0, 2026-08-24, Firefox 154 | GitHub releases API; the release notes also describe a geckodriver 0.37.1 incompatibility, which is where the “record the driver version too” advice comes from |
| Firefox releases are retained | https://archive.mozilla.org/pub/firefox/releases/88.0/ returns 200 |
| EasyList self-identifies with a version and a commit | https://easylist.to/easylist/easylist.txt header, reproduced verbatim on the content page |
| The EasyList git history begins 2016-05-15 | GitHub repositories API, created_at |
| HTTP Archive crawls monthly, 2nd Tuesday, from CrUX | https://httparchive.org/faq |
| HTTP Archive's environment changelog is dead | the FAQ links github.com/HTTPArchive/httparchive/blob/main/docs/changelog.json, which 404s; the file resolves on the master branch, contains 12 entries, and its newest is dated 2017-06-01 |
| WebREC is real but tiny | GitHub repositories API for cispa/WebREC: not archived, last push 2025-03-26, single-digit stars |
| Playwright pins an exact browser build per release | https://raw.githubusercontent.com/microsoft/playwright/main/packages/playwright-core/browsers.json — the chromium entry is titled “Chrome for Testing” and carries browserVersion 152.0.7977.54 at revision 1241 |
Rejected
| Source or claim | Why it was rejected |
|---|---|
Tranco's Configure page as evidence for which providers are selectable today | login-walled; only the API documentation could be read, so the claim on the page is restricted to what the documentation states rather than to an observed option list |
| A date for HTTP Archive's Alexa→CrUX switch | no primary announcement located in the time available. The FAQ states the current source and the changelog does not cover the change; the page says exactly that and gives no date |
| A between-wave attrition figure from a full-text probe | a probe for no longer reachable / went offline / dropped out over the 250 returns 18 hits; reading them, only two are genuinely about targets lost between waves of the same study. Not published — biases already carries the defensible version of this claim |
The raw maximum of temporal.snapshots | mis-extracted, see §6 |
9. What could not be established
- Whether a paper held a pin constant across waves. Closed on 2026-09-07 — see §18 below. All 250 were hand-coded rather than a sample, and the content page now publishes estimates beside the stated-value figures. What is left open by that work is in §18.8, and the largest residue is that
same-implied— one configuration described and never contradicted — cannot be distinguished from a pin that silently moved. - Whether the 2025–2026 slice changes the trend. The repeat rate is flat at roughly one crawling paper in five across four buckets, and the last bucket is provisional by construction. Nothing in the 2025–2026 material suggests a change, but those venue-years are the thinnest in the corpus.
- Whether pinning actually changes a published answer. The whole case rests on single-wave variation studies ([1Demir, Nurullah; Große-Kampmann, Matteo; Urban, Tobias; Wressnegger, Christian; Holz, Thorsten; Pohlmann, Norbert (2022): "Reproducibility and Replicability of Web Measurement Studies", in: Proceedings of the ACM Web Conference. (DOI)], [2Demir, Nurullah; Hörnemann, Jan; Große-Kampmann, Matteo; Urban, Tobias; Pohlmann, Norbert; Holz, Thorsten; Wressnegger, Christian (2023): "On the Similarity of Web Measurements Under Different Experimental Setups", in: Proceedings of the ACM Internet Measurement Conference, pp. 356-369. ACM DOI 10.1145/3618257.3624795 is listed by DBLP but was not registered with the DOI resolver as of 2026-08-12; the DOI above resolves to the authors' institutional record of the same paper (DOI)], [3Jueckstock, Jordan; Sarker, Shaown; Snyder, Peter; Beggs, Aidan; Papadopoulos, Panagiotis; Varvello, Matteo; Livshits, Benjamin; Kapravelos, Alexandros (2021): "Towards Realistic and Reproducible Web Crawl Measurements", in: Proceedings of the ACM Web Conference. (DOI)], [4Ahmad, Syed Suleman; Dar, Muhammad Daniyal; Zaffar, Muhammad Fareed; Vallina-Rodriguez, Narseo; Nithyanand, Rishab (2020): "Apophanies or Epiphanies? How Crawlers Impact Our Understanding of the Web", in: Proceedings of The Web Conference, pp. 271-280. (DOI)]). No study repeats one measurement under pinned and unpinned conditions over a year. Listed as an open question on the content page.
- A denominator for “how many longitudinal studies got their trend wrong”. Unknowable from this data and not attempted.
10. The report script
Every figure on the content page comes from this script. It prints each population with its denominator, folds the one free-text field it aggregates, and prints that fold's unmapped residue in full.
- report_longitudinal.mjs
#!/usr/bin/env node // Every figure on measuretheweb.org/design:longitudinal, with its denominator. // // node scripts/report_longitudinal.mjs [--run <dir>] // // Default run dir: /workspace/publications_dataset/data/extract/run1 // // Rules this script obeys, from the site's own methodology: // * paper-counted, never tuple-counted; // * sentinels (`not-stated`, `not-applicable`, ...) are never an answer; // * every free-text field is folded before aggregating and the unmapped // residue is printed in full; // * every block names its denominator on the line above the numbers. import fs from 'node:fs'; import path from 'node:path'; import { foldCadence } from './cadence_fold.mjs'; const argv = process.argv.slice(2); const RUN = argv.includes('--run') ? argv[argv.indexOf('--run') + 1] : '/workspace/publications_dataset/data/extract/run1'; const FULLTEXT = argv.includes('--fulltext') ? argv[argv.indexOf('--fulltext') + 1] : '/workspace/publications_dataset/data/fulltext'; const P = fs .readFileSync(path.join(RUN, 'extractions.jsonl'), 'utf8') .split('\n') .filter(Boolean) .map((s) => JSON.parse(s)); const pct = (a, b) => (b ? ((100 * a) / b).toFixed(1) : '0.0'); const rule = (t) => console.log(`\n${'='.repeat(74)}\n${t}\n${'='.repeat(74)}`); const head = (t, d) => console.log(`\n-- ${t}\n population: ${d}`); // -------------------------------------------------------------- populations // `crawled` is the same definition extract_overview.mjs uses, so the counts on // this page line up with the ones in OVERVIEW.md. const crawled = P.filter((p) => p.crawlConfig != null || p.studyTypes.includes('automated-web-crawl')); const empirical = P.filter((p) => p.isEmpirical); // A paper is `repeated` when any temporal tuple states more than one snapshot. // This is the same rule statistics:biases uses, so the two pages agree. const snapOf = (p) => Math.max(0, ...p.temporal.map((t) => (t.snapshots == null ? 0 : t.snapshots))); const repeated = crawled.filter((p) => snapOf(p) > 1); const singleShot = crawled.filter((p) => snapOf(p) <= 1); // ------------------------------------------------------------------- pins // Four things a repeat crawl has to hold constant to be comparable with its own // earlier waves. Each predicate asks only "did the paper state it at all" — // the extraction cannot tell us whether the value was the SAME across waves, so // every figure below is an upper bound on comparability, not a measure of it. const statedListVersion = (p) => p.population.some((x) => x.listVersion != null && !/^(?:not[- ]stated|unknown|n\/a)$/i.test(x.listVersion.trim())); const namedBrowser = (p) => p.crawlConfig != null && p.crawlConfig.browsers.length > 0; const browserWithVersion = (p) => p.crawlConfig != null && p.crawlConfig.browsers.some((b) => /\d/.test(b)); const statedVantage = (p) => p.vantage.some((v) => v.locations.some((l) => !/^(?:not[- ]stated|unknown|n\/a)$/i.test(String(l).trim()))); const statedInfra = (p) => p.vantage.some((v) => v.infrastructure !== 'not-stated'); const ccStated = (p, f) => p.crawlConfig != null && !['not-stated', 'not-applicable'].includes(p.crawlConfig[f]); const allFourPins = (p) => statedListVersion(p) && namedBrowser(p) && statedVantage(p) && ccStated(p, 'statefulness'); const PINS = [ ['population list version stated', statedListVersion], ['browser named', namedBrowser], [' ... with a version number in the name', browserWithVersion], ['vantage location stated', statedVantage], ['vantage infrastructure stated', statedInfra], ['statefulness stated', (p) => ccStated(p, 'statefulness')], ['consent action stated', (p) => ccStated(p, 'consentAction')], ['interaction depth stated', (p) => ccStated(p, 'interactionDepth')], ['headless stated', (p) => ccStated(p, 'headless')], ['authentication stated', (p) => ccStated(p, 'authentication')], ['span start AND end stated', (p) => p.temporal.some((t) => t.spanStart != null) && p.temporal.some((t) => t.spanEnd != null)], ['cadence stated', (p) => p.temporal.some((t) => t.cadence != null)], ]; // ---------------------------------------------------------------- span // Longest stated span on any temporal tuple, in months. CAVEAT, and it matters: // a paper's longest span is often a REUSED dataset arm, not the span of its own // repeated crawl. The "multi-year" group below therefore mixes both, which is // itself part of why it pins less — see the mode breakdown printed with it. const MONTH_NAMES = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; // Parse a stated date into {year, month}. spanStart/spanEnd are free-text as the // paper wrote them, so BOTH forms occur and both must be handled: // numeric "2019-03", "2019/03", "2019" // textual "March 2019", "Mar. 2019", "26 March 2019", "April 17th, 2023" // An earlier version of this function only understood the numeric form, so every // textual month silently became January. That put a same-year span like // "February 2011 -> July 2011" into the "<1 month" band and a cross-year span // like "June 2010 -> January 2011" into "1-2 years". 129 stated dates in the // corpus are textual, so this was not a rare edge case; it moved five bands and // the multi-year/sub-year split. Caught by review, fixed, and both tables re-run. // // The year must be a standalone token, not a run of digits inside a larger // number: "2000 domains, March 2020" would otherwise parse 2000 as the year, and // 2000 sits inside the sanity window so nothing would catch it. function parseDate(s) { if (!s) return null; const t = String(s).toLowerCase(); const ym = /(?:^|[^\d])(\d{4})(?:[-/](0?[1-9]|1[0-2]))(?![\d])/.exec(t); if (ym) return { y: +ym[1], m: +ym[2] }; const yOnly = /(?:^|[^\d])(\d{4})(?![\d])/.exec(t); if (!yOnly) return null; const y = +yOnly[1]; if (y < 1990 || y > 2027) return null; const name = new RegExp(`\\b(${MONTH_NAMES.join('|')})[a-z]*\\b`).exec(t); return { y, m: name ? MONTH_NAMES.indexOf(name[1]) + 1 : 1 }; } function spanMonths(a, b) { const pa = parseDate(a); const pb = parseDate(b); if (!pa || !pb) return null; if (pa.y < 1990 || pb.y < 1990 || pa.y > 2027 || pb.y > 2027) return null; const d = (pb.y - pa.y) * 12 + (pb.m - pa.m); return d < 0 ? null : d; } const longestSpan = (p) => { let best = null; for (const t of p.temporal) { const m = spanMonths(t.spanStart, t.spanEnd); if (m != null && (best == null || m > best)) best = m; } return best; }; // ================================================================== output console.log(`design:longitudinal — figures with denominators`); console.log(`run: ${RUN}`); console.log(`corpus: ${P.length.toLocaleString()} papers, 7 venues (CCS, IMC, NDSS, PETS, USENIX Sec, TheWebConf, IEEE S&P), 2010-2026`); rule('1. POPULATIONS'); console.log(`all papers ${P.length}`); console.log(`empirical (measured the world as it is) ${empirical.length}`); console.log(`crawled (crawlConfig != null OR studyType crawl) ${crawled.length}`); console.log(`repeated (crawled AND temporal.snapshots > 1) ${repeated.length} = ${pct(repeated.length, crawled.length)}% of crawled`); console.log(`single-shot or snapshot count unstated ${singleShot.length} = ${pct(singleShot.length, crawled.length)}% of crawled`); const anyRepeat = P.filter((p) => snapOf(p) > 1); console.log(`\nany paper (not only crawls) with snapshots > 1 ${anyRepeat.length} = ${pct(anyRepeat.length, P.length)}% of all papers`); rule('2. IS THE MEASUREMENT PERIOD EVEN STATED'); head('temporal reporting fields', `${empirical.length} empirical papers`); for (const [label, f] of [ ['spanStart stated', (p) => p.temporal.some((t) => t.spanStart != null)], ['spanEnd stated', (p) => p.temporal.some((t) => t.spanEnd != null)], ['both stated', (p) => p.temporal.some((t) => t.spanStart != null) && p.temporal.some((t) => t.spanEnd != null)], ['snapshots stated', (p) => p.temporal.some((t) => t.snapshots != null)], ['cadence stated', (p) => p.temporal.some((t) => t.cadence != null)], ]) { const n = empirical.filter(f).length; console.log(` ${label.padEnd(22)} ${String(n).padStart(5)} ${pct(n, empirical.length).padStart(5)}%`); } head('the same fields', `${crawled.length} crawling papers`); for (const [label, f] of [ ['spanStart stated', (p) => p.temporal.some((t) => t.spanStart != null)], ['spanEnd stated', (p) => p.temporal.some((t) => t.spanEnd != null)], ['both stated', (p) => p.temporal.some((t) => t.spanStart != null) && p.temporal.some((t) => t.spanEnd != null)], ['snapshots stated', (p) => p.temporal.some((t) => t.snapshots != null)], ['cadence stated', (p) => p.temporal.some((t) => t.cadence != null)], ]) { const n = crawled.filter(f).length; console.log(` ${label.padEnd(22)} ${String(n).padStart(5)} ${pct(n, crawled.length).padStart(5)}%`); } rule('3. HOW MANY WAVES, AND HOW FAR APART'); head('stated snapshot count, banded', `${crawled.length} crawling papers`); { const bands = new Map(); const order = ['not stated', '1', '2-3', '4-6', '7-12', '13-52', '>52']; for (const p of crawled) { const s = snapOf(p); const b = s === 0 ? 'not stated' : s === 1 ? '1' : s <= 3 ? '2-3' : s <= 6 ? '4-6' : s <= 12 ? '7-12' : s <= 52 ? '13-52' : '>52'; bands.set(b, (bands.get(b) || 0) + 1); } for (const b of order) console.log(` ${b.padEnd(12)} ${String(bands.get(b) || 0).padStart(5)} ${pct(bands.get(b) || 0, crawled.length).padStart(5)}%`); const wild = crawled.filter((p) => snapOf(p) > 1000).length; console.log(` NOTE: ${wild} papers carry a snapshot count above 1,000. Spot-checking these finds`); console.log(` page counts and record counts mis-extracted as snapshots, so the top band is`); console.log(` unreliable as a count of waves. Bands are used on the page, never the raw number.`); } head('longest stated span, banded', `${repeated.length} repeated crawling papers`); { const bands = new Map(); const order = ['<1 month', '1-2 months', '3-5 months', '6-11 months', '1-2 years', '2-5 years', '5+ years', 'no parsable end date']; for (const p of repeated) { const m = longestSpan(p); const b = m == null ? 'no parsable end date' : m < 1 ? '<1 month' : m < 3 ? '1-2 months' : m < 6 ? '3-5 months' : m < 12 ? '6-11 months' : m < 24 ? '1-2 years' : m < 60 ? '2-5 years' : '5+ years'; bands.set(b, (bands.get(b) || 0) + 1); } for (const b of order) console.log(` ${b.padEnd(22)} ${String(bands.get(b) || 0).padStart(5)} ${pct(bands.get(b) || 0, repeated.length).padStart(5)}%`); } head('cadence, folded (scripts/cadence_fold.mjs)', `${crawled.filter((p) => p.temporal.some((t) => t.cadence != null)).length} crawling papers that state any cadence`); { const denom = crawled.filter((p) => p.temporal.some((t) => t.cadence != null)).length; const fam = new Map(); const residue = new Map(); for (const p of crawled) { const seen = new Set(); for (const t of p.temporal) { if (t.cadence == null) continue; const f = foldCadence(t.cadence); if (f === 'unmapped') residue.set(t.cadence.trim(), (residue.get(t.cadence.trim()) || 0) + 1); if (!seen.has(f)) { seen.add(f); fam.set(f, (fam.get(f) || 0) + 1); } } } for (const [f, n] of [...fam.entries()].sort((a, b) => b[1] - a[1])) { console.log(` ${f.padEnd(26)} ${String(n).padStart(5)} ${pct(n, denom).padStart(5)}%`); } // The families are multi-valued, so summing the non-cadence rows double-counts. // The publishable figure is the number of papers whose cadence values are ALL // non-cadences — a duration, a bare round count, or "periodically". const NOTACADENCE = new Set(['duration-not-cadence', 'n-rounds-no-interval', 'irregular-or-unspecified', 'sentinel']); let noRealCadence = 0; for (const p of crawled) { const fams = new Set(p.temporal.filter((t) => t.cadence != null).map((t) => foldCadence(t.cadence))); if (fams.size && [...fams].every((f) => NOTACADENCE.has(f))) noRealCadence++; } console.log(`\n papers that fill the cadence slot but never state a cadence:`); console.log(` ${noRealCadence} / ${denom} = ${pct(noRealCadence, denom)}% (counting 'unmapped' as a possible cadence, i.e. conservative)`); console.log(`\n A paper can land in several families (it may state more than one cadence), so`); console.log(` the column does not sum to ${denom}. 'duration-not-cadence' is the field being used`); console.log(` to state how long the study ran rather than how often it repeated — those papers`); console.log(` have NOT stated a cadence and must not be counted as if they had.`); console.log(`\n UNMAPPED RESIDUE — ${residue.size} distinct strings, printed in full:`); for (const [s, n] of [...residue.entries()].sort((a, b) => b[1] - a[1])) console.log(` ${n} x ${s}`); } rule('4. THE FOUR PINS: WHAT A REPEATED CRAWL HOLDS FIXED'); head('stated at all (an upper bound on holding it fixed)', `${repeated.length} repeated vs ${singleShot.length} other crawling papers`); console.log(` ${'pin'.padEnd(42)} ${'repeated'.padStart(14)} ${'other crawls'.padStart(14)}`); for (const [label, f] of PINS) { const a = repeated.filter(f).length; const b = singleShot.filter(f).length; console.log(` ${label.padEnd(42)} ${`${a} (${pct(a, repeated.length)}%)`.padStart(14)} ${`${b} (${pct(b, singleShot.length)}%)`.padStart(14)}`); } { const a = repeated.filter(allFourPins).length; const b = singleShot.filter(allFourPins).length; const none = repeated.filter((p) => !statedListVersion(p) && !namedBrowser(p) && !statedVantage(p) && !ccStated(p, 'statefulness')).length; const strict = repeated.filter((p) => statedListVersion(p) && browserWithVersion(p) && statedVantage(p) && ccStated(p, 'statefulness')).length; console.log(`\n all four (list version + browser + vantage + statefulness)`); console.log(` repeated ${a} / ${repeated.length} = ${pct(a, repeated.length)}%`); console.log(` other crawls ${b} / ${singleShot.length} = ${pct(b, singleShot.length)}%`); console.log(` all four, with a browser VERSION rather than a browser name`); console.log(` repeated ${strict} / ${repeated.length} = ${pct(strict, repeated.length)}%`); console.log(` none of the four`); console.log(` repeated ${none} / ${repeated.length} = ${pct(none, repeated.length)}%`); } rule('5. THE LONGER THE PANEL, THE LESS IT PINS'); { const multi = repeated.filter((p) => { const m = longestSpan(p); return m != null && m >= 12; }); const sub = repeated.filter((p) => { const m = longestSpan(p); return m != null && m < 12; }); const unk = repeated.length - multi.length - sub.length; head('by longest stated span', `${repeated.length} repeated crawling papers (${unk} have no parsable end date and are excluded)`); for (const [name, g] of [['span >= 12 months', multi], ['span < 12 months', sub]]) { const f = g.filter(allFourPins).length; console.log(` ${name} n=${String(g.length).padStart(4)} list ${String(g.filter(statedListVersion).length).padStart(3)} browser ${String(g.filter(namedBrowser).length).padStart(3)} vantage ${String(g.filter(statedVantage).length).padStart(3)} statefulness ${String(g.filter((p) => ccStated(p, 'statefulness')).length).padStart(3)} all four ${f} (${pct(f, g.length)}%)`); } console.log(`\n Why the multi-year group pins less — its data provenance is different.`); console.log(` temporal.mode, paper-counted, multi-valued:`); const modes = (g) => { const c = new Map(); for (const p of g) for (const m of new Set(p.temporal.map((t) => t.mode))) c.set(m, (c.get(m) || 0) + 1); return c; }; for (const [name, g] of [['span >= 12 months', multi], ['span < 12 months', sub]]) { const c = modes(g); console.log(` ${name} ` + [...c.entries()].sort((a, b) => b[1] - a[1]).map(([m, n]) => `${m} ${n} (${pct(n, g.length)}%)`).join(', ')); } const liveOnly = (g) => g.filter((p) => p.temporal.every((t) => ['live-crawl', 'active-probing', 'not-stated'].includes(t.mode))); console.log(`\n Restricted to papers whose every temporal tuple is a live crawl or active probe`); console.log(` (no archive, no reused dataset), so the pins were at least available:`); for (const [name, g] of [['span >= 12 months', liveOnly(multi)], ['span < 12 months', liveOnly(sub)]]) { const f = g.filter(allFourPins).length; console.log(` ${name} n=${String(g.length).padStart(4)} all four ${f} (${pct(f, g.length)}%) << small n, report as a count`); } } rule('6. IS REPEATING GETTING MORE COMMON'); head('share of crawling papers that repeat, by year', 'crawling papers in each year'); { const years = [...new Set(crawled.map((p) => p.year))].sort(); for (const y of years) { const g = crawled.filter((p) => p.year === y); const r = g.filter((p) => snapOf(p) > 1).length; const flag = y >= 2025 ? ' << provisional venue-year, under-represented by construction' : ''; console.log(` ${y} crawled ${String(g.length).padStart(4)} repeated ${String(r).padStart(3)} ${pct(r, g.length).padStart(5)}%${flag}`); } const bucket = (a, b) => { const g = crawled.filter((p) => p.year >= a && p.year <= b); const r = g.filter((p) => snapOf(p) > 1).length; return `${a}-${b}: ${r}/${g.length} = ${pct(r, g.length)}%`; }; console.log(`\n four-year buckets: ${bucket(2010, 2011)} | ${bucket(2012, 2015)} | ${bucket(2016, 2019)} | ${bucket(2020, 2023)} | ${bucket(2024, 2026)} (last bucket provisional)`); } rule('7. DATA PROVENANCE OF EMPIRICAL WORK'); head('temporal.mode', `${empirical.length} empirical papers, multi-valued, paper-counted`); { const c = new Map(); for (const p of empirical) for (const m of new Set(p.temporal.map((t) => t.mode))) c.set(m, (c.get(m) || 0) + 1); for (const [m, n] of [...c.entries()].sort((a, b) => b[1] - a[1])) console.log(` ${m.padEnd(22)} ${String(n).padStart(5)} ${pct(n, empirical.length).padStart(5)}%`); } rule('8. FULL-TEXT PROBES OVER THE REPEATED-CRAWL PAPERS'); console.log(` Whitespace is collapsed before matching, because a PDF line break inside a`); console.log(` phrase would otherwise hide it. Every probe is a MENTION count and therefore`); console.log(` an upper bound; the two load-bearing ones were hand-read in full and the`); console.log(` audited counts are printed beside them.`); head('probe hits', `${repeated.length} repeated crawling papers, all with readable full text`); { const PROBES = [ ['self-describes as longitudinal', /\blongitudinal\b/i, null], ['re-crawl / repeat crawl / second crawl', /\bre-?crawl|\brepeat(?:ed)? (?:the )?(?:crawl|measurement|experiment)|\bsecond crawl\b|\bfollow-?up crawl\b/i, null], ['wave / round / iteration terminology', /\b(?:each|every|first|second|third|per) (?:wave|round|run|iteration)\b|\bcrawl(?:ing)? (?:wave|round)s?\b/i, null], ['comparable / comparability', /\bcomparab(?:le|ility)\b/i, null], ['reproducib* / replicab* / replicat*', /\breproducib\w*|\breplicab\w*|\breplicat\w*/i, null], ['same version / list / snapshot / configuration', /\bsame (?:version|list|snapshot|configuration|setup|browser|profile|seed list|instance)\b/i, null], ['pinned / froze / fixed the version (wide)', /\bpinn?ed\b|\bfroze[nd]?\b|\bfixed\s+(?:the\s+)?(?:version|browser|list|snapshot|commit|configuration|seed)|\bversion[- ]controll?ed\b/i, null], ['Docker / container / VM image', /\bdocker\b|\bcontaineri[sz]ed\b|\bVM image\b|\bvirtual machine image\b/i, null], ['names a filter list or blocklist', /\bEasyList\b|\bEasyPrivacy\b|\bDisconnect\b|\bfilter\s+lists?\b|\bblock\s?lists?\b/i, null], ['filter list pinned to a version or date', /\b(?:EasyList|EasyPrivacy|Disconnect|filter list|blocklist|block list)\b.{0,80}\b(?:version|as of|snapshot|dated|commit|downloaded on)\b/i, null], ['attrition or survivorship', /\battrition\b|\bsurvivorship\b/i, null], ['churn', /\bchurn\b/i, null], ['<browser> <number> anywhere in the text', /\b(?:Chrome|Chromium|Firefox|Edge|Safari|Tor Browser|Brave)\s*(?:browser\s*)?(?:v(?:ersion)?\.?\s*)?\d{1,3}(?:\.\d+)*\b/i, 26], ['browser auto-update / version drift (wide)', /\bauto-?updat\w*|\bautomatic(?:ally)?\s+updat\w*|\bbrowser\s+(?:version\s+)?(?:updates?|upgrad\w*)|\bdisabl\w+\s+updates?/i, 0], ]; const texts = repeated.map((p) => { const f = path.join(FULLTEXT, String(p.year), p.venue, p.slug, 'paper.cols.txt'); return fs.existsSync(f) ? fs.readFileSync(f, 'utf8').replace(/\s+/g, ' ') : null; }); const readable = texts.filter((t) => t != null).length; console.log(` full text readable for ${readable} of ${repeated.length}\n`); for (const [label, re, audited] of PROBES) { const n = texts.filter((t) => t != null && re.test(t)).length; const aud = audited == null ? '' : ` -> hand-read all ${n}: ${audited} genuine (${pct(audited, readable)}% of the population)`; console.log(` ${label.padEnd(46)} ${String(n).padStart(4)} ${pct(n, readable).padStart(5)}%${aud}`); } console.log(`\n The two audited probes, in words:`); console.log(` * "<browser> <number>" is a candidate set, not an answer. Reading all of them,`); console.log(` 26 state the version of a browser the study's OWN measurement drove. The rest`); console.log(` are bibliography entries, ecosystem history ("Chrome 43 shipped ..."), the`); console.log(` version of a browser being measured rather than driven, and outright false`); console.log(` positives (footnote markers, table cells, "Chrome browser 100 times").`); console.log(` * The auto-update probe: reading all hits, NOT ONE discusses pinning or`); console.log(` reporting the crawler's own browser version across waves. The closest is`); console.log(` a note that measurement tools need maintenance as browsers change.`); } rule('9. PINNING THE LIST: DOES ANYONE CITE A PERMANENT TRANCO ID'); { // Tranco has published permanent, retrievable list IDs since 2019, and a list // ID is the only mechanism in this literature that makes a population frame // recoverable years later (verified 2026-08-27: the 2019 id GVWK still // resolves). So: of the papers that use Tranco at all, how many cite one? // // Two wide probes were run over the full text of every Tranco paper; their // union was 37 candidates; ALL 37 were read by hand. The 23 below are the ones // that genuinely cite an identifier. The rest matched legal section numbers // and reference codes sitting near the word "Tranco". The allowlist is written // out rather than recomputed because a probe cannot make this call. const CITES_TRANCO_ID = new Set([ '2020/WWW/beyond-the-front-page-measuring-third-party-dynamics-in-the-field', '2021/NDSS/reining-in-the-webs-inconsistencies-with-site-policy', '2021/PETS/privacy-preference-signals-past-present-and-future', '2022/CCS/helping-or-hindering-how-browser-extensions-undermine-security', '2022/PETS/setting-the-bar-low-are-websites-complying-with-the-minimum-requirements-of-the', '2023/CCS/you-call-this-archaeology-evaluating-web-archives-for-reproducible-web-security', '2023/IEEE-SP/its-dom-clobbering-time-attack-techniques-prevalence-and-defenses', '2023/USENIX/autofr-automated-filter-rule-generation-for-adblocking', '2023/USENIX/mobileatlas-geographically-decoupled-measurements-in-cellular-networks-for-secur', '2023/WWW/the-hitchhikers-guide-to-facebook-web-tracking-with-invisible-pixels-and-click-i', '2024/IEEE-SP/to-auth-or-not-to-auth-a-comparative-analysis-of-the-pre-and-post-login-security', '2024/IEEE-SP/where-are-the-red-lines-towards-ethical-server-side-scans-in-security-and-privac', '2024/PETS/a-bilingual-longitudinal-analysis-of-privacy-policies-measuring-the-impacts-of-t', '2024/PETS/johnny-still-cant-opt-out-assessing-the-iab-ccpa-compliance-framework', '2025/CCS/in-the-dom-we-trust-exploring-the-hidden-dangers-of-reading-from-the-dom-on-the', '2025/IEEE-SP/a-wall-behind-a-wall-emerging-regional-censorship-in-china', '2025/IMC/chaos-in-the-chain-evaluate-deployment-and-construction-compliance-of-web-pki-ce', '2025/IMC/towards-a-non-binary-view-of-ipv6-adoption', '2025/PETS/johnny-can-t-revoke-consent-either-measuring-compliance-of-consent-revocation-on', '2025/PETS/understanding-privacy-norms-through-web-forms', '2025/USENIX/exposing-and-circumventing-sni-based-quic-censorship-of-the-great-firewall-of-ch', '2025/USENIX/navigating-cookie-consent-violations-across-the-globe', '2026/USENIX/inconsistent-incomplete-and-insecure-a-survey-of-account-security-interfaces', ]); const usesTranco = (p) => p.population.some((x) => /\btranco\b/i.test(x.sourceList || '')); const T = P.filter(usesTranco); head('Tranco users', `${T.length} papers naming Tranco as a population source`); const withVersion = T.filter((p) => p.population.some((x) => /\btranco\b/i.test(x.sourceList || '') && x.listVersion != null)); console.log(` states any list version or date (extraction field) ${withVersion.length} ${pct(withVersion.length, T.length)}%`); const cited = T.filter((p) => CITES_TRANCO_ID.has(`${p.year}/${p.venue}/${p.slug}`)); console.log(` cites a permanent Tranco list ID (hand-audited) ${cited.length} ${pct(cited.length, T.length)}%`); const years = [...new Set(T.map((p) => p.year))].sort(); console.log(`\n by year (Tranco users / of them, citing an ID):`); for (const y of years) { const g = T.filter((p) => p.year === y); const c = cited.filter((p) => p.year === y).length; console.log(` ${y} ${String(g.length).padStart(3)} ${String(c).padStart(2)} ${pct(c, g.length).padStart(5)}%`); } const stray = [...CITES_TRANCO_ID].filter((k) => !T.some((p) => `${p.year}/${p.venue}/${p.slug}` === k)); if (stray.length) console.log(`\n WARNING: ${stray.length} allowlist entries no longer match a Tranco paper: ${stray.join(', ')}`); } rule('10. EXEMPLARS — REPEATED CRAWLS THAT STATE ALL FOUR PINS'); console.log(` ${repeated.filter(allFourPins).length} papers. Most recent first. These are the papers to copy a methods section from.`); for (const p of repeated.filter(allFourPins).sort((a, b) => b.year - a.year || a.venue.localeCompare(b.venue))) { const cad = p.temporal.map((t) => t.cadence).filter(Boolean).join(' / ') || '-'; console.log(`\n ${p.year} ${p.venue} ${p.slug}`); console.log(` snapshots=${snapOf(p)} cadence=${cad} longest span=${longestSpan(p) == null ? 'n/a' : longestSpan(p) + 'mo'}`); console.log(` browsers=${JSON.stringify(p.crawlConfig.browsers)} statefulness=${p.crawlConfig.statefulness} consent=${p.crawlConfig.consentAction}`); console.log(` listVersion=${JSON.stringify(p.population.map((x) => x.listVersion).filter(Boolean))}`); console.log(` vantage=${JSON.stringify([...new Set(p.vantage.flatMap((v) => v.locations))])}`); } // Compact form of the same 23 papers, in the column order the wiki table uses, // so the page is copied from real output rather than retyped. console.log(`\n -- compact table (year | venue | slug | snapshots | distinct stated vantage locations)`); for (const p of repeated.filter(allFourPins).sort((a, b) => b.year - a.year || a.venue.localeCompare(b.venue))) { const locs = new Set(p.vantage.flatMap((v) => v.locations).filter((l) => !/^(?:not[- ]stated|unknown|n\/a)$/i.test(String(l).trim()))); console.log(` ${p.year} | ${p.venue} | ${p.slug} | ${snapOf(p)} | ${locs.size}`); } console.log(); rule('11. CORPUS-WIDE ADOPTION PROBES FOR THE MODERN PINNING MECHANISMS'); { // These four probes are corpus-wide, not restricted to `repeated`, because the // question is "has this mechanism any footprint in these seven venues at all". // Each regex is printed with its count so a reader can re-run the exact query; // a different regex gives a different number, and that is the point of showing it. // // Every one of these is a MENTION count. A paper can pin a container digest in // its repository and never say so in the text. Read them as reporting figures. const PROBES = [ ['Docker OR containerised', /\bdocker\b|\bcontaineri[sz]ed\b/i, null], [' ...Docker alone', /\bdocker\b/i, null], ['image digest or sha256: image reference', /\bimage digest\b|\bsha256:[0-9a-f]{12}/i, null], ['"Chrome for Testing", exact phrase', /chrome[-\s]?for[-\s]?testing/i, null], ['Chrome for Testing, widened to the tooling and the acronym', /chrome[-\s]?for[-\s]?testing|googlechromelabs|chrome[-\s]?labs|known[-\s]good[-\s]versions|\bCfT\b/i, 0], ['WebREC or "web execution bundle"', /\bWebREC\b|\bweb execution bundle/i, 0], ]; let readable = 0; const counts = PROBES.map(() => 0); for (const p of P) { const f = path.join(FULLTEXT, String(p.year), p.venue, p.slug, 'paper.cols.txt'); if (!fs.existsSync(f)) continue; readable++; const t = fs.readFileSync(f, 'utf8').replace(/\s+/g, ' '); PROBES.forEach(([, re], i) => { if (re.test(t)) counts[i]++; }); } head('mentions anywhere in the full text', `${readable} papers with readable full text, of ${P.length}`); PROBES.forEach(([label, re, audited], i) => { const aud = audited == null ? '' : ` -> hand-read all ${counts[i]}: ${audited} genuine`; console.log(` ${label.padEnd(58)} ${String(counts[i]).padStart(4)} ${pct(counts[i], readable).padStart(5)}%${aud}`); console.log(` ${' '.repeat(58)} ${String(re)}`); }); console.log(`\n Hand audits:`); console.log(` * The widened Chrome-for-Testing probe: every hit is a false positive —`); console.log(` CFT as "Combating the Financing of Terrorism" (three papers), "Control Flow`); console.log(` Trimming", "Call Flow Tree", "Crash-Fault Tolerant", a certificate subject`); console.log(` O=CFT, a CFt variable in an algorithm listing, CFT-IDF, and a fitness app`); console.log(` named "CFT & PFT". The exact-phrase probe is 0, and so is the audited count.`); console.log(` * WebREC: one hit is the WebREC paper itself; the other cites it in its`); console.log(` reference list only. No paper in the corpus USES it.`); }
11. The cadence fold
temporal.cadence is free text: 334 distinct strings fire on the 1,120 crawling papers. This is the fold, its rule order, and the reasoning behind the first rule.
- cadence_fold.mjs
// Folds the free-text `temporal.cadence` string into a small family vocabulary. // // WHY THIS EXISTS. `temporal.cadence` is free text and 334 distinct strings fire // on the 1,120 crawling papers alone. Counting exact strings would report // "daily" at 66 papers and bury the other 268 values in a tail nobody reads. // It is also *contaminated*: a large minority of the values state how LONG the // measurement ran ("one week", "5-day experiment", "month-long") rather than how // OFTEN it repeated. Those are not cadences and are folded into their own family // so they cannot be counted as one. // // Rules, in order. The FIRST rule that matches wins, and rules are ordered from // fastest interval to slowest, so "hourly and daily" folds to `hourly` — the // fastest repeat the paper states is the one that bounds its time resolution. // // Anything no rule matches lands in `unmapped` and is PRINTED IN FULL by the // report. A residue that only exists inside a script is a residue nobody reads. const SENTINEL = /^(?:not[- ]stated|none[- ]mentioned|unclear|unknown|n\/?a)$/i; const RULES = [ // A duration is not a cadence. This rule runs FIRST for the bare forms // ("one week", "15 days", "month-long", "5-day experiment") because they // would otherwise be swept up by the weekly/monthly rules below. It is // deliberately anchored: "every 5 minutes for 8 days" states a real cadence // and must not land here, so a leading `every|per|once` disqualifies it. // "seven days", "over one month", "two-week measurement period", "single crawl // lasting 12 days", "24-hour runs", "one-week collection period" — the paper is // stating how long it ran, not how often it repeated. // A string containing "between" or "apart" states an INTERVAL, not a duration // ("15 days between batch starts"), so it must never reach the duration rules. // The interval rules below pick it up by unit. ['duration-not-cadence', /^(?!.*\b(?:between|apart)\b)(?:.*\b(?:collection|measurement|study|crawl(?:ing)?|campaign|experiment|observation)\s+(?:period|window|cycle)\b|\b(?:period|window)\s+of\s+\d+|\bsingle\s+(?:day|crawl|scrape|run|visit|snapshot)\b|\bpilot\s+study\b|\blasting\s+\d+|\b\d+-hour\s+runs?\b|^(?:about|approximately|around|over|nearly|spread\s+across|during)\s+(?:a\s+)?(?:couple\s+of\s+|few\s+|multiple\s+|one\s+|two\s+|three\s+|several\s+|\d+\s*)?(?:day|week|month|year)s?\b|^(?:several|multiple)\s+(?:day|week|month|year)s?\b|^\d+\s*(?:day|week|month|year)s?$|^(?:one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+(?:consecutive\s+)?(?:day|week|month|year)s?$|\b(?:one|two|three|four|five|six|\d+)-(?:day|week|month|year)\s+(?:campaign|collection|trace|slot|phishing))/i], ['duration-not-cadence', /^(?!.*\b(?:every|per|once|between|apart|times?\s+(?:a|per)|twice|daily|hourly|weekly|monthly|yearly|annual|continuous|periodic|regular)\b)(?:(?:about|over|nearly|one|two|three|four|five|six|\d+)[- ]?)?(?:\d+[- ])?(?:second|minute|hour|day|week|month|year)s?[- ]?(?:long|period|experiment|crawl|campaign|study|snapshot|window|trace)?$|^\d+\s*(?:second|minute|hour|day|week|month|year)s?\b(?!.*\b(?:every|between|apart)\b)|(?:^|\b)(?:one|two|three|four|five|a)?[- ]?(?:day|week|month|year)-long\b/i], ['sub-minute', /\bmillisecond|\b\d+\s*(?:ms|milliseconds)\b|every\s+\d+(?:\.\d+)?\s*seconds?\b|every\s+(?:few|five|ten|fifteen|thirty)\s+seconds?\b|per\s+second\b/i], ['minutes', /\bevery\s+(?:\d+|a|one|two|three|five|ten|twelve|fifteen|thirty|sixty)\s*minutes?\b|\b\d+-minute\s+(?:interval|snapshot)|\bminute\s+intervals?\b|every\s+minute\b|once\s+per\s+minute\b|\bper\s+minute\b/i], ['hourly', /\bhourly\b|\bevery\s+(?:\d+|a|an|one|two|three|four|six|eight|twelve|24)\s*hours?\b|\b\d+-hour(?:ly)?\s+(?:interval|window)|\bhours?\s+apart\b|\bfour-hourly\b|\bevery\s+few\s+hours\b|\b(?:twice|two|three)\s+(?:times\s+)?(?:a|per)\s+day\b|\bmultiple\s+(?:scrapes|observations)\s+per\s+day\b|\btwice\s+daily\b|\bfive\s+times\s+daily\b|\b12\s+hours\b/i], ['daily', /\bdaily\b|\bnightly\b|\bevery\s+day\b|\bper\s+day\b|\bday\b.*\bintervals?\b|\bat\s+least\s+24\s+hours\s+between\b|\bmultiple\s+times\s+a\s+day\b/i], ['every-few-days', /\bevery\s+(?:\d+|a\s+few|two|three|four|five|six|ten|other)\s*days?\b|\bevery\s+two\s+to\s+three\s+days\b|\b(?:two|three|\d+)-day\s+(?:retry\s+)?interval\b|\bonce\s+every\s+\d+\s*days?\b|\bevery\s+\d+(?:\.\d+)?\s*days?\b|\bone\s+day\s+between\b|\b\d+\s*days?\s+between\b/i], ['weekly', /\bweekly\b|\bevery\s+week\b|\bper\s+week\b|\bweekdays?\b|\bevery\s+seven\s+days\b|\bat\s+least\s+one\s+week\s+between\b|\bone\s+week\s+between\b/i], ['fortnightly', /\bfortnight|\bbi-?weekly\b|\bevery\s+(?:two\s+weeks|14\s*days|15\s*days)\b|\btwo-week\s+intervals?\b|\btwo\s+weeks\s+between\b/i], ['monthly', /\bmonthly\b|\bbimonthly\b|\bevery\s+(?:four|twelve)\s+(?:months|weeks)\b|\bevery\s+month\b|\bper\s+month\b|\bevery\s+30\s*days\b|\bmonths?\s+apart\b|\bat\s+least\s+one\s+month\s+between\b|\bmonths?\s+between\b/i], ['quarterly', /\bquarterly\b|\btrimester\b|\bevery\s+three\s+months\b|\bevery\s+quarter\b|\bthree-month\s+intervals?\b/i], ['yearly', /\byearly\b|\bannual\w*\b|\bevery\s+year\b|\bone\s+year\s+apart\b/i], ['continuous', /\bcontinuous\w*\b|\breal[- ]?time\b|\biteratively\s+without\s+sleep\b|\bstreaming\b/i], ['irregular-or-unspecified', /\bperiodic\w*\b|\bsporadic\b|\bsimultaneous\b|\btemporally\s+close\b|\bas\s+domains\s+appeared\b|\bincreasing\s+(?:historical\s+)?intervals?\b|\bregular\w*\b|\badaptive\b|\bon[- ]demand\b|\bas\s+needed\b|\bwhen(?:ever)?\b|\bvariable\b|\bad\s?hoc\b/i], ['n-rounds-no-interval', /\b(?:one|two|three|four|five|six|seven|eight|nine|ten|\d+)\s+(?:repeated\s+|sequential\s+|distinct\s+|measurement\s+|querying\s+|collection\s+)*(?:rounds?|repetitions?|iterations?|visits?|scans?|scrapes?|queries|campaigns?|snapshots?|periods?|timestamps?|crawls?|groups?|runs?|windows?|days?|weeks?|intervals?)\b|\bonce\b|\brepeated\s+(?:experiments?|collection\s+iterations?)\b|\bup\s+to\s+\w+\s+sequential\s+visits\b|\bconsecutive\s+crawls\b/i], ]; export function foldCadence(raw) { const s = String(raw).trim().toLowerCase().replace(/\s+/g, ' '); if (SENTINEL.test(s)) return 'sentinel'; for (const [family, re] of RULES) if (re.test(s)) return family; return 'unmapped'; } export const FAMILIES = RULES.map(([f]) => f).concat('unmapped'); export const REAL_FAMILIES = FAMILIES.filter((f) => f !== 'duration-not-cadence' && f !== 'unmapped');
12. The quote check
- quotecheck_longitudinal.mjs
#!/usr/bin/env node // Verifies every quotation used on design:longitudinal against the paper text. // // node scripts/quotecheck_longitudinal.mjs // // Two-column PDFs are repaired into paper.cols.txt but not perfectly, so a // sentence can arrive spliced with text from the neighbouring column, and a word // can be hyphen-split across a line break. Each quote is checked against FOUR // renderings: paper.cols.txt, paper.norm.txt, paper.txt, and paper.cols.txt with // end-of-line hyphenation joined. The verdicts: // EXACT — the quote, whitespace-collapsed, appears verbatim // SPLICED — every word is covered by contiguous runs of >= MIN_RUN words, // so only the reading order was broken // PARTIAL — some words matched only in shorter runs. Read it by hand. // MISSING — a word of the quote does not appear at all. A real failure. import fs from 'node:fs'; import path from 'node:path'; const ROOT = '/workspace/publications_dataset/data/fulltext'; const norm = (s) => s.replace(/[‐-―−]/g, '-').replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, ' ').trim(); const QUOTES = [ ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'Over twelve days, we saw a variation of up to 27% (max on day 3-80,274; min on day 9-58,951) in observed tracking requests.'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'the number of distinct tracking domains remains almost stable during the experiment (variation of 3.5%)'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'studies that analyze the ecosystem will find similar results, while studies that aim to analyze the extent of a tracking phenomenon might see different results based on the measurement day'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'we find that the identified trackers on pages can vary by 25% based on the used browser configuration'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'even the same setup measures different levels of tracking on different days, which might lead to different conclusions of a study'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'We use the popular Open-WPM Framework [21] (v0.15.0 - Firefox version 88)'], ['2023/IMC/on-the-similarity-of-web-measurements-under-different-experimental-setups', 'when comparing two different profiles, 48% of the underlying data varies'], ['2026/PETS/overcoming-language-barriers-multilingual-analysis-of-the-2023-swiss-privacy-law', 'we restrict the analysis to the balanced panel of websites with an observed policy in both snapshots'], ['2026/PETS/overcoming-language-barriers-multilingual-analysis-of-the-2023-swiss-privacy-law', 'Standard errors are clustered at the website level to account for repeated observations of the same policy across snapshots.'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'The standard deviation of such requests is 8,203.'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'For our continuous measurement, we visit the top 1k sites from our website corpus (18,377 distinct pages) daily'], ['2022/WWW/reproducibility-and-replicability-of-web-measurement-studies', 'privacy measurements and analyses can vary up to 65% depending on the region'], ['2023/IMC/on-the-similarity-of-web-measurements-under-different-experimental-setups', 'only 32% of the cookies appear in all profiles and 42% only in one profile'], ['2021/WWW/towards-realistic-and-reproducibleweb-crawl-measurements', 'Around 5% of content-providing domains show significant measurement bias across VP'], ['2020/WWW/apophanies-or-epiphanies-how-crawlers-impact-our-understanding-of-the-web', 'variation of over 16% in the number of successful page loads'], ['2026/PETS/overcoming-language-barriers-multilingual-analysis-of-the-2023-swiss-privacy-law', 'The balanced panel comprises N = 11,800 websites observed in both August and October 2023'], ['2025/USENIX/web-execution-bundles-reproducible-accurate-and-archivable-web-measurements', 'could be conducted using WebREC as is'], ['2026/PETS/the-empire-strikes-back-at-your-privacy-an-archaeology-of-tracking-on-government', 'We therefore label trackers using the June 2025 EasyList and EasyPrivacy snapshot for all years, keeping the labeling rule fixed'], ]; // A two-column splice interleaves the neighbouring column every dozen words or // so, which destroys a naive substring test and most n-gram tests too. The check // that survives it: greedily segment the quote into the LONGEST contiguous word // runs that each appear in the text. If every word of the quote is covered by a // run of at least MIN_RUN words, the quote is present in the source and only the // reading order was broken. // // EXACT the whole quote appears verbatim // SPLICED every word covered by runs of >= MIN_RUN words // PARTIAL some words only matched in short runs -> read it by hand // MISSING a run of the quote does not appear at all const MIN_RUN = 4; function segment(words, text) { const runs = []; let i = 0; while (i < words.length) { let j = words.length; while (j > i && !text.includes(words.slice(i, j).join(' '))) j--; if (j === i) { runs.push({ len: 0, text: words[i] }); i++; } else { runs.push({ len: j - i, text: words.slice(i, j).join(' ') }); i = j; } } return runs; } let bad = 0; for (const [slug, q] of QUOTES) { // Fourth rendering: paper.cols.txt with PDF end-of-line hyphenation repaired // ("varia- tion" -> "variation"). Without it a quote that straddles a line // break inside a word reports MISSING even though it is plainly in the paper. const files = ['paper.cols.txt', 'paper.norm.txt', 'paper.txt', 'paper.cols.txt (de-hyphenated)'].map((f) => path.join(ROOT, slug, f.replace(' (de-hyphenated)', ''))); const texts = files.map((f, i) => { if (!fs.existsSync(f)) return null; const raw = fs.readFileSync(f, 'utf8'); return norm(i === 3 ? raw.replace(/(\w)-\s+(\w)/g, '$1$2') : raw); }); const LABEL = ['paper.cols.txt', 'paper.norm.txt', 'paper.txt', 'paper.cols.txt (de-hyphenated)']; const nq = norm(q); const words = nq.split(' '); let verdict = 'MISSING', where = '', detail = ''; for (let i = 0; i < files.length; i++) { if (texts[i] != null && texts[i].includes(nq)) { verdict = 'EXACT'; where = LABEL[i]; break; } } if (verdict !== 'EXACT') { let bestScore = -Infinity; for (let i = 0; i < files.length; i++) { if (texts[i] == null) continue; const runs = segment(words, texts[i]); const zero = runs.filter((r) => r.len === 0).length; const short = runs.filter((r) => r.len > 0 && r.len < MIN_RUN).length; const score = -zero * 100 - short; if (score > bestScore) { bestScore = score; where = LABEL[i]; detail = `${runs.length} run(s): ${runs.map((r) => r.len).join('+')} words` + (zero ? `, ${zero} word(s) NOT FOUND` : ''); verdict = zero > 0 ? 'MISSING' : short > 0 ? 'PARTIAL' : 'SPLICED'; } } } if (verdict === 'MISSING' || verdict === 'PARTIAL') bad++; console.log(`${verdict.padEnd(8)} ${slug}\n "${q.slice(0, 110)}${q.length > 110 ? '...' : ''}"\n ${where}${detail ? ' — ' + detail : ''}\n`); } console.log(bad === 0 ? 'every quote located in its source (EXACT or SPLICED)' : `${bad} quote(s) need a hand read`);
13. Unedited output of the report script
node scripts/report_longitudinal.mjs, run on 2026-08-27 against data/extract/run1. Nothing below is edited.
design:longitudinal — figures with denominators
run: /workspace/publications_dataset/data/extract/run1
corpus: 5,859 papers, 7 venues (CCS, IMC, NDSS, PETS, USENIX Sec, TheWebConf, IEEE S&P), 2010-2026
==========================================================================
1. POPULATIONS
==========================================================================
all papers 5859
empirical (measured the world as it is) 5118
crawled (crawlConfig != null OR studyType crawl) 1120
repeated (crawled AND temporal.snapshots > 1) 250 = 22.3% of crawled
single-shot or snapshot count unstated 870 = 77.7% of crawled
any paper (not only crawls) with snapshots > 1 818 = 14.0% of all papers
==========================================================================
2. IS THE MEASUREMENT PERIOD EVEN STATED
==========================================================================
-- temporal reporting fields
population: 5118 empirical papers
spanStart stated 2882 56.3%
spanEnd stated 2888 56.4%
both stated 2773 54.2%
snapshots stated 1237 24.2%
cadence stated 1304 25.5%
-- the same fields
population: 1120 crawling papers
spanStart stated 803 71.7%
spanEnd stated 795 71.0%
both stated 776 69.3%
snapshots stated 409 36.5%
cadence stated 389 34.7%
==========================================================================
3. HOW MANY WAVES, AND HOW FAR APART
==========================================================================
-- stated snapshot count, banded
population: 1120 crawling papers
not stated 711 63.5%
1 159 14.2%
2-3 93 8.3%
4-6 41 3.7%
7-12 33 2.9%
13-52 46 4.1%
>52 37 3.3%
NOTE: 9 papers carry a snapshot count above 1,000. Spot-checking these finds
page counts and record counts mis-extracted as snapshots, so the top band is
unreliable as a count of waves. Bands are used on the page, never the raw number.
-- longest stated span, banded
population: 250 repeated crawling papers
<1 month 35 14.0%
1-2 months 35 14.0%
3-5 months 41 16.4%
6-11 months 26 10.4%
1-2 years 27 10.8%
2-5 years 23 9.2%
5+ years 18 7.2%
no parsable end date 45 18.0%
-- cadence, folded (scripts/cadence_fold.mjs)
population: 389 crawling papers that state any cadence
daily 102 26.2%
duration-not-cadence 58 14.9%
hourly 46 11.8%
monthly 44 11.3%
weekly 43 11.1%
irregular-or-unspecified 31 8.0%
n-rounds-no-interval 29 7.5%
minutes 27 6.9%
continuous 27 6.9%
every-few-days 20 5.1%
unmapped 16 4.1%
sub-minute 9 2.3%
yearly 9 2.3%
quarterly 4 1.0%
fortnightly 4 1.0%
sentinel 1 0.3%
papers that fill the cadence slot but never state a cadence:
88 / 389 = 22.6% (counting 'unmapped' as a possible cadence, i.e. conservative)
A paper can land in several families (it may state more than one cadence), so
the column does not sum to 389. 'duration-not-cadence' is the field being used
to state how long the study ran rather than how often it repeated — those papers
have NOT stated a cadence and must not be counted as if they had.
UNMAPPED RESIDUE — 16 distinct strings, printed in full:
1 x four increasing cumulative periods for threshold selection
1 x four APKs per year per browser family
1 x July and December
1 x 15-minute trend monitoring
1 x one hour per website
1 x two six-month intervals per year for 2009–2019
1 x longitudinal measurement
1 x eleven-minute time slots
1 x Crawler B ran at the middle of each month
1 x irregular confirmation crawls
1 x collection cycles over six months
1 x during the day, paused overnight
1 x December 2023, May 2024, and December 2024
1 x sequential experimental windows
1 x four-month interval
1 x before and after the second rollout
==========================================================================
4. THE FOUR PINS: WHAT A REPEATED CRAWL HOLDS FIXED
==========================================================================
-- stated at all (an upper bound on holding it fixed)
population: 250 repeated vs 870 other crawling papers
pin repeated other crawls
population list version stated 167 (66.8%) 493 (56.7%)
browser named 143 (57.2%) 386 (44.4%)
... with a version number in the name 17 (6.8%) 49 (5.6%)
vantage location stated 89 (35.6%) 212 (24.4%)
vantage infrastructure stated 121 (48.4%) 324 (37.2%)
statefulness stated 70 (28.0%) 149 (17.1%)
consent action stated 105 (42.0%) 244 (28.0%)
interaction depth stated 208 (83.2%) 633 (72.8%)
headless stated 42 (16.8%) 98 (11.3%)
authentication stated 179 (71.6%) 600 (69.0%)
span start AND end stated 210 (84.0%) 566 (65.1%)
cadence stated 158 (63.2%) 231 (26.6%)
all four (list version + browser + vantage + statefulness)
repeated 23 / 250 = 9.2%
other crawls 32 / 870 = 3.7%
all four, with a browser VERSION rather than a browser name
repeated 6 / 250 = 2.4%
none of the four
repeated 28 / 250 = 11.2%
==========================================================================
5. THE LONGER THE PANEL, THE LESS IT PINS
==========================================================================
-- by longest stated span
population: 250 repeated crawling papers (45 have no parsable end date and are excluded)
span >= 12 months n= 68 list 53 browser 36 vantage 22 statefulness 14 all four 3 (4.4%)
span < 12 months n= 137 list 94 browser 84 vantage 55 statefulness 43 all four 18 (13.1%)
Why the multi-year group pins less — its data provenance is different.
temporal.mode, paper-counted, multi-valued:
span >= 12 months live-crawl 52 (76.5%), existing-dataset 27 (39.7%), web-archive 20 (29.4%), active-probing 15 (22.1%), passive-collection 12 (17.6%)
span < 12 months live-crawl 132 (96.4%), existing-dataset 35 (25.5%), active-probing 32 (23.4%), passive-collection 12 (8.8%), web-archive 3 (2.2%)
Restricted to papers whose every temporal tuple is a live crawl or active probe
(no archive, no reused dataset), so the pins were at least available:
span >= 12 months n= 16 all four 1 (6.3%) << small n, report as a count
span < 12 months n= 94 all four 13 (13.8%) << small n, report as a count
==========================================================================
6. IS REPEATING GETTING MORE COMMON
==========================================================================
-- share of crawling papers that repeat, by year
population: crawling papers in each year
2010 crawled 25 repeated 2 8.0%
2011 crawled 22 repeated 2 9.1%
2012 crawled 29 repeated 6 20.7%
2013 crawled 26 repeated 6 23.1%
2014 crawled 35 repeated 3 8.6%
2015 crawled 41 repeated 12 29.3%
2016 crawled 40 repeated 12 30.0%
2017 crawled 51 repeated 16 31.4%
2018 crawled 61 repeated 14 23.0%
2019 crawled 97 repeated 23 23.7%
2020 crawled 75 repeated 19 25.3%
2021 crawled 75 repeated 17 22.7%
2022 crawled 110 repeated 27 24.5%
2023 crawled 125 repeated 20 16.0%
2024 crawled 110 repeated 22 20.0%
2025 crawled 129 repeated 34 26.4% << provisional venue-year, under-represented by construction
2026 crawled 69 repeated 15 21.7% << provisional venue-year, under-represented by construction
four-year buckets: 2010-2011: 4/47 = 8.5% | 2012-2015: 27/131 = 20.6% | 2016-2019: 65/249 = 26.1% | 2020-2023: 83/385 = 21.6% | 2024-2026: 71/308 = 23.1% (last bucket provisional)
==========================================================================
7. DATA PROVENANCE OF EMPIRICAL WORK
==========================================================================
-- temporal.mode
population: 5118 empirical papers, multi-valued, paper-counted
existing-dataset 2142 41.9%
active-probing 1629 31.8%
live-crawl 1236 24.2%
passive-collection 988 19.3%
not-stated 338 6.6%
web-archive 65 1.3%
==========================================================================
8. FULL-TEXT PROBES OVER THE REPEATED-CRAWL PAPERS
==========================================================================
Whitespace is collapsed before matching, because a PDF line break inside a
phrase would otherwise hide it. Every probe is a MENTION count and therefore
an upper bound; the two load-bearing ones were hand-read in full and the
audited counts are printed beside them.
-- probe hits
population: 250 repeated crawling papers, all with readable full text
full text readable for 250 of 250
self-describes as longitudinal 123 49.2%
re-crawl / repeat crawl / second crawl 42 16.8%
wave / round / iteration terminology 54 21.6%
comparable / comparability 80 32.0%
reproducib* / replicab* / replicat* 78 31.2%
same version / list / snapshot / configuration 33 13.2%
pinned / froze / fixed the version (wide) 13 5.2%
Docker / container / VM image 18 7.2%
names a filter list or blocklist 84 33.6%
filter list pinned to a version or date 5 2.0%
attrition or survivorship 2 0.8%
churn 19 7.6%
<browser> <number> anywhere in the text 58 23.2% -> hand-read all 58: 26 genuine (10.4% of the population)
browser auto-update / version drift (wide) 14 5.6% -> hand-read all 14: 0 genuine (0.0% of the population)
The two audited probes, in words:
* "<browser> <number>" is a candidate set, not an answer. Reading all of them,
26 state the version of a browser the study's OWN measurement drove. The rest
are bibliography entries, ecosystem history ("Chrome 43 shipped ..."), the
version of a browser being measured rather than driven, and outright false
positives (footnote markers, table cells, "Chrome browser 100 times").
* The auto-update probe: reading all hits, NOT ONE discusses pinning or
reporting the crawler's own browser version across waves. The closest is
a note that measurement tools need maintenance as browsers change.
==========================================================================
9. PINNING THE LIST: DOES ANYONE CITE A PERMANENT TRANCO ID
==========================================================================
-- Tranco users
population: 266 papers naming Tranco as a population source
states any list version or date (extraction field) 159 59.8%
cites a permanent Tranco list ID (hand-audited) 23 8.6%
by year (Tranco users / of them, citing an ID):
2020 7 1 14.3%
2021 28 2 7.1%
2022 29 2 6.9%
2023 48 5 10.4%
2024 56 4 7.1%
2025 76 8 10.5%
2026 22 1 4.5%
==========================================================================
10. EXEMPLARS — REPEATED CRAWLS THAT STATE ALL FOUR PINS
==========================================================================
23 papers. Most recent first. These are the papers to copy a methods section from.
2026 PETS clicking-into-exposure-uncovering-privacy-risks-of-google-click-identifier-in-yo
snapshots=5 cadence=five repeated iterations longest span=0mo
browsers=["Chrome","Firefox","Edge"] statefulness=stateless consent=accept-and-reject
listVersion=["v3"]
vantage=["Germany","Australia","India","Brazil","USA (California)","USA (New York)"]
2026 PETS privacy-vs-profit-the-impact-of-googles-manifest-version-3-mv3-update-on-ad-bloc
snapshots=5 cadence=- longest span=0mo
browsers=["Google Chrome","Firefox"] statefulness=stateless consent=accept-all
listVersion=["January 9, 2024","July 12, 2025"]
vantage=["Europe"]
2026 PETS more-space-less-privacy-measuring-the-effectiveness-of-ip-based-website-fingerpr
snapshots=5 cadence=15 days between batch starts / hourly refreshes longest span=4mo
browsers=["Google Chrome"] statefulness=stateless consent=no-interaction
listVersion=["January 20, 2024","January 20, 2024","January 20, 2024","January 20, 2024"]
vantage=["US"]
2025 PETS beyond-the-request-harnessing-http-response-headers-for-cross-browser-web-tracke
snapshots=18 cadence=- longest span=0mo
browsers=["Chrome","Brave","Firefox"] statefulness=stateful consent=not-stated
listVersion=["16th of May 2022","March 29th, 2023"]
vantage=["Frankfurt, Germany"]
2025 PETS intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-banner-interaction-and
snapshots=4 cadence=- longest span=0mo
browsers=["Firefox v121.0","Chrome"] statefulness=both consent=accept-and-reject
listVersion=["ID K2NZW, generated 07 December 2023","ID K2NZW, generated 07 December 2023","ID K2NZW, generated 07 December 2023"]
vantage=["EU"]
2025 PETS more-and-scammier-ads-the-perils-of-youtubes-ad-privacy-settings
snapshots=6 cadence=at least one week between consecutive runs / at least 24 hours between runs longest span=2mo
browsers=["Google Chrome"] statefulness=stateful consent=not-stated
listVersion=["data from September 9, 2022","March 14, 2024","November 2023","March 20, 2024 availability check"]
vantage=["Sydney (Australia)","Toronto (Canada)","Dublin (Ireland)","London (the United Kingdom)","Oregon (the United States)"]
2025 USENIX navigating-cookie-consent-violations-across-the-globe
snapshots=10 cadence=over one week longest span=0mo
browsers=["Google Chrome"] statefulness=stateless consent=reject-all
listVersion=["November 2023, ID: 5Y3LN","July 2021, ID: 9QK2"]
vantage=["Ireland","United Kingdom","California, USA","Michigan, USA","Canada","South Africa","Singapore","Australia"]
2025 WWW before-after-the-effect-of-eus-2022-code-of-practice-on-disinformation
snapshots=2 cadence=- longest span=24mo
browsers=["Playwright-controlled browser"] statefulness=stateful consent=accept-all
listVersion=["accessed October 2023","compiled October 2023 and earlier"]
vantage=["Netherlands","Greece","USA"]
2024 IEEE-SP targeted-and-troublesome-tracking-and-advertising-on-childrens-websites
snapshots=7 cadence=- longest span=1mo
browsers=["Puppeteer/TRC desktop browser","Puppeteer/TRC emulated mobile browser"] statefulness=stateless consent=accept-all
listVersion=["May 2022","June-July 2022 crawl snapshot"]
vantage=["Frankfurt","Amsterdam","London","San Francisco","New York City"]
2024 PETS the-devil-is-in-the-details-detection-measurement-and-lawfulness-of-server-side
snapshots=3 cadence=- longest span=26mo
browsers=["Firefox"] statefulness=stateless consent=no-interaction
listVersion=["2021"]
vantage=["Europe","EU"]
2024 PETS a-large-scale-study-of-cookie-banner-interaction-tools-and-their-impact-on-users
snapshots=3 cadence=temporally close repeated measurements longest span=1mo
browsers=["Firefox 100.0"] statefulness=stateful consent=accept-and-reject
listVersion=["generated 29/08/2022; X568N","not-stated"]
vantage=["Frankfurt am Main, DEU","Council Bluffs, IA, USA","Frankfurt, DEU","Paris, FRA","Stockholm, SWE"]
2023 USENIX a-study-of-multi-factor-and-risk-based-authentication-availability
snapshots=2 cadence=two months between suspicious-login attempts longest span=n/a
browsers=["Chrome 89.0","Firefox 91.0","Safari 15.0"] statefulness=stateless consent=not-stated
listVersion=["generated June 21, 2021"]
vantage=["Boston, Massachusetts","Sofia, Bulgaria","San Francisco, California","Auckland, New Zealand","Dallas, Texas"]
2022 IMC respect-the-origin-a-best-case-evaluation-of-connection-coalescing-in-the-wild
snapshots=2 cadence=- longest span=5mo
browsers=["Google Chrome v88.0"] statefulness=stateless consent=not-stated
listVersion=["14 February 2021","14–18 February 2021","August 2021"]
vantage=["East US","not-stated"]
2022 USENIX a-large-scale-investigation-into-geodifferences-in-mobile-apps
snapshots=31 cadence=daily longest span=0mo
browsers=["Selenium"] statefulness=stateful consent=no-interaction
listVersion=["June 2020"]
vantage=["Canada","Germany","USA","UK","Australia","Japan","Hungary","Kenya","Colombia","South Korea","Tunisia","Mexico","Singapore","Ukraine","India","Zimbabwe","Turkey","Russia","Venezuela","Bahrain","UAE","Egypt","Iran","Hong Kong","Ireland","Israel","not-stated"]
2022 WWW reproducibility-and-replicability-of-web-measurement-studies
snapshots=12 cadence=daily longest span=60mo
browsers=["Chrome","Chrome headless","Firefox","Firefox headless"] statefulness=stateless consent=no-interaction
listVersion=["2016-2021","2016-2021","2021-06-23","2021-06-24"]
vantage=["Germany (EU)","Japan (AS)","United States (NA)"]
2020 NDSS encrypted-dns-privacy-a-traffic-analysis-perspective
snapshots=5 cadence=two-week intervals longest span=3mo
browsers=["Firefox","Chrome"] statefulness=stateless consent=not-stated
listVersion=["26 March 2018","26 March 2018","accessed 2019-09-11"]
vantage=["Lausanne","Leuven","Singapore"]
2020 PETS missed-by-filter-lists-detecting-unknown-third-party-trackers-with-invisible-pix
snapshots=6 cadence=simultaneous crawls / simultaneous measurements longest span=0mo
browsers=["Firefox"] statefulness=stateful consent=no-interaction
listVersion=["February 2019","November 2019","November 2019"]
vantage=["France"]
2020 WWW beyond-the-front-page-measuring-third-party-dynamics-in-the-field
snapshots=3 cadence=- longest span=0mo
browsers=["Firefox 52"] statefulness=stateful consent=no-interaction
listVersion=["2019-03-26, ID W9L9"]
vantage=["Europe (DE)","North America (US)","Asia (JP)"]
2020 WWW the-representativeness-of-automated-web-crawls-as-a-surrogate-for-human-browsing
snapshots=2 cadence=variable / two seven-day periods separated by seven days longest span=3mo
browsers=["Firefox 68","Firefox 69"] statefulness=stateless consent=not-stated
listVersion=["2019; accessed 29-July-2019","top 10,000; accessed 29-July-2019","top 10,000","data collected July 16–August 13, 2019"]
vantage=["not-stated","Brazil","Texas, USA"]
2019 WWW measuring-political-personalization-of-google-news-search
snapshots=7 cadence=daily longest span=n/a
browsers=["Firefox"] statefulness=stateful consent=not-stated
listVersion=["two weeks in March 2017"]
vantage=["United States","Oregon"]
2017 NDSS thou-shalt-not-depend-on-me-analysing-the-use-of-outdated-javascript-libraries-o
snapshots=2 cadence=- longest span=0mo
browsers=["Chromium"] statefulness=stateless consent=no-interaction
listVersion=["May 2016"]
vantage=["US"]
2016 IEEE-SP cloak-of-visibility-detecting-when-machines-browse-a-different-web
snapshots=3 cadence=- longest span=3mo
browsers=["Chrome","basic HTTP fetcher"] statefulness=stateless consent=no-interaction
listVersion=["collected February, 2015-May, 2015"]
vantage=["not-stated","United States"]
2016 USENIX tracing-information-flows-between-ad-exchanges-using-retargeted-ads
snapshots=9 cadence=- longest span=0mo
browsers=["instrumented Chromium"] statefulness=stateful consent=not-stated
listVersion=["January 2015"]
vantage=["Boston, United States"]
-- compact table (year | venue | slug | snapshots | distinct stated vantage locations)
2026 | PETS | clicking-into-exposure-uncovering-privacy-risks-of-google-click-identifier-in-yo | 5 | 6
2026 | PETS | privacy-vs-profit-the-impact-of-googles-manifest-version-3-mv3-update-on-ad-bloc | 5 | 1
2026 | PETS | more-space-less-privacy-measuring-the-effectiveness-of-ip-based-website-fingerpr | 5 | 1
2025 | PETS | beyond-the-request-harnessing-http-response-headers-for-cross-browser-web-tracke | 18 | 1
2025 | PETS | intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-banner-interaction-and | 4 | 1
2025 | PETS | more-and-scammier-ads-the-perils-of-youtubes-ad-privacy-settings | 6 | 5
2025 | USENIX | navigating-cookie-consent-violations-across-the-globe | 10 | 8
2025 | WWW | before-after-the-effect-of-eus-2022-code-of-practice-on-disinformation | 2 | 3
2024 | IEEE-SP | targeted-and-troublesome-tracking-and-advertising-on-childrens-websites | 7 | 5
2024 | PETS | the-devil-is-in-the-details-detection-measurement-and-lawfulness-of-server-side | 3 | 2
2024 | PETS | a-large-scale-study-of-cookie-banner-interaction-tools-and-their-impact-on-users | 3 | 5
2023 | USENIX | a-study-of-multi-factor-and-risk-based-authentication-availability | 2 | 5
2022 | IMC | respect-the-origin-a-best-case-evaluation-of-connection-coalescing-in-the-wild | 2 | 1
2022 | USENIX | a-large-scale-investigation-into-geodifferences-in-mobile-apps | 31 | 26
2022 | WWW | reproducibility-and-replicability-of-web-measurement-studies | 12 | 3
2020 | NDSS | encrypted-dns-privacy-a-traffic-analysis-perspective | 5 | 3
2020 | PETS | missed-by-filter-lists-detecting-unknown-third-party-trackers-with-invisible-pix | 6 | 1
2020 | WWW | beyond-the-front-page-measuring-third-party-dynamics-in-the-field | 3 | 3
2020 | WWW | the-representativeness-of-automated-web-crawls-as-a-surrogate-for-human-browsing | 2 | 2
2019 | WWW | measuring-political-personalization-of-google-news-search | 7 | 2
2017 | NDSS | thou-shalt-not-depend-on-me-analysing-the-use-of-outdated-javascript-libraries-o | 2 | 1
2016 | IEEE-SP | cloak-of-visibility-detecting-when-machines-browse-a-different-web | 3 | 1
2016 | USENIX | tracing-information-flows-between-ad-exchanges-using-retargeted-ads | 9 | 1
==========================================================================
11. CORPUS-WIDE ADOPTION PROBES FOR THE MODERN PINNING MECHANISMS
==========================================================================
-- mentions anywhere in the full text
population: 5855 papers with readable full text, of 5859
Docker OR containerised 308 5.3%
/\bdocker\b|\bcontaineri[sz]ed\b/i
...Docker alone 293 5.0%
/\bdocker\b/i
image digest or sha256: image reference 0 0.0%
/\bimage digest\b|\bsha256:[0-9a-f]{12}/i
"Chrome for Testing", exact phrase 0 0.0%
/chrome[-\s]?for[-\s]?testing/i
Chrome for Testing, widened to the tooling and the acronym 10 0.2% -> hand-read all 10: 0 genuine
/chrome[-\s]?for[-\s]?testing|googlechromelabs|chrome[-\s]?labs|known[-\s]good[-\s]versions|\bCfT\b/i
WebREC or "web execution bundle" 2 0.0% -> hand-read all 2: 0 genuine
/\bWebREC\b|\bweb execution bundle/i
Hand audits:
* The widened Chrome-for-Testing probe: every hit is a false positive —
CFT as "Combating the Financing of Terrorism" (three papers), "Control Flow
Trimming", "Call Flow Tree", "Crash-Fault Tolerant", a certificate subject
O=CFT, a CFt variable in an algorithm listing, CFT-IDF, and a fitness app
named "CFT & PFT". The exact-phrase probe is 0, and so is the audited count.
* WebREC: one hit is the WebREC paper itself; the other cites it in its
reference list only. No paper in the corpus USES it.
14. Unedited output of the quote check
PARTIAL 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"Over twelve days, we saw a variation of up to 27% (max on day 3-80,274; min on day 9-58,951) in observed track..."
paper.cols.txt — 4 run(s): 6+2+12+3 words
EXACT 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"the number of distinct tracking domains remains almost stable during the experiment (variation of 3.5%)"
paper.cols.txt
PARTIAL 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"studies that analyze the ecosystem will find similar results, while studies that aim to analyze the extent of ..."
paper.cols.txt — 4 run(s): 6+11+10+3 words
EXACT 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"we find that the identified trackers on pages can vary by 25% based on the used browser configuration"
paper.cols.txt
SPLICED 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"even the same setup measures different levels of tracking on different days, which might lead to different con..."
paper.cols.txt — 3 run(s): 6+11+4 words
EXACT 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"We use the popular Open-WPM Framework [21] (v0.15.0 - Firefox version 88)"
paper.cols.txt
PARTIAL 2023/IMC/on-the-similarity-of-web-measurements-under-different-experimental-setups
"when comparing two different profiles, 48% of the underlying data varies"
paper.cols.txt — 2 run(s): 2+9 words
PARTIAL 2026/PETS/overcoming-language-barriers-multilingual-analysis-of-the-2023-swiss-privacy-law
"we restrict the analysis to the balanced panel of websites with an observed policy in both snapshots"
paper.cols.txt — 3 run(s): 6+10+1 words
SPLICED 2026/PETS/overcoming-language-barriers-multilingual-analysis-of-the-2023-swiss-privacy-law
"Standard errors are clustered at the website level to account for repeated observations of the same policy acr..."
paper.cols.txt — 2 run(s): 11+8 words
PARTIAL 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"The standard deviation of such requests is 8,203."
paper.cols.txt — 2 run(s): 7+1 words
EXACT 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"For our continuous measurement, we visit the top 1k sites from our website corpus (18,377 distinct pages) dail..."
paper.cols.txt
PARTIAL 2022/WWW/reproducibility-and-replicability-of-web-measurement-studies
"privacy measurements and analyses can vary up to 65% depending on the region"
paper.cols.txt — 3 run(s): 2+10+1 words
EXACT 2023/IMC/on-the-similarity-of-web-measurements-under-different-experimental-setups
"only 32% of the cookies appear in all profiles and 42% only in one profile"
paper.cols.txt
SPLICED 2021/WWW/towards-realistic-and-reproducibleweb-crawl-measurements
"Around 5% of content-providing domains show significant measurement bias across VP"
paper.cols.txt — 2 run(s): 6+5 words
EXACT 2020/WWW/apophanies-or-epiphanies-how-crawlers-impact-our-understanding-of-the-web
"variation of over 16% in the number of successful page loads"
paper.cols.txt
PARTIAL 2026/PETS/overcoming-language-barriers-multilingual-analysis-of-the-2023-swiss-privacy-law
"The balanced panel comprises N = 11,800 websites observed in both August and October 2023"
paper.cols.txt — 3 run(s): 4+1+10 words
EXACT 2025/USENIX/web-execution-bundles-reproducible-accurate-and-archivable-web-measurements
"could be conducted using WebREC as is"
paper.cols.txt
EXACT 2026/PETS/the-empire-strikes-back-at-your-privacy-an-archaeology-of-tracking-on-government
"We therefore label trackers using the June 2025 EasyList and EasyPrivacy snapshot for all years, keeping the l..."
paper.cols.txt
7 quote(s) need a hand read
15. Review passes, 2026-08-27
Four reviewers, all told explicitly that the author's context might not be exhaustive, all handed the page text, the report script and its unedited output, and these notes. The three focused passes ran in parallel first; the generic pass ran afterwards, on the corrected text.
Pass A — figures against the script (Claude Sonnet)
| Finding | Disposition |
|---|---|
| The corpus-wide probe figures were not in any committed script. The Docker, image-digest, Chrome-for-Testing and WebREC counts came from throwaway scratch files, while the content page told the reader the provenance page carried “the full query log”. The reviewer independently reproduced them and got different numbers for three of the four, because it guessed different regexes. | Accepted, and it is the most useful finding of the run. The probes are now §11 of report_longitudinal.mjs, and the script prints each regex beside its count so a reader can see exactly what was asked. The reviewer's disagreements were all regex differences, and two of them were real defects in the labelling: “308 mention Docker” was actually Docker-or-containerised (Docker alone is 293, now both are published under their own labels), and “two papers mention WebREC” hid the fact that neither uses it — one is the WebREC paper and the other cites it in its reference list. Both corrected on the page. |
The published cadence table silently dropped the sentinel row that the script's own output contains. | Accepted. The row is back: one paper writes not-stated into the cadence field. It never affected the headline figure — the script already counted sentinel as a non-cadence — but a table presented as the fold's output must be the fold's output. |
spanMonths() parses the year with an unanchored /(\d{4})[-/ ]?(\d{2})?/, so a string like “2000 domains, March 2020” would silently parse 2000 as the year, and 2000 is inside the 1990–2027 sanity window so nothing would catch it. No current value triggers it. | Accepted. The parser now requires the year to be a standalone token and the month to be 01–12. Re-running the script produces a byte-identical output, so no published figure moved; the fix removes a dependence on luck. |
The cadence fold sent “15 days between batch starts” to duration-not-cadence while the parallel “one week between…” was carved out to weekly — an inconsistency in the rule design. | Accepted. The cause was a regex bug rather than a design choice: the duration rule's “no between, no apart” guard sat on only one branch of a top-level alternation, so a second branch matched anyway. Fixed. Consequences, all published: duration-not-cadence 62 → 58, monthly 42 → 44, every-few-days 19 → 20, residue 17 → 16 strings, and the headline “fills the cadence slot without stating a cadence” 89 (22.9%) → 88 (22.6%). |
The page's “roughly 2% of empirical work” for web-archive use is a different measure from the temporal.mode = web-archive figure the report script itself computes (1.3%), and the page did not flag that. | Accepted. The table cell now gives both and names which query each comes from. |
Everything else — the script re-runs byte-identical to its committed output; every population, four-pin percentage, span band, by-year and four-year-bucket figure, the Tranco 266/159/23 block, all 23 exemplar rows with their snapshot and vantage counts, and the OVERVIEW.md cross-checks (219, 349, 529, 140, 779, 841 over 1,120) match exactly. | no change |
Pass B — citations and quotes (Claude Sonnet)
| Finding | Disposition |
|---|---|
| The Public Suffix List footnote said “five commits in the eleven days to 19 August 2026”. The reviewer queried the same endpoint and got ten. | Accepted. The original query was run with per_page=5 and the author read the page size as the answer — a self-inflicted truncation, and exactly the class of error a footnote is supposed to make catchable. Re-queried with an explicit since/until window: ten commits on three distinct days. The surrounding wording moved from “several times a month” to “most weeks”. |
| “one filter-list version held fixed across twenty years of archived pages” understates [7Singh, Sachin Kumar; Mahmud, Faisal; Ricci, Robert; Siby, Sandra (2026): "The Empire Strikes Back (at Your Privacy): An Archaeology of Tracking on Government Websites", Proceedings on Privacy Enhancing Technologies 2026(2):108-126. (DOI)], whose own abstract says “nearly three decades (1996-2025)” and whose method says the June 2025 snapshot was used “for all years”. | Accepted. Now quotes the paper's own phrase and its span. |
| All 31 citekeys resolve; the 6 new BibTeX entries have no key, DOI or title collision and were each verified against Crossref or the publisher page; every quotation matched its source verbatim; all five specifically flagged claims confirmed; all 23 exemplar citekey-to-paper mappings correct; every footnote is a primary source. | no change |
Pass C — external currency (Claude Sonnet)
| Finding | Disposition |
|---|---|
The Chrome for Testing link developer.chrome.com/blog/chrome-for-testing now 301-redirects to /docs/automation-and-testing/chrome-for-testing. | Accepted. Link updated to the target. |
Playwright already pins browsers and the page did not say so. Its browsers.json pins an exact build per Playwright release, and the Chromium entry is literally titled “Chrome for Testing”. | Accepted, and it improved the advice. The page previously offered a Chrome answer and a Firefox/OpenWPM answer and left everyone else out. It now says that pinning Playwright is itself a browser pin, with the browsers.json entry quoted in a footnote, and the currency table has a row for it. |
[[Artifacts]] in Related Pages is a red link — the page does not exist. | Rejected on the merits; the reviewers were right on the fact. artifacts is a red link this wiki already promises from fourteen other pages including start and programming:crawler, which renders exactly the same red link. Leaving it advertises a gap the site has decided to advertise; creating the page is a different work item. Recorded because the author got this wrong first: ?do=export_raw on a missing page returns the HTML “topic does not exist” page rather than an error, so a byte count on that response reads as 10,843 bytes of content. It is not. Check the first line, not the length. |
design:longitudinal and provenance:design:longitudinal did not resolve when checked. | Not a finding — they were unpublished at review time. Every other internal link and every quoted anchor resolved with real content. |
| Chrome for Testing's version list, the Tranco ids and provider sets for all five dated rows, OpenWPM's release and Firefox tag, EasyList's header and repository creation date, HTTP Archive's FAQ text and its 12-entry 2017 changelog, WebREC's repository, and the Mozilla release archive were all independently re-fetched and matched the page exactly. | no change |
Pass D — generic (Claude Fable)
| Finding | Disposition |
|---|---|
The span parser silently mis-dated 40 of the 250 papers. spanMonths() understood only numeric months (2023-05), so every textual month (“May 2023”) became January. 129 stated dates in the corpus are textual. A same-year span like “February 2011 → July 2011” landed in <1 month; a cross-year span like “June 2010 → January 2011” landed in 1–2 years. | Accepted, and it is the finding of the run. The figures-versus-script pass could not have caught it: the page matched the script exactly, and the script was what was wrong. The parser now reads month names. Consequences, all republished: the span-band table moves on five of its seven rows (<1 month 49 → 35, 1–2 months 29 → 35, 3–5 months 33 → 41, 6–11 months 21 → 26, 1–2 years 31 → 27); “under one month” 19.6% → 14.0% and “under two months” 31.2% → 28.0%; and the multi-year split becomes 68 against 137 with 3 (4.4%) against 18 (13.1%) pinning all four. The page's claim survives and is slightly stronger than it was. The function's own comment had boasted that the year parser “should not depend on luck” while the month parser did exactly that; the comment now records the defect. |
The provenance page shipped with an unfilled REVIEWLOG placeholder in §15. | Accepted. This table is it. |
| §6 claimed “every quotation used on the content page was checked”, which was false: the Jueckstock vantage-bias quote and Singh et al.'s “all years” were not in the checker, and six figures taken from cited papers (SD 8,203, the 32%/42% cookie profile split, Ahmad's 16%, Nenadić's 11,800, Hantke's 70%, Demir's 65%-by-region) appeared nowhere in the log. | Accepted. All nine were added to the checker, which now covers 18 quotations and figures, and §6 lists every one with its verdict. |
| The lead generalised a single experiment into a law — “event counts are noisy; entity sets are stable” — and invented ranges (“roughly 3–4%”, “roughly 25–30%”) from single numbers, pooling a browser-configuration figure into a day-to-day range. It also asserted the wobble was “larger than most of the year-on-year effects people publish”, which nothing measures. | Accepted in full. The lead now says there is one experiment, gives its two numbers, and says explicitly that one study on one pair of outcomes does not license a general law. The ranges and the “most effects” clause are gone; the design question they were there to raise is stated directly instead. |
| The page said Demir et al. crawled “from the same place” for the twelve-day experiment. The paper does not say that, and the study as a whole uses three geolocations. | Accepted. Removed; see §6. |
“250 (22.3%) crawled more than once” and the heading “Repeating is not becoming more common” both elide that repeated means stated repetition, when 63.5% of crawling papers state no snapshot count at all — the same distinction the page is scrupulous about for the pins. | Accepted. The lead now says “say they crawled more than once … so 250 is a floor”, the heading is “Stated repetition is not becoming more common”, and the trend section carries a paragraph saying a change in reporting would masquerade as a change in practice. |
| Two universal negatives with no documented search behind them: “no released tool pins [the Public Suffix List] for you”, and “we found no repeated-crawl paper in this corpus that reports its own [noise floor]”. | Accepted. The first is now “a library that vendors a snapshot only pins it if you pin the library”. The second says plainly that no probe could settle it and to read the row as an impression from reading the exemplars. |
| Structure: the “follow a cohort / redraw the list” bullets duplicated the first two rows of the Choosing a Design table ten lines later; and Pinning in Practice had subsections for four pins but not for the fifth the page had just made a fuss of naming. | Accepted, both. The duplicated cost cells now point back rather than restate, and the EasyList header block and the “record ! Version: and ! Commit:” instruction have moved into a fifth Pinning in Practice subsection, The classifier, where the structure matches the argument. |
| Small overstatements: “EasyList changes daily” evidenced by one header capture; “anything older cannot” be re-instantiated (true of Chrome for Testing, not of Chromium snapshots); “two thirds” for 63.5%; and “the crawl is the expensive part, not the analysis”, which is self-undercutting because doing both designs enlarges the crawl. | Accepted, all four. |
Mechanical: the report script printed its sections 9, 11, 10; the quote checker's header comment described three renderings and 6-word runs when the code uses four and MIN_RUN = 4; §3 of this page had a garbled “70 to 70+”; §16 opened with an empty ^ ^ ^ header; and a stray space before a footnote marker on the content page. | Accepted, all five. |
[[Artifacts]] is a red link. | The red link is intended (see Pass C), but it pointed at the wrong page and that was a real bug. DokuWiki resolves a single-word id relative to the current namespace, so from design:longitudinal the link resolved to design:artifacts rather than artifacts. Caught only by counting wikilink2 classes and reading data-wiki-id attributes in the rendered DOM after publication; three reviewers and the author all read the source and none of us saw it. Fixed to [[:Artifacts]], which is how programming:crawler and design:sampling already write it. The same bug had put this page's own [[start]] at provenance:design:start. Rule for the next run: after saving, diff the rendered data-wiki-id list against the ids you intended. |
| The lead is the right lead; voice and length match the neighbouring pages; the page teaches the measurement rather than the platform; the provenance is honest apart from the placeholder and the §6 overclaim. | no change |
16. Run log
| Item | Value |
|---|---|
| Date | 2026-08-27 |
| Corpus at the time | data/extract/run1, 5,859 papers, 7 venues, 2010–2026, IEEE S&P complete at 780/780 |
| Model | Claude Opus 5 for the page, the scripts and the folds; sub-agents for the review layer only (§15) |
| Scope | New page. No earlier version, no figures carried over from any dossier or METHOD.md. |
| Scripts written | scripts/cadence_fold.mjs (new), scripts/report_longitudinal.mjs (new), scripts/quotecheck_longitudinal.mjs (new), scripts/build_provenance_longitudinal.py (new, builds this page from the committed files so the listings are the real scripts) |
| Bibliography | 6 entries added: ahmad2026_ipfp, papadogiannakis2025_before, gavazzi2023_multi, siby2020_encrypted, le2019_political, invernizzi2016_cloak. Checked against the live bibliography for key, DOI, URL and title collisions before appending; none found. Authors for the PETS and USENIX entries were fetched from the venue landing pages with curl and a browser User-Agent, because those two venues have no author data in the corpus index. |
| Pre-existing defect noticed, not touched | lerner2016_internet and lerner2016internet are the same paper under two keys in the shared bibliography. Neither is cited by this page, so fixing it was left as separate work rather than folded into this edit. |
| Reachability | Nothing linked design:longitudinal, so a bullet was added to start in the same sitting. |
| Discussion block | None on this page, following the convention set by the other provenance: pages — comments belong on the content page. There is likewise no <bibtex bibliography> block, so the citekeys here render as markers without a reference list; that is the existing convention, not an omission. |
| Publication order | literature:bibliography (6 entries) → design:longitudinal → provenance:design:longitudinal → start. |
17. Amendment, 2026-09-03: dating the Tranco provider swap
Contemporaneous. Made during a different work item — a site-wide audit of how this wiki dates Alexa's retirement, whose full notes, queries, script and review log are in website_selection §13. Nothing else on longitudinal was touched: no corpus query was re-run and no corpus figure changed.
| Item | Value |
|---|---|
| Content page | longitudinal, §“The frame moves even when you pin it” |
| Saves | rev 1788465991 (–if-rev 1787798449), rev 1788467004 (–if-rev 1788465991), rev 1788467915 (–if-rev 1788467004) |
| Corpus queried | none — this is a vendor date |
| External source | Tranco's HTTP API and front page, fetched 2026-09-03 |
| Audit script | scripts/alexa_date_audit.mjs, published with its output in website_selection §13.4–13.6 |
| Published after | the §13 appendix on website_selection |
What was wrong. Not a wrong date — an imprecise one, on the page whose whole argument is that a pinned list id does not pin the frame. The provider-set table sampled 1 January of each year, so it showed Alexa present on 1 Jan 2023 and absent on 1 Jan 2024, and the sentence under it read:
//"Quantcast dropped out, Farsight came in, and then in 2023–24 Alexa was replaced by CrUX and Cloudflare Radar."//
“2023–24” is the gap between two sampled rows, not the date of the change. The footnote then gave Amazon's API retirement (15 December 2022) as the cause without giving the swap date at all — so the page put two different organisations' events side by side with neither dated to the day. That is the shape the “Alexa was discontinued on 1 August 2023” error on website_selection grew out of.
What it says now. All three provider transitions are dated in the sentence, and the Alexa one is dated to the day from the Tranco API rather than bracketed:
| Daily list | ID | Providers | listPrefix |
|---|---|---|---|
| 30 Jul 2023 | 99P42 | alexa, umbrella, majestic, farsight | full |
| 31 Jul 2023 | W9ZN9 | alexa, umbrella, majestic, farsight | full |
| 1 Aug 2023 | 25299 | crux, farsight, majestic, radar, umbrella | 1000000 |
| 2 Aug 2023 | 8289V | crux, farsight, majestic, radar, umbrella | 1000000 |
All four read from https://tranco-list.eu/api/lists/date/YYYYMMDD on 2026-09-03. The 31 July and 1 August rows are on the content page; 30 July and 2 August were read as controls, to check the change is a single-day step and not a run of churn. The prefix change from full to one million lands on the same day, which independently corroborates the claim tranco already makes about the prefix.
Whose date is whose. The distinction the amendment exists to enforce:
| Event | Date | Whose | Source, fetched 2026-09-03 |
|---|---|---|---|
alexa.com switched off | 1 May 2022 | Amazon's | Alexa Support notice via Wayback: “we made the difficult decision to retire Alexa.com on May 1, 2022” |
| Alexa Top Sites / Web Information Service APIs switched off | 15 December 2022 | Amazon's | same notice: “The APIs will be retired on December 15, 2022.” |
| Alexa dropped from Tranco's default list; CrUX and Cloudflare Radar folded in | 1 August 2023 | Tranco's | https://tranco-list.eu/: “The Chrome User Experience Report and Cloudflare Radar rankings have been integrated into the default Tranco list, starting from the daily updated list of August 1, 2023.” |
Eight months separate the second from the third, so Alexa's removal from the list was forced long before it happened. Any page naming one of these dates has to say which organisation it belongs to; the audit in website_selection §13.3 is the site-wide check that all 160 pages still do.
Verified after the last save. Counted from the wiki source and confirmed in the rendered DOM: 32 headings, 11 ((…)) footnotes matching 11 distinct fn__N ids, 14 tables, 0 wikilink2 (red links), 45 citation markers over 32 distinct keys rendering as 90 bibtex_citekey spans and 32 numbered references — all unchanged from before the edit except the table and the sentence. node scripts/check_wrap.mjs clean before each save. An earlier draft of this section quoted “34 headings, 33 footnotes”; both were miscounts of rendered elements rather than source constructs, and are explained in website_selection §13.7.
Not changed, on purpose. The four 1 January rows stay: they are what the API returns for those dates, and re-sampling the whole table around each transition would be a different table answering a different question. The pre-swap id W9ZN9 is only on longitudinal — the natural second home is tranco's id table, and adding it there is filed as separate work rather than folded into this edit, because that page has its own provenance page and its own review obligation.
Amendment, 2026-09-04: citekey consolidation
ahmad2026_ipfpwas one of two keys for the same paper in bibliography. The wiki-wide consolidation of 2026-09-04 (drain itemdedup-regional-filter-lists-bibkey) keptahmad2026_moreand deleted the other entry.- 1 citation marker on longitudinal was repointed to the kept key. No prose on either page changed, and no figure moved. Statements above that name the deleted key describe the state when they were written. Full query log and the invariants checked before saving: bibliography.
18. The four-pin hand-coding, 2026-09-07
§9 above named one follow-up as the most valuable: “Whether a paper held a pin constant across waves. The extraction records what a paper stated, once. Every pin figure on the content page is a ceiling … Closing this needs a hand-coded sample of the 250, reading each paper's per-wave methods description.” This section is that work. It was not a sample: all 250 were read.
18.1 What was built, and what each piece is for
| Artefact | What it does | Committed as |
|---|---|---|
| Population | Re-derives the 250 from the extraction with the predicate copied verbatim out of report_longitudinal.mjs | scripts/pins_population.mjs → out_pins/population.jsonl |
| Reading packet | Per-paper keyword-window excerpt of the .cols text, with the full-text path in its header | scripts/pins_excerpt.mjs → out_pins/excerpts/ |
| Codebook | repeatKind + four pins, six values each, a verbatim quote per pin | out_pins/CODEBOOK.md |
| Coding | 250 JSON files, one per paper | out_pins/coded/ |
| Reliability sample | Deterministic draw of 30 from the 250 | scripts/pins_valsample.mjs → out_pins/valsample.tsv |
| Blind second coding | The same 30, re-coded from the full text by a coder who never saw the first coding | out_pins/coded_val/ |
| Aggregation | Every published figure, with its denominator, plus the DokuWiki tables | scripts/report_pins_audit.mjs |
| Quote check | Every evidence quote located in the source, in four escalating modes | scripts/pins_quotecheck.mjs |
| Agreement | Per-pin exact and collapsed agreement, Cohen's κ, every disagreement printed | scripts/pins_agreement.mjs |
| Citekey map | Slug → the citekey already on the content page, so the exemplar table is emitted, not retyped | out_pins/citekeys.json |
| Quote adjudication | Hand verdicts on every quote no matching mode could locate | out_pins/qaudit/verdict_1..3.tsv |
18.2 The population, and that it reproduces
cd /workspace/artifacts/wiki node scripts/pins_population.mjs > out_pins/population.jsonl node scripts/pins_excerpt.mjs node scripts/pins_valsample.mjs > out_pins/valsample.tsv node scripts/report_pins_audit.mjs > scripts/report_pins_audit-output.txt node scripts/report_pins_audit.mjs --wiki # the page's tables node scripts/pins_quotecheck.mjs > scripts/pins_quotecheck-output.txt node scripts/pins_agreement.mjs > scripts/pins_agreement-output.txt
Both derived files were re-run on 2026-09-07 and diffed against the committed copies: population.jsonl and valsample.tsv reproduce byte-identically. node scripts/report_longitudinal.mjs was also re-run and diffed against scripts/report_longitudinal-output.txt — zero lines differ, so nothing on the rest of the content page moved under this edit.
The predicate is crawled AND max(temporal[].snapshots) > 1, copied out of report_longitudinal.mjs rather than re-implemented, so §3's repeated population and this one agree by construction rather than by luck.
report_pins_audit.mjs refuses to print anything if fewer than 250 codings are present, rather than silently shrinking its denominator:
if (coded.length !== pop.length) {
console.error(`\nFATAL: coded ${coded.length} of ${pop.length}. Figures would be computed over the wrong population.`);
It has the same guard on the citekey map: if the map and the all-four-stated set disagree by one slug, the exemplar table would silently lose a row, so it exits instead.
18.3 The reading packet, and what it costs
250 papers is 20.9 MB of .cols text. Each coder was given a packet instead: the first 3,000 characters, plus context windows around two tiers of keyword matches (wave/repetition language, “same/unchanged/pinned”, statefulness, the named ranking lists and filter lists, browsers, cloud regions), merged and capped at 32,000 characters — and the full-text path in the packet header, with an instruction to open it whenever the packet is not enough. The packets instruct the coder to open the full text whenever the windows are not enough. Whether they did is not logged, so the recall of the packet rule is bounded only by the blind full-text pass below, not by any record of what each coder read.
The packet is a recall risk, and it was measured rather than assumed — that is what the blind full-text second coder in §18.4 is for. The answer, in short: the packet does not lose “fixed” evidence, it loses the absence of it. See the direction-of-disagreement table below.
18.4 The blind second coder, and the agreement
30 of the 250 were drawn with a seeded shuffle (mulberry32, seed 20260907) and re-coded from the full .cols text by a second coder given CODEBOOK_VALIDATION.md, told explicitly not to open out_pins/coded/ or out_pins/excerpts/. Ten agents, three papers each. No coder saw another's output.
| Measure | Exact | κ | Collapsed to fixed / not fixed | κ |
|---|---|---|---|---|
| Population list | 20/30 (66.7%) | 0.49 | 26/30 (86.7%) | 0.59 |
| Browser | 21/30 (70.0%) | 0.57 | 22/30 (73.3%) | 0.48 |
| Vantage point | 21/30 (70.0%) | 0.56 | 25/30 (83.3%) | 0.67 |
| Statefulness | 24/30 (80.0%) | 0.64 | 25/30 (83.3%) | 0.64 |
| All 120 pin codings | 86/120 (71.7%) | 0.60 | 98/120 (81.7%) | 0.64 |
repeatKind | 25/30 (83.3%) | 0.77 | own repeat crawl vs everything else: 27/30 (90.0%) | 0.80 |
| “all four fixed” | 27/30 (90.0%) | 0.52 | A: 3/30, B: 4/30 | — |
An earlier draft of this section said κ on “all four fixed” is low only because the class is rare, “not because the coders disagree about which papers they are”. That was wrong and the files say the opposite. Five papers are called all-four by at least one coder; the two agree on two of the five, and disagree on three. The class is indeed rare at n = 30, but the disagreement is real, and the 15-of-124 headline should be read with that in mind.
Direction of disagreement. This is the part that changes how the content page is written:
| Pin | Both call it fixed | Only the excerpt coder | Only the full-text coder | Neither |
|---|---|---|---|---|
| Population list | 22 | 4 | 0 | 4 |
| Browser | 9 | 7 | 1 | 13 |
| Vantage point | 11 | 1 | 4 | 14 |
| Statefulness | 8 | 4 | 1 | 17 |
The excerpt coder calls the browser fixed seven times where the full-text coder finds it never stated, and only once the other way; statefulness leans the same way, four to one. The vantage point goes the opposite way — the full-text coder finds four vantage statements the packet's windows missed, because vantage is often mentioned once, far from any wave language, in an infrastructure or ethics paragraph.
The most likely cause of the browser lean is a design mistake that is worth recording: the packet header prints the structured extraction's own values (browsers=[“Selenium”], statefulness=not-stated, …) so the coder can see what a machine thought. The codebook's first rule tells the coder not to infer from it. The agreement data suggests that instruction was not fully obeyed. A rerun should put that line at the end of the packet, or drop it.
Consequence for the content page: the browser row of the estimate column is more likely too high than too low, and the page says so.
Every disagreement is printed paper by paper in the unedited output in §18.12.
18.5 The quote check, and the 43 that did not locate
Every non-empty quote must appear in the paper it is attributed to. 1,000 pin codings, 464 with no quote (all of them not-stated, which the codebook permits and the script verifies: 0 codings carry a value other than not-stated with no quote), 536 checked.
The naive substring test is wrong here, and getting it wrong once already produced a scare. Run against paper.cols.txt alone it reported 49 unlocated quotes (9.1%), which reads as a fabrication rate. It is not. The first one inspected was:
- coder's quote: “We implemented FourthParty as an extension to Mozilla Firefox.”
- what
.colsactually holds:We implemented FourthParty as an extension to Mozilla \n B. Design Principles Firefox. It currently instruments
The two-column repair still splices a quarter of pages, and a sentence crossing a column boundary comes back with the neighbouring column injected into the middle of it. A coder who de-splices that correctly writes a quote that is a substring of nothing. Failing it would call a correct reading a hallucination. So the check now searches paper.cols.txt, paper.norm.txt, paper.txt and the packet, in four escalating modes, the last of which cuts the quote into 6-word shingles and passes at 80% found.
| Mode | Codings | Share of 536 |
|---|---|---|
| Exact substring | 489 | 91.2% |
| Whitespace-collapsed | 0 | 0.0% |
| Loose (de-hyphenated, quotes and dashes folded, case-insensitive) | 2 | 0.4% |
| De-spliced (6-word shingles, ≥80%) | 3 | 0.6% |
| Located by no mode | 42 | 7.8% |
The counts above are after the one correction described below. Before it: 488 exact, 43 unlocated (8.0%).
All 43 were then read by hand — three sub-agents, each given the paper's three renderings and the PDF, asked to classify each quote as a faithful de-splice, a supported paraphrase, an unsupported paraphrase, or a quote lifted from the wrong part of the paper, and independently to say whether the paper supports the coded pin value at all.
| Verdict | Codings |
|---|---|
| Faithful de-splice of a real sentence | 42 |
| Quote is real but from the wrong part of the paper | 1 |
| Unsupported paraphrase / fabrication | 0 |
The one bad coding, and what was done about it:
- [8Singanamalla, Sudheesh; Paracha, Muhammad Talha; Ahmad, Suleman; Hoyland, Jonathan; Valenta, Luke; Safronov, Yevgen; Wu, Peter; Galloni, Andrew; Heimerl, Kurtis; Sullivan, Nick; Wood, Christopher A.; Fayed, Marwan (2022): "Respect the ORIGIN!: a best-case evaluation of connection coalescing in the wild", in: Proceedings of the ACM Internet Measurement Conference. (DOI)],
vantage=changed. The evidence quote — “the ORIGIN Frame implementation was deployed globally at over 275 points of presence” — is a real sentence, correctly de-spliced, and describes the CDN's own deployment footprint, not where the authors' crawler ran. The paper's methods section says the opposite: “We used the same machines throughout our evaluations for consistency, and saw no observable change in datacenter provisioning or capacity over time.” The blind second coder had independently coded that pinsame-implied, which is the same direction. - Corrected to
same-explicitwith the methods-section sentence as its quote, and the correction is recorded in the paper'snotesfield. The same pass offered a stronger quote for that paper'sstatepin (“Client caches are intentionally deleted between measurements in all of our active measurements”); the quote was swapped, the value was alreadysame-explicitand did not move. - What moved on the content page: the vantage row over the 124 went from 25/44/10 to 26/45/9 (words / estimate / moved), and the all-pins fixed-versus-not agreement from 80.8% to 81.7%. The all-four figure, the 124, and the 6-of-23 survivor count did not move.
The verdict files are out_pins/qaudit/verdict_1.tsv … verdict_3.tsv. The lesson is the check, not the count. A plain substring test on paper.cols.txt alone would have reported a 9.1% fabrication rate here, of which the real rate is 0.2% (1 of 536) — and the one real defect was not a fabrication at all but a quote from the wrong section, which no substring test can catch. One reviewer flagged a further boundary case worth recording rather than changing: two codings use same-explicit on the strength of a pinned identifier in papers whose repeatKind is single-crawl, where the codebook's rule 4 would suggest same-implied. Both readings are inside the codebook as written; the rule needs tightening before a rerun, and neither paper is in the 124.
18.6 What changed on the content page
| Where | Before | After |
|---|---|---|
| Lead paragraph | “Of those 250, 23 (9.2%) state all four” | 124 are really repeat crawls; 15 of 124 (12.1%) plausibly held all four fixed, 2 say so in words |
The four pins, measured | title, and “every figure here is an upper bound on comparability” | retitled The four pins: how often they are stated; the upper-bound sentence now points forward instead of standing as the last word. No figure in that table changed — it is the reporting-rate table and it is still correct as such |
| — | — | new section Were the pins actually held fixed?: the repeatKind table, the four-pin table over the 124, the divergence between “stated” and “held fixed”, the span and period splits, and the agreement figures |
Papers that pin all four | 23 papers, “These are the methods sections to copy from” | retitled Papers that stated all four, and what they actually did; same 23 rows, each with its hand-coded verdict, and 6 survive |
Use in Publications intro | the 250 rule, no caveat | adds that the rule is right about half the time and names the 124 |
| Methodology bullets | one bullet: treat the pin figures as ceilings | three bullets: what the two columns each measure, the coder-agreement caveat, and that every other share-of-250 figure on the page inherits the false-positive rate and has not been recomputed |
Also corrected in the same edit, outside the four-pin work. Four external claims the currency reviewer found stale — Quantcast's exit date, the OpenWPM version, the Playwright browsers.json source, and ICLab's maintenance status. Each is described in §18.10 Pass C with what was re-fetched.
Not changed, on purpose. The cadence, span, probe and Tranco-ID figures are still shares of 250 rather than of 124. Recomputing them is a different piece of work with its own review obligation; folding it into this edit would have meant republishing a dozen tables under a population change that nothing in this hand-coding validates for them. The content page says plainly that they were not recomputed, which is the honest state, and it is filed below as the follow-up.
18.7 Judgement calls
- Two populations are published, and the 124 is the one the prose leans on. Publishing only the 250 would keep the page consistent with itself and answer the wrong question; publishing only the 124 would break every cross-reference to biases. Both tables are emitted by the script; the content page carries the 124 and points at the 250 version here.
same-impliedcounts toward the estimate. It is the common case — one methods paragraph, no statement that anything changed — and excluding it would give a “floor” of 2 papers in 250, which is a measurement of how rarely people write “unchanged”, not of how often things were unchanged. It is published as a separate column so a reader can take the strict reading, and the page calls the estimate generous.- A list re-downloaded each wave is
changed, notsame. Scientifically that is often the right thing to do; the codebook measures whether the frame is constant, not whether the authors were wise. 28 of the 124 are codedchangedon the list pin, and several are deliberate and defensible. - The ceiling is not a ceiling for the list pin. The hand-coded estimate for the population list (95 of 124) is above the structured figure (86). They measure different things: the extraction looks for
population.listVersion, a version string; the hand-coding asks whether the frame was constant, which “the same 200 sites throughout” satisfies without any version string. 62 of the 250 are in that gap. Presenting the structured column as a ceiling without saying this would be wrong, so the content page's column is now labelled “stated at all” and the divergence has its own paragraph. - The 23-paper exemplar table was kept, not deleted. It is the clearest possible demonstration of what “stated” was hiding, and deleting it would erase the correction along with the error.
repeatKindwas added to the codebook after a pilot batch showed the mechanical predicate misfiring. Without it the four-pin figures would be computed over a population half of which cannot have waves. This turned out to be the largest single finding.
18.8 What still cannot be established
- Whether a pin was actually held fixed, as opposed to described once and never contradicted.
same-impliedis 47 of the 124 on the list pin and 55 on the browser. Only the authors know. Nothing short of asking them closes this, and the page publishes the strict and generous readings side by side rather than picking one. - Whether the reliability figures hold at n=250. They were measured on 30. κ = 0.60 on exact values is moderate; the confidence interval at n=30 is wide and is not published because it would imply a precision the design does not have.
- The other share-of-250 figures on the content page. Cadence, span bands, the probe table and the Tranco-ID rate are all still computed over the 250. The 124 exists now, so they could be recomputed; they have not been, and the page says so.
- Whether the pin figures differ by venue. n per venue-year is small and the 124 splits into groups of two and three. Not attempted.
- Whether a paper that pinned got a different answer than one that did not. Unchanged from §9: this remains the open question the whole page rests on and no study in the corpus answers it.
18.9 Run log
| Item | Value |
|---|---|
| Date | 2026-09-07 |
| Corpus | data/extract/run1, 5,859 papers, 7 venues, 2010–2026. Unchanged since the page was written on 2026-08-27. |
| Coder A | Claude Sonnet, 63 batches of ≤5 papers, excerpt packet plus full-text access, one paper coded once |
| Coder B | Claude Sonnet, 10 batches of 3, full text only, blind |
| Quote adjudication | Claude Sonnet, 3 batches, over the 43 quotes no matching mode located; 1 correction applied |
| Review | Three focused passes (Claude Sonnet: figures-vs-script, citations-and-quotes, external currency) and one generic pass (Claude Fable), all handed the page text, the scripts, their unedited output and these notes; findings and rejections in §18.10 and §18.11 |
| Orchestration, scripts, page text | Claude Opus 5 |
| Interruption | The run hit an API quota limit with 185 of 250 coded and resumed the same day. Same model, same codebook, same prompt; the resumed batches are out_pins/rebatch/. Nothing had been published at the point of interruption. |
| Bibliography | No entries added and none changed. The exemplar table reuses the 23 citekeys already on the content page, via out_pins/citekeys.json; the builder exits if that map and the population disagree. |
| Guards run before saving | node scripts/check_wrap.mjs on both pages; heading, table and <file>-block counts compared before and after by scripts/build_provenance_pins.py, which exits non-zero if any of the three falls |
| Discussion block | Still none on this page, following the convention in §16. |
18.10 Review passes, 2026-09-07
Four reviewers, each told explicitly that the author's context may not be exhaustive, each handed both draft pages, all six scripts, their unedited output and these notes. Every finding was accepted except one, marked “noted, not changed” in Pass B. Two of the four independently found the same defect, which is the only evidence there is that the layer is worth its cost — and the generic pass, which nearly did not run at all, produced the most serious finding of the four (§18.11).
Pass A — figures against the script (Claude Sonnet)
Re-ran all six scripts and confirmed every committed -output.txt, population.jsonl and valsample.tsv reproduces byte-identically, and that pins_population.mjs lines 17 and 19 are character-for-character the predicate in report_longitudinal.mjs.
- Stale table cell. The 23-row exemplar table's vantage cell for [8Singanamalla, Sudheesh; Paracha, Muhammad Talha; Ahmad, Suleman; Hoyland, Jonathan; Valenta, Luke; Safronov, Yevgen; Wu, Peter; Galloni, Andrew; Heimerl, Kurtis; Sullivan, Nick; Wood, Christopher A.; Fayed, Marwan (2022): "Respect the ORIGIN!: a best-case evaluation of connection coalescing in the wild", in: Proceedings of the ACM Internet Measurement Conference. (DOI)] still read
changed. The table had been pasted from–wikioutput generated before the §18.5 correction was applied. Accepted and fixed by regenerating the table; it was the only divergent cell of 207. - Wrong hand-written count. The prose said “nine turn out not to be repeated crawls at all”. Recounting from the coding files: of the 23, eight are
parallel-not-temporal, one of which is a survivor, so among the 17 non-survivors seven are not repeat crawls and ten are. The script never prints this aggregate, which is exactly why it was wrong. Accepted and fixed. - The structural guard in
build_provenance_pins.pyasserted much less than it claimed. It compared heading, table and file-block counts over the whole output against the whole live page. Because the appended section adds eleven headings of its own, the check had eleven headings of slack: the reviewer mutation-tested it by deleting a heading from the preserved live content and gotheadings: 27 → 37and exit 0. Accepted and fixed — the counts are now compared on the preserved text alone, which must equal the live page byte for byte, and the appended section is separately required to be non-empty. This is a page-matches-its-own-script failure of the kind the guard existed to prevent, and reading the script would not have found it; mutating it did. - Mutation-tested and confirmed sound: the partial-population refusal in
report_pins_audit.mjs(deleting one of 250 codings givesFATAL: coded 249 of 250), the citekey-map guard (removing one slug exits 1),pins_quotecheck.mjs(an injected fabricated quote raises NOT FOUND from 42 to 43),pins_agreement.mjs(a removed coder-B file drops n to 29 and printsMISSINGrather than silently continuing at 30), and the missing-file and empty-file guards in the builder. - Hand-computed Cohen's κ for the list pin from the raw pairs: observed 0.667, expected 0.351, κ = 0.486 → 0.49, matching the script. No bug.
- Checked the figures outside the edited window against
report_longitudinal-output.txtand found none stale.
Pass B — citations and quotes (Claude Sonnet)
- All 32 keys on the content page and all 8 on this page resolve against the live
literature:bibliography(850 entries, fetched today, not a local copy). The content page's key set is unchanged from the live version; this page gains exactly one key, [8Singanamalla, Sudheesh; Paracha, Muhammad Talha; Ahmad, Suleman; Hoyland, Jonathan; Valenta, Luke; Safronov, Yevgen; Wu, Peter; Galloni, Andrew; Heimerl, Kurtis; Sullivan, Nick; Wood, Christopher A.; Fayed, Marwan (2022): "Respect the ORIGIN!: a best-case evaluation of connection coalescing in the wild", in: Proceedings of the ACM Internet Measurement Conference. (DOI)], which is already in the bibliography and already cited on the content page. No entry added, none changed. - Verified all 23 rows of
out_pins/citekeys.jsonagainst the corpus index — title, venue, year and first-author surname — including thatfouad2020_missedandfouad2024_devilare two distinct papers and are not conflated. No swapped keys. - Independently found the same stale vantage cell as Pass A, by diffing the published table against a fresh
–wikirun, and confirmed all three quotes in the §18.5 account by locating them in the paper itself. - Spot-checked 24 evidence quotes (10
changed, 14same-explicit) against all three renderings: 24 of 24 located and supporting their coded value. Flagged one as loose rather than wrong — the Zoom-appsbrowserquote is about a crawler and parser rebuild rather than a browser version. Noted, not changed: the coding ischangedeither way and the codebook's browser pin covers the driven stack. - Verified the “12 further papers outside this 23” arithmetic (18 all-four minus the 6 in the table) and every external figure quoted from a cited paper.
Pass C — external currency (Claude Sonnet)
This pass reviewed the whole of both pages, not the diff, and found four stale external claims — all of them in text this edit did not touch. All four accepted:
- Quantcast's exit date was imprecise. The page said “dropped out after 1 April 2020”, which was only as precise as the rows sampled in the table above it. Bisecting the Tranco API on 2026-09-07 puts it between 14 and 15 August 2020 (
date/20200814→GK9K, providers includequantcast;date/20200815→Z7NG, providers do not). Re-verified independently before the edit. Every other row and date in that table and its footnote was checked against the live API and is exact. - OpenWPM had moved on. The page cited v0.36.0 / Firefox 154.0 as its worked example; v0.37.0 shipped on 6 September 2026 pinning Firefox 155.0.1. Confirmed via
releases/latestand by readinginstall-firefox.sh(# FIREFOX_155_0_1_RELEASE). Updated, with the drift itself now noted in the footnote, since it is the paragraph's own point. - The Playwright footnote quoted
main, not a release. It citedbrowsers.jsonfrom the development branch —revision 1241,browserVersion 152.0.7977.54— as evidence that pinning Playwright pins a browser. That snapshot shipped in no release, so it was the wrong evidence for the claim. Re-fetched from the tagged releasev1.63.0:revision 1243,browserVersion 153.0.8010.12. The reviewer reported different values for that tag than the author's own fetch did; a third fetch during the generic pass returnedrevision 1243,browserVersion 153.0.8010.12, matching the author's, so the published numbers stand. - ICLab is dormant. The page called Censored Planet and ICLab “continuous, longitudinal and public” in one breath. Censored Planet is active. ICLab's site serves an expired TLS certificate as of 2026-09-07 (
curlfails with exit 60 without-k), its data page returns 404, and its news stops in 2019 — all re-verified before the edit. The sentence now recommends the paper and warns off the feed. - Confirmed accurate and unchanged: the HTTP Archive changelog 404 (still 12 entries, last 2017-06-01, on
masterafter the repo rename), the HTTP Archive cadence text, all seven Tranco list ids, the EasyList commit cadence, the Public Suffix List footnote, the Chrome for Testing endpoints, and the WebREC repository facts. Neither page names an RFC or W3C specification, so that category is empty.
Pass D — generic (Claude Fable)
Run last, on the published pages, after A–C were applied. Findings and their disposition are in §18.11.
18.11 Generic review pass
Commissioned, nearly missed, and completed late. A Claude Fable pass was given both published pages, the scripts and their output, and asked for whatever the three focused passes were not looking for — overstated claims, structure, a section that does not answer its own question, and specifically whether same-implied is doing too much work, whether the split between the 250 and the 124 is legible, whether n = 30 and κ = 0.60 are being asked to carry more than they can, and whether the page states plainly enough that both coders and the adjudicator were LLMs reading LLM-extracted data. The run's budget all but ran out before it returned, and this section was published saying it never would; it arrived afterwards and its findings are below.
This is worth stating rather than quietly leaving a gap, because the generic pass is the one that catches overstatement, and §18 is the section most at risk of it. The specific things a reader should discount accordingly:
- The estimate column rests on
same-implied, which is a judgement that silence about change means no change. It is 47 of 124 on the list pin and 55 of 124 on the browser. The strict reading is published beside it and is much smaller. - The reliability figures are n = 30, and the κ for “all four fixed” (0.52) is computed on three and four positives respectively.
- Every coder in this exercise was a language model, reading text extracted from PDFs by a pipeline, coding a schema produced by another language model. The blind second coder and the quote adjudication bound the error; they do not remove it, and no human read any of the 250 papers.
Re-running this pass was filed as follow-up work — and then it returned.
Findings, and what was done. The pass returned after the first save of this section. Applied to the content page: the disclosure that both coders and the adjudicator were language-model agents reading LLM-extracted text, which the content page had nowhere stated and which “hand-coded” actively obscures; the softening of “two say so in words”, after the reviewer read the two same-explicit quote sets and found them to be permanent identifiers and one-off constructions rather than statements that anything was unchanged; Fisher exact p-values on the span reversal (0.10, 0.08 and 0.69 across three cuts, none significant) in place of “yes, and more sharply”; the floor-versus-false-positive contradiction in the lead; and a flag that one of the 15 all-four papers is, by its own coder's note, a cellular walk test rather than a web crawl. Applied here: the corrected κ defence in §18.4, the deletion of an unverifiable claim about coders opening the full text, and the resolution of the Playwright value discrepancy.
Accepted and not done, for budget: re-adjudicating the 73 same-explicit codings against the codebook's own wording; re-coding the 5G paper and recomputing 15/124 as 14/123; reading the 12 all-four papers outside the exemplar table and printing their notes; adjudicating the seven browser disagreements, which the reviewer argues may be the full-text coder's misses rather than the excerpt coder's over-calls, making §18.4's directional claim unsafe; publishing the κ confidence interval; recomputing the repeated-versus-single-shot comparison over the 124; per-table “of 250, unaudited” markers; and the waveCount-stratified middle reading of same-implied. Each is recorded as follow-up rather than silently dropped. Until they are done, §18.4's “the browser row is more likely too high than too low” and the content page's browser sentence rest on an untested attribution of the disagreement to one coder.
18.12 The scripts, and their unedited output
Everything below is read from the committed file by scripts/build_provenance_pins.py at build time. None of it is retyped.
# Four-pin hand-coding codebook — design:longitudinal
You are hand-coding papers for a systematization page on measuretheweb.org. The
page currently publishes, for 250 repeated-crawl papers, how many **stated** each
of four crawl parameters. "Stated once" is only a CEILING on "held the same in
every wave". Your job is to turn the ceiling into an estimate by reading what the
paper actually says about its repeated measurement.
## Step 0 — is this really a repeated crawl by the authors?
The 250 were selected mechanically (`temporal.snapshots > 1` in a structured
extraction). That predicate has false positives. Code `repeatKind` as exactly one:
| value | meaning |
|---|---|
| `own-repeat-crawl` | the authors themselves ran their crawler more than once over time and the waves are compared or aggregated |
| `own-repeat-nonweb` | repeated measurement, but not a web crawl the four pins apply to (e.g. repeated DNS probing, repeated app scraping, repeated API pulls, network scans) |
| `parallel-not-temporal` | the "snapshots" are parallel conditions run at one time (browsers, vantage points, profiles), not waves separated in time |
| `reused-waves` | the multiple time points come from a dataset or archive somebody else collected (web archive, existing corpus), not from the authors' own repeated crawling |
| `single-crawl` | the paper ran one crawl; the snapshot count is a mis-extraction (page counts, record counts, number of sites, number of runs of an unrelated thing) |
| `unclear` | you genuinely cannot tell after reading |
Also record `waveCount`: the number of distinct time-separated waves of the
authors' OWN crawl, as an integer, or `null` if not determinable. A continuous
daily/weekly crawl over N periods counts as N waves if the paper says so;
otherwise `null`.
## Step 1 — the four pins
Code each of the four independently. **The question is not "did they say it" but
"was it the same in every wave".**
* `list` — the **population / seed list**: which sites or targets were crawled
(Tranco, Alexa, CrUX, Majestic, a custom seed set …). NOT the filter list.
* `browser` — the browser (and version, if given) the crawler drove.
* `vantage` — the network location the crawl ran from (country, cloud region,
university network, VPN exit, residential proxy …).
* `state` — stateful vs stateless crawling: fresh profile per site, persistent
cookie jar, shared profile, cleared between waves, etc.
Each pin takes exactly one value:
| value | when to use |
|---|---|
| `same-explicit` | the paper says in words that this was the same / unchanged / the same identifier across the waves. A single permanent identifier used for all waves (one Tranco list ID, one pinned browser version, one container image) counts. |
| `same-implied` | the paper gives ONE value in a single methods description that plainly covers the whole repeated measurement, and never says it changed. This is the common case. It is weaker evidence than `same-explicit` — do not upgrade it. |
| `varied-deliberate` | the value differs across waves ON PURPOSE, as part of the design (rotating vantage points, comparing browsers across waves). |
| `changed` | the value changed or was refreshed between waves — the top list re-downloaded per wave, the filter list or browser updated, a move to a different host — whether stated as deliberate or as drift. |
| `not-stated` | the paper never gives the value at all. |
| `unclear` | the value is given somewhere but you cannot tell which wave(s) it applies to, or the description is contradictory. |
Rules that matter:
1. **Do not infer from the structured extraction line at the top of the packet.**
It is shown so you can see what a machine thought; it records a value stated
*once*. If the paper's text does not support it, code what the text says.
2. **A list that is re-downloaded each wave is `changed`, not `same-implied`** —
even though that is often the right thing to do scientifically. We are
measuring whether the frame is constant, not whether the authors were wise.
3. **"We used Chrome" with no version, across a two-year study, is
`same-implied` for the browser pin** only if the paper describes one crawler
setup for the whole study. If the paper says the crawler was rebuilt, updated,
or migrated, it is `changed`.
4. If `repeatKind` is `single-crawl` or `parallel-not-temporal`, still code the
four pins, but the "across waves" question is vacuous — use `same-implied`
only if one configuration is described, and note it in `notes`.
5. `not-stated` is a finding, not a failure. Do not guess.
## Step 2 — evidence
For each pin, give `quote`: a **verbatim** substring of the packet, ≤ 220
characters, that is your evidence. Copy it exactly — it will be checked against
the source file automatically, and a paraphrase will fail. If your value is
`not-stated`, set `quote` to `""`.
Set `confidence` per paper: `high` | `medium` | `low`.
## Output
For EACH paper write one file `out_pins/coded/<KEY_WITH_SLASHES_AS___>.json`
(same base name as its packet, `.json` instead of `.txt`), containing:
```json
{
"key": "2024/PETS/some-slug",
"repeatKind": "own-repeat-crawl",
"waveCount": 3,
"pins": {
"list": {"value": "same-explicit", "quote": "..."},
"browser": {"value": "same-implied", "quote": "..."},
"vantage": {"value": "not-stated", "quote": ""},
"state": {"value": "changed", "quote": "..."}
},
"confidence": "high",
"notes": "one or two sentences: what the paper actually did across waves"
}
```
Write valid JSON. No markdown fences in the file.
# Blind re-coding — second coder You are the SECOND coder in a two-coder reliability check. A first coder has already coded these papers; you will not see their answers, and you must not go looking for them. Files under `/workspace/artifacts/wiki/out_pins/coded/` are the first coder's output — **do not open that directory**. Read `/workspace/artifacts/wiki/out_pins/CODEBOOK.md` and apply it exactly. Two differences from the first pass: 1. **You read the FULL paper text**, not a keyword excerpt. The path is given to you. Read the whole methodology and any limitations section. 2. **Write your output to `/workspace/artifacts/wiki/out_pins/coded_val/`**, same file naming (`<year>__<venue>__<slug>.json`), same JSON schema. Everything else — the `repeatKind` values, the six pin values, the verbatim `quote` requirement, `confidence`, `notes` — is unchanged. Code what the paper says, not what you think it probably did.
- pins_population.mjs
#!/usr/bin/env node // Emit the 250 repeated-crawl papers that design:longitudinal's four-pin table // is computed over, with the extraction's own value for each pin, as JSONL. // // node scripts/pins_population.mjs [--run <dir>] > out_pins/population.jsonl // // The population predicate is copied verbatim from scripts/report_longitudinal.mjs // (`crawled` AND max stated temporal.snapshots > 1) so the two agree by construction. import fs from 'node:fs'; import path from 'node:path'; const argv = process.argv.slice(2); const RUN = argv.includes('--run') ? argv[argv.indexOf('--run') + 1] : '/workspace/publications_dataset/data/extract/run1'; const FULLTEXT = '/workspace/publications_dataset/data/fulltext'; const P = fs.readFileSync(path.join(RUN, 'extractions.jsonl'), 'utf8').split('\n').filter(Boolean).map((s) => JSON.parse(s)); const crawled = P.filter((p) => p.crawlConfig != null || p.studyTypes.includes('automated-web-crawl')); const snapOf = (p) => Math.max(0, ...p.temporal.map((t) => (t.snapshots == null ? 0 : t.snapshots))); const repeated = crawled.filter((p) => snapOf(p) > 1); const SENT = /^(?:not[- ]stated|unknown|n\/a)$/i; const statedListVersion = (p) => p.population.some((x) => x.listVersion != null && !SENT.test(x.listVersion.trim())); const namedBrowser = (p) => p.crawlConfig != null && p.crawlConfig.browsers.length > 0; const statedVantage = (p) => p.vantage.some((v) => v.locations.some((l) => !SENT.test(String(l).trim()))); const ccStated = (p, f) => p.crawlConfig != null && !['not-stated', 'not-applicable'].includes(p.crawlConfig[f]); for (const p of repeated.sort((a, b) => a.year - b.year || a.venue.localeCompare(b.venue) || a.slug.localeCompare(b.slug))) { const key = `${p.year}/${p.venue}/${p.slug}`; const f = path.join(FULLTEXT, String(p.year), p.venue, p.slug, 'paper.cols.txt'); process.stdout.write(JSON.stringify({ key, year: p.year, venue: p.venue, slug: p.slug, title: p.title, snapshots: snapOf(p), cadence: p.temporal.map((t) => t.cadence).filter(Boolean), spans: p.temporal.map((t) => [t.spanStart, t.spanEnd]).filter(([a, b]) => a || b), modes: [...new Set(p.temporal.map((t) => t.mode))], x_listVersion: statedListVersion(p), x_browser: namedBrowser(p), x_vantage: statedVantage(p), x_statefulness: ccStated(p, 'statefulness'), v_listVersion: [...new Set(p.population.map((x) => x.listVersion).filter((s) => s && !SENT.test(s.trim())))], v_sourceList: [...new Set(p.population.map((x) => x.sourceList).filter(Boolean))], v_browsers: p.crawlConfig ? p.crawlConfig.browsers : [], v_statefulness: p.crawlConfig ? p.crawlConfig.statefulness : null, v_vantage: [...new Set(p.vantage.flatMap((v) => v.locations).filter((l) => l && !SENT.test(String(l).trim())))], textPath: f, textBytes: fs.existsSync(f) ? fs.statSync(f).size : 0, }) + '\n'); }
- pins_excerpt.mjs
#!/usr/bin/env node // Build the per-paper reading packet a hand-coder gets for the four-pin audit. // // node scripts/pins_excerpt.mjs [--out out_pins/excerpts] [--budget 32000] // // Why an excerpt and not the whole paper: 250 papers is 20.9 MB of `.cols` text. // The coder needs the parts that describe the repeated measurement, not the // results or the related work. The rule below is deliberately over-inclusive — // two keyword tiers with context windows, merged — and the coder is also given // the full-text path and told to open it when the packet is not enough. The // recall of this rule is measured, not assumed: see the validation pass in // provenance:design:longitudinal. import fs from 'node:fs'; import path from 'node:path'; const argv = process.argv.slice(2); const OUT = argv.includes('--out') ? argv[argv.indexOf('--out') + 1] : 'out_pins/excerpts'; const BUDGET = argv.includes('--budget') ? +argv[argv.indexOf('--budget') + 1] : 32000; const POP = 'out_pins/population.jsonl'; // Tier 1: the sentence is about a wave, a repetition, or a pin being held. const CORE = [ /\b(?:re-?crawl|re-?ran|re-?run|repeat(?:ed|ing)?|second|third|fourth|fifth|subsequent|follow-?up|later|earlier)\b[^.]{0,60}\b(?:crawl|measur|experiment|round|wave|run|snapshot|iteration|visit|scan)/i, /\b(?:crawl|measur\w+|experiment|round|wave|run|snapshot|iteration|scan)s?\b[^.]{0,60}\b(?:re-?crawl|repeat|again|monthly|weekly|daily|bi-?weekly|quarterly|yearly|annual|every|each)\b/i, /\b(?:each|every|all|both|per|the (?:first|second|third|last|final))\s+(?:crawl|wave|round|run|iteration|snapshot|batch|measurement|visit|scan)\b/i, /\b(?:same|identical|unchanged|consistent(?:ly)?|kept|keep|held|fixed|pinn?ed|froze[nd]?|constant)\b[^.]{0,80}\b(?:version|list|browser|profile|configuration|setup|vantage|location|machine|instance|seed|snapshot|settings?|parameters?)\b/i, /\b(?:version|list|browser|profile|configuration|setup|vantage|location|machine|instance|seed)\b[^.]{0,60}\b(?:same|identical|unchanged|kept|held|fixed|pinn?ed|froze[nd]?|constant|updated|upgraded|refreshed|re-?download)/i, /\bstate(?:less|ful)\b|\bcookie jar\b|\b(?:fresh|clean|new|persistent|shared|separate)\s+(?:browser\s+)?profiles?\b/i, /\b(?:Tranco|Alexa|Majestic|Umbrella|CrUX|Chrome UX Report|Quantcast|BuiltWith|Cloudflare Radar)\b/i, /\b(?:EasyList|EasyPrivacy|Disconnect|uBlock|AdBlock|Ghostery|filter\s+lists?|block\s?lists?|Whotracks)\b/i, /\b(?:OpenWPM|Selenium|Puppeteer|Playwright|WebDriver|Xvfb|Chromedriver|Geckodriver|Docker|container)\b/i, /\b(?:Chrome|Chromium|Firefox|Brave|Edge|Safari|Tor Browser|Opera)\b[^.]{0,30}\b(?:v(?:ersion)?\.?\s*)?\d{1,3}(?:\.\d+)*\b/i, /\bvantage\b|\bVPN\b|\bproxy\b|\b(?:EC2|AWS|Azure|GCP|Google Cloud|DigitalOcean|Hetzner|Linode|OVH|Amazon)\b|\bdata\s?cent(?:er|re)\b|\bresidential\s+(?:IP|proxy|network)\b|\bexit node\b/i, /\bsnapshots?\b|\bwaves?\b|\blongitudinal\b|\btime series\b|\bover\s+(?:a|the|\d+)\s+(?:period|year|month|week|day)/i, ]; // Tier 2: methodology furniture — useful surroundings, cheaper windows. const CTX = [ /\b(?:we|our)\s+(?:crawl|collect|gather|visit|measure|configure|deploy|instrument|run|download|use[d]?|set\s?up)/i, /\b(?:methodolog|data collection|experimental setup|measurement setup|crawl(?:er|ing) setup|apparatus|infrastructure|limitations|threats to validity)\b/i, /\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\b[^.]{0,20}\b(?:19|20)\d\d\b/, /\b(?:19|20)\d\d[-/](?:0?[1-9]|1[0-2])\b/, /\b(?:headless|headful|user[- ]agent|screen resolution|viewport|GDPR|consent|banner|accept|reject)\b/i, /\btop[- ](?:\d[\d,.]*\s*[km]?|million|thousand)\b|\bseed (?:list|set)\b|\bsite list\b|\bdomain list\b/i, ]; const CORE_W = 700; const CTX_W = 300; function windows(text) { const spans = []; for (const [res, w, tier] of [[CORE, CORE_W, 1], [CTX, CTX_W, 2]]) { for (const re of res) { const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g'); let m; while ((m = g.exec(text)) !== null) { spans.push({ a: Math.max(0, m.index - w), b: Math.min(text.length, m.index + m[0].length + w), tier }); if (m.index === g.lastIndex) g.lastIndex++; } } } return spans; } function merge(spans) { spans.sort((x, y) => x.a - y.a); const out = []; for (const s of spans) { const last = out[out.length - 1]; if (last && s.a <= last.b + 40) { last.b = Math.max(last.b, s.b); last.tier = Math.min(last.tier, s.tier); } else out.push({ ...s }); } return out; } fs.mkdirSync(OUT, { recursive: true }); const pop = fs.readFileSync(POP, 'utf8').trim().split('\n').map(JSON.parse); const stats = []; for (const p of pop) { const raw = fs.readFileSync(p.textPath, 'utf8'); const text = raw.replace(/\r/g, ''); const HEAD = text.slice(0, 3000); // title + abstract, always included let sp = merge(windows(text)); let kept = sp; let dropped = 0; const total = (s) => s.reduce((a, c) => a + (c.b - c.a), 0); if (total(kept) > BUDGET) { // Drop tier-2-only windows first, longest-shortfall last: keep every core window. const core = kept.filter((s) => s.tier === 1); dropped = kept.length - core.length; kept = core; } if (total(kept) > BUDGET) { // Still over: shrink core windows symmetrically rather than cutting the tail, // because limitations sections sit at the end and carry the pin admissions. const f = BUDGET / total(kept); kept = kept.map((s) => { const mid = (s.a + s.b) / 2, h = ((s.b - s.a) * f) / 2; return { a: Math.round(mid - h), b: Math.round(mid + h), tier: s.tier }; }); kept = merge(kept); } const body = kept.map((s) => `[…${s.a}] ` + text.slice(s.a, s.b).replace(/\s+/g, ' ').trim()).join('\n\n'); const packet = `# ${p.key}\n# title: ${p.title}\n# full text: ${p.textPath} (${p.textBytes} bytes) — OPEN IT if this packet is not enough\n# extraction recorded: snapshots=${p.snapshots} browsers=${JSON.stringify(p.v_browsers)} statefulness=${p.v_statefulness} listVersion=${JSON.stringify(p.v_listVersion)} sourceList=${JSON.stringify(p.v_sourceList)} vantage=${JSON.stringify(p.v_vantage)}\n\n== HEAD (first 3000 chars) ==\n${HEAD.replace(/\s+/g, ' ').trim()}\n\n== KEYWORD WINDOWS (offsets in […N]) ==\n${body}\n`; const f = path.join(OUT, p.key.replace(/\//g, '__') + '.txt'); fs.writeFileSync(f, packet); stats.push({ key: p.key, src: text.length, packet: packet.length, windows: kept.length, droppedCtx: dropped }); } const ps = stats.map((s) => s.packet).sort((a, b) => a - b); console.log(`wrote ${stats.length} packets to ${OUT}`); console.log(`packet chars min ${ps[0]} median ${ps[Math.floor(ps.length / 2)]} p90 ${ps[Math.floor(ps.length * 0.9)]} max ${ps[ps.length - 1]}`); console.log(`total packet MB ${(ps.reduce((a, c) => a + c, 0) / 1e6).toFixed(2)} (source 20.9 MB)`); console.log(`coverage: median packet is ${(100 * stats.map((s) => s.packet / s.src).sort((a, b) => a - b)[Math.floor(stats.length / 2)]).toFixed(0)}% of its source`); console.log(`packets where tier-2 windows had to be dropped: ${stats.filter((s) => s.droppedCtx > 0).length}`); fs.writeFileSync(path.join(OUT, '_stats.json'), JSON.stringify(stats, null, 1));
- pins_valsample.mjs
#!/usr/bin/env node // Draw the blind re-coding sample for the four-pin audit. // // node scripts/pins_valsample.mjs [--n 30] [--seed 20260907] // // A second coder re-codes these papers from the FULL `.cols` text, without // seeing the first coding and without the keyword packet. That measures two // things at once: how reliable the coding is, and how much the excerpting rule // in scripts/pins_excerpt.mjs loses. Deterministic PRNG so the draw is // reproducible from the seed alone. import fs from 'node:fs'; const argv = process.argv.slice(2); const N = argv.includes('--n') ? +argv[argv.indexOf('--n') + 1] : 30; const SEED = argv.includes('--seed') ? +argv[argv.indexOf('--seed') + 1] : 20260907; // mulberry32 let s = SEED >>> 0; const rnd = () => { s |= 0; s = (s + 0x6d2b79f5) | 0; let t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; const pop = fs.readFileSync('out_pins/population.jsonl', 'utf8').trim().split('\n').map(JSON.parse); const idx = pop.map((_, i) => i); for (let i = idx.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [idx[i], idx[j]] = [idx[j], idx[i]]; } const pick = idx.slice(0, N).map((i) => pop[i]).sort((a, b) => a.key.localeCompare(b.key)); console.error(`seed ${SEED}, n=${N} of ${pop.length}`); for (const p of pick) console.log(`${p.key}\t${p.textPath}`);
- report_pins_audit.mjs
#!/usr/bin/env node // The four-pin HAND-CODED audit for design:longitudinal. // // node scripts/report_pins_audit.mjs [--coded out_pins/coded] [--run <dir>] // // design:longitudinal used to publish, for each of four crawl parameters, how // many of the 250 repeated-crawl papers STATED it. Stating a value once is only a // ceiling on holding it fixed across waves. This script aggregates a hand-coding // of all 250 papers against out_pins/CODEBOOK.md and turns each ceiling into a // three-number estimate: // // floor same-explicit the paper says it was unchanged // estimate same-explicit + same-implied one configuration described, no change stated // ceiling the old "stated at all" figure what report_longitudinal.mjs prints // // Rules obeyed: papers are counted, never tuples; sentinels are never an answer; // every denominator is named on the line above the numbers; every value in the // codebook is printed even when it is zero, so nothing vanishes. import fs from 'node:fs'; import path from 'node:path'; const argv = process.argv.slice(2); const CODED = argv.includes('--coded') ? argv[argv.indexOf('--coded') + 1] : 'out_pins/coded'; const RUN = argv.includes('--run') ? argv[argv.indexOf('--run') + 1] : '/workspace/publications_dataset/data/extract/run1'; const POP = argv.includes('--pop') ? argv[argv.indexOf('--pop') + 1] : 'out_pins/population.jsonl'; const pct = (a, b) => (b ? ((100 * a) / b).toFixed(1) : '0.0'); const rule = (t) => console.log(`\n${'='.repeat(78)}\n${t}\n${'='.repeat(78)}`); const head = (t, d) => console.log(`\n-- ${t}\n population: ${d}`); const pop = fs.readFileSync(POP, 'utf8').trim().split('\n').map(JSON.parse); const byKey = new Map(pop.map((p) => [p.key, p])); // ------------------------------------------------------------------ load const PINS = ['list', 'browser', 'vantage', 'state']; const VALUES = ['same-explicit', 'same-implied', 'varied-deliberate', 'changed', 'not-stated', 'unclear']; const KINDS = ['own-repeat-crawl', 'own-repeat-nonweb', 'parallel-not-temporal', 'reused-waves', 'single-crawl', 'unclear']; const coded = []; const problems = []; for (const p of pop) { const f = path.join(CODED, p.key.replace(/\//g, '__') + '.json'); if (!fs.existsSync(f)) { problems.push(`MISSING coding: ${p.key}`); continue; } const raw = fs.readFileSync(f, 'utf8').replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/, ''); let c; try { c = JSON.parse(raw); } catch (e) { problems.push(`UNPARSABLE: ${p.key}: ${e.message}`); continue; } if (c.key !== p.key) problems.push(`KEY MISMATCH in ${f}: says ${c.key}`); if (!KINDS.includes(c.repeatKind)) problems.push(`BAD repeatKind "${c.repeatKind}" in ${p.key}`); let ok = true; for (const k of PINS) { if (!c.pins || !c.pins[k] || !VALUES.includes(c.pins[k].value)) { problems.push(`BAD/MISSING pin ${k} in ${p.key}: ${JSON.stringify(c.pins && c.pins[k])}`); ok = false; } } if (!ok) continue; coded.push({ ...c, key: p.key, year: p.year, venue: p.venue, slug: p.slug, ex: p }); } // The whole audit rests on every paper being coded. Refuse to print figures // computed over a partial population rather than quietly shrinking the // denominator — a shelled-out denominator is how percentages go wrong silently. if (coded.length !== pop.length) { console.error(`\nFATAL: coded ${coded.length} of ${pop.length}. Figures would be computed over the wrong population.`); for (const s of problems) console.error(' ' + s); process.exit(1); } console.log(`design:longitudinal — the four pins, HAND-CODED`); console.log(`coded dir: ${CODED}`); console.log(`extraction run: ${RUN}`); console.log(`corpus: 5,859 papers, 7 venues (CCS, IMC, NDSS, PETS, USENIX Sec, TheWebConf, IEEE S&P), 2010-2026`); if (problems.length) { console.log(`\nSCHEMA PROBLEMS (${problems.length}):`); for (const s of problems) console.log(' ' + s); } else console.log(`\nschema: all ${coded.length} codings parse and use only codebook values.`); // ------------------------------------------------- 1. is it a repeat crawl rule('1. IS THE MECHANICALLY SELECTED POPULATION REALLY REPEATED CRAWLING'); head('repeatKind, hand-coded', `${coded.length} papers selected by temporal.snapshots > 1 AND crawled`); for (const k of KINDS) { const n = coded.filter((c) => c.repeatKind === k).length; console.log(` ${k.padEnd(24)} ${String(n).padStart(4)} ${pct(n, coded.length).padStart(5)}%`); } const OWN = coded.filter((c) => c.repeatKind === 'own-repeat-crawl'); console.log(`\n TRUE POSITIVES for "the authors re-crawled the web over time": ${OWN.length} / ${coded.length} = ${pct(OWN.length, coded.length)}%`); console.log(` The remaining ${coded.length - OWN.length} are the mechanical predicate's false positives for this question.`); // ------------------------------------------------------- 2. the four pins rule('2. THE FOUR PINS — CEILING, ESTIMATE, FLOOR'); for (const [POPNAME, G] of [['all 250 mechanically selected', coded], [`the ${OWN.length} hand-confirmed own repeat crawls`, OWN]]) { head('hand-coded pin values', `${G.length} papers — ${POPNAME}`); console.log(` ${'pin'.padEnd(9)} ${VALUES.map((v) => v.padStart(17)).join('')}`); for (const k of PINS) { const row = VALUES.map((v) => G.filter((c) => c.pins[k].value === v).length); console.log(` ${k.padEnd(9)} ${row.map((n) => `${n} (${pct(n, G.length)}%)`.padStart(17)).join('')}`); } console.log(`\n ${'pin'.padEnd(9)} ${'floor'.padStart(14)} ${'estimate'.padStart(16)} ${'ceiling'.padStart(16)}`); console.log(` ${''.padEnd(9)} ${'explicit'.padStart(14)} ${'expl+implied'.padStart(16)} ${'stated at all'.padStart(16)}`); const XF = { list: 'x_listVersion', browser: 'x_browser', vantage: 'x_vantage', state: 'x_statefulness' }; for (const k of PINS) { const fl = G.filter((c) => c.pins[k].value === 'same-explicit').length; const es = G.filter((c) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)).length; const ce = G.filter((c) => c.ex[XF[k]]).length; console.log(` ${k.padEnd(9)} ${`${fl} (${pct(fl, G.length)}%)`.padStart(14)} ${`${es} (${pct(es, G.length)}%)`.padStart(16)} ${`${ce} (${pct(ce, G.length)}%)`.padStart(16)}`); } const four = (c, set) => PINS.every((k) => set.includes(c.pins[k].value)); const f4fl = G.filter((c) => four(c, ['same-explicit'])).length; const f4es = G.filter((c) => four(c, ['same-explicit', 'same-implied'])).length; const f4ce = G.filter((c) => c.ex.x_listVersion && c.ex.x_browser && c.ex.x_vantage && c.ex.x_statefulness).length; console.log(`\n ALL FOUR held fixed`); console.log(` floor (all four same-explicit) ${f4fl} / ${G.length} = ${pct(f4fl, G.length)}%`); console.log(` estimate (all four explicit or implied) ${f4es} / ${G.length} = ${pct(f4es, G.length)}%`); console.log(` ceiling (all four merely stated — old figure) ${f4ce} / ${G.length} = ${pct(f4ce, G.length)}%`); const none = G.filter((c) => PINS.every((k) => c.pins[k].value === 'not-stated')).length; console.log(` none of the four stated at all ${none} / ${G.length} = ${pct(none, G.length)}%`); const anyMoved = G.filter((c) => PINS.some((k) => ['changed', 'varied-deliberate'].includes(c.pins[k].value))).length; console.log(` at least one pin demonstrably NOT fixed ${anyMoved} / ${G.length} = ${pct(anyMoved, G.length)}%`); } // -------------------------------------------- 3. where the ceiling overstates rule('3. HOW FAR THE OLD CEILING OVERSTATED EACH PIN'); head('ceiling minus estimate, per pin', `${coded.length} papers (the population the published table used)`); const XF = { list: 'x_listVersion', browser: 'x_browser', vantage: 'x_vantage', state: 'x_statefulness' }; console.log(` ${'pin'.padEnd(9)} ${'ceiling'.padStart(9)} ${'estimate'.padStart(9)} ${'over by'.padStart(9)} what the difference is`); for (const k of PINS) { const ce = coded.filter((c) => c.ex[XF[k]]).length; const es = coded.filter((c) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)).length; const why = coded.filter((c) => c.ex[XF[k]] && !['same-explicit', 'same-implied'].includes(c.pins[k].value)); const brk = new Map(); for (const c of why) brk.set(c.pins[k].value, (brk.get(c.pins[k].value) || 0) + 1); console.log(` ${k.padEnd(9)} ${String(ce).padStart(9)} ${String(es).padStart(9)} ${String(ce - es).padStart(9)} ${[...brk.entries()].sort((a, b) => b[1] - a[1]).map(([v, n]) => `${v} ${n}`).join(', ') || '-'}`); } console.log(`\n The reverse direction — hand-coding finds the pin fixed where the extraction`); console.log(` recorded nothing (the paper says it in prose the extractor did not tuple):`); for (const k of PINS) { const n = coded.filter((c) => !c.ex[XF[k]] && ['same-explicit', 'same-implied'].includes(c.pins[k].value)).length; console.log(` ${k.padEnd(9)} ${String(n).padStart(4)}`); } // ------------------------------------------------- 4. by span, by year rule('4. DOES THE LONGER PANEL STILL PIN LESS'); { const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; // Same parser as report_longitudinal.mjs, including the textual-month fix. const parseDate = (s) => { if (!s) return null; const t = String(s).toLowerCase(); const ym = /(?:^|[^\d])(\d{4})(?:[-/](0?[1-9]|1[0-2]))(?![\d])/.exec(t); if (ym) return { y: +ym[1], m: +ym[2] }; const yO = /(?:^|[^\d])(\d{4})(?![\d])/.exec(t); if (!yO) return null; const y = +yO[1]; if (y < 1990 || y > 2027) return null; const nm = new RegExp(`\\b(${MONTHS.join('|')})[a-z]*\\b`).exec(t); return { y, m: nm ? MONTHS.indexOf(nm[1]) + 1 : 1 }; }; const longestSpan = (p) => { let best = null; for (const [a, b] of p.spans) { const pa = parseDate(a), pb = parseDate(b); if (!pa || !pb) continue; const d = (pb.y - pa.y) * 12 + (pb.m - pa.m); if (d >= 0 && (best == null || d > best)) best = d; } return best; }; const four = (c) => PINS.every((k) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)); head('all four fixed (estimate), by longest stated span', `${OWN.length} hand-confirmed own repeat crawls`); const multi = OWN.filter((c) => { const m = longestSpan(c.ex); return m != null && m >= 12; }); const sub = OWN.filter((c) => { const m = longestSpan(c.ex); return m != null && m < 12; }); const unk = OWN.length - multi.length - sub.length; for (const [name, g] of [['span >= 12 months', multi], ['span < 12 months', sub]]) { console.log(` ${name} n=${String(g.length).padStart(3)} all four ${g.filter(four).length} (${pct(g.filter(four).length, g.length)}%) ` + PINS.map((k) => `${k} ${g.filter((c) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)).length}`).join(' ')); } console.log(` (${unk} have no parsable end date and are excluded from this split)`); head('all four fixed (estimate), by four-year bucket', `${OWN.length} hand-confirmed own repeat crawls`); for (const [a, b] of [[2010, 2011], [2012, 2015], [2016, 2019], [2020, 2023], [2024, 2026]]) { const g = OWN.filter((c) => c.year >= a && c.year <= b); const flag = b === 2026 ? ' << provisional' : ''; console.log(` ${a}-${b} n=${String(g.length).padStart(3)} all four ${g.filter(four).length} (${pct(g.filter(four).length, g.length)}%)${flag}`); } } // ------------------------------------------------- 5. explicit-pin exemplars rule('5. PAPERS THAT SAY IN WORDS THAT A PIN WAS HELD FIXED'); { for (const k of PINS) { const g = coded.filter((c) => c.pins[k].value === 'same-explicit').sort((a, b) => b.year - a.year); console.log(`\n-- ${k}: ${g.length} papers state explicitly that it was unchanged across waves`); for (const c of g) console.log(` ${c.year} ${c.venue.padEnd(9)} ${c.slug.slice(0, 62).padEnd(62)} "${(c.pins[k].quote || '').slice(0, 110)}"`); } } rule('6. PAPERS WHERE A PIN DEMONSTRABLY MOVED BETWEEN WAVES'); { for (const k of PINS) { const g = coded.filter((c) => c.pins[k].value === 'changed').sort((a, b) => b.year - a.year); console.log(`\n-- ${k}: ${g.length} papers state the value changed or was refreshed between waves`); for (const c of g) console.log(` ${c.year} ${c.venue.padEnd(9)} ${c.slug.slice(0, 62).padEnd(62)} "${(c.pins[k].quote || '').slice(0, 110)}"`); } for (const k of PINS) { const g = coded.filter((c) => c.pins[k].value === 'varied-deliberate'); console.log(`\n-- ${k}: ${g.length} papers vary it across waves BY DESIGN`); for (const c of g) console.log(` ${c.year} ${c.venue.padEnd(9)} ${c.slug.slice(0, 62)}`); } } // ------------------------------------------------------- 7. coder confidence rule('7. CODER CONFIDENCE AND THE ALL-FOUR EXEMPLAR LIST'); { head('self-reported confidence', `${coded.length} papers`); for (const v of ['high', 'medium', 'low']) { const n = coded.filter((c) => c.confidence === v).length; console.log(` ${v.padEnd(8)} ${String(n).padStart(4)} ${pct(n, coded.length).padStart(5)}%`); } const bad = coded.filter((c) => !['high', 'medium', 'low'].includes(c.confidence)); if (bad.length) console.log(` OTHER/UNSET: ${bad.length} -> ${bad.map((c) => c.key).join(', ')}`); const four = (c) => PINS.every((k) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)); const g = coded.filter(four).sort((a, b) => b.year - a.year || a.venue.localeCompare(b.venue)); console.log(`\n-- the ${g.length} papers estimated to hold all four fixed (any repeatKind), most recent first`); console.log(` year | venue | slug | repeatKind | waves | explicit pins`); for (const c of g) { const ex = PINS.filter((k) => c.pins[k].value === 'same-explicit'); console.log(` ${c.year} | ${c.venue} | ${c.slug} | ${c.repeatKind} | ${c.waveCount == null ? '?' : c.waveCount} | ${ex.join(',') || '-'}`); } const ownFour = OWN.filter(four); console.log(`\n of those, hand-confirmed own repeat crawls: ${ownFour.length}`); for (const c of ownFour.sort((a, b) => b.year - a.year || a.venue.localeCompare(b.venue))) console.log(` ${c.year} | ${c.venue} | ${c.slug}`); } // ---------------------------------------------- 8. old exemplar list, rechecked rule('8. WHAT HAPPENED TO THE 23 PAPERS THE OLD CEILING TABLE LISTED'); { const old = coded.filter((c) => c.ex.x_listVersion && c.ex.x_browser && c.ex.x_vantage && c.ex.x_statefulness); const four = (c) => PINS.every((k) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)); console.log(` ${old.length} papers stated all four. Hand-coding each:\n`); console.log(` year venue slug kind list/browser/vantage/state`); for (const c of old.sort((a, b) => b.year - a.year || a.venue.localeCompare(b.venue))) { console.log(` ${c.year} ${c.venue.padEnd(10)} ${c.slug.slice(0, 43).padEnd(43)} ${c.repeatKind.padEnd(20)} ${PINS.map((k) => c.pins[k].value).join(' / ')}`); } console.log(`\n survive as all-four-fixed (estimate): ${old.filter(four).length} / ${old.length}`); } console.log(); // ============================================================ wiki emitter // The page's tables are pasted from here, not retyped. A retyped table is how a // stale cell survives a refresh. if (argv.includes('--wiki')) { const four = (c, set) => PINS.every((k) => set.includes(c.pins[k].value)); const XW = { list: 'x_listVersion', browser: 'x_browser', vantage: 'x_vantage', state: 'x_statefulness' }; const LBL = { list: 'Population list', browser: 'Browser', vantage: 'Vantage point', state: 'Statefulness' }; rule('WIKI MARKUP'); console.log(`\n---- table: repeatKind over the ${coded.length} mechanically selected papers ----`); const KL = { 'own-repeat-crawl': 'The authors re-crawled the web over time', 'own-repeat-nonweb': 'Repeated, but not a web crawl (DNS probes, app stores, API pulls)', 'parallel-not-temporal': 'Parallel conditions at one time, not waves', 'reused-waves': 'Time points come from an archive or somebody else\'s dataset', 'single-crawl': 'One crawl — the snapshot count is a mis-extraction', unclear: 'Undecidable after reading', }; console.log(`^ What the "more than one snapshot" really is ^ Papers ^ Share of ${coded.length} ^`); for (const k of KINDS) { const n = coded.filter((c) => c.repeatKind === k).length; console.log(`| ${KL[k]} | ${n} | ${pct(n, coded.length)}% |`); } for (const [G, name] of [[coded, `all ${coded.length}`], [OWN, `${OWN.length} hand-confirmed own repeat crawls`]]) { console.log(`\n---- table: four pins over ${name} ----`); console.log(`^ Pin ^ Held fixed, stated in words ^ Held fixed, estimate ^ Stated at all (old ceiling) ^ Moved between waves ^ Never stated ^`); for (const k of PINS) { const fl = G.filter((c) => c.pins[k].value === 'same-explicit').length; const es = G.filter((c) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)).length; const ce = G.filter((c) => c.ex[XW[k]]).length; const mv = G.filter((c) => ['changed', 'varied-deliberate'].includes(c.pins[k].value)).length; const ns = G.filter((c) => c.pins[k].value === 'not-stated').length; console.log(`| ${LBL[k]} | ${fl} (${pct(fl, G.length)}%) | **${es} (${pct(es, G.length)}%)** | ${ce} (${pct(ce, G.length)}%) | ${mv} (${pct(mv, G.length)}%) | ${ns} (${pct(ns, G.length)}%) |`); } const f4fl = G.filter((c) => four(c, ['same-explicit'])).length; const f4es = G.filter((c) => four(c, ['same-explicit', 'same-implied'])).length; const f4ce = G.filter((c) => c.ex.x_listVersion && c.ex.x_browser && c.ex.x_vantage && c.ex.x_statefulness).length; const f4mv = G.filter((c) => PINS.some((k) => ['changed', 'varied-deliberate'].includes(c.pins[k].value))).length; const f4ns = G.filter((c) => PINS.every((k) => c.pins[k].value === 'not-stated')).length; console.log(`| **All four** | ${f4fl} (${pct(f4fl, G.length)}%) | **${f4es} (${pct(f4es, G.length)}%)** | ${f4ce} (${pct(f4ce, G.length)}%) | ${f4mv} (${pct(f4mv, G.length)}%) | ${f4ns} (${pct(f4ns, G.length)}%) |`); console.log(` (last two columns of the All-four row are "at least one pin moved" and "none of the four stated")`); } } // ------------------------------------------------- the exemplar table, regenerated // The content page's "papers that pin all four" table is emitted here rather than // edited in place, so a hand-coded verdict cannot drift from the table cell that // reports it. Citekeys come from out_pins/citekeys.json, which is a copy of the // keys ALREADY on design:longitudinal — this adds no bibliography entries. if (argv.includes('--wiki')) { const MAPF = argv.includes('--citekeys') ? argv[argv.indexOf('--citekeys') + 1] : 'out_pins/citekeys.json'; const map = JSON.parse(fs.readFileSync(MAPF, 'utf8')); delete map._comment; const old = coded.filter((c) => c.ex.x_listVersion && c.ex.x_browser && c.ex.x_vantage && c.ex.x_statefulness); // If the map and the population disagree the table would silently lose a row. const mapped = new Set(Object.keys(map)); const inPop = new Set(old.map((c) => c.slug)); const onlyMap = [...mapped].filter((s) => !inPop.has(s)); const onlyPop = [...inPop].filter((s) => !mapped.has(s)); if (onlyMap.length || onlyPop.length) { console.error(`\nFATAL: citekey map does not match the all-four-stated set.`); for (const s of onlyMap) console.error(` in map, not in set: ${s}`); for (const s of onlyPop) console.error(` in set, not in map: ${s}`); process.exit(1); } const VEN = { 'IEEE-SP': 'IEEE S&P', USENIX: 'USENIX', WWW: 'WWW', PETS: 'PETS', IMC: 'IMC', NDSS: 'NDSS', CCS: 'CCS' }; const KIND = { 'own-repeat-crawl': 'own repeat crawl', 'own-repeat-nonweb': 'repeated, not a web crawl', 'parallel-not-temporal': 'parallel, not waves', 'reused-waves': 'reused waves', 'single-crawl': 'single crawl', unclear: 'unclear', }; const SH = { 'same-explicit': 'fixed, in words', 'same-implied': 'fixed, implied', 'varied-deliberate': 'varied by design', changed: '**changed**', 'not-stated': 'not stated', unclear: 'unclear' }; const four = (c) => PINS.every((k) => ['same-explicit', 'same-implied'].includes(c.pins[k].value)); console.log(`\n---- table: the ${old.length} papers that STATED all four, with what hand-coding found ----`); console.log(`^ Year ^ Venue ^ Paper ^ Really a repeat crawl? ^ List ^ Browser ^ Vantage ^ State ^ All four fixed? ^`); for (const c of old.sort((a, b) => b.year - a.year || a.venue.localeCompare(b.venue) || a.slug.localeCompare(b.slug))) { const m = map[c.slug]; console.log(`| ${c.year} | ${VEN[c.venue]} | ${m.name} {[${m.key}]} | ${KIND[c.repeatKind]} | ${PINS.map((k) => SH[c.pins[k].value]).join(' | ')} | ${four(c) ? '**yes**' : 'no'} |`); } console.log(`\n survivors: ${old.filter(four).length} of ${old.length}`); }
- pins_quotecheck.mjs
#!/usr/bin/env node // Verify every evidence quote the four-pin hand-coding produced. // // node scripts/pins_quotecheck.mjs [--coded out_pins/coded] [--pop ...] [--exc ...] // // A hand-coder that paraphrases is a hand-coder that hallucinates. Each non-empty // `quote` must appear in the paper it is attributed to. // // WHY THIS IS NOT A PLAIN SUBSTRING TEST. The corpus stores three renderings of // each PDF and they disagree sentence by sentence: // paper.cols.txt two-column reading order repaired (what the coders were given) // paper.norm.txt the unrepaired extraction // paper.txt the raw pdftotext dump // Even `.cols` still splices a quarter of pages: a sentence that runs across a // column boundary comes back with the neighbouring column's text injected into // the middle of it, e.g. // "We implemented FourthParty as an extension to Mozilla // B. Design Principles Firefox. It currently instruments ..." // A coder who reads that correctly and writes down the real sentence produces a // quote that is NOT a substring of any rendering. Failing it as unlocated would // call a correct reading a hallucination. // // So matching is tried in four escalating modes, against all three renderings // and the excerpt packet: // exact raw substring // collapsed runs of whitespace -> one space on both sides // loose collapsed + de-hyphenation across line breaks, unicode quotes and // dashes folded to ASCII, case ignored // spliced the quote is cut into overlapping 6-word shingles and a quote // passes if SHINGLE_MIN of them are found individually. This is the // de-splicing mode and it is reported separately and listed in full, // because it is the weakest evidence and has to be read by a human. // Anything failing all four is printed in full. import fs from 'node:fs'; import path from 'node:path'; const argv = process.argv.slice(2); const CODED = argv.includes('--coded') ? argv[argv.indexOf('--coded') + 1] : 'out_pins/coded'; const POP = argv.includes('--pop') ? argv[argv.indexOf('--pop') + 1] : 'out_pins/population.jsonl'; const EXC = argv.includes('--exc') ? argv[argv.indexOf('--exc') + 1] : 'out_pins/excerpts'; const SHINGLE_N = 6; const SHINGLE_MIN = 0.8; const collapse = (s) => s.replace(/\s+/g, ' ').trim(); const loose = (s) => collapse(s) .replace(/(\w)-\s+(\w)/g, '$1$2') .replace(/[‘’ʼ′]/g, "'") .replace(/[“”″]/g, '"') .replace(/[‐-―−]/g, '-') .replace(/[ ]/g, ' ') .toLowerCase(); const pop = fs.readFileSync(POP, 'utf8').trim().split('\n').map(JSON.parse); let total = 0, empty = 0; const modes = { exact: 0, collapsed: 0, loose: 0, spliced: 0 }; const fails = []; const splicedList = []; let missing = 0; for (const p of pop) { const f = path.join(CODED, p.key.replace(/\//g, '__') + '.json'); if (!fs.existsSync(f)) { missing++; continue; } const raw = fs.readFileSync(f, 'utf8').replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/, ''); const c = JSON.parse(raw); // Every rendering the corpus holds, plus the packet the coder was handed. const dir = path.dirname(p.textPath); const parts = []; for (const name of ['paper.cols.txt', 'paper.norm.txt', 'paper.txt']) { const q = path.join(dir, name); if (fs.existsSync(q)) parts.push(fs.readFileSync(q, 'utf8')); } const packet = path.join(EXC, p.key.replace(/\//g, '__') + '.txt'); if (fs.existsSync(packet)) parts.push(fs.readFileSync(packet, 'utf8')); const hay = parts.join('\n'); const hayC = collapse(hay); const hayL = loose(hay); for (const [pin, v] of Object.entries(c.pins)) { total++; const q = (v.quote || '').trim(); if (!q) { empty++; continue; } if (hay.includes(q)) { modes.exact++; continue; } if (hayC.includes(collapse(q))) { modes.collapsed++; continue; } if (hayL.includes(loose(q))) { modes.loose++; continue; } // de-splicing mode const w = loose(q).split(' ').filter(Boolean); if (w.length >= SHINGLE_N) { const sh = []; for (let i = 0; i + SHINGLE_N <= w.length; i++) sh.push(w.slice(i, i + SHINGLE_N).join(' ')); const hit = sh.filter((s) => hayL.includes(s)).length; if (hit / sh.length >= SHINGLE_MIN) { modes.spliced++; splicedList.push({ key: p.key, pin, value: v.value, quote: q, frac: hit / sh.length }); continue; } fails.push({ key: p.key, pin, value: v.value, quote: q, frac: hit / sh.length }); } else { fails.push({ key: p.key, pin, value: v.value, quote: q, frac: null }); } } } const checked = total - empty; const pc = (n) => `${n} ${((100 * n) / (checked || 1)).toFixed(1)}%`; console.log(`design:longitudinal four-pin audit — evidence quote check`); console.log(`coded dir: ${CODED}`); console.log(`renderings searched per paper: paper.cols.txt, paper.norm.txt, paper.txt, and the excerpt packet`); console.log(`de-splicing mode: ${SHINGLE_N}-word shingles, pass at >= ${(100 * SHINGLE_MIN).toFixed(0)}% found`); console.log(`\npapers with no coding file (skipped): ${missing}`); console.log(`pin codings: ${total}`); console.log(` no quote given: ${empty} (permitted only for not-stated; checked below)`); console.log(` checked: ${checked}`); console.log(` exact match: ${pc(modes.exact)}`); console.log(` whitespace-collapsed match: ${pc(modes.collapsed)}`); console.log(` loose match: ${pc(modes.loose)}`); console.log(` spliced match (de-spliced): ${pc(modes.spliced)}`); console.log(` NOT FOUND: ${pc(fails.length)}`); // A quote-less coding that is not `not-stated` is a coding with no evidence. const noEvidence = []; for (const p of pop) { const f = path.join(CODED, p.key.replace(/\//g, '__') + '.json'); if (!fs.existsSync(f)) continue; const c = JSON.parse(fs.readFileSync(f, 'utf8').replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '')); for (const [pin, v] of Object.entries(c.pins)) { if (!(v.quote || '').trim() && v.value !== 'not-stated') noEvidence.push(`${p.key} ${pin}=${v.value}`); } } console.log(`\ncodings with no quote and a value other than not-stated: ${noEvidence.length}`); for (const s of noEvidence) console.log(' ' + s); console.log(`\nMATCHED ONLY AFTER DE-SPLICING (${splicedList.length}) — these are read by hand, the source page is two-column spliced:`); for (const f of splicedList.sort((a, b) => a.frac - b.frac)) { console.log(` ${f.key} [${f.pin}=${f.value}] shingles ${(100 * f.frac).toFixed(0)}%\n ${f.quote}`); } console.log(`\nQUOTES NOT LOCATED, printed in full (${fails.length}):`); for (const f of fails) console.log(` ${f.key} [${f.pin}=${f.value}] shingles ${f.frac == null ? 'n/a' : (100 * f.frac).toFixed(0) + '%'}\n ${f.quote}`);
- pins_agreement.mjs
#!/usr/bin/env node // Inter-coder agreement for the design:longitudinal four-pin hand-coding. // // node scripts/pins_agreement.mjs [--a out_pins/coded] [--b out_pins/coded_val] // // Coder A read a keyword-window excerpt packet; coder B read the whole paper and // never saw A's output. The sample is out_pins/valsample.tsv. Agreement is // reported three ways per pin, because the three answer different questions: // // exact the same codebook value // fixed/not both collapse to {fixed = same-explicit|same-implied} vs // {not = varied-deliberate|changed|not-stated|unclear} — this is // the partition the published "estimate" column actually uses // Cohen kappa chance-corrected, on the exact values // // A high exact agreement with a low kappa means the coders agree because one // value dominates, not because they are reading the same thing. Both are printed. import fs from 'node:fs'; import path from 'node:path'; const argv = process.argv.slice(2); const A = argv.includes('--a') ? argv[argv.indexOf('--a') + 1] : 'out_pins/coded'; const B = argv.includes('--b') ? argv[argv.indexOf('--b') + 1] : 'out_pins/coded_val'; const SAMPLE = argv.includes('--sample') ? argv[argv.indexOf('--sample') + 1] : 'out_pins/valsample.tsv'; const PINS = ['list', 'browser', 'vantage', 'state']; const VALUES = ['same-explicit', 'same-implied', 'varied-deliberate', 'changed', 'not-stated', 'unclear']; const FIXED = new Set(['same-explicit', 'same-implied']); const pct = (a, b) => (b ? ((100 * a) / b).toFixed(1) : '0.0'); const keys = fs.readFileSync(SAMPLE, 'utf8').trim().split('\n').map((l) => l.split('\t')[0]); const load = (dir, key) => { const f = path.join(dir, key.replace(/\//g, '__') + '.json'); if (!fs.existsSync(f)) return null; const raw = fs.readFileSync(f, 'utf8').replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/, ''); return JSON.parse(raw); }; const pairs = []; const missing = []; for (const k of keys) { const a = load(A, k), b = load(B, k); if (!a || !b) { missing.push(`${k} ${a ? '' : 'A missing '}${b ? '' : 'B missing'}`); continue; } pairs.push({ key: k, a, b }); } // Refuse to report an agreement rate over a shrunken sample without saying so. console.log('design:longitudinal four-pin hand-coding — inter-coder agreement'); console.log(`coder A (excerpt packets): ${A}`); console.log(`coder B (full text, blind): ${B}`); console.log(`\nreliability sample: ${keys.length} papers drawn from the 250`); console.log(`pairs available: ${pairs.length}`); if (missing.length) { console.log(`MISSING (${missing.length}):`); for (const m of missing) console.log(' ' + m); } const kappa = (rows) => { const n = rows.length; if (!n) return NaN; const obs = rows.filter(([x, y]) => x === y).length / n; const ma = new Map(), mb = new Map(); for (const [x, y] of rows) { ma.set(x, (ma.get(x) || 0) + 1); mb.set(y, (mb.get(y) || 0) + 1); } let exp = 0; for (const v of new Set([...ma.keys(), ...mb.keys()])) exp += ((ma.get(v) || 0) / n) * ((mb.get(v) || 0) / n); return exp === 1 ? NaN : (obs - exp) / (1 - exp); }; console.log(`\n${'='.repeat(78)}\nPER-PIN AGREEMENT (n=${pairs.length} papers)\n${'='.repeat(78)}`); console.log(` ${'pin'.padEnd(9)} ${'exact'.padStart(14)} ${'fixed/not'.padStart(14)} ${'kappa(exact)'.padStart(14)} ${'kappa(fixed)'.padStart(14)}`); for (const k of PINS) { const rows = pairs.map((p) => [p.a.pins[k].value, p.b.pins[k].value]); const bin = rows.map(([x, y]) => [FIXED.has(x) ? 'fixed' : 'not', FIXED.has(y) ? 'fixed' : 'not']); const ex = rows.filter(([x, y]) => x === y).length; const bi = bin.filter(([x, y]) => x === y).length; const ke = kappa(rows), kb = kappa(bin); console.log(` ${k.padEnd(9)} ${`${ex}/${rows.length} (${pct(ex, rows.length)}%)`.padStart(14)} ${`${bi}/${rows.length} (${pct(bi, rows.length)}%)`.padStart(14)} ${(Number.isNaN(ke) ? 'n/a' : ke.toFixed(2)).padStart(14)} ${(Number.isNaN(kb) ? 'n/a' : kb.toFixed(2)).padStart(14)}`); } { const rows = pairs.flatMap((p) => PINS.map((k) => [p.a.pins[k].value, p.b.pins[k].value])); const bin = rows.map(([x, y]) => [FIXED.has(x) ? 'fixed' : 'not', FIXED.has(y) ? 'fixed' : 'not']); const ex = rows.filter(([x, y]) => x === y).length; const bi = bin.filter(([x, y]) => x === y).length; console.log(` ${'ALL PINS'.padEnd(9)} ${`${ex}/${rows.length} (${pct(ex, rows.length)}%)`.padStart(14)} ${`${bi}/${rows.length} (${pct(bi, rows.length)}%)`.padStart(14)} ${kappa(rows).toFixed(2).padStart(14)} ${kappa(bin).toFixed(2).padStart(14)}`); } console.log(`\n-- repeatKind`); { const rows = pairs.map((p) => [p.a.repeatKind, p.b.repeatKind]); const ex = rows.filter(([x, y]) => x === y).length; console.log(` exact ${ex}/${rows.length} (${pct(ex, rows.length)}%) kappa ${kappa(rows).toFixed(2)}`); const own = (v) => (v === 'own-repeat-crawl' ? 'own' : 'other'); const bin = rows.map(([x, y]) => [own(x), own(y)]); const bi = bin.filter(([x, y]) => x === y).length; console.log(` own-repeat-crawl vs everything else: ${bi}/${bin.length} (${pct(bi, bin.length)}%) kappa ${kappa(bin).toFixed(2)}`); } console.log(`\n-- "all four fixed", the figure the page publishes`); { const f = (c) => PINS.every((k) => FIXED.has(c.pins[k].value)); const rows = pairs.map((p) => [String(f(p.a)), String(f(p.b))]); const ex = rows.filter(([x, y]) => x === y).length; console.log(` A says all four fixed: ${rows.filter(([x]) => x === 'true').length}/${rows.length}`); console.log(` B says all four fixed: ${rows.filter(([, y]) => y === 'true').length}/${rows.length}`); console.log(` agree: ${ex}/${rows.length} (${pct(ex, rows.length)}%) kappa ${kappa(rows).toFixed(2)}`); } console.log(`\n${'='.repeat(78)}\nDIRECTION OF DISAGREEMENT — does the excerpt coder over- or under-call "fixed"\n${'='.repeat(78)}`); for (const k of PINS) { let aOnly = 0, bOnly = 0, both = 0, neither = 0; for (const p of pairs) { const x = FIXED.has(p.a.pins[k].value), y = FIXED.has(p.b.pins[k].value); if (x && y) both++; else if (x) aOnly++; else if (y) bOnly++; else neither++; } console.log(` ${k.padEnd(9)} both fixed ${String(both).padStart(3)} only A (excerpt) ${String(aOnly).padStart(3)} only B (full text) ${String(bOnly).padStart(3)} neither ${String(neither).padStart(3)}`); } console.log(`\n${'='.repeat(78)}\nEVERY DISAGREEMENT, PRINTED IN FULL\n${'='.repeat(78)}`); let nd = 0; for (const p of pairs) { const diffs = PINS.filter((k) => p.a.pins[k].value !== p.b.pins[k].value); const kindDiff = p.a.repeatKind !== p.b.repeatKind; if (!diffs.length && !kindDiff) continue; nd++; console.log(`\n${p.key}`); if (kindDiff) console.log(` repeatKind A=${p.a.repeatKind} B=${p.b.repeatKind}`); for (const k of diffs) console.log(` ${k.padEnd(9)} A=${p.a.pins[k].value.padEnd(18)} B=${p.b.pins[k].value}`); } console.log(`\npapers with at least one disagreement: ${nd}/${pairs.length} (${pct(nd, pairs.length)}%)`); console.log(`\n${'='.repeat(78)}\nVALUE DISTRIBUTION, EACH CODER (so a dominant value is visible)\n${'='.repeat(78)}`); console.log(` ${'value'.padEnd(20)} ${'A'.padStart(6)} ${'B'.padStart(6)}`); for (const v of VALUES) { const a = pairs.flatMap((p) => PINS.map((k) => p.a.pins[k].value)).filter((x) => x === v).length; const b = pairs.flatMap((p) => PINS.map((k) => p.b.pins[k].value)).filter((x) => x === v).length; console.log(` ${v.padEnd(20)} ${String(a).padStart(6)} ${String(b).padStart(6)}`); }
- build_provenance_pins.py
#!/usr/bin/env python3 """Append the four-pin hand-coding section to provenance:design:longitudinal. Reads the live page text and the prose fragment, and splices in <file> blocks read from the committed scripts and their committed outputs — never retyped, so the published listing is the code that produced the published figures. python3 scripts/build_provenance_pins.py \ --live pages/provenance_design_longitudinal.LIVE.txt \ --prose prov/pins_section.txt \ --out pages/provenance_design_longitudinal.NEW.txt The prose fragment carries {{BLOCK:<relpath>|<lang>|<name>}} placeholders. Each is replaced by the file's real contents. A placeholder naming a missing file, or a file containing a literal file-closing tag (which would end the block early and render the rest of the page as markup), is a hard error. """ import argparse, pathlib, re, sys CLOSER = "</" + "file>" ap = argparse.ArgumentParser() ap.add_argument("--live", required=True) ap.add_argument("--prose", required=True) ap.add_argument("--out", required=True) ap.add_argument("--anchor", default="[[design:longitudinal|← back to the content page]]") a = ap.parse_args() root = pathlib.Path(__file__).resolve().parent.parent live = pathlib.Path(a.live).read_text(encoding="utf8") prose = pathlib.Path(a.prose).read_text(encoding="utf8") def expand(m): rel, lang, name = m.group(1), m.group(2), m.group(3) p = root / rel if not p.exists(): sys.exit(f"FATAL: {rel} does not exist; the section would publish a placeholder.") src = p.read_text(encoding="utf8").rstrip() if CLOSER in src: sys.exit(f"FATAL: {rel} contains a literal file-closing tag; the block would end early.") if not src.strip(): sys.exit(f"FATAL: {rel} is empty.") return f"<file {lang} {name}>\n{src}\n{CLOSER}" prose, n = re.subn(r"\{\{BLOCK:([^|]+)\|([^|]*)\|([^}]+)\}\}", expand, prose) if n == 0: sys.exit("FATAL: no {{BLOCK:...}} placeholders expanded; the section would carry no code.") # §9 said the hand-coding "needs" doing. It is done, and leaving the old wording # would leave the page contradicting its own §18. Patched here rather than by hand # so the edit is regenerable and fails loudly if the target text ever moves. PATCHES = [( " * **Whether a paper held a pin //constant// across waves.** The extraction records what a " "paper stated, once. Every pin figure on the content page is a ceiling and is labelled as one. " "Closing this needs a hand-coded sample of the 250, reading each paper's per-wave methods " "description. That is a day of work and would turn every ceiling into an estimate; it is the " "single most valuable follow-up.", " * **Whether a paper held a pin //constant// across waves.** **Closed on 2026-09-07** — see " "**§18** below. All 250 were hand-coded rather than a sample, and the content page now publishes " "estimates beside the stated-value figures. What is left open by that work is in §18.8, and the " "largest residue is that ''same-implied'' — one configuration described and never contradicted — " "cannot be distinguished from a pin that silently moved." )] for _old, _new in PATCHES: if _old not in live: sys.exit("FATAL: patch target not found in the live page; §9 would keep contradicting §18.") live = live.replace(_old, _new, 1) if a.anchor not in live: sys.exit(f"FATAL: anchor not found in the live page: {a.anchor!r}") head, sep, tail = live.rpartition(a.anchor) out = head + prose.rstrip() + "\n\n" + sep + tail # Structural guard: an in-place generator that silently drops sections is the # failure mode this check exists for. # # A whole-output count is NOT enough. The appended section adds ~11 headings of # its own, so a total-count check has that much slack: drop one heading from the # preserved live content and the total still exceeds the original. Mutation # testing on 2026-09-07 confirmed exactly that — a heading deleted from `head` # gave "27 -> 37" and exit 0. So the counts are compared on the PRESERVED live # text alone (head + sep + tail), which must match the live page exactly, and the # appended section is counted separately and required to be non-empty. preserved = head + sep + tail for tag, pat in (("headings", r"(?m)^=====? "), ("tables", r"(?m)^\^ "), ("file blocks", r"<file ")): b, keep, add = len(re.findall(pat, live)), len(re.findall(pat, preserved)), len(re.findall(pat, prose)) if keep != b: sys.exit(f"FATAL: {tag} in the preserved live text went from {b} to {keep}; the append altered content it must not touch.") print(f"{tag}: live {b} kept {keep} + new {add} = {keep + add}") if add == 0 and tag != "tables": sys.exit(f"FATAL: the appended section contains no {tag}; it is not the section that was written.") # Byte-level check that nothing in the live page was silently rewritten, beyond # the patches applied above, which are themselves verified to have a target. if preserved != live: sys.exit("FATAL: the preserved text is not byte-identical to the (patched) live page.") pathlib.Path(a.out).write_text(out, encoding="utf8") print(f"expanded {n} code blocks; wrote {a.out} ({len(out)} bytes)")
Unedited output of the audit — node scripts/report_pins_audit.mjs, 2026-09-07. Sections 5 and 6 list, paper by paper with its evidence quote, every pin coded as explicitly fixed and every pin coded as having moved; section 8 is the 23-paper recheck.
design:longitudinal — the four pins, HAND-CODED
coded dir: out_pins/coded
extraction run: /workspace/publications_dataset/data/extract/run1
corpus: 5,859 papers, 7 venues (CCS, IMC, NDSS, PETS, USENIX Sec, TheWebConf, IEEE S&P), 2010-2026
schema: all 250 codings parse and use only codebook values.
==============================================================================
1. IS THE MECHANICALLY SELECTED POPULATION REALLY REPEATED CRAWLING
==============================================================================
-- repeatKind, hand-coded
population: 250 papers selected by temporal.snapshots > 1 AND crawled
own-repeat-crawl 124 49.6%
own-repeat-nonweb 44 17.6%
parallel-not-temporal 30 12.0%
reused-waves 21 8.4%
single-crawl 31 12.4%
unclear 0 0.0%
TRUE POSITIVES for "the authors re-crawled the web over time": 124 / 250 = 49.6%
The remaining 126 are the mechanical predicate's false positives for this question.
==============================================================================
2. THE FOUR PINS — CEILING, ESTIMATE, FLOOR
==============================================================================
-- hand-coded pin values
population: 250 papers — all 250 mechanically selected
pin same-explicit same-impliedvaried-deliberate changed not-stated unclear
list 73 (29.2%) 117 (46.8%) 6 (2.4%) 38 (15.2%) 16 (6.4%) 0 (0.0%)
browser 23 (9.2%) 88 (35.2%) 14 (5.6%) 7 (2.8%) 116 (46.4%) 2 (0.8%)
vantage 32 (12.8%) 38 (15.2%) 20 (8.0%) 2 (0.8%) 155 (62.0%) 3 (1.2%)
state 24 (9.6%) 38 (15.2%) 8 (3.2%) 0 (0.0%) 177 (70.8%) 3 (1.2%)
pin floor estimate ceiling
explicit expl+implied stated at all
list 73 (29.2%) 190 (76.0%) 167 (66.8%)
browser 23 (9.2%) 111 (44.4%) 143 (57.2%)
vantage 32 (12.8%) 70 (28.0%) 89 (35.6%)
state 24 (9.6%) 62 (24.8%) 70 (28.0%)
ALL FOUR held fixed
floor (all four same-explicit) 2 / 250 = 0.8%
estimate (all four explicit or implied) 18 / 250 = 7.2%
ceiling (all four merely stated — old figure) 23 / 250 = 9.2%
none of the four stated at all 10 / 250 = 4.0%
at least one pin demonstrably NOT fixed 79 / 250 = 31.6%
-- hand-coded pin values
population: 124 papers — the 124 hand-confirmed own repeat crawls
pin same-explicit same-impliedvaried-deliberate changed not-stated unclear
list 48 (38.7%) 47 (37.9%) 2 (1.6%) 26 (21.0%) 1 (0.8%) 0 (0.0%)
browser 14 (11.3%) 55 (44.4%) 4 (3.2%) 6 (4.8%) 44 (35.5%) 1 (0.8%)
vantage 26 (21.0%) 19 (15.3%) 8 (6.5%) 1 (0.8%) 69 (55.6%) 1 (0.8%)
state 16 (12.9%) 22 (17.7%) 2 (1.6%) 0 (0.0%) 81 (65.3%) 3 (2.4%)
pin floor estimate ceiling
explicit expl+implied stated at all
list 48 (38.7%) 95 (76.6%) 86 (69.4%)
browser 14 (11.3%) 69 (55.6%) 83 (66.9%)
vantage 26 (21.0%) 45 (36.3%) 50 (40.3%)
state 16 (12.9%) 38 (30.6%) 45 (36.3%)
ALL FOUR held fixed
floor (all four same-explicit) 2 / 124 = 1.6%
estimate (all four explicit or implied) 15 / 124 = 12.1%
ceiling (all four merely stated — old figure) 15 / 124 = 12.1%
none of the four stated at all 0 / 124 = 0.0%
at least one pin demonstrably NOT fixed 42 / 124 = 33.9%
==============================================================================
3. HOW FAR THE OLD CEILING OVERSTATED EACH PIN
==============================================================================
-- ceiling minus estimate, per pin
population: 250 papers (the population the published table used)
pin ceiling estimate over by what the difference is
list 167 190 -23 changed 27, not-stated 9, varied-deliberate 3
browser 143 111 32 not-stated 20, varied-deliberate 12, changed 7, unclear 2
vantage 89 70 19 varied-deliberate 19, not-stated 18, unclear 1
state 70 62 8 not-stated 8, varied-deliberate 6, unclear 3
The reverse direction — hand-coding finds the pin fixed where the extraction
recorded nothing (the paper says it in prose the extractor did not tuple):
list 62
browser 9
vantage 19
state 9
==============================================================================
4. DOES THE LONGER PANEL STILL PIN LESS
==============================================================================
-- all four fixed (estimate), by longest stated span
population: 124 hand-confirmed own repeat crawls
span >= 12 months n= 33 all four 1 (3.0%) list 21 browser 12 vantage 11 state 5
span < 12 months n= 77 all four 12 (15.6%) list 64 browser 51 vantage 31 state 28
(14 have no parsable end date and are excluded from this split)
-- all four fixed (estimate), by four-year bucket
population: 124 hand-confirmed own repeat crawls
2010-2011 n= 2 all four 0 (0.0%)
2012-2015 n= 14 all four 0 (0.0%)
2016-2019 n= 32 all four 3 (9.4%)
2020-2023 n= 41 all four 8 (19.5%)
2024-2026 n= 35 all four 4 (11.4%) << provisional
==============================================================================
5. PAPERS THAT SAY IN WORDS THAT A PIN WAS HELD FIXED
==============================================================================
-- list: 73 papers state explicitly that it was unchanged across waves
2026 PETS more-space-less-privacy-measuring-the-effectiveness-of-ip-base "Tranco list: https://tranco-list.eu/list/YXQ4G"
2026 PETS overcoming-language-barriers-multilingual-analysis-of-the-2023 "The set of websites is held constant across both snapshots;"
2025 IEEE-SP sok-decoding-the-enigma-of-encrypted-network-traffic-classifie "load 660 urls of shortlisted domains"
2025 IMC canvassing-the-fingerprinters-characterizing-canvas-fingerprin "we revisited the top 20k and tail 20k sites with our crawler twice more"
2025 IMC exploration-of-the-dynamics-of-buy-and-sale-of-social-media-ac "Out of 58 trading markets, 11 contain selling offers with publicly visible social media account handles, which"
2025 IMC somesite-i-used-to-crawl-awareness-agency-and-efficacy-in-prot "We restrict the list to sites that appear in all of the top 100k lists over this period to avoid having our re"
2025 IMC toward-better-efficiency-vs-fidelity-tradeoffs-in-web-archives "Starting in January 2025, we crawled 10K pages each week for 16 consecutive weeks."
2025 IMC towards-a-non-binary-view-of-ipv6-adoption "the Tranco [31] top 1M list dated 2024-10-16 (ID:"
2025 PETS buy-it-now-track-me-later-attacking-user-privacy-via-wi-fi-ap- "We continued to look these MAC addresses up every day for a month, from mid-January to mid-February 2025."
2025 PETS intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-bann "This list, generated on 07 December 2023 with ID K2NZW, is available at: https:"
2025 PETS measuring-the-accuracy-and-effectiveness-of-pii-removal-servic "we use a script to automatically crawl the data broker coverage list on the official website of the PII remova"
2025 USENIX double-edged-shield-on-the-fingerprintability-of-customized-ad "Available on https://tranco-list.eu/list/24P99/1000"
2025 USENIX evaluating-the-effectiveness-and-robustness-of-visual-similari "For each domain, we collect monthly"
2025 USENIX navigating-cookie-consent-violations-across-the-globe "this initial crawl, we recrawled the union of the sites across all"
2025 USENIX no-way-to-sign-out-unpacking-non-compliance-with-google-plays- "291 apps missing an account deletion link after one month."
2025 USENIX websites-global-privacy-control-compliance-at-scale-and-over-t "crawled our crawl set of 11,708 sites three times"
2025 WWW welcome-to-the-dark-side-analyzing-the-revenue-flows-of-fraud- "we perform a temporal analysis and re-crawl the same list of 2,600 domains with sellers.json files from Sectio"
2024 IMC browsing-without-third-party-cookies-what-do-you-see "We deploy our crawler on the top 10,000 domains of the Tranco [35] list generated on Feb. 18th, 2024."
2024 PETS a-large-scale-study-of-cookie-banner-interaction-tools-and-the "we resort to the widely used quasi-standard Tranco list [30] generated on 29/08/2022"
2024 PETS johnny-still-cant-opt-out-assessing-the-iab-ccpa-compliance-fr "We use the Tranco list dated April 17th, 2023, with ID GZ7NK."
2024 PETS two-steps-forward-and-one-step-back-the-right-to-opt-out-of-sa "the October 29, 2022 Tranco top websites list"
2024 USENIX darkfleece-probing-the-dark-side-of-android-subscription-apps "apps (75 suspected and 25 benign) that were collected in June 2021. We downloaded their versions in August 202"
2023 CCS cookiegraph-understanding-and-detecting-first-party-tracking-c "We successfully conducted the four parallel crawls for 99.31% of the 20K websites."
2023 IMC a-longitudinal-study-of-vulnerable-client-side-resources-and-w "We utilize the single snapshot of the Alexa Top 1M domains of Mar. 2018."
2023 IMC tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-sp "(from 09/07/2021) [49] and probe for prebid.js version. We treat"
2023 PETS blocking-javascript-without-breaking-the-web-an-empirical-inve "In 2021, we crawled 100K webpages and classified the collected JS"
2023 USENIX a-study-of-multi-factor-and-risk-based-authentication-availabi "generated on June 21, 2021, and then chose 50 random sites"
2023 USENIX know-your-cybercriminal-evaluating-attacker-preferences-by-mea "Crawling started in Nov 2020, but as IMPaaS.ru went offline for an infrastructural upgrade from 11 Dec 2020 to"
2023 WWW automatic-discovery-of-emerging-browser-fingerprinting-techniq "we redid the crawling on the same 10K domains in September 2022 (11-month gap) and applied our system to this "
2023 WWW scan-me-if-you-can-understanding-and-detecting-unwanted-vulner "we run each scanner for 10 rounds, against both our WordPress and Joomla web applications."
2022 CCS helping-or-hindering-how-browser-extensions-undermine-security "we employ the Tranco Top 100 domains based on the list of November 1, 2021 (ID: Y3JG)"
2022 CCS understanding-security-issues-in-the-nft-ecosystem "we crawled the same set of assets three times with a three-month interval between two subsequent crawls: in Ju"
2022 IEEE-SP surakav-generating-realistic-traces-for-a-strong-website-finge "We collect data on the Tranco top 1 million [43] sites. The list was generated on 21st January 2021."
2022 IEEE-SP wtagraph-web-tracking-and-advertising-detection-using-graph-ne "We selected the top 10K websites from the Alexa top one million list dated on June 8th, 2020 for data collecti"
2022 IMC html-violations-and-where-to-find-them-a-longitudinal-analysis "To get a reproducible list of popular websites, we rely on the Tranco lists [44]. From these lists, we take th"
2022 IMC respect-the-origin-a-best-case-evaluation-of-connection-coales "Our initial sample set was the 5000 domains with the most requests to the third party, as indicated by the Ref"
2022 PETS a-global-survey-of-android-dual-use-applications-used-in-intim "we crawled Google Play again in all fifteen languages for one day in February 2022 with the queries we obtaine"
2022 PETS atom-ad-network-tomography "We create one interest group with 45 manually curated websites (cf. §3.2.1)."
2022 WWW measuring-the-privacy-vs-compatibility-trade-off-in-preventing "But each of our 8 parallel crawls must visit the same sequence of page URLs to produce comparable results."
2021 NDSS whos-hosting-the-block-party-studying-third-party-blockage-of- "For each crawl, our crawlers visited the start pages from the fixed list and followed every same-site link."
2021 PETS deja-vu-abusing-browser-cache-headers-to-identify-and-track-on "websites from the Tranco list [33] on the 2nd and the"
2021 PETS domain-name-encryption-is-not-enough-privacy-leakage-via-ip-ba "The list was created on March 3rd 2020, and is available at https://tranco-list.eu/list/J2KY."
2021 WWW tls-1-3-in-practice-how-tls-1-3-contributes-to-the-internet "We utilize a single snapshot of the Alexa 1M websites generated in April 2018 during our observation period"
2021 WWW towards-realistic-and-reproducibleweb-crawl-measurements "Tranco list of top sites [18] (snapshot 77PX)"
2020 IMC analyzing-third-party-service-dependencies-in-modern-web-servi "we use a snapshot of the rankings from December 2016 and collect data for these websites in 2016 and 2020 from"
2020 IMC out-of-sight-not-out-of-mind-a-user-view-on-the-criticality-of "2020, with the same list of countries and websites."
2020 NDSS complex-security-policy-a-longitudinal-analysis-of-deployed-co "Given that our list of Web sites remains stable, we can attribute the increased CSP adoption to Web developers"
2020 PETS a-comparative-measurement-study-of-web-tracking-on-mobile-and- "in both experiments, we collected data from all the 116,000 websites, but the data for each website in Experim"
2020 WWW beyond-the-front-page-measuring-third-party-dynamics-in-the-fi "We used the list generated on 03/26/2019 (ID: W9L9)."
2020 WWW facebook-ads-monitor-an-independent-auditing-system-for-politi "we repeated the task of querying for an empty string periodically during the period of Aug 2018 to December 20"
2020 WWW the-representativeness-of-automated-web-crawls-as-a-surrogate- "All of the crawls described in this section used the AlexaTop1k [4] as their seed list and were performed betw"
2019 IMC no-more-chasing-waterfalls-a-measurement-study-of-the-header-b "we crawled these 5k websites every day for a period of 34 days in Feb'19, collecting metadata about the HB auc"
2019 IMC visiblev8-in-browser-monitoring-of-javascript-in-the-wild "to visit the Alexa top 50k web domains"
2019 WWW before-and-after-gdpr-the-changes-in-third-party-presence-at-p "from the 1,363 websites for each of the 21 harvests, capturing all"
2019 WWW outguard-detecting-in-browser-covert-cryptocurrency-mining-in- "We continuously monitored all cryptojacking websites from our first deployment, which observed 5,873 websites "
2019 WWW revisiting-user-mobility-and-social-relationships-in-lbsns-a-h "we keep (a) Node (user) degree (b) Number of check-ins per user only users appearing in both social network sn"
2019 WWW who-watches-the-watchmen-exploring-complaints-on-the-web "Each week, we exclude URLs that have already been deleted."
2018 USENIX plug-and-prey-measuring-the-commoditization-of-cybercrime-via- "We use longitudinal data from eight online anonymous marketplaces over six years, from the original Silk Road "
2018 WWW uncovering-http-header-inconsistencies-and-the-impact-on-deskt "using the top 70,000 website as ranked by Alexa [1] as of"
2017 CCS poster-probing-tor-hidden-service-with-dockers "The number of Onion addresses in this experiment was 16,683, and the addresses were crawled twice a day at 12-"
2017 IEEE-SP xhound-quantifying-the-fingerprintability-of-browser-extension "we waited for four months since the original collection of Chrome extensions and examined the updated versions"
2017 PETS cross-device-tracking-measurement-and-disclosures "browse the same 100 popular websites - the top 20 sites for Games, Sports, News, Shopping, and Reference accor"
2017 USENIX characterizing-the-nature-and-dynamics-of-tor-exit-blocking "We crawled the Alexa Top 500 web pages from a control host and a subset of Tor exit relays."
2017 USENIX measuring-the-insecurity-of-mobile-deep-links-of-android "In May 2017, we scan these domains again."
2016 CCS cspautogen-black-box-enforcement-of-content-security-policy-up "we use these templates to match the same website captured on 02/01/2016, 03/01/2016 and 04/01/2016"
2016 IEEE-SP cloak-of-visibility-detecting-when-machines-browse-a-different "we crawl each URL three times per browsing profile which we denote C1 , C2 , C3 for clarity."
2016 USENIX the-ever-changing-labyrinth-a-large-scale-analysis-of-wildcard "we monitored the structural changes of SSP for 25 days within Jan 2016."
2016 USENIX tracing-information-flows-between-ad-exchanges-using-retargete "During each round of crawling, the personas visit their associated e-commerce sites, then visit the 2,250 publ"
2015 IMC who-is-com-learning-to-parse-whois-records "Some domains in the February 2015 zone file snapshot were expired by the time we crawled them"
2015 NDSS i-do-not-know-what-you-visited-last-summer-protecting-users-fr "This process is repeated twice: once starting with a clean browser and once more after priming the client-side"
2015 WWW early-detection-of-spam-mobile-apps "For instance, all apps in Crawl 1 were checked again during Crawl 2."
2012 IEEE-SP lastor-a-low-latency-as-aware-tor-client "To quantify the extent of this latency overhead, we measured
the latency of visiting the top 200 websites [17]"
2012 WWW understanding-and-combating-link-farming-in-the-twitter-social "We re-crawled the profile page of each user in the above dataset in February 2011"
-- browser: 23 papers state explicitly that it was unchanged across waves
2026 NDSS eviction-notice-reviving-and-advancing-page-cache-attacks "We target Firefox 133.0, which uses the shared library"
2025 IEEE-SP sok-decoding-the-enigma-of-encrypted-network-traffic-classifie "we chose two opensource web browsers: Firefox [22] and Chromium [11]."
2025 USENIX double-edged-shield-on-the-fingerprintability-of-customized-ad "we use Chrome v.113"
2024 NDSS flow-correlation-attacks-on-tor-onion-service-sessions-with-sl "Clients also run on an isolated Docker container that executes a 0.4.7.10 Tor process and issues requests to T"
2024 PETS a-large-scale-study-of-cookie-banner-interaction-tools-and-the "Each profile is based on OpenWPM [12] (v0.20.0), uses the Firefox browser (v100.0 with the default user agent1"
2024 PETS johnny-still-cant-opt-out-assessing-the-iab-ccpa-compliance-fr "Each of our four crawls used the same baseline configuration."
2024 PETS two-steps-forward-and-one-step-back-the-right-to-opt-out-of-sa "All data collection was conducted using the Chrome web browser."
2023 CCS cookiegraph-understanding-and-detecting-first-party-tracking-c "We use OpenWPM (v0.17.0) and Firefox (v102) [52] to crawl a sample of 20K out of the top-million websites."
2023 IMC tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-sp "personas. Specifically, we use identical hardware/software, collect"
2023 WWW automatic-discovery-of-emerging-browser-fingerprinting-techniq "applied our system to this data set by following the same procedure."
2022 IEEE-SP the-state-of-the-samesite-studying-the-usage-effectiveness-and "The exact user-agent strings used by our crawler are detailed below. • Chrome Desktop: Mozilla/5.0 (Windows NT"
2021 IMC polls-clickbait-and-commemorative-2-bills-problematic-politica "we ran 312 daily crawls, on 4 machines, using Chromium 88.0.4298.0, on a Debian 9 Docker image."
2021 PETS deja-vu-abusing-browser-cache-headers-to-identify-and-track-on "3rd of June, using Headless Chrome instrumented with"
2021 PETS domain-name-encryption-is-not-enough-privacy-leakage-via-ip-ba "we repeatedly crawled the 220K websites from our test list curated in §5.1, using the Chrome browser (desktop "
2020 PETS a-comparative-measurement-study-of-web-tracking-on-mobile-and- "The version of both Firefox for Android and desktop Firefox is 53.0 without the tracking protection function."
2020 WWW beyond-the-front-page-measuring-third-party-dynamics-in-the-fi "For each visit, we use the same user agent (Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52"
2020 WWW the-representativeness-of-automated-web-crawls-as-a-surrogate- "44 crawls using Firefox 68 were performed, with a variable cadence, over 54 days, using a single OSX machine l"
2019 IMC visiblev8-in-browser-monitoring-of-javascript-in-the-wild "Chrome 64, which was used for primary data collection"
2019 PETS p1-fp-extraction-classification-and-prediction-of-website-fing "Tor version 0.4.0.8 to collect our datasets"
2018 CCS rendered-insecure-gpu-side-channel-attacks-are-practical "the Chrome browser version is 63.0.3239.84"
2018 IMC needle-in-a-haystack-tracking-down-elite-phishing-domains-in-t "We set "User-Agent" for iPhone 6 and Chrome 65 to obtain the mobile and web pages respectively."
2016 IMC browser-feature-usage-on-the-modern-web "browser (version 46.0.1) with a custom browser extension"
2011 CCS surf-detecting-and-measuring-search-poisoning "we implemented SURF on top of an instrumented version of Internet Explorer 8."
-- vantage: 32 papers state explicitly that it was unchanged across waves
2026 PETS more-space-less-privacy-measuring-the-effectiveness-of-ip-base "crawler machine used a distinct, static public IP address throughout"
2026 PETS privacy-vs-profit-the-impact-of-googles-manifest-version-3-mv3 "held geography constant so that any differences reflect"
2025 IEEE-SP sok-decoding-the-enigma-of-encrypted-network-traffic-classifie "All data was collected using a university network, providing a realistic environment from January 13, 2024, to"
2025 IMC canvassing-the-fingerprinters-characterizing-canvas-fingerprin "We performed the crawls in May 2025 using a UCSD IP address."
2025 PETS intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-bann "executed from a server located in the EU, and each crawl within"
2025 PETS more-and-scammier-ads-the-perils-of-youtubes-ad-privacy-settin "we run five such virtual machines simultaneously with local IP addresses in Sydney (Australia), Toronto (Canad"
2025 USENIX navigating-cookie-consent-violations-across-the-globe "Each measurement iteration involved crawling all 8 regions"
2025 USENIX websites-global-privacy-control-compliance-at-scale-and-over-t "16 GB RAM set to a Los Angeles IP address using Mullvad"
2024 PETS a-bilingual-longitudinal-analysis-of-privacy-policies-measurin "For all crawls, we used a server located in California to simulate the geolocation of Californian residents in"
2024 PETS automatic-generation-of-web-censorship-probe-lists "we repeat our measurements 50 times in each vantage point (apart from US-West 1 and US-West 2, where measureme"
2024 PETS johnny-still-cant-opt-out-assessing-the-iab-ccpa-compliance-fr "All crawls were conducted using virtual machines from Amazon Web Services with IP addresses in California."
2024 PETS the-devil-is-in-the-details-detection-measurement-and-lawfulne "Location EU EU"
2024 PETS two-steps-forward-and-one-step-back-the-right-to-opt-out-of-sa "All of the data collection was done on AWS EC2 c6i.8xlarge instances that were located in a California data ce"
2024 USENIX fledging-will-continue-until-privacy-improves-empirical-analys "While all the measurements were performed through US-based IP addresses"
2023 CCS cookiegraph-understanding-and-detecting-first-party-tracking-c "We run all crawls in the US to minimize the impact of the EU GDPR and do not interact with cookie banners."
2023 IMC tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-sp "bids at the same time (simultaneously), from the same location, and"
2023 WWW automatic-discovery-of-emerging-browser-fingerprinting-techniq "applied our system to this data set by following the same procedure."
2022 IMC respect-the-origin-a-best-case-evaluation-of-connection-coales "We used the same machines throughout our evaluations for consistency, and saw no observable change in datacent"
2022 PETS a-global-survey-of-android-dual-use-applications-used-in-intim "The EC2 servers we used for our crawling in 2020 and 2022 were all located in the US."
2022 WWW measuring-the-privacy-vs-compatibility-trade-off-in-preventing "All crawls were performed in parallel and simultaneously from a single network vantage point."
2021 PETS deja-vu-abusing-browser-cache-headers-to-identify-and-track-on "For our evaluation, we performed crawls from a single address in our research lab."
2020 IMC analyzing-third-party-service-dependencies-in-modern-web-servi "We conduct our measurements from a single vantage point on the US East Coast."
2020 IMC out-of-sight-not-out-of-mind-a-user-view-on-the-criticality-of "2020, with the same list of countries and websites."
2020 WWW the-representativeness-of-automated-web-crawls-as-a-surrogate- "44 crawls using Firefox 68 were performed, with a variable cadence, over 54 days, using a single OSX machine l"
2019 WWW measuring-political-personalization-of-google-news-search "we use a static DNS entry for Google News to ensure that our search queries are routed to the same datacenter"
2018 IEEE-SP tracking-certificate-misissuance-in-the-wild "from the University of Illinois Urbana- Champaign campus between September 1-20, 2017"
2018 WWW auditing-the-personalization-and-composition-of-politically-re "we utilized built in features to restrict the visibility of our recruitment ads to participants within the US"
2017 NDSS dial-one-for-scam-a-large-scale-analysis-of-technical-support- "We deployed the ROBOVIC Crawler on three different sites (our campus, Amazon's Elastic Compute Cloud [25], and"
2017 NDSS thou-shalt-not-depend-on-me-analysing-the-use-of-outdated-java "We conducted the two crawls in May 2016 from IP addresses in a /24 range in the US."
2016 USENIX investigating-commercial-pay-per-install-and-the-distribution- "our pipeline runs exclusively from United States IP addresses."
2016 USENIX tracing-information-flows-between-ad-exchanges-using-retargete "All crawls were done from Northeastern University's IP addresses in Boston."
2012 IEEE-SP lastor-a-low-latency-as-aware-tor-client "To study this, we measure path latencies on
the Tor network when visiting the top 200 websites from 50
PlanetL"
-- state: 24 papers state explicitly that it was unchanged across waves
2026 NDSS characterizing-the-implementation-of-censorship-policies-in-ch "For the majority of services, we keep the browser session
persistent across a given test run."
2026 PETS more-space-less-privacy-measuring-the-effectiveness-of-ip-base "interference between successive website visits, ensuring consistency across crawl batches."
2025 IEEE-SP sok-decoding-the-enigma-of-encrypted-network-traffic-classifie "the browser is quit to clean up session data and free up system resources (Line 11)."
2025 NDSS evocrawl-exploring-web-application-code-and-state-using-evolut "reset all tested web application instances before each crawling
session. This ensures that all scanners commen"
2025 PETS unmasking-the-shadows-a-cross-country-study-of-online-tracking "in stateless mode, where no session data or cookies were preserved between page loads"
2025 WWW welcome-to-the-dark-side-analyzing-the-revenue-flows-of-fraud- "only issue a single HTTP(S) request to fetch either the ads.txt"
2024 PETS a-large-scale-study-of-cookie-banner-interaction-tools-and-the "In our experiment, we perform a stateful crawl for each visited site, meaning we keep the browser's state when"
2024 PETS the-devil-is-in-the-details-detection-measurement-and-lawfulne "Crawl state Stateless Stateless"
2023 IMC tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-sp "same browser profiles that are logged into the Amazon account and"
2023 PETS blocking-javascript-without-breaking-the-web-an-empirical-inve "and stateless crawls."
2023 PETS raven-stateless-rapid-ip-address-variation-for-enterprise-netw "The browser cache was cleaned"
2023 USENIX a-study-of-multi-factor-and-risk-based-authentication-availabi "performed with an empty browser cache and cleared cookies."
2022 IMC respect-the-origin-a-best-case-evaluation-of-connection-coales "Client caches are intentionally deleted between measurements in all of our active measurements to prevent bias"
2022 USENIX a-large-scale-investigation-into-geodifferences-in-mobile-apps "We also clear the Play Store cache on the phones between downloads."
2022 WWW measuring-the-privacy-vs-compatibility-trade-off-in-preventing "The crawlers maintained independent, persistent user profiles for each policy instance to maintain state acros"
2020 PETS a-comparative-measurement-study-of-web-tracking-on-mobile-and- "Each data collec- tion experiment includes two stateless2 measurements on the mobile and desktop environments,"
2020 WWW a-first-look-at-commercial-5g-performance-on-smartphones "clear the browser's cache before each loading"
2019 IMC an-empirical-study-of-the-cost-of-dns-over-https "Caches of both Firefox and the page load times, suggesting that it is possible to obtain the improved DNS stub"
2019 IMC no-more-chasing-waterfalls-a-measurement-study-of-the-header-b "We initiated a clean slate instance before visiting each website, in order to keep the crawling process statel"
2019 PETS p1-fp-extraction-classification-and-prediction-of-website-fing "reset the Tor process after each website visit, but otherwise used the default options"
2017 NDSS thou-shalt-not-depend-on-me-analysing-the-use-of-outdated-java "our crawler remains on each page for a fixed delay of 60 seconds before clearing its entire state, restarting,"
2017 PETS cross-device-tracking-measurement-and-disclosures "much like a browser would with a consumer that has reopened her browser without clearing browsing data"
2016 IEEE-SP cloak-of-visibility-detecting-when-machines-browse-a-different "we tear down our environment and clear all cookies between fetches"
2015 IMC location-location-location-the-impact-of-geolocation-on-web-se "all of our treatments were configured and behaved identically."
==============================================================================
6. PAPERS WHERE A PIN DEMONSTRABLY MOVED BETWEEN WAVES
==============================================================================
-- list: 38 papers state the value changed or was refreshed between waves
2026 PETS quicstep-evaluating-connection-migration-based-quic-censorship "daily Tranco top 1M websites"
2025 NDSS the-unusual-suspects-studying-reasons-for-lacking-updates-in-w "as described for Sampling Round 1 (May 9-19, 2023)"
2025 PETS beyond-the-request-harnessing-http-response-headers-for-cross- "using T.EX and visiting the landing pages with Chrome to explore the top 10K websites from the Tranco ranking "
2025 PETS the-effect-of-platform-policies-on-app-privacy-compliance-a-st "we re-queued all of the previous apps for testing and added 302 additional apps that we subsequently identifie"
2025 PETS who-s-watching-you-zoom-investigating-privacy-of-third-party-z "between December 2023 and December 2024, 667 new apps were added, 212 apps were removed, and 49 apps were rena"
2025 WWW detecting-and-understanding-the-promotion-of-illicit-goods-and "The newly captured PIPs will be further fed into the keyword generator so as to extract new keywords which in "
2024 PETS a-bilingual-longitudinal-analysis-of-privacy-policies-measurin "crawls in February 2021 we visited the homepages of the top 10K Tranco domains using the list from January 31,"
2024 USENIX unpacking-privacy-labels-a-measurement-and-developer-perspecti "we observed a total of 2.72M unique apps and 2.17M com- mon apps."
2023 USENIX policycomp-counterpart-comparison-of-privacy-policies-uncovers "we compared the results from the dataset described in Sec. 5.1 (collected in August 2021) with those from anot"
2023 USENIX timeless-timing-attacks-and-preload-defenses-in-tors-dns-cache "the weekend before each of the first four weeks of data collection (see Section 3), we downloaded fresh popula"
2022 WWW leveraging-googles-publisher-specific-ids-to-detect-website-ad "We perform our analysis on websites which have been crawled in all snapshots (i.e., their intersection) and co"
2021 IEEE-SP crawlphish-large-scale-analysis-of-client-side-cloaking-techni "CrawlPhish collected the source code of 28,973 phishing websites from June to December 2018 and 100,000 websit"
2021 PETS privacy-preference-signals-past-present-and-future "Netograph continuously ingests a live feed of social media posts, extracts all URLs, and visits them from craw"
2021 USENIX can-i-take-your-subdomain-exploring-same-site-attacks-in-the-m "Concerning discontinued services, we focused on the 3 largest providers (WordPress, Shopify and Tumblr), repre"
2021 WWW privacy-policies-over-time-curation-and-analysis-of-a-million- "We obtained 541,616 websites by combining all domains that appear in the top 100K of these 22 Alexa lists (two"
2021 WWW where-are-you-taking-me-understanding-abusive-traffic-distribu "We generate a new set of target URLs for every run of an experiment. The only exception is the URL shortening "
2020 IMC demystifying-the-messaging-platforms-ecosystem-through-the-len "We commence the metadata collection for each group from the date when we discovered it and repeat it every day"
2020 WWW read-between-the-lines-an-empirical-measurement-of-sensitive-a "Note that even for the same store, the different snapshots do not necessarily contain the same set of skills."
2019 IEEE-SP empoweb-empowering-web-applications-with-browser-extensions "In the middle of May 2018, we did another crawl and analysis. The results presented here are for this second d"
2019 IEEE-SP phishfarm-a-scalable-framework-for-measuring-the-effectiveness "we solely used nondeceptive paths and hostnames (i.e. with randomly-chosen English words) in order to remove U"
2019 IMC a-longitudinal-analysis-of-the-ads-txt-standard "before each crawl, we fetched the latest Alexa Top-100K list, computed the union of it and our existing list o"
2019 IMC an-empirical-study-of-the-cost-of-dns-over-https "The Alexa list was retrieved on 15 September 2018."
2019 NDSS we-value-your-privacy-now-take-some-cookies-measuring-the-gdpr "To extend the scope of our study, we retrieved updated top lists once per month."
2019 WWW a-first-look-at-deep-learning-apps-on-smartphones "We have crawled two datasets at different moments, June 2018 and September 2018, which are three months apart."
2019 WWW mobile-app-risk-ranking-via-exclusive-sparse-coding "There are no overlaps for the above datasets, and the apps are from a wide range of functionalities, including"
2019 WWW understanding-the-evolution-of-mobile-app-ecosystems-a-longitu "we repeated the same process as described above to crawl Google Play apps, except that we take the previous 1."
2018 IEEE-SP the-spyware-used-in-intimate-partner-violence "The set of queries retrieved every day changed over time even though the seed queries were the same."
2018 IMC needle-in-a-haystack-tracking-down-elite-phishing-domains-in-t "From April 09 to April 29 in 2018, we collect three additional snapshots only for the detected squatting phish"
2018 WWW betrayed-by-your-dashboard-discovering-malicious-campaigns-via "We use our system to crawl 145K malicious URLs provided by VirusTotal on a daily basis for a period of two wee"
2017 NDSS dial-one-for-scam-a-large-scale-analysis-of-technical-support- "Note that ROBOVIC was originally relying just on domain parking in order to find technical support scams and w"
2017 WWW extended-tracking-powers-measuring-the-privacy-diffusion-enabl "we collect a second set of top 10K browser extensions (Enew ) four months after our first one (Eold )"
2016 USENIX internet-jones-and-the-raiders-of-the-lost-trackers-an-archaeo "For 2010-2016, we use Wayback Machine archives of Alexa's top million sites list (a csv file)."
2015 CCS caronte-detecting-location-leaks-for-deanonymizing-tor-hidden- "as C ARONTE explores the hidden services, it adds any new onion URLs found in their content to the list, so th"
2015 USENIX measuring-the-longitudinal-evolution-of-the-online-anonymous-m "We started collecting data from Evolution in July, so it is possible that we miss quite a bit in the early par"
2014 CCS a-nearly-four-year-longitudinal-study-of-search-engine-poisoni "The size and composition of the query set varies 4.3 over time, but, on average, consists of 64 queries."
2014 CCS poster-proactive-blacklist-update-for-anti-phishing "We hourly downloaded the brand-new blacklist containing phishing URLs belonging to PayPal target on 1449 6th J"
2011 USENIX deseo-combating-search-result-poisoning "We collect three sampled sets of URLs from Bing. These"
2010 IEEE-SP a-practical-attack-to-de-anonymize-social-network-users "entire network, we only downloaded the group directory and"
-- browser: 7 papers state the value changed or was refreshed between waves
2025 PETS who-s-watching-you-zoom-investigating-privacy-of-third-party-z "We created another crawler and parser to handle a major change in the app category listing after May 2024."
2024 PETS the-devil-is-in-the-details-detection-measurement-and-lawfulne "Browser FF86.0.1 FF95"
2022 IMC respect-the-origin-a-best-case-evaluation-of-connection-coales "Results shown are for Firefox (v91) for later comparison, since only Firefox has client-side support for ORIGI"
2019 IEEE-SP phishfarm-a-scalable-framework-for-measuring-the-effectiveness "We freshly installed the latest stable version of each browser at the time of each test and kept default secur"
2019 NDSS we-value-your-privacy-now-take-some-cookies-measuring-the-gdpr "using the latest version of Firefox (version 57 onward)"
2018 WWW uncovering-http-header-inconsistencies-and-the-impact-on-deskt "there are notable differences in the HSTS header and the HTTPS Redirection configuration where the newer versi"
2014 CCS a-nearly-four-year-longitudinal-study-of-search-engine-poisoni "we use an automated (command-line) script, increasing the level of automation in collecting search results."
-- vantage: 2 papers state the value changed or was refreshed between waves
2017 USENIX characterizing-the-nature-and-dynamics-of-tor-exit-blocking "several of the selected exit relays intermittently went offline, with a total of 0, 12, 19, and 28 offline dur"
2015 CCS seeing-through-network-protocol-obfuscation "In addition, we employed two other campus network traces, which we call CloudDataset and WifiDataset. These we"
-- state: 0 papers state the value changed or was refreshed between waves
-- list: 6 papers vary it across waves BY DESIGN
2011 CCS surf-detecting-and-measuring-search-poisoning
2017 NDSS thou-shalt-not-depend-on-me-analysing-the-use-of-outdated-java
2017 USENIX how-the-web-tangled-itself-uncovering-the-history-of-client-si
2022 USENIX web-cache-deception-escalates
2024 PETS website-data-transparency-in-the-browser
2025 PETS more-and-scammier-ads-the-perils-of-youtubes-ad-privacy-settin
-- browser: 14 papers vary it across waves BY DESIGN
2016 IEEE-SP cloak-of-visibility-detecting-when-machines-browse-a-different
2019 NDSS time-does-not-heal-all-wounds-a-longitudinal-analysis-of-secur
2020 USENIX phishtime-continuous-longitudinal-measurement-of-the-effective
2020 WWW apophanies-or-epiphanies-how-crawlers-impact-our-understanding
2022 IMC muzeel-assessing-the-impact-of-javascript-dead-code-eliminatio
2022 USENIX leaky-forms-a-study-of-email-and-password-exfiltration-before-
2023 USENIX a-study-of-multi-factor-and-risk-based-authentication-availabi
2023 USENIX glowing-in-the-dark-uncovering-ipv6-address-discovery-and-scan
2024 IEEE-SP targeted-and-troublesome-tracking-and-advertising-on-childrens
2025 NDSS cross-origin-web-attacks-via-http-2-server-push-and-signed-htt
2025 PETS intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-bann
2026 PETS clicking-into-exposure-uncovering-privacy-risks-of-google-clic
2026 PETS privacy-vs-profit-the-impact-of-googles-manifest-version-3-mv3
2026 USENIX bridges-to-self-silent-web-to-app-tracking-on-mobile-via-local
-- vantage: 20 papers vary it across waves BY DESIGN
2014 CCS a-nearly-four-year-longitudinal-study-of-search-engine-poisoni
2015 NDSS parking-sensors-analyzing-and-detecting-parked-domains
2016 IEEE-SP cloak-of-visibility-detecting-when-machines-browse-a-different
2016 IMC recommended-for-you-a-first-look-at-content-recommendation-net
2020 WWW beyond-the-front-page-measuring-third-party-dynamics-in-the-fi
2021 IMC polls-clickbait-and-commemorative-2-bills-problematic-politica
2021 USENIX scalable-detection-of-promotional-website-defacements-in-black
2022 IMC muzeel-assessing-the-impact-of-javascript-dead-code-eliminatio
2022 PETS from-onion-not-found-to-guard-discovery
2022 USENIX a-large-scale-investigation-into-geodifferences-in-mobile-apps
2022 USENIX leaky-forms-a-study-of-email-and-password-exfiltration-before-
2023 USENIX a-study-of-multi-factor-and-risk-based-authentication-availabi
2023 USENIX know-your-cybercriminal-evaluating-attacker-preferences-by-mea
2023 USENIX timeless-timing-attacks-and-preload-defenses-in-tors-dns-cache
2024 IEEE-SP targeted-and-troublesome-tracking-and-advertising-on-childrens
2024 PETS a-large-scale-study-of-cookie-banner-interaction-tools-and-the
2024 PETS website-data-transparency-in-the-browser
2025 PETS unmasking-the-shadows-a-cross-country-study-of-online-tracking
2026 NDSS characterizing-the-implementation-of-censorship-policies-in-ch
2026 PETS clicking-into-exposure-uncovering-privacy-risks-of-google-clic
-- state: 8 papers vary it across waves BY DESIGN
2015 NDSS i-do-not-know-what-you-visited-last-summer-protecting-users-fr
2016 USENIX investigating-commercial-pay-per-install-and-the-distribution-
2021 WWW cookie-swap-party-abusing-first-party-cookies-for-web-tracking
2022 USENIX web-cache-deception-escalates
2023 CCS cookiegraph-understanding-and-detecting-first-party-tracking-c
2024 IMC browsing-without-third-party-cookies-what-do-you-see
2025 PETS intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-bann
2026 PETS clicking-into-exposure-uncovering-privacy-risks-of-google-clic
==============================================================================
7. CODER CONFIDENCE AND THE ALL-FOUR EXEMPLAR LIST
==============================================================================
-- self-reported confidence
population: 250 papers
high 116 46.4%
medium 131 52.4%
low 3 1.2%
-- the 18 papers estimated to hold all four fixed (any repeatKind), most recent first
year | venue | slug | repeatKind | waves | explicit pins
2026 | PETS | more-space-less-privacy-measuring-the-effectiveness-of-ip-based-website-fingerpr | own-repeat-crawl | 5 | list,vantage,state
2025 | IEEE-SP | sok-decoding-the-enigma-of-encrypted-network-traffic-classifiers | own-repeat-crawl | 100 | list,browser,vantage,state
2025 | USENIX | navigating-cookie-consent-violations-across-the-globe | own-repeat-crawl | 10 | list,vantage
2024 | USENIX | fledging-will-continue-until-privacy-improves-empirical-analysis-of-googles-priv | own-repeat-crawl | 3 | vantage
2023 | IMC | tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-speaker-ecosystem | own-repeat-crawl | 31 | list,browser,vantage,state
2023 | PETS | blocking-javascript-without-breaking-the-web-an-empirical-investigation | own-repeat-crawl | 2 | list,state
2023 | PETS | raven-stateless-rapid-ip-address-variation-for-enterprise-networks | own-repeat-nonweb | ? | state
2022 | WWW | measuring-the-privacy-vs-compatibility-trade-off-in-preventing-third-party-state | own-repeat-crawl | 2 | list,vantage,state
2021 | WWW | towards-realistic-and-reproducibleweb-crawl-measurements | own-repeat-crawl | 3 | list
2020 | NDSS | encrypted-dns-privacy-a-traffic-analysis-perspective | own-repeat-crawl | 5 | -
2020 | PETS | a-comparative-measurement-study-of-web-tracking-on-mobile-and-desktop-environmen | own-repeat-crawl | 2 | list,browser,state
2020 | PETS | missed-by-filter-lists-detecting-unknown-third-party-trackers-with-invisible-pix | parallel-not-temporal | ? | -
2020 | PETS | no-boundaries-data-exfiltration-by-third-parties-embedded-on-web-pages | single-crawl | ? | -
2020 | WWW | a-first-look-at-commercial-5g-performance-on-smartphones | own-repeat-crawl | ? | state
2020 | WWW | the-representativeness-of-automated-web-crawls-as-a-surrogate-for-human-browsing | own-repeat-crawl | 44 | list,browser,vantage
2019 | WWW | before-and-after-gdpr-the-changes-in-third-party-presence-at-public-and-private | own-repeat-crawl | 21 | list
2019 | WWW | measuring-political-personalization-of-google-news-search | own-repeat-crawl | 7 | vantage
2018 | WWW | auditing-the-personalization-and-composition-of-politically-related-search-engin | own-repeat-crawl | 6 | vantage
of those, hand-confirmed own repeat crawls: 15
2026 | PETS | more-space-less-privacy-measuring-the-effectiveness-of-ip-based-website-fingerpr
2025 | IEEE-SP | sok-decoding-the-enigma-of-encrypted-network-traffic-classifiers
2025 | USENIX | navigating-cookie-consent-violations-across-the-globe
2024 | USENIX | fledging-will-continue-until-privacy-improves-empirical-analysis-of-googles-priv
2023 | IMC | tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-speaker-ecosystem
2023 | PETS | blocking-javascript-without-breaking-the-web-an-empirical-investigation
2022 | WWW | measuring-the-privacy-vs-compatibility-trade-off-in-preventing-third-party-state
2021 | WWW | towards-realistic-and-reproducibleweb-crawl-measurements
2020 | NDSS | encrypted-dns-privacy-a-traffic-analysis-perspective
2020 | PETS | a-comparative-measurement-study-of-web-tracking-on-mobile-and-desktop-environmen
2020 | WWW | a-first-look-at-commercial-5g-performance-on-smartphones
2020 | WWW | the-representativeness-of-automated-web-crawls-as-a-surrogate-for-human-browsing
2019 | WWW | before-and-after-gdpr-the-changes-in-third-party-presence-at-public-and-private
2019 | WWW | measuring-political-personalization-of-google-news-search
2018 | WWW | auditing-the-personalization-and-composition-of-politically-related-search-engin
==============================================================================
8. WHAT HAPPENED TO THE 23 PAPERS THE OLD CEILING TABLE LISTED
==============================================================================
23 papers stated all four. Hand-coding each:
year venue slug kind list/browser/vantage/state
2026 PETS clicking-into-exposure-uncovering-privacy-r parallel-not-temporal same-implied / varied-deliberate / varied-deliberate / varied-deliberate
2026 PETS more-space-less-privacy-measuring-the-effec own-repeat-crawl same-explicit / same-implied / same-explicit / same-explicit
2026 PETS privacy-vs-profit-the-impact-of-googles-man own-repeat-crawl same-implied / varied-deliberate / same-explicit / same-implied
2025 PETS beyond-the-request-harnessing-http-response own-repeat-crawl changed / same-implied / unclear / unclear
2025 PETS intractable-cookie-crumbs-unveiling-the-nex own-repeat-crawl same-explicit / varied-deliberate / same-explicit / varied-deliberate
2025 PETS more-and-scammier-ads-the-perils-of-youtube own-repeat-crawl varied-deliberate / same-implied / same-explicit / same-implied
2025 USENIX navigating-cookie-consent-violations-across own-repeat-crawl same-explicit / same-implied / same-explicit / same-implied
2025 WWW before-after-the-effect-of-eus-2022-code-of own-repeat-crawl same-implied / not-stated / not-stated / not-stated
2024 IEEE-SP targeted-and-troublesome-tracking-and-adver parallel-not-temporal same-implied / varied-deliberate / varied-deliberate / same-implied
2024 PETS a-large-scale-study-of-cookie-banner-intera parallel-not-temporal same-explicit / same-explicit / varied-deliberate / same-explicit
2024 PETS the-devil-is-in-the-details-detection-measu own-repeat-crawl same-implied / changed / same-explicit / same-explicit
2023 USENIX a-study-of-multi-factor-and-risk-based-auth own-repeat-crawl same-explicit / varied-deliberate / varied-deliberate / same-explicit
2022 IMC respect-the-origin-a-best-case-evaluation-o own-repeat-crawl same-explicit / changed / same-explicit / same-explicit
2022 USENIX a-large-scale-investigation-into-geodiffere parallel-not-temporal same-implied / same-implied / varied-deliberate / same-explicit
2022 WWW reproducibility-and-replicability-of-web-me own-repeat-crawl same-implied / not-stated / not-stated / unclear
2020 NDSS encrypted-dns-privacy-a-traffic-analysis-pe own-repeat-crawl same-implied / same-implied / same-implied / same-implied
2020 PETS missed-by-filter-lists-detecting-unknown-th parallel-not-temporal same-implied / same-implied / same-implied / same-implied
2020 WWW beyond-the-front-page-measuring-third-party parallel-not-temporal same-explicit / same-explicit / varied-deliberate / same-implied
2020 WWW the-representativeness-of-automated-web-cra own-repeat-crawl same-explicit / same-explicit / same-explicit / same-implied
2019 WWW measuring-political-personalization-of-goog own-repeat-crawl same-implied / same-implied / same-explicit / same-implied
2017 NDSS thou-shalt-not-depend-on-me-analysing-the-u parallel-not-temporal varied-deliberate / same-implied / same-explicit / same-explicit
2016 IEEE-SP cloak-of-visibility-detecting-when-machines parallel-not-temporal same-explicit / varied-deliberate / varied-deliberate / same-explicit
2016 USENIX tracing-information-flows-between-ad-exchan own-repeat-crawl same-explicit / same-implied / same-explicit / not-stated
survive as all-four-fixed (estimate): 6 / 23
Unedited output of the quote check.
design:longitudinal four-pin audit — evidence quote check
coded dir: out_pins/coded
renderings searched per paper: paper.cols.txt, paper.norm.txt, paper.txt, and the excerpt packet
de-splicing mode: 6-word shingles, pass at >= 80% found
papers with no coding file (skipped): 0
pin codings: 1000
no quote given: 464 (permitted only for not-stated; checked below)
checked: 536
exact match: 489 91.2%
whitespace-collapsed match: 0 0.0%
loose match: 2 0.4%
spliced match (de-spliced): 3 0.6%
NOT FOUND: 42 7.8%
codings with no quote and a value other than not-stated: 0
MATCHED ONLY AFTER DE-SPLICING (3) — these are read by hand, the source page is two-column spliced:
2023/WWW/automatic-discovery-of-emerging-browser-fingerprinting-techniques [browser=same-explicit] shingles 86%
applied our system to this data set by following the same procedure.
2023/WWW/automatic-discovery-of-emerging-browser-fingerprinting-techniques [vantage=same-explicit] shingles 86%
applied our system to this data set by following the same procedure.
2015/NDSS/parking-sensors-analyzing-and-detecting-parked-domains [list=same-implied] shingles 93%
we sampled 100 domains from each parking service and setup an additional crawler which crawled them daily for a week.
QUOTES NOT LOCATED, printed in full (42):
2012/IEEE-SP/third-party-web-tracking-policy-and-technology [browser=same-implied] shingles 75%
We implemented FourthParty as an extension to Mozilla Firefox.
2012/WWW/understanding-and-combating-link-farming-in-the-twitter-social-network [list=same-explicit] shingles 50%
We re-crawled the profile page of each user in the above dataset in February 2011
2015/NDSS/parking-sensors-analyzing-and-detecting-parked-domains [vantage=varied-deliberate] shingles 47%
To assess any geographical differences, we performed this crawl in parallel from the United States and one country from Europe.
2015/USENIX/measuring-the-longitudinal-evolution-of-the-online-anonymous-marketplace-ecosyst [state=same-implied] shingles 38%
We managed to emulate typical browser behavior in all but one case (BlueSky).
2016/WWW/stress-testing-the-booters-understanding-and-undermining-the-business-of-ddos-se [list=same-implied] shingles 58%
In order to measure the stability of these amplifiers we probed them periodically for 13 weeks to understand how many were still located at the same IP and misconfiged.
2017/CCS/poster-probing-tor-hidden-service-with-dockers [list=same-explicit] shingles 69%
The number of Onion addresses in this experiment was 16,683, and the addresses were crawled twice a day at 12-hour intervals.
2018/CCS/how-you-get-shot-in-the-back-a-systematical-study-about-cryptojacking-in-the-rea [list=same-implied] shingles 50%
we can only continuously monitor 1,000 samples. Besides, we re-visit these samples every 3 days
2019/IMC/visiblev8-in-browser-monitoring-of-javascript-in-the-wild [list=same-explicit] shingles 0%
to visit the Alexa top 50k web domains
2019/WWW/a-first-look-at-deep-learning-apps-on-smartphones [list=changed] shingles 52%
We have crawled two datasets at different moments, June 2018 and September 2018, which are three months apart. The two app datasets have more than 2/3 overlapped apps.
2019/WWW/before-and-after-gdpr-the-changes-in-third-party-presence-at-public-and-private [browser=same-implied] shingles 43%
We installed the widely used OpenWPM framework and configured it to use four vanilla Mozilla Firefox browsers in parallel
2019/WWW/before-and-after-gdpr-the-changes-in-third-party-presence-at-public-and-private [vantage=same-implied] shingles 50%
the physical machine on which the VM was deployed is located in the EU, meaning that the visited websites must comply with GDPR
2019/WWW/before-and-after-gdpr-the-changes-in-third-party-presence-at-public-and-private [state=same-implied] shingles 43%
we did not install any extension on the browser that would stop the websites from storing or reading cookies
2019/WWW/mobile-app-risk-ranking-via-exclusive-sparse-coding [list=changed] shingles 77%
There are no overlaps for the above datasets, and the apps are from a wide range of functionalities, including game, news, music, social, movies/TV, travel, life, etc.
2020/WWW/beyond-the-front-page-measuring-third-party-dynamics-in-the-field [vantage=varied-deliberate] shingles 53%
we conduct the measurements from three different locations (Europe (DE), North America (US), and Asia (JP)) to account for possible geographical differences
2020/WWW/beyond-the-front-page-measuring-third-party-dynamics-in-the-field [state=same-implied] shingles 53%
We used the same method to create the browser profile for our experiment crawls that we utilized in the pre-study.
2021/WWW/towards-realistic-and-reproducibleweb-crawl-measurements [list=same-explicit] shingles 0%
Tranco list of top sites [18] (snapshot 77PX)
2021/WWW/towards-realistic-and-reproducibleweb-crawl-measurements [browser=same-implied] shingles 33%
We crawl using two variants of Puppeteer [4] controlling Chromium 80: a lower-realism naive variant and a higher-realism stealth variant.
2021/WWW/towards-realistic-and-reproducibleweb-crawl-measurements [state=same-implied] shingles 70%
the browser is launched with a clean user profile (i.e., no cookies or cached content)
2021/WWW/where-are-you-taking-me-understanding-abusive-traffic-distribution-systems [browser=same-implied] shingles 50%
we rely on a fully-featured, headless Chrome browser, governed by Selenium.
2021/WWW/where-are-you-taking-me-understanding-abusive-traffic-distribution-systems [vantage=same-implied] shingles 47%
ODIN uses university IP addresses (one per profile) and a /24 subnet from a research-friendly, but less well-known VPS provider
2022/IMC/muzeel-assessing-the-impact-of-javascript-dead-code-elimination-on-mobile-web-pe [browser=varied-deliberate] shingles 17%
We now evaluate Muzeel across browsers: Chrome, Edge, Firefox, and Brave.
2022/IMC/respect-the-origin-a-best-case-evaluation-of-connection-coalescing-in-the-wild [list=same-explicit] shingles 57%
Our initial sample set was the 5000 domains with the most requests to the third party, as indicated by the Referer field in the CDN's third party logs.
2022/IMC/respect-the-origin-a-best-case-evaluation-of-connection-coalescing-in-the-wild [browser=changed] shingles 38%
Results shown are for Firefox (v91) for later comparison, since only Firefox has client-side support for ORIGIN Frame.
2022/IMC/respect-the-origin-a-best-case-evaluation-of-connection-coalescing-in-the-wild [state=same-explicit] shingles 57%
Client caches are intentionally deleted between measurements in all of our active measurements to prevent bias from caching effects
2022/PETS/a-global-survey-of-android-dual-use-applications-used-in-intimate-partner-survei [list=same-explicit] shingles 52%
we crawled Google Play again in all fifteen languages for one day in February 2022 with the queries we obtained and used in our 2020 crawling.
2022/PETS/atom-ad-network-tomography [state=same-implied] shingles 79%
Each persona was associated with a unique browser running on an isolated virtual machine with a unique IP address.
2022/USENIX/measurement-by-proxy-on-the-accuracy-of-online-marketplace-measurements [list=same-implied] shingles 59%
We scraped Hansa 17 times between late 2015 and mid-2017, collecting a total of 332,795 pages amounting to 39.5 GB of data.
2022/WWW/leveraging-googles-publisher-specific-ids-to-detect-website-administration [list=changed] shingles 58%
We perform our analysis on websites which have been crawled in all snapshots (i.e., their intersection) and contain at least one Publisher ID. There are over 191,000 such websites.
2023/CCS/cookiegraph-understanding-and-detecting-first-party-tracking-cookies [list=same-explicit] shingles 63%
We successfully conducted the four parallel crawls for 99.31% of the 20K websites.
2023/CCS/cookiegraph-understanding-and-detecting-first-party-tracking-cookies [vantage=same-explicit] shingles 71%
We run all crawls in the US to minimize the impact of the EU GDPR and do not interact with cookie banners.
2023/CCS/cookiegraph-understanding-and-detecting-first-party-tracking-cookies [state=varied-deliberate] shingles 46%
We conduct four parallel crawls: two with third-party cookies enabled (3P-Allowed) and two with third-party cookies blocked (3P-Blocked).
2023/WWW/automatic-discovery-of-emerging-browser-fingerprinting-techniques [list=same-explicit] shingles 59%
we redid the crawling on the same 10K domains in September 2022 (11-month gap) and applied our system to this data set by following the same procedure.
2024/NDSS/flow-correlation-attacks-on-tor-onion-service-sessions-with-sliding-subset-sum [list=same-implied] shingles 57%
clients repeatedly accessed onion services and the top 150 accessed websites according to the Tranco ranking [68], in parallel.
2024/NDSS/flow-correlation-attacks-on-tor-onion-service-sessions-with-sliding-subset-sum [browser=same-explicit] shingles 61%
Clients also run on an isolated Docker container that executes a 0.4.7.10 Tor process and issues requests to Tor via the Python libraries selenium [58] and tbselenium [3]
2025/IMC/learning-as-to-organization-mappings-with-borges [list=same-implied] shingles 64%
we used the PeeringDB snapshot from July 24, 2024, and CAIDA's AS2Org from July 1, 2024.
2025/IMC/somesite-i-used-to-crawl-awareness-agency-and-efficacy-in-protecting-content-cre [list=same-explicit] shingles 77%
We restrict the list to sites that appear in all of the top 100k lists over this period to avoid having our results affected by list churn
2025/PETS/intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-banner-interaction-and [browser=varied-deliberate] shingles 48%
Since OpenWPM only supports Firefox and, unlike Chrome, Firefox does not expose the partitioned_key field in its cookie storage, we modified BannerClick to operate in Chrome's default mode.
2025/PETS/intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-banner-interaction-and [state=varied-deliberate] shingles 70%
We use OpenWPM [24] to conduct a combination of stateful and stateless crawls aiming to observe the interplay of the setter and the sender website
2025/PETS/tracker-installations-are-not-created-equal-understanding-tracker-configuration [list=same-implied] shingles 55%
we joined the top one million websites from Tranco [49] with SimilarWeb, a web traffic estimator
2025/PETS/tracker-installations-are-not-created-equal-understanding-tracker-configuration [browser=same-implied] shingles 56%
Each website was opened in a Chrome instance with two installed custom Chrome extensions.
2025/PETS/tracker-installations-are-not-created-equal-understanding-tracker-configuration [vantage=same-implied] shingles 47%
All website visits took place on Linux virtual machines in Google Cloud's US Central region between September and November 2024.
2025/PETS/who-s-watching-you-zoom-investigating-privacy-of-third-party-zoom-apps [list=changed] shingles 40%
between December 2023 and December 2024, 667 new apps were added, 212 apps were removed, and 49 apps were renamed
Unedited output of the agreement check, including every disagreement between the two coders.
design:longitudinal four-pin hand-coding — inter-coder agreement coder A (excerpt packets): out_pins/coded coder B (full text, blind): out_pins/coded_val reliability sample: 30 papers drawn from the 250 pairs available: 30 ============================================================================== PER-PIN AGREEMENT (n=30 papers) ============================================================================== pin exact fixed/not kappa(exact) kappa(fixed) list 20/30 (66.7%) 26/30 (86.7%) 0.49 0.59 browser 21/30 (70.0%) 22/30 (73.3%) 0.57 0.48 vantage 21/30 (70.0%) 25/30 (83.3%) 0.56 0.67 state 24/30 (80.0%) 25/30 (83.3%) 0.64 0.64 ALL PINS 86/120 (71.7%) 98/120 (81.7%) 0.60 0.64 -- repeatKind exact 25/30 (83.3%) kappa 0.77 own-repeat-crawl vs everything else: 27/30 (90.0%) kappa 0.80 -- "all four fixed", the figure the page publishes A says all four fixed: 3/30 B says all four fixed: 4/30 agree: 27/30 (90.0%) kappa 0.52 ============================================================================== DIRECTION OF DISAGREEMENT — does the excerpt coder over- or under-call "fixed" ============================================================================== list both fixed 22 only A (excerpt) 4 only B (full text) 0 neither 4 browser both fixed 9 only A (excerpt) 7 only B (full text) 1 neither 13 vantage both fixed 11 only A (excerpt) 1 only B (full text) 4 neither 14 state both fixed 8 only A (excerpt) 4 only B (full text) 1 neither 17 ============================================================================== EVERY DISAGREEMENT, PRINTED IN FULL ============================================================================== 2013/CCS/protocol-misidentification-made-easy-with-format-transforming-encryption repeatKind A=single-crawl B=own-repeat-nonweb list A=same-implied B=unclear browser A=same-implied B=not-stated vantage A=not-stated B=same-implied state A=same-implied B=not-stated 2015/CCS/seeing-through-network-protocol-obfuscation list A=not-stated B=changed 2015/IMC/location-location-location-the-impact-of-geolocation-on-web-search-personalizati browser A=same-implied B=same-explicit vantage A=not-stated B=same-explicit 2016/IMC/a-first-look-at-quality-of-mobile-live-streaming-experience-the-case-of-periscop list A=same-implied B=not-stated vantage A=same-implied B=not-stated state A=same-implied B=not-stated 2018/IEEE-SP/the-spyware-used-in-intimate-partner-violence repeatKind A=own-repeat-crawl B=own-repeat-nonweb browser A=same-implied B=not-stated state A=same-implied B=not-stated 2019/WWW/spiders-like-onions-on-the-network-of-tor-hidden-services browser A=same-implied B=not-stated 2020/WWW/apophanies-or-epiphanies-how-crawlers-impact-our-understanding-of-the-web vantage A=same-implied B=same-explicit 2022/IMC/respect-the-origin-a-best-case-evaluation-of-connection-coalescing-in-the-wild list A=same-explicit B=same-implied vantage A=same-explicit B=same-implied 2022/USENIX/a-large-scale-investigation-into-geodifferences-in-mobile-apps repeatKind A=parallel-not-temporal B=own-repeat-nonweb browser A=same-implied B=not-stated vantage A=varied-deliberate B=same-implied state A=same-explicit B=not-stated 2023/IEEE-SP/fashion-faux-pas-implicit-stylistic-fingerprints-for-bypassing-browsers-anti-fin list A=same-implied B=not-stated state A=not-stated B=same-implied 2023/IMC/tracking-profiling-and-ad-targeting-in-the-alexa-echo-smart-speaker-ecosystem browser A=same-explicit B=not-stated 2024/PETS/two-steps-forward-and-one-step-back-the-right-to-opt-out-of-sale-under-cpra browser A=same-explicit B=unclear vantage A=same-explicit B=same-implied 2025/IMC/exploration-of-the-dynamics-of-buy-and-sale-of-social-media-accounts list A=same-explicit B=same-implied 2025/IMC/toward-better-efficiency-vs-fidelity-tradeoffs-in-web-archives list A=same-explicit B=same-implied 2025/PETS/intractable-cookie-crumbs-unveiling-the-nexus-of-stateful-banner-interaction-and list A=same-explicit B=varied-deliberate 2025/PETS/tracker-installations-are-not-created-equal-understanding-tracker-configuration repeatKind A=own-repeat-crawl B=single-crawl 2026/IEEE-SP/parasites-in-the-toolchain-a-large-scale-analysis-of-attacks-on-the-mcp-ecosyste vantage A=not-stated B=same-implied 2026/NDSS/cellshift-rtt-aware-trace-transduction-for-real-world-website-fingerprinting list A=same-implied B=same-explicit browser A=same-implied B=not-stated 2026/NDSS/characterizing-the-implementation-of-censorship-policies-in-chinese-llm-services repeatKind A=own-repeat-crawl B=own-repeat-nonweb vantage A=varied-deliberate B=unclear 2026/PETS/privacy-vs-profit-the-impact-of-googles-manifest-version-3-mv3-update-on-ad-bloc list A=same-implied B=same-explicit browser A=varied-deliberate B=same-implied state A=same-implied B=same-explicit papers with at least one disagreement: 20/30 (66.7%) ============================================================================== VALUE DISTRIBUTION, EACH CODER (so a dominant value is visible) ============================================================================== value A B same-explicit 28 25 same-implied 38 31 varied-deliberate 6 4 changed 4 5 not-stated 44 52 unclear 0 3
← back to the content page · the 2026-09-03 Alexa date audit · corpus-level provenance
