LH012 study brief
Brief, written before any outcome was read, 29 September 2026.
study: LH012 edition: 0.1 date opened: 2026-09-29, written after reading AGENTS.md, programme.md's Next (the checkpoint of 28 September 2026, as revised), studies/LH011/ (record version 0.3, brief with amendment 1, sources, scripts/common.py, data/post_hoc_reading.txt) and the phase 1 reads listed under "What was read before this brief", none of which returns a download count. written by: a Lighthouse research subagent in Claude Code, model Opus 5.5 (claude-opus-5-5, as reported by its harness), dispatched by the driving session for issue #14; edited by the driving session (Opus 5.5) before the snapshot and before any count was read, to settle the six choices the writer left open (listed at the end, "Decisions before the snapshot") grounded at: repository commit c2e4d4e, with this study's uncommitted files in studies/LH012/ issue: #14 (JKershaw/lighthouse); programme.md, Next
Question
After a new release of one of the most downloaded Python libraries, what keeps older versions downloaded, and which explanations can the public record rule out: other libraries' version bounds, Pythons the new release no longer supports, or environments rebuilt from frozen lists?
What the writer already knows of the outcome, disclosed
- LH011's Answer and Findings (record version 0.3): in 26 of the 37 projects most releases reached half of downloads within two days, in eleven none did; the post hoc readings in its Findings and data/post_hoc_reading.txt, including that for 34 of 37 projects the median at-or-newer share on day 30 was within ten points of day 1 (the ten-point band was chosen after the counts by LH011's writing agent), and that one minus the day-30 median was 17.8 to 47.1 per cent for the 26 projects (median 33.9) and 63.0 to 97.5 per cent for the eleven (median 73.1). The per-project medians of that file are known to the writer.
- The same releases read two ways (LH011 record, Findings; notes/R-0015.md; studies/LH011/review/r0015_check.py): of LH008's 102 pinned repository pairs on urllib3 2.7.0, cryptography 48.0.1, pyjwt 2.13.0 and starlette 1.1.0, 50 moved to the fix within 30 days of its release, while the four releases' at-or-newer download shares rose by 3.4 to 14.7 points over the same days. That check was made after both sets of outcomes were known.
- Metadata read for this brief (below), which touches no outcome but shapes expectations: the reference releases' Requires-Python (data/reference.csv) exclude Python 3.9 for 26 of the 37 projects (numpy's requires 3.12, pandas's and rpds-py's 3.11, 23 others' 3.10), eight require 3.9, tqdm's 3.8, and certifi's and charset-normalizer's 3.7; among the requirements of the dependents' versions that were newest during the read week, the ones that admit no version at or above the reference release are aiobotocore's on botocore, pydantic's exact pin on pydantic-core (==2.46.5, below pydantic-core's reference 2.48.0), eight dependents' upper bounds below 7 on protobuf, dbt-core's and dbt-common's
<1.1on pathspec, datasets's<=2026.6.0on fsspec and virtualenv's on filelock for Pythons below 3.10 (data/pairs.csv). - General knowledge, not a source here: boto3 and botocore release together on most working days; Python 3.9 reached its end of life in October 2025.
- No download count by version, Python, installer, CI flag, system, libc, file, country or day has been read for this study. LH011's retained counts (data/counts/) hold, for the read week below, each project's downloads at versions at or above its first qualifying release by day, with older versions folded; the writer has not computed anything from them for these days.
Population and unit
- Projects. The 37 projects of LH011 that had a qualifying release between April and August 2026 (studies/LH011/data/releases.csv), all of them. Choosing the eleven slow projects and explaining them would select on the outcome; the 26 fast projects, which also kept 18 to 47 per cent of downloads on older versions a month on, are part of the population, not a contrast.
- Unit. The project. Each measure is computed per project; pooled figures over all 37 projects' downloads are reported beside the per-project ones and never in their place. Where an interval is given it is a percentile interval over 10,000 resamples of projects with replacement, seed 20260929.
- Reuse, disclosed. LH011's frame (projects, release rule, day definitions, gap rule, at-or-newer share) is reused, so this is a continuation on the same projects, not a replication. LH010's top 500 (studies/LH010/data/top500.csv, ClickPy's August 2026 totals read 27 September 2026 at 09:53:19 UTC) chooses the dependents. LH008's retained lags and LH011's retained counts are read offline for the first step below.
The read week and the reference release
- Read week W: the UTC days Monday 21 to Sunday 27 September 2026, the last full Monday to Sunday held for all 37 projects in every per-day table when coverage was read (last day held 28 September 2026, read 06:19 UTC on 29 September; the 28th is the most recent day and may be incomplete, so it is not read). Every measure below sums the seven days, so weekday and weekend fetches are both in it. These days were also read by LH011 (above).
- Reference release R (data/reference.csv, fixed now): for each project, the highest version by PEP 440 that is not a pre-release or development release, not wholly yanked at the read, and first uploaded on or before 2026-08-21T23:59:59Z. On every day of W, R is at least 31 days old. Every R is one of LH011's qualifying releases.
- Older means a version below R. For a project that releases daily this makes "older" versions superseded for at least a month: for boto3 and botocore, R is 1.43.78 of 21 August and every older version was uploaded before it. At or newer means R or any higher non-pre-release version listed by PyPI; pre-releases above R and versions ClickPy holds but PyPI does not list are other, reported with their volume and left out of both classes. Newest-on-the-day share is reported beside, for context.
What each measure counts
Every share here is a share of downloads, and a download is a file PyPI served, counted once per fetch (LH011, Limits). A share of downloads weights each environment by how often it fetches: a build that installs from scratch many times a day without a cache counts many times, one behind a cache or mirror once or not at all. So "older share" is the share of fetches that went to older versions, not the share of people, projects or machines that run them, and a low at-or-newer share is not by itself slow uptake. A dependent's version mix is likewise a mix of fetches. No measure here counts environments.
Sources and events
ClickPy's public SQL (user demo; tables and limits in data/clickpy_schema.csv, clickpy_columns.csv, clickpy_server.csv) and PyPI's JSON API, both anonymous.
- Older share O per project: older downloads over all downloads on W, from
pypi_downloads_per_day_by_version. - Python per download:
pypi_downloads_per_day_by_version_by_python(date, project, version, python_minor, count), a materialised view ofpypi.pypi.python_minoris the first two dot-separated parts of the client's reported Python version by ClickPy's documented import (ClickPy README, read 06:26 UTC); the live transformation is not in the catalogue and was not checked. pip reportsplatform.python_version()of the interpreter running it; uv reports the full Python version of the marker environment it is given, or nothing (pip 26.2.1 and uv 0.12.8 source, read 06:26 UTC).pip download --python-versionand similar cross-target fetches report the running interpreter, not the target; this is not separable in the log. - Admission: a version admits a Python minor X.Y when any of its files' Requires-Python (PyPI JSON, per file) contains some patch release X.Y.0 to X.Y.30; a blank Requires-Python admits all. A Python minor is excluded on day d when no version at or above R that is not a pre-release, not wholly yanked and first uploaded by the end of day d admits it (data/python_admission.csv holds the week-end table). The Python edge E(p) is the highest version below R that admits p.
- Dependents (rule fixed before any requirement was read): every project in LH010's top 500 whose declared requirements (
requires_dist), in the version latest at the read or in any version that was its newest at some moment of W, name one of the 37 (names compared after PEP 503 normalisation), excluding the project itself. 297 of the 500 name one; 201 do so outside an extra (data/top500_screen.csv, requirements.csv, pairs.csv). Requirements that apply only under an extra (extra == ...) are treated as unknown, since the log cannot say which extras were requested: they count as holding in B's upper estimate and as not holding in its lower estimate, and are also reported separately. - Which versions of a dependent are read, and why. A bound holds back downloads only if the dependent's versions being fetched carry it. Phase 1 read the latest version at the read (the screen) and every version that was newest at some moment of W (what a fresh resolve of the dependent would pick during W). Phase 2 reads, after the dependents' W version mix, the requirements of each dependent's versions in descending order of W downloads until 95 per cent of its W downloads are covered or 25 versions are read (200 versions for a dependent that is the X of a tightly coupled pair below), whichever comes first, and of every version at or above R of each of the 37 (for the one-step rule below). A version whose requirements are not read is unknown.
- Held: a version x of dependent X holds project D on day d when x's requirement on D admits no version of D at or above R(D) that was available by the end of day d. Markers: one that tests only
python_versionorpython_full_versionis evaluated per Python minor from X'sby_pythonmix; one that tests the platform or implementation is treated as unknown; one under an extra is treated as unknown, as above. One step through another of the 37: x also holds D when x's requirement on another of the 37, Y, excludes every version of Y that each available version of D at or above R requires (for example a narrow range on botocore that excludes the botocore every newer boto3 requires). Longer chains are not followed. - Frozen-list descriptors, per project, older against at-or-newer downloads on W: the CI flag within pip and uv downloads (LH011's rule: pip and uv send true or null, stored as 'false'; every other installer is "flag not known"), the installer mix, the system, the libc library within pip and within uv separately (pip sends glibc or nothing; uv sends glibc or musl, from its source), and the file type, from one aggregate pass of
pypi.pypiover W grouped by class, installer,ci, system,libc.liband type; version dispersion (the fewest older versions carrying 80 per cent of older downloads) and fetch-weighted age of older downloads (days from the version's first upload), from the per-version table. Country is not read. - Files (secondary), for projects whose R has any wheel with a platform tag other than
any: older downloads grouped by file name and Python minor frompypi.pypion W. An older download is wheel-excluded when no version at or above R has a wheel for the same operating system family and architecture, with a glibc, musl or macOS floor no higher than the downloaded file's, whose interpreter or ABI tag installs on the download's Python minor; an sdist download is its own class. Since every such project also publishes an sdist that could be built, wheel exclusion is reported beside the Python measure, never added to it in the primary reading. - Days: LH011's gap rule, with the total taken over the 37 projects: a day on which ClickPy holds no row for a project that had downloads on the days either side, or on which the 37 projects' total falls below half the median of days d-3 to d+3, is a gap, and every measure on it is unknown.
The three explanations and their traces
Each may hold for some projects, for none, or differently by project.
- Pythons the newer releases no longer support. Trace: older downloads from Python minors that are excluded (no version at or above R admits them). Measure: P = older downloads from excluded Pythons, as a share of older downloads; lower estimate with "Python not reported" counted as not excluded, upper with it counted as excluded. This is a count of fetches, not an estimate. Beside it, the edge share: of those downloads, the share at E(p). A fresh resolve on an excluded Python lands on E(p); older downloads well below it on the same Python were resolved or frozen earlier.
- Other libraries' version bounds. Trace: older downloads of D in step with dependents' versions that hold D. Measure: B, the fetches of held dependent versions on W as a share of D's older fetches, under the stated assumption that each fetch of a held dependent version comes with one fetch of D. Lower estimate: the largest single dependent's held fetches, unknown versions counted as not holding. Upper: the sum over dependents, unknown versions counted as holding. Both capped at one. Reported in two parts: held by the dependent's versions newest during W (a hold that applies even to a fresh install of the dependent) and held by its older versions (a hold that applies only where the dependent is itself held back, so it moves the question to the dependent). A bound whose upper limit still admits a version at or above R holds nothing back unless something else holds the dependent, and this measure counts it as not holding. The co-fetch assumption fails where D is cached or already installed when the dependent is fetched; the ratio of each dependent's W fetches to D's is reported so a reader can see where it is strained.
- The number test, for tightly coupled pairs. A pair (X, D) is tightly coupled when X's requirement on D, outside any extra and in a version of X newest during W, has an upper bound (
<,<=,==,===or~=), admits ten or fewer of D's versions released by the end of W, and X's August 2026 downloads in LH010's top 500 are at least a tenth of D's. By this rule, applied to the metadata now, 37 pairs qualify, among them boto3 with botocore and with s3transfer, aiobotocore with botocore, pydantic with pydantic-core, s3fs with fsspec, grpcio-status and googleapis-common-protos with protobuf, and litellm with aiohttp and python-dotenv; phase 2 recomputes the list with the same code. For each pair, from X's W version mix and each version's requirement: F (fresh), the share of X's W fetches at versions that hold D, which is the older share of D predicted if those environments resolved D afresh within the bound; Z (frozen), the share of X's W fetches at versions whose D, resolved at the last moment the version was X's newest, would be below R(D), which is the prediction if those environments kept the list they had then; and the observed O(D). The comparison is made in fetches, not shares, because D has fetches that do not come with X: the predicted older fetches of D are F times X's W fetches (fresh) and Z times X's W fetches (frozen), each set against D's observed older fetches, O(D) times D's W fetches, with the ratio of X's W fetches to D's reported beside. Where X's covered versions carry less than all of X's W fetches, F and Z are ranges, with uncovered versions counted as not holding (lower) and as holding (upper). A pair is read as fresh resolution within X's bound accounting for at least half of D's older fetches when F's lower estimate times X's W fetches is at least half of D's older fetches, and as not when F's upper estimate falls below that; otherwise undetermined. Z is read the same way. Where every covered version of X pins D exactly, F equals Z, and the predicted version mix of D is also compared with the observed one (total variation distance), as a description.
- The number test, for tightly coupled pairs. A pair (X, D) is tightly coupled when X's requirement on D, outside any extra and in a version of X newest during W, has an upper bound (
- Environments rebuilt from frozen lists. Trace: older downloads spread over many versions, off every identified edge, more often flagged CI, with their own installer, system and libc mix. The log cannot count this: a fetch does not say whether it came from a lockfile, a pin, a cache miss or a bound not identified here. So it is described, not measured, and a project shows the frozen-list trace when more than half of its older downloads are at versions that are neither a Python edge E(p) for their Python nor the highest version a held dependent version admits (for dependent versions with at least 0.1 per cent of that dependent's W downloads), and its older pip and uv downloads are flagged CI more often than its at-or-newer ones. That is a description consistent with frozen lists and also with bounds outside the top 500, private pins and chains longer than one step.
The residual. The part of O that neither testable trace accounts for is reported per project as a range, from O times (1 minus the smaller of one and P upper plus B upper) to O times (1 minus the larger of P lower and B lower), under the label "not accounted for by Python exclusion or identified bounds", with the frozen-list descriptors beside it. It is never reported as a count of frozen environments.
Reporting convention (fixed now)
- Per project and per explanation (P and B): accounts for most of the older downloads if the lower estimate exceeds one half; cannot be the main reason if the upper estimate is below one half; accounts for little if the upper estimate is below one tenth (which is also "cannot be the main reason"); otherwise undetermined. The public record rules out an explanation as the main reason for a project exactly when it "cannot be the main reason".
- A guard on B, because its co-fetch assumption can inflate the lower estimate as well as the upper (a dependent fetched more often than D, for example where D is cached, makes the largest dependent's held fetches exceed the D fetches they bring): B is classed accounts for most only when, in addition, more than half of D's older fetches are at versions of D that the lower estimate's dependent, in its held versions, admits. Where B's lower estimate exceeds one half and this check fails, the project is undetermined for B and named, with the dependent's fetch ratio.
- Bounds are only those identified here: requirements declared by the read versions of the 500 most downloaded projects. "Cannot be the main reason" for B always carries that qualifier, since bounds declared by other projects, or in private code, are not read.
- Across projects: for each explanation, the number and list of projects in each class, the share of projects with a known measure in each class with its interval, and the median of project shares with its interval. "As a rule" is used when at least three quarters of projects with a known measure are in a class, "for most projects" above one half, "for some projects" otherwise; "none" and "all" carry the window (W, the 37 projects).
- Number of comparisons: two explanations classified per project (74 classifications), one across-project summary per explanation, one number test per tightly coupled pair (37 pairs by the rule applied to the metadata, each read for F and for Z as accounting for at least half of D's older fetches, not, or undetermined: 74 readings), and the frozen-list trace described per project. Nothing else is tested; any other reading made after counts are seen is labelled post hoc with its number of comparisons.
The first-day observation, measured
- Measure (fixed now). For a release, Δ = at-or-newer share on day 30 minus at-or-newer share on day 2, in percentage points (LH011's definitions of day and share). A release settles by day 2 when |Δ| is at most 10 points. A project settles when more than half of its releases with a known Δ settle. Secondary, described only: the ratio of the day-2 to the day-30 share, and Δ from day 1.
- Releases. The 37 projects' releases first uploaded 2025-11-01T00:00:00Z to 2026-03-31T23:59:59Z by LH011's release rule (not a pre-release, higher than every earlier-uploaded non-pre-release version): 355 releases in 32 projects (data/releases_nov_mar.csv; annotated-types, cffi, idna, typing-extensions and typing-inspection had none; boto3 and botocore 99 each, litellm 40). Four are wholly yanked now and are kept, flagged. LH011 did not read these releases. Its reads began on 20 March 2026, so the late-February and March releases' days 2 to 30 overlap days LH011 read, but only inside its folded "~other" rows, from which no share of these releases can be computed.
- Reading. The share of projects with a known outcome that settle, with its interval. The first-day pattern holds as a rule if at least three quarters settle, holds for some projects from one half to three quarters, and fails below one half; one test. It is inconclusive if fewer than 20 projects have a known outcome. The same measure computed on LH011's April to August releases from its retained counts is reported beside, labelled as measured on data already read, not a test.
- Order. Only if the main question's reads finish inside the ceiling.
The same releases read two ways (phase 2's first step, offline)
From LH008's retained events.csv and lags.csv and LH011's retained counts: for each of the four fixes and each day 1 to 30, the cumulative share of LH008's pinned repository pairs that had moved to the fix and the fix's at-or-newer download share. A description of two counts of different things (repositories once each, downloads by the fetch), all known before this brief, with no test.
Missing evidence
Unknown stays unknown and is never counted as a negative.
- Downloads with no Python minor reported are their own class and enter P only through its lower and upper estimates. If, for more than a third of the projects, more than a third of older downloads have no Python reported, P is reported as a description only.
- Dependent versions whose requirements are not read enter B only through its lower and upper estimates. A requirement that cannot be parsed is unknown.
- A day that is a gap is left out of that project's W sums and named; with more than two gap days in W, the project's measures are unknown.
- Versions ClickPy holds and PyPI does not list are "other".
- The CI flag outside pip and uv is not known (LH011); the libc library is compared only within an installer, since pip never sends musl.
- A dependent that returned an error from PyPI, or a project in the top 500 that no longer resolves, is listed and left out (none did in phase 1).
ClickPy's limits and how the reads are bounded
The demo user returns at most 10,000 rows, reads at most 1,000,000,000 rows or 50 GB and runs at most 60 seconds per query (data/clickpy_server.csv, read 06:19 UTC). pypi.pypi is ordered by (project, date, version, country_code, python_minor, system); the 37 projects fetched about 1.35 billion files a day in August (LH011's August totals), so one pass of pypi.pypi over W reads about 9.4 billion rows. Hence: every aggregation is server-side; versions are mapped to classes in the query from the metadata (R and its successors listed in the query); pypi.pypi is read for W only, in one grouped pass for all 37 projects and one file-name pass for the compiled projects, split by project and by day where a query would pass a limit; the per-version, by-Python, by-system and by-installer tables, which are small, carry everything else. The first-day question reads the per-version table on days 1, 2 and 30 of each release (and the daily totals for the gap rule), with versions outside a release's numerator set folded server-side.
Resource ceiling
- ClickPy: at most 900 queries and 40 billion rows read in all, by ClickHouse's own statistics, including phase 1's 9 queries and 272,080,987 rows; every query logged in data/read_log.csv. Expected use is under 15 billion rows.
- PyPI JSON: at most 5,000 requests in all, including phase 1's 628. Dependent versions left unread at this ceiling are unknown, as above.
- Documentation and literature: at most 30 page reads in all, including phase 1's 11 page reads and two web searches.
- About five dollars of the phase 2 agent's own model spend at list rates. Scratch space at most 2 GB. Anonymous reads only; no key or token is read.
Stopping condition
Close when, for every one of the 37 projects, O, P, B and the frozen-list descriptors on W are read, every tightly coupled pair's F, Z and O(D) are computed, the offline "two ways" table is written, and, within the ceiling, every Nov to Mar release's Δ is read; or at a ceiling, with the unread part stated. Phase 2's code sets LH012_PHASE=2 only after this brief is snapshotted. The question, R, W, the dependents rule, the coverage rule, the measures, the conventions and the missing-evidence rules are not changed after any count is read; a later change is a dated amendment, labelled post hoc if made after counts were seen.
What would leave it inconclusive
- Frozen lists, by design. The log can describe the frozen-list trace but cannot count frozen environments, so this study can at most rule Python exclusion or identified bounds in or out as the main reason for a project's older downloads. Whether the residual is frozen lists stays unmeasured, and the record will say so whatever the descriptors show.
- Fewer than 25 projects with both P and B known.
- B's co-fetch assumption: where a dependent's fetches are several times D's (boto3's were 2.75 times botocore's in August, by LH010's top 500), B's upper estimate can reach one without saying much; a project whose B range spans one half is "undetermined", not ruled either way.
- The first-day question: fewer than 20 projects with a known Δ.
- ClickPy's tables disagree: if, for a project, the W total of the by-Python table differs from the per-version table's by more than 2 per cent, that project's P is reported as a description only.
Overlap with earlier studies
The 37 projects, the release rule and the day and share definitions are LH011's; W was read by LH011 at each project's numerator set. The dependents come from LH010's top 500. The "two ways" step rereads LH008's and LH011's retained files. The pip and uv sources are the versions LH011 read (26.2.1 and 0.12.8), read again here for the Python, system and libc fields. LH011's scripts are adapted, each docstring saying what changed. None of this makes LH012 a replication of LH011; its first-day question on November to March releases is the only part read on releases LH011 did not read.
Intended output
LH012.md (version 0.1), sources.md, data/ with read_log.csv and every query result the analysis uses, scripts/ with an offline analysis and a replay command over the retained results, a line in investigations/software-updates.md, and a piece only if the record earns one.
What was read before this brief (phase 1), all logged in data/read_log.csv
On 29 September 2026, UTC. None of it is a download count.
- ClickPy, 9 queries, 06:19:15 to 06:19:22, 272,080,987 rows read: the server's version (26.10.1.39301, time zone UTC) and limits; the
pypidatabase's table definitions and the columns of eleven tables; the first and last day held for the top 500 (pypi_downloads_max_min: last day 28 September for all 500) and, since 1 September 2025, for the 37 in five per-day tables (last day 28 September for all). The phase gate in scripts/common.py refused any query that could return a count. - PyPI JSON, 628 requests, 06:19:49 to 06:23:15: the project JSON of each of the top 500 (all returned 200), and 128 single-version reads of dependents' versions that were newest during W. Retained: data/versions.csv (9,431 versions of the 37: Requires-Python per file, sdist, wheel interpreter, ABI and platform tags, yanked), data/files.csv.gz (65,913 files of the 37), data/dependent_versions.csv (42,304 versions of 311 projects), data/top500_screen.csv, requirements.csv (1,533 requirement rows naming one of the 37), week_versions.csv, and the offline frame from scripts/frame.py (reference.csv, python_admission.csv, pairs.csv, releases_nov_mar.csv).
- Source distributions, 2, 06:26:17: pip 26.2.1 and uv 0.12.8 from files.pythonhosted.org, for the fields they send (above).
- Documentation and literature, 11 page reads, 06:26:13 to 06:27:44 (one returned 404), and two web searches between 06:26 and 06:27; sources.md lists each, with what was opened and what was not. Also, pypdf 6.19.0 was installed from PyPI into scratch space between 06:19 and 06:23 to read the two PDFs, a download of the kind the sources count.
Bounded check of the primary literature
What could change the design or the reading, from pages opened on 29 September 2026 (sources.md):
- SPEC 0 recommends dropping support for Python versions three years after their initial release and for core package dependencies two years after theirs; NEP 29, which SPEC 0 supersedes, recommended supporting Python minors released in the prior 42 months. These are policies, context only: what any release supports is its own Requires-Python and files, read above.
- Decan, Mens and Constantinou (2018) measure technical lag in npm's dependency network from declared constraints over 1.4 million releases; Stringer, Tahir, Blincoe and Dietrich (2020) measure it across 14 package managers from libraries.io's declarations, and report that 10 to 20 per cent of PyPI's declarations there are fixed versions. Both measure what packages declare, not what is downloaded; neither says which versions are fetched. They support separating current-version holds from holds carried by older dependent versions, which the design does.
- The Python Packaging User Guide's page on analysing downloads names
details.pythonas the Python version (examples 2.7.12, 3.6.4); linehaul's parser takes the whole JSON user agent for pip and uv; ClickPy's documented import derivespython_minorfrom its first two parts and, in that import, drops rows with an empty Python or system. Whether the live ingestion drops them is not known, so "Python not reported" may be under-represented in ClickPy; LH011's cross-check against pypistats, which counts every row, agreed within 1.6 per cent for five projects on 15 days, which bounds how much was missing there. - pip's page on repeatable installs describes pinning every package and its dependencies with
==, the mechanism the frozen-list explanation names. - Hugo van Kemenade's "Python version share over time" series (2016 to 2019, via BigQuery and pypinfo) reads the Python field of pip downloads across all packages; it does not read which versions of a library are fetched.
- No published measure of which older versions a library's downloads sit on, or of what explains them, was found in two searches and the pages opened, which says little about whether one exists.
Decisions before the snapshot
Made by the driving session on 29 September 2026, before any count was read, on the six choices the writer left open. They are written into the sections above.
- The read week stays 21 to 27 September 2026, days LH011 also read (disclosed above): a week LH011 never read would mean a week's wait, and LH011 kept those days only at each project's numerator set, with older versions folded.
- B's co-fetch assumption stays, with a guard: the lower estimate can be inflated as well as the upper, so "accounts for most" by B also needs more than half of D's older fetches to sit at versions the held dependent admits; and the number test for tightly coupled pairs is stated in fetches, with ranges where coverage is incomplete.
- Extras are unknown, not left out: holding in B's upper estimate, not holding in its lower.
- Dependent versions read: up to 200 versions for the X of a tightly coupled pair, 25 otherwise, within a PyPI ceiling raised to 5,000 requests; versions left unread are unknown.
- Wheel exclusion is a secondary measure, reported beside P and never added to it.
- Length: the brief is kept whole.
- Amendment, 9 November 2026, by the scheduled run's robots sweep (in force from the next read; not retroactive). The keeper decided on 8 November 2026 that Lighthouse obeys robots.txt as its own agent and as whatever runs it (AGENTS.md, Rules that matter; scripts/robots_rules.py, scripts/robots_tokens.json). The gate is scripts/robots_gate.py, which reads each host's robots.txt once per run. Every read goes through scripts/common.py's curl helper (ClickPy, PyPI JSON, files.pythonhosted.org and documentation pages), which now asks the gate first; a URL not allowed is not requested and its row in data/read_log.csv has the status "not fetched: ..." with the deciding group and rule, a missing reading and never a negative. For ClickPy the endpoint URL is checked. The review re-read review/requery.py asks the gate too and stops at the first URL not allowed. The User-Agent now carries Lighthouse's token (
Mozilla/5.0 (compatible; Lighthouse-research/1.0; +https://lighthouse.harbour.cat)), and email addresses are withheld from ClickPy results and documentation pages before they are written. Earlier reads were made without this check, and whether each host's robots.txt allowed them on those days was not checked. The study is finished and no further read is scheduled.
From studies/LH012/brief.md in the repository, last changed 9 November 2026.