User Tools

Site Tools


provenance:design:longitudinal

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-versions and the bare token CfT, 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 name O=CFT, a CFt variable in an algorithm listing, CFT-IDF, and a fitness-test app named CFT & PFT.
  • Docker. /\bdocker\b/i gives 293 (5.0%); adding containerised/containerized gives 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}/i returns 0.
  • WebREC. /\bWebREC\b|\bweb execution bundle/i returns 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.shTAG='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:longitudinalprovenance:design:longitudinalstart.

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_ipfp was one of two keys for the same paper in bibliography. The wiki-wide consolidation of 2026-09-04 (drain item dedup-regional-filter-lists-bibkey) kept ahmad2026_more and 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.mjsout_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.mjsout_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.mjsout_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.txtzero 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 .cols actually 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 pin same-implied, which is the same direction.
  • Corrected to same-explicit with the methods-section sentence as its quote, and the correction is recorded in the paper's notes field. The same pass offered a stronger quote for that paper's state pin (“Client caches are intentionally deleted between measurements in all of our active measurements”); the quote was swapped, the value was already same-explicit and 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.tsvverdict_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-implied counts 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, not same. 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 coded changed on 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.
  • repeatKind was 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-implied is 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.

  1. 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 –wiki output generated before the §18.5 correction was applied. Accepted and fixed by regenerating the table; it was the only divergent cell of 207.
  2. 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.
  3. The structural guard in build_provenance_pins.py asserted 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 got headings: 27 → 37 and 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.
  4. Mutation-tested and confirmed sound: the partial-population refusal in report_pins_audit.mjs (deleting one of 250 codings gives FATAL: 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 prints MISSING rather than silently continuing at 30), and the missing-file and empty-file guards in the builder.
  5. 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.
  6. Checked the figures outside the edited window against report_longitudinal-output.txt and found none stale.

Pass B — citations and quotes (Claude Sonnet)

  1. 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.
  2. Verified all 23 rows of out_pins/citekeys.json against the corpus index — title, venue, year and first-author surname — including that fouad2020_missed and fouad2024_devil are two distinct papers and are not conflated. No swapped keys.
  3. Independently found the same stale vantage cell as Pass A, by diffing the published table against a fresh –wiki run, and confirmed all three quotes in the §18.5 account by locating them in the paper itself.
  4. Spot-checked 24 evidence quotes (10 changed, 14 same-explicit) against all three renderings: 24 of 24 located and supporting their coded value. Flagged one as loose rather than wrong — the Zoom-apps browser quote is about a crawler and parser rebuild rather than a browser version. Noted, not changed: the coding is changed either way and the codebook's browser pin covers the driven stack.
  5. 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:

  1. 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/20200814GK9K, providers include quantcast; date/20200815Z7NG, 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.
  2. 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/latest and by reading install-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.
  3. The Playwright footnote quoted main, not a release. It cited browsers.json from 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 release v1.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 returned revision 1243, browserVersion 153.0.8010.12, matching the author's, so the published numbers stand.
  4. 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 (curl fails 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.
  5. Confirmed accurate and unchanged: the HTTP Archive changelog 404 (still 12 entries, last 2017-06-01, on master after 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 auditnode 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

provenance/design/longitudinal.txt · Last modified: by karel.kubicek.claude