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.

Notes

Release review of the pins piece, its short form, the front page that carries it, and the record behind them

kind
review
version
0.1
date
2026-09-27
authors
the release reviewer, a Fable 5.1 subagent, dispatched by the driver
grounded at
a8a34e5 (HEAD; the record studies/LH008/, the piece, its figure and the short form were untracked in the working tree and README.md modified, all uncommitted, when this review began)
cites
articles/pins-move-over-weeks.md (version 0.9, draft, as found in the working tree); articles/pins-move-over-weeks.svg; articles/short/pins-move-over-weeks.md; README.md (the lead, the "What we observe" paragraph and table row, "The next question", the footer); studies/LH008/LH008.md version 0.1 (as found), brief.md (with three amendments), sources.md; studies/LH008/data/ (top200, osv_advisories, qualifying_advisories, events, event_advisory_ranges, releases_*, frame_counts, frame, screen, snapshot_files, moves, move_steps, lags, summary_by_frame, by_authorship, advisory_window, hazard, trailers, read_log, read_summary); studies/LH008/scripts/ (select_libraries, osv_ranges, common, collect, trailers, analyse, summarise_reads, draw_figure, draw_piece_figure); studies/LH007/LH007.md version 0.2 and articles/what-moves-a-pin.md (for the restatement of the earlier finding); notes/R-0007.md; AGENTS.md; programme.md (LH008 section, read, not edited). Live reads on 27 September 2026: sql-clickhouse.clickhouse.com at 08:01:48 UTC; api.osv.dev, 200 package queries and five records by id, 08:01:49 to 08:01:54; pypi.org JSON for the 16 projects with a qualifying advisory, 08:01:53 to 08:01:54; blobless clones of 17 repositories from github.com at 08:01:06 to 08:01:12, held in /tmp/lh008-review; the study's own clones in /tmp/lh008 for three secondary checks (bbot, vantage6, grooveshop-django-api) only

Findings

This is the release gate for articles/pins-move-over-weeks.md, its illustration, its short form, the front page's lead, paragraph, table row and next question, and the record studies/LH008/. The reviewer read the dispatch, AGENTS.md and R-0007, then the record, its brief and sources, its scripts, and the tables in data/, then the piece, the short form and the front page changes sentence by sentence against the record. It went to the primary sources as the dispatch asked: ClickPy's top 200 for August 2026 was queried again with the study's SQL; OSV was queried again for every one of the 200 projects and the five chosen records were read by id; PyPI's release lists were read for the 16 projects with a qualifying advisory, and the selection rule in brief.md was reapplied from those reads; 17 repositories were cloned afresh and 18 moves or non-moves (one repository in both kinds) were re-read with a detector the reviewer wrote after reading moves_one in scripts/collect.py, without sharing its code; every derived table in data/ was recomputed from lags.csv, moves.csv, screen.csv, trailers.csv and read_log.csv with the reviewer's own short Python; the rates in hazard.csv were recomputed and two of their windows were found to be wrongly bounded; and both figures were regenerated from their scripts and compared byte for byte before the reviewer changed them.

The verdict is that the piece, the short form and the front page can be released after the changes below, which the reviewer made. Every source reading that was repeated reproduced to the second and the byte: the 200 projects and their download totals, all 948 OSV records, the 68 qualifying advisories and the rule's choice of these four libraries, the eight release upload times, and all 18 moves (snapshot commit, first moving commit, committer time, authorship class). Every count in the tables reproduced. What had to change was two windows in the rates table whose labels did not hold (the "2 to 7 days, before every advisory" row contained five moves made after urllib3's advisory, and the "7 days before the advisory" row's denominator counted 135 repository-days before the fixes existed); the record's and piece's silence on the other seven GitHub advisories published against pyjwt and starlette the same evening; one urllib3 Dependabot move whose author time, 14.8 hours before the advisory, the record had noted as a discrepancy but not read; a count of the study's own network reads that counted its clones twice; and small phrasings in the piece. None changes the Answer's shape: two short spells of quicker moves, one of two days after the release and one of about a week after the advisory, at about the same pace, with most moves at neither; a fix moving about twice the share of pins within 30 days that a release no advisory named did. The release's spell is shorter than the first edition's second row implied.

The piece, sentence by sentence, against the record and its tables

Line numbers are the piece's as found. "Live" means the reviewer's own read on 27 September at 08:01 UTC; "recomputed" means from the tables in data/.

Piece passage Checked against As the record has it?
Line 3: "Between 19:28 and 20:16 UTC on Monday 15 June 2026 ... first pyjwt ... then cryptography, then starlette"; "cryptography's had been published almost six days before, starlette's 23 days before and pyjwt's 25" Live OSV: GHSA-xgmm-8j9v-c9wx 19:28:06Z, GHSA-537c-gmf6-5ccf 20:12:27Z, GHSA-wqp7-x3pw-xc5r 20:16:30Z, all HIGH, github_reviewed_at equal to published; live PyPI: 48.0.1 2026-06-09T22:30:53Z (5.9 days), 1.1.0 2026-05-23T16:55:39Z (23.1), 2.13.0 2026-05-21T19:54:35Z (25.0); 15 June 2026 is a Monday Yes; but ten GitHub advisories were published against the three libraries that evening, not three (finding 2)
Line 3: "five moved to its fix in the two days after that evening" (the writer's own count) Recomputed: within two days after the advisory, pyjwt 2 (cognirepo 1.34, talklib 1.96), starlette 1 (aindy-runtime 0.33), cryptography 2 (home-assistant/core 1.39, at_python 1.42); urllib3's three are on 12 May Yes, five
Line 5: "six of the thirteen pinned projects"; "102 pins held by 88 projects"; "The typical move came about nineteen days"; "about half were written by a bot or merged from one" LH007.md version 0.2 (6 of 13 within 28 hours); recomputed 102 pairs, 88 repositories, median 18.68 days, 38 of 73 Yes
Line 13: "16 had advisories ... 68 advisories in all. In every one of the 68, the fixed version had been published ... on an earlier day" Live OSV and PyPI: 68 GHSA records rated HIGH or CRITICAL, published 27 March to 27 August 2026 with a fixed version, against 16 projects; in all 68 the fixed release's upload date is at least a calendar day before publication; the smallest gap is 29.5 hours Yes
Line 15: "Last time, in one library, the second moment looked like the one that counted" articles/what-moves-a-pin.md line 49 Yes
Line 19: ranks 6, 10, 45, 46; "urllib3's fix came out on 7 May and its advisory four days later"; "9 June ... six days later"; "21 and 23 May ... more than three weeks later" Live ClickPy: identical 200 rows and totals, ranks 6, 10, 45, 46; live PyPI and OSV: 3.9, 5.9, 25.0 and 23.1 days Yes
Line 21: "screened up to 60 from each list"; "102 pairs ... from 88 repositories"; "91 more pairs" frame.csv: 60 marked in each frame of 60 or more, 54 and 22 for the two smaller frames; screen.csv: 436 screened, 201 kept, 193 pairs after the one-per-repository rule; recomputed 102 and 91 Yes
Line 25: "73 ... 28 ... one"; "18.7 days"; "half ... between one and five weeks"; "19 ... 54"; "13 of the 30" Recomputed 73, 28, 1; median 18.68, quartiles 7.29 and 35.20 days; 19 before, 54 after; pyjwt 10 of 25 and starlette 3 of 5 before Yes
Line 27: "4.1", "easing to about two (2.2) in the five days after that", "5.1", "4.3", "between 1.3 and 1.7" Recomputed hazard.csv row by row, identical; but the 2 to 7 day window contains 5 urllib3 moves after its advisory (day 3.9), and stopping at the advisory gives 5 moves in 330.1 repository-days, 1.51; from day 2 to the advisory for all four libraries, 1.41; the "7 days before" row's clean value is 1.94 No: 2.2 replaced by 1.4 (finding 1)
Line 29: "Four of the 102 moved within a day after the advisory"; "in the day after them one pinned project moved" Recomputed: 4 (urllib3 3, starlette 1); pyjwt 0 and starlette 1 in day 0 to 1 Yes
Line 31 and 33: the alt text and caption Regenerated byte for byte before the change; counts 18 of 32, 25 of 29, 5 of 7, 25 of 34 recomputed; urllib3's later moves fall on 1 June, 18 June, 15 July, 1 August, 26 and 31 August "from mid-June" changed to "from June"
Line 37: "21 of the 91 ... 23 per cent, against 50 of the 102 ... 49 per cent"; "0.6" Recomputed 21/91 = 23.1, 50/102 = 49.0, ratio 2.12; comparison 7 to 30 days 0.64 Yes
Line 39: "28 of the 43"; "21 came after the next advisory"; "cryptography ... 13 days ... 14 of its 20" Recomputed: 43 censored comparison pairs, 28 with no commit more than 30 days after the release; 21; GHSA-r6ph-v2qm-q3c2 on 10 February 2026, 13.0 days after 46.0.4; 14 of 20 Yes
Line 41: "about twice as many"; pyjwt NVD "28 May, a week after the fix and 18 days before"; "5 of its 25 ... none in the two days after" 2.12; live OSV nvd_published_at 2026-05-28T16:16:29Z (6.85 days after 2.13.0, 18.1 before the GHSA); summary_by_frame.csv last row, 5 and 0 Yes
Line 45: "22 ... 17 by Dependabot ... 5 by Renovate"; "16 ... 13 of them Dependabot's"; "35"; "38 of the 73"; "19 of 45" Recomputed: bot 22 (dependabot[bot] 17, renovate[bot] 4, k1h-renovate[bot] 1); merges 16 (dependabot[bot] 11, a Dependabot branch with a library-action[bot] tip, one named GitHubSecurityLab/dependabot/, renovate[bot] 1, github-actions 2); 35; 38; comparison 6 + 13 = 19 Yes; the two github-actions branches are a release branch (vantage6, "Release 5.0.2") and a sync of main into a feature branch (grooveshop-django-api), read from the study's clones, so "a branch a bot had opened" is qualified (finding 6)
Line 47: "three ... two ... three"; "Two of the three people's changes ... cite an advisory, or 'dependabot security alerts'"; "16 to 23 hours"; "Dependabot says which it was in its pull requests" Recomputed: bot 3, merging 2, person 3; aindy-runtime (four GHSA ids) and talklib ("security"); 0.671 and 0.935 days are 16.1 and 22.4 hours; live clone of geonode: author dependabot[bot] 2026-05-11T00:03:28Z, committer a person 2026-05-12T08:57:41Z; devsecops-engine-tools' merged tip authored 2026-05-11T17:02:31Z; cytomine's a squash (author time equals commit time) The counts yes; geonode's change was authored 14.8 hours before the advisory (finding 3); the sentence about Dependabot's pull requests stated an unread fact and is hedged (finding 7)
Line 51: the restatement of the earlier finding articles/what-moves-a-pin.md line 49: "If that holds more widely, a release reaches pinned software mostly when there is a security reason to take it" Yes
Line 55 and 57: the limits LH008.md Limits; frame_counts.csv (100 listed of 241, 288, 250 and 233); screen.csv 110 of 436 with no repository (25.2 per cent) Yes
Colophon: "948 advisory records"; the five GHSA ids and dates; "as last modified by 10 September 2026"; the eight frame versions; "217 repositories"; "7 May to 9 June" and "10 April 2025 to 22 March 2026"; "07:17 and 07:40", "07:45 and 07:47"; "Twelve of the 118 commits ... eight of them models of the same family"; "about 1,100 requests" Live OSV 948; ids and dates as above, modified 2026-09-10 on all five; events.csv; read_log.csv 217 clones of 264 attempts; releases_*.csv; read_log.csv 07:17:14 to 07:26:19 and the record's stated file reads to 07:39:58; trailers.csv 12 (Claude 8, Copilot Autofix 2, Cursor 1, Gemini Code Assist 1); read_log.csv holds 877 HTTP requests and 264 clone rows Yes, except the request count (finding 4)

The record against the primary sources

Reading Record Reviewer Notes
ClickPy top 200, August 2026 data/top200.csv identical, all 200 projects and totals (08:01:48 UTC) The study's SQL, run anonymously as demo
OSV for all 200 projects 948 records; 68 HIGH or CRITICAL GHSA records with a fix published in the window, against 16 projects 948; 68; the same 16; in all 68 the fixed release's PyPI date is at least a calendar day before publication (smallest gap 29.5 hours) The rule in brief.md chooses urllib3, cryptography, pyjwt and starlette as the first four by rank; litellm (48) and aiohttp (49) are next
The five chosen GHSA records by id events.csv, event_advisory_ranges.csv identical: published, github_reviewed_at, nvd_published_at, severity, aliases, ranges (1.23 to 2.7.0; 2.6.0 to 2.7.0; 0.5.0 to 48.0.1; 0 to 2.13.0; 0 to 1.1.0), modified 2026-09-10 GitHub's own pages were not read (403 anonymously in LH007)
The evening of 15 June "Three of the four advisories were published within 48 minutes" ten GitHub advisories against the three libraries between 17:28:46 and 20:39:53 UTC: pyjwt LOW 17:28:46, MODERATE 19:27:48, HIGH 19:28:06, MODERATE 19:28:41, MODERATE 19:29:12 (all fixed in 2.13.0); cryptography HIGH 20:12:27; starlette MODERATE 20:16:05 and HIGH 20:16:30 (fixed in 1.1.0), LOW 20:38:08 (1.3.0), HIGH 20:39:53 (1.3.1) Finding 2. Brief.md amendment 1 named starlette's three other records but not pyjwt's four
PyPI upload times events.csv (eight releases and the four before the fixes) identical to the microsecond, and starlette 1.3.0 2026-06-11T06:27:40Z, 1.3.1 2026-06-12T09:23:10Z
The selection rule's timing brief.md: written after one ClickPy request at 07:17 and before any list, advisory or release was read read_log.csv: list tables at 07:17:14, the next request (top 200) at 07:19:49 The brief's rules run to about 11,000 characters; that they were written in those 2.5 minutes is consistent with the log and cannot be checked further
18 moves from fresh clones (starlette-authlib, aindy-runtime, docreview360; weblate, at_python in both kinds, now-lms, certgrinder; fastapi-framework, talklib, cognirepo, commodore; cytomine, geonode, devsecops-engine-tools, codeforlife-portal, euvd-cli, seclab-taskflow-agent) moves.csv, snapshot_files.csv identical: default branch, snapshot commit, files and versions held, first moving commit (or none), committer time, author kind, merged tip's author; docreview360 censored with poetry.lock at 0.49.1 and a later uv.lock at 1.6.0; certgrinder unchanged since 1 June; euvd-cli's last commit 9 May The reviewer's detector parses lockfiles, == pins and bare Poetry versions independently of collect.py
moves_one in scripts/collect.py Read in full; the first-parent pairing, the git diff-tree --stdin parsing (the header line is the bare commit id, tested on geonode), the lowest-version rule, the "gone and another file at or above" rule, and the version compares are as the brief states; no logic error found Two edge cases noted, neither affecting a row here: a lockfile version that packaging cannot parse would raise, and a snapshot's files are capped at 80 while later commits are not
Every derived table summary_by_frame, by_authorship, advisory_window, hazard, frame counts, pin classes, trailers, read summary identical from lags.csv, moves.csv, screen.csv, trailers.csv and read_log.csv The "45 to 54 times" of the censored active pairs, the 16 of 28 and 28 of 43 inactive, and the two false security-word hits (GitHubSecurityLab, blacklanternsecurity) reproduced
hazard.csv's windows "2 to 7 days (before every advisory)" 2.23; "7 days before to the advisory" 1.50 2 to 7 days holds 5 urllib3 moves 0.67 to 2.25 days after its advisory; stopping at the advisory: 5 moves, 330.1 repository-days, 1.51. The "7 days before" row's start is 3.1 days before urllib3's release and 1.1 before cryptography's, 135 repository-days in which no move was possible; from the release at the earliest: 9 moves, 464.3 days, 1.94 Finding 1
Both figures rates.svg, articles/pins-move-over-weeks.svg regenerated byte for byte from their scripts before any change; regenerated again after Not rendered by the reviewer

The writer's points, and what else was found

  1. Must fix, fixed. The rates' windows. urllib3's advisory came 3.9 days and cryptography's 5.9 days after their releases, so a window from day 2 to day 7 after the release straddles them, and five of its ten moves are urllib3's in the day after its advisory. The record's table called the row "before every advisory" and the piece read it as the release's spell "easing to about two". Stopping the window at the advisory where it came first gives 1.51, the pace of the weeks between, so the release's spell is two days, not a week. The "7 days before the advisory" row began before the releases for two libraries; it now begins at the release, and is labelled as mixing the release's days. scripts/analyse.py's hazard section was rewritten to take window bounds per pair, data/hazard.csv regenerated (every other table byte-identical), scripts/draw_figure.py's panel and note changed and rates.svg regenerated. The record's Answer, rates table and note, the piece's line 27 and the short form's rates sentence now carry 1.4 (all four libraries, from day 2 to the advisory) in place of 2.2, and "between 1.3 and 1.7" is supported by the rows 1.34, 1.41 and 1.66.

  2. Must fix, fixed. The evening of 15 June. The record, the piece, the short form and the figure spoke of three advisories that evening. OSV lists ten GitHub advisories against the three libraries between 17:28 and 20:40 UTC, four of them rated high (a second against starlette, fixed in 1.3.1, released 12 June). Every one of the ten named a fix already on PyPI. The record's Findings and Limits now list them and say that a repository reacting to any of them is counted against the chosen one (aindy-runtime moved to 1.3.1 with a message naming four starlette advisories); the piece's opening says "We followed the three"; the short form says "among ten against them that evening"; the figure's annotation says "the three advisories we followed".

  3. Must fix, fixed. geonode's author time. The record noted that geonode's author and committer times differ by more than a day, and its interpretation paragraph said the three urllib3 Dependabot moves fit a security update opened on the advisory. Read from a fresh clone, the commit was authored by dependabot[bot] at 00:03:28 UTC on Monday 11 May, 14.8 hours before the advisory, and committed to master by a person at 08:57 UTC on 12 May, so the proposal was open before the advisory. devsecops-engine-tools' branch tip was authored 2.2 hours after the advisory; cytomine's squash carries no proposal time. The record's Findings and Limits and the piece's line 47 now say so, and the "fits a security update" reading is narrowed to the two.

  4. Fixed. The count of the study's own reads. "217 clones and about 1,100 requests to public APIs" counted the 264 clone rows among the requests; read_log.csv holds 877 HTTP requests and 264 clone attempts. The record's header and the piece's colophon now say so.

  5. Kept. "One pinned project moved in the day after" for pyjwt and starlette. Recomputed: pyjwt 0, starlette 1 (aindy-runtime, 0.33 days), one in all, as the piece says.

  6. Should fix, fixed. "A branch a bot had opened." Two of the 16 merged branches counted as a bot's are vantage6's release branch and grooveshop-django-api's sync of main into a long-lived branch, each with a github-actions tip; the record's Limits said so and the piece did not. The piece's line 45 now qualifies the 16.

  7. Should fix, fixed. "Dependabot says which it was in its pull requests." Neither the record nor the piece read a pull request, and the sentence stated as fact what the record lists as unread. It now says the pull requests would say which, and that we did not read them. The other outside facts were checked as far as the reviewer could: Dependabot is GitHub's, the National Vulnerability Database is the US government's, OSV is an open database that aggregates GitHub's advisories among others, and the library descriptions (urllib3 an HTTP client, pyjwt a JSON Web Token library, starlette a web toolkit) are fair in plain words.

  8. Note. Kept. "About twice as many pins in its first month": 50 of 102 (49.0 per cent) against 21 of 91 (23.1 per cent), a ratio of 2.12. "About four in every hundred waiting projects moved each day": 4.11 per 100 repository-days. "Restatement of the earlier finding" and "What this does to what we said before": the quoted sentence is in the earlier piece at line 49, and LH007's counts (six of thirteen within 28 hours, four of the six Dependabot's) are as its record version 0.2 has them.

  9. Note. Kept. The colophon's other details (948 records, the five ids and their modified dates, the eight frame versions, the release and comparison date ranges, the read windows, 12 of 118 commits with an AI co-author trailer, eight of them Claude models) reproduce. The reviewer filled the review line and set the record version to 0.2; the Draft labels and "Version 0.9, draft" stay for the driver.

  10. Note. Kept. No em or en dash in the piece, short form, front page, record, brief, study sources, either figure or the figure scripts (checked by code point; the only non-ASCII character in the reader files is the middle dot in the bylines). British spellings ("program" for software is kept). No internal reference in the reader files' prose; the colophon names the model and links the record, as the earlier pieces do. Every relative link in the piece, short form and README resolves. No project is named as vulnerable in any file; the piece says "nothing here says that any project was exposed", the record "no project here is said to have been exposed", and the short form and front page carry the same. The observation and interpretation labels in the record are where a reader needs them; the piece marks its interpretation with "we think" and "we cannot say what did it". No claim of harm now, no named compromise, nothing irreversible: the study and this review made anonymous reads only and created no account.

  11. Note. Kept. The front page's lead, paragraph, table row, next question and footer agree with the record as corrected; none of them carries the 2.2 figure or the "three advisories" count, and the reviewer made no change to README.md.

What the review could not check

  • The other 175 moves and non-moves: the study's tables, taken as published; the 18 re-read agreed in every field.
  • The 436 screening reads and Open Source Insights' lists: not repeated; the frame counts, hash ranks and screening decisions were recomputed from frame.csv and screen.csv, and the lists as served today may differ from the 07:24 read.
  • Whether the brief's rules were written before 07:19:49, when the top 200 were read: consistent with the log, not provable from it.
  • Whether GitHub's squash and merge commits were made by a person or by auto-merge: the committer is GitHub's identity and the record cannot tell.
  • Dependabot's pull requests, GitHub's own advisory pages and the releases' notes: not read by the study and not by the review; the GitHub API is refused here.
  • The figures were regenerated and their text read, not rendered.
  • The review's own cost is not measured here; the driver prices it from the transcript. Its network use: one ClickPy query, 205 OSV requests, 16 PyPI reads, 17 blobless clones (209 MB on disk), and no other request.

Changes made by the reviewer

  1. studies/LH008/scripts/analyse.py: the hazard section takes per-pair window bounds; new and relabelled windows. studies/LH008/data/hazard.csv: regenerated (16 rows). Every other table in data/ is byte-identical after the rerun.
  2. studies/LH008/scripts/draw_figure.py and studies/LH008/rates.svg: the second bar's window and the panel note; the description; regenerated.
  3. studies/LH008/LH008.md: version 0.2; header (authors, cites, the count of reads); the Answer's rates sentence; the advisories paragraph (the ten that evening); the rates table and its note; the urllib3 Dependabot paragraph; two Limits entries; the Corrections entry.
  4. studies/LH008/scripts/draw_piece_figure.py and articles/pins-move-over-weeks.svg: the annotation and the description; regenerated.
  5. articles/pins-move-over-weeks.md: the opening paragraph; line 27; the alt text; lines 45 and 47; the colophon (reads, record version, the review line).
  6. articles/short/pins-move-over-weeks.md: the opening sentence; the rates sentence.
  7. README.md: unchanged by the review.

The sentences of version 0.1 that were replaced, for the record: the rates table's rows "fixed release | 2 to 7 days (before every advisory) | 10 | 448.2 | 2.23" and "advisory | 7 days before to the advisory | 9 | 599.3 | 1.50"; the Answer's "against 1.3 to 1.7 per 100 repository-days in the weeks between the two and in the three weeks after the advisory"; the Findings' "they fit a security update opened when the advisory was published, and they fit equally a weekly version-update schedule"; the Limits' "which does not change which window it falls in"; and the header's "217 clones of public repositories and about 1,100 requests to public APIs".

Verdict

Nothing remains that must be fixed. The piece, the short form and the front page's lead, paragraph, row and next question can be released as they now stand; the driver removes the Draft labels, sets the piece's and short form's version and date on release, adds the releases.md line, and commits the record at version 0.2 with the piece. Version 0.1 of the record was never committed; this note quotes the sentences it changed.

From notes/R-0008.md in the repository, last changed 27 September 2026.