Lighthouse

Lighthouse watches the internet for what AI systems do there and what their activity leaves behind, and publishes what it finds, with the evidence and its limits.

Studies LH012

LH012 amendments

brief.md is kept byte-identical to the snapshot posted on issue #14 at 06:35 UTC on 29 September 2026 (https://github.com/JKershaw/lighthouse/issues/14#issuecomment-5885000372; the hashes are copied into data/snapshot_sha256.txt and checked by replay.sh). Later changes are recorded here, dated and numbered, and labelled post hoc when made after any count was seen.

Amendment 1, 29 September 2026: how phase 2 reads, fixed before any count

Written by the phase 2 research agent (Opus 5.5) after the snapshot and before any download count was read. Not post hoc. None of it changes a measure, a rule or a convention of the brief; it fixes practical choices the brief left open.

  1. A second log. Phase 2's reads are logged in data/read_log_phase2.csv, so that the snapshotted data/read_log.csv stays byte-identical. scripts/common.py is unchanged; scripts/phase2.py points its logger at the new file, sets the phase to 2, and sets each ceiling to the brief's less what phase 1 used (891 ClickPy queries, 39,727,919,013 rows read, 4,372 PyPI requests, 17 page reads). replay.sh checks both logs together against the brief's ceilings.
  2. Order of the PyPI reads, so that a ceiling, if reached, leaves the least-downloaded dependents unknown rather than an arbitrary set: first every version at or above R of each of the 37 (for the one-step rule), then the dependents in order of their rank in LH010's top 500, each dependent's versions in descending order of W downloads, as the brief says. Versions already read in phase 1 (the latest at the read and those newest during W) are not read again.
  3. Splitting queries. A query whose result would pass 10,000 rows, or that fails a limit, is rerun split by day and then by Python minor. A failed query is logged and counts against the ceilings.
  4. Classes in pypi.pypi. The two passes of pypi.pypi classify versions in the query by two lists from the metadata: the versions at or above R, and the pre-releases above R ("other"). Every other version string, including any PyPI does not list, falls into "older" in those passes; the volume of versions ClickPy holds that PyPI does not list is measured from the per-version table and reported, so the size of that mixing is known.
  5. Python exclusion over the week. pypi_downloads_per_day_by_version_by_python is read as W sums per version and Python minor, not per day, because the metadata shows that no project's set of excluded Python minors changes during W (every day's set equals the week-end set in data/python_admission.csv, checked before any count). Dependents' version mixes and the projects' own are read per day, since a hold depends on which versions of D were available on the day. A dependent's Python marker is evaluated with its W sums by version and Python minor.
  6. The file-name pass covers the ten projects whose R has a wheel with a platform tag other than any: aiohttp, cffi, charset-normalizer, cryptography, litellm, numpy, pandas, protobuf, pydantic-core and rpds-py.
  7. The first-day reads take, for each project with a November to March release, the per-version table on days 1, 2 and 30 of each release, with versions below the project's earliest such release folded into one row a day, and the 37 projects' daily totals from 1 October 2025 to 31 May 2026 for the gap rule.
  8. The "two ways" table counts a pinned pair as moved by day d when LH008's lag_release_days (committer time, days after the fix's upload) is at most d, as LH011's post hoc reading did for day 30, and takes the at-or-newer share on day d from LH011's analysis/release_days.csv.

Amendment 2, 29 September 2026: the file pass groups by the file's tags

Written by the phase 2 research agent at about 06:45 UTC, after the first attempt at the file-name pass for aiohttp failed: the week, each day, and each day split by Python minor all passed ClickPy's 10,000-row limit, and ClickHouse streamed the error into bodies that began with HTTP 200 (logged in data/read_log_phase2.csv as labels "W4 aiohttp files"). Those responses held no count. One count had been seen by then: while diagnosing the failure at about 06:41 UTC, the agent printed four rows and the class totals of aiohttp's grouped pass (W3; about 36 million older and 70 million at-or-newer downloads over W). The change below does not depend on it; it is recorded so that a reader can judge. The brief's file pass ("older downloads grouped by file name and Python minor") is read instead grouped by Python minor and the downloaded file's interpreter, ABI and platform tags, taken from the file name in the query (the last three dash-separated parts of a wheel's name; "(not a wheel)" for an sdist or other file, "(no file name)" where the column is empty). The wheel-exclusion test uses only those tags, so the measure is unchanged. The collector also now treats a 200 response whose body carries a ClickHouse exception as failed, and splits it; the early W1 and W2 reads of boto3 and botocore that ended this way were discarded and read again.

The same day, before any count of it was seen, boto3's W2 read (W sums by version and Python minor) passed the row limit whole and in two halves; it and any other such read is split by single Python minor instead (each minor from 2.7 and 3.4 to 3.15, the empty value, and the rest).

Amendment 3, 29 September 2026: readings fixed in the analysis code before its output was seen

Written at about 06:50 UTC, when scripts/analyse.py was written and before it was first run; by then the collector's logs had been watched for progress and errors only, and no table of counts had been looked at apart from the aiohttp rows named in amendment 2. Not post hoc. Where the brief leaves a choice:

  1. Python edges count only for excluded Pythons. The frozen-list trace's "Python edge E(p) for their Python" is applied only where p is excluded: for a Python that some version at or above R admits, a fresh resolve lands at or above R, so the highest version below R is no landing point.
  2. Z for a version that never was its project's newest (a backport uploaded after a higher version) resolves D at that version's own upload time. A version still newest at the end of W resolves D at the end of W.
  3. The number test uses X's own bound only: holds through another of the 37 (the one-step rule) count in B but not in F or Z.
  4. B's guard cannot be computed when the largest dependent's holds come through the one-step rule (the versions of D compatible with it are those whose own requirement on the third project fits, and older versions' requirements are mostly unread); such a project is "undetermined" for B if its lower estimate exceeds one half, and named.
  5. Markers. A requirement with a Python marker is evaluated per Python minor of the dependent version's W downloads; its downloads with no Python reported are unknown. A requirement with a platform or implementation marker that would hold counts as unknown. A requirement under an extra that would hold counts as "extra, unknown": in B's upper estimate, not its lower. A dependent version whose requirements were read and name nothing that holds is "not holding"; one never read is unknown.
  6. Gap days of D are left out of both D's and its dependents' W sums. The 37 projects' daily totals were also read for 14 to 28 September 2026, for the gap rule on W (amendment 1 named only the first-day window).
  7. Settling: a release whose day 2 or day 30 is a gap, or has no row, has an unknown Δ.
  8. Across projects, "cannot be the main reason" counts include "accounts for little"; bootstrap intervals are percentile intervals over 10,000 resamples of projects with a known class, seed 20260929.

Amendment 4, 29 September 2026: wheels tagged any are not testable for wheel exclusion (post hoc)

Written by the release reviewer (Fable 5.1, notes/R-0016.md) at about 08:05 UTC, after every count and the analysis output had been seen, so post hoc. The brief's secondary file measure calls an older download wheel-excluded "when no version at or above R has a wheel for the same operating system family and architecture" as the downloaded file. A wheel tagged any names no operating system or architecture, so the test cannot be made for it; version 0.1's code counted such a download as wheel-excluded whenever the newer versions ship only platform-tagged wheels, which is how 58.1 per cent of litellm's older downloads (its older py3-none-any wheels, against its newer cp310-abi3 platform wheels) came to be reported as excluded. By the brief's rule that unknown stays unknown, scripts/analyse.py now sets those downloads apart as "wheel tagged any, platform not known", and data/analysis/files.csv was regenerated by replay.sh --write. Only litellm's figure changes materially (58.1 to 0.0 per cent, with 58.1 in the new class); aiohttp's moves from 33.5 to 33.4. The measure is secondary and never entered P, so no classification changes.

From studies/LH012/amendments.md in the repository, last changed 29 September 2026.