Release review of the announced-fixes piece, its short form and figure, and the record behind them
Findings
This is the release gate for articles/fixes-that-said-so.md, its figure, its short form and the record studies/LH009/. The reviewer read AGENTS.md and R-0008, then the record, its brief and sources and the tables in data/, then the piece, the short form and the figure's text sentence by sentence against the record. It went to the primary sources as the dispatch asked: seven fixed releases' changelog entries were re-read at their tags from fresh clones (pyjwt 2.13.0, gitpython 3.1.47, urllib3 2.7.0, mako 1.3.11, starlette 1.1.0, python-multipart 0.0.27, cryptography 48.0.1), with the tag's type, the tag's commit time and the annotation, and gitpython's 3.1.48, 3.1.49 and 3.1.50 entries beside them for the judgement calls; 14 releases' first upload times and descriptions were read from PyPI and 18 advisory records from OSV; twelve moves in eleven dependents' repositories were re-read with a detector the reviewer wrote without reading collect.py's; every rate in data/hazard_by_class.csv, every share and count in summary_by_class.csv and by_authorship.csv, the leave-one-out rates, the event gap medians and the distinct-repository counts were recomputed from lags.csv, fix_classes.csv, fix_texts.csv and screen.csv with the reviewer's own short Python; and the figure was regenerated from its script and compared byte for byte.
The verdict is that the piece, the short form and the figure can be released after the changes below, which the reviewer made. Every source reading that was repeated reproduced: the seven entries' words and their absence of security words where the table says none, the tag commit times to the second, the 14 upload times to the microsecond, the 18 advisory publication times, severities, NVD dates and ranges, and all twelve moves (snapshot commit, first moving commit, committer and author time, authorship, merge, message). Every rate and count in the tables reproduced. What had to change was two counts in the record's prose that its own table contradicted (18 announced and 19 silent, where the table and the piece have 19 and 18; "164 packages in 155 repositories", where 155 is pairs and the repositories are 136), one loose phrase in the piece ("only days earlier" for a gap of a week), and the colophons' review lines. None changes the Answer's shape: the same share of moves before the advisory in both classes, about 1.7 times the pace from release to advisory for announced fixes, the difference sitting in days two to seven and in people's changes, on 13 moves against 5, in a design where class and library cannot be separated.
The record against the primary sources
| Reading | Record | Reviewer | Notes |
|---|---|---|---|
pyjwt 2.13.0, CHANGELOG.rst at tag 2.13.0 (a lightweight tag on commit 7144e45) |
announced; "Security" section; GHSA-xgmm-8j9v-c9wx named; commit 2026-05-21T19:34:15Z, 0.34 h before upload | identical: the v2.13.0 entry opens with a "Security" heading and names five GHSA ids, the first GHSA-xgmm-8j9v-c9wx; commit time 15:34:15 -0400 | The piece's opening sentence is as the file has it |
gitpython 3.1.47, doc/source/changes.rst at annotated tag 3.1.47 |
announced; "Address various security issues related to bypassing injection-protection of unsafe Git flags"; commit 2026-04-22T02:35:03Z | identical; tagger 02:35:27Z; the entry names no GHSA id | |
urllib3 2.7.0, CHANGES.rst at annotated tag 2.7.0 ("Release: 2.7.0") |
announced; "Security" section naming both GHSA ids; commit 16:01:47Z, tagger 16:03:24Z | identical | |
mako 1.3.11, doc/build/changelog.rst at rel_1_3_11 (lightweight, commit 217822b) |
announced by the flaw named: "could bypass the directory traversal check ... allowing reads of arbitrary files"; no security word; commit 20:18:18Z, upload 20:19:51Z | identical at the tag and at the head; no security word in the entry; the advisory is "Path traversal via double-slash URI prefix in TemplateLookup" | The judgement is within amendment 1's rule (a traversal, a bypass of a check) and the strict class without it is reported |
starlette 1.1.0, docs/release-notes.md at 1.1.0 (commit a4ff83b) |
silent, near: "Reject absolute paths in StaticFiles.lookup_path" |
identical; the entry's three lines carry no security word; the advisory is "SSRF and NTLM credential theft via UNC paths in StaticFiles on Windows" | |
python-multipart 0.0.27, CHANGELOG.md at 0.0.27 |
silent, near: "Add multipart header limits" | identical; two lines, no security word; advisory "Denial of Service via unbounded multipart part headers" | |
cryptography 48.0.1, CHANGELOG.rst at annotated tag 48.0.1 |
silent, near: "Updated ... wheels to be compiled with OpenSSL 4.0.1" | identical, one line; the advisory is "Vulnerable OpenSSL included in cryptography wheels" | The piece quotes both correctly |
| gitpython 3.1.48, 3.1.49, 3.1.50 entries | 3.1.48 silent ("Safe reference creation in the face of untrusted input"), 3.1.49 announced by the flaw named ("inject"), 3.1.50 silent | as the files have them; consistent with the rule in amendment 1 | A reader could count 3.1.48's "untrusted input" as naming a flaw; it is not an event and the record says the judgement is the agent's |
| PyPI first upload times, 14 releases (the seven above, pyasn1 0.6.4, msgpack 1.2.1, soupsieve 2.8.4, gitpython 3.1.58, litellm 1.83.0, aiohttp 3.14.3, lxml 6.1.0) | fix_classes.csv uploaded |
identical to the microsecond; only lxml's description names its version, as the table's description_part has it |
|
| OSV, 18 records (the events' advisories for the seven libraries, pyasn1's three, msgpack's, soupsieve's two, mcp's two, mako 1.3.12's) | fix_classes.csv first_advisory; NVD dates in the Findings (pyjwt 28 May, pyasn1 7 days before, mcp 1 day before); mcp's range from 1.23.0 |
identical: published times to the second, all HIGH, nvd_published_at 2026-05-28T16:16:29Z for pyjwt, 14 July for pyasn1's three, 15 July for mcp's; GHSA-hvrp-rf83-w775 (the event) introduced 1.23.0 |
|
| Twelve moves from fresh clones: dcspy, netbox-manager, repository-updater, commodore, git-batch (gitpython, all within two days); kerbside (mako, a Dependabot branch merged 0.98 days after the advisory); codeforlife-portal and rapid-router (pyasn1, one person, 08:16 and 08:46 UTC on 14 July); grimoirelab for pyasn1 and for soupsieve; an-website (soupsieve); hermes-agent (pyjwt, the one security-word message before an advisory) | moves.csv, lags.csv, LH008's moves.csv for hermes-agent | identical in every field read: snapshot commit, first first-parent moving commit, committer and author time (to the second), parent count, author, first line, files moved and version moved to; lags 1.288, 1.191, 0.214, 1.269, 0.604, 3.022, 5.295, 5.316, 69.397, 25.735, 6.295, 18.809 days | The reviewer's detector reads first-parent history and grep-matches the library name followed by the fixed version within a window, independently of collect.py |
| Classes counted from fix_classes.csv | Answer and Findings: 18 announced, 19 silent; 15 by a security word, 14 in the changelog at the tag; "eleven of the 18" | 19 announced (16 by a security word, every one in the changelog at the tag, lxml's also in the annotation and description; 3 by the flaw named), 18 silent; the Findings' own by-library sentence (gitpython nine announced and two silent) adds to 19; the piece and short form say 19 and 18 | Finding 1, fixed |
| GHSA ids named ahead of publication | 11 of the announced entries, 4 among the events, 1.2 to 25 days ahead | 11 (urllib3, pyjwt, pyasn1, msgpack, gitpython 3.1.51 to 3.1.58); leads 1.2 (msgpack) to 25.0 days (pyjwt) | |
| The frames and screening | 332 screened; 69 no repository, 27 refused, 22 no commit before the release, 50 not pinned; 164 kept, 65/56/43 by pin class; "164 packages in 155 repositories"; 257 pairs from 204 repositories, 19 in both classes | 332; 69, 27, 22, 50; 164; 65, 56, 43; the 164 kept packages sit in 136 distinct repositories and give 155 pairs; 257 pairs, 204 repositories, 19 in both classes; aiohttp's frame 4, msgpack's 1 with no repository, langchain's 0; pillow's list 100 and litellm's 68, the only two over 60 | Finding 2, fixed |
| The releases before the fixes | "came days before the fix" | aiohttp 3.14.2 2.25 days before 3.14.3; langchain 0.3.29 1.8 days; msgpack 1.2.0 7.5 days | Finding 3 |
| Every rate in hazard_by_class.csv (class and strict class, all ten windows), the "near misses counted as announced" rows quoted in Limits, the shares, medians, authorship table, bot names, leave-one-out rates, event gap medians | as tabulated | identical from lags.csv and fix_classes.csv: 29 of 105 and 17 of 68; 2.36 on 1,228.8 against 1.36 on 1,246.1; 3.77 (11) and 3.99 (8); 3.22 (13) against 1.45 (5); 0.94 (5, all pyjwt) and 0.87 (4); 5.56 (13) and 3.63 (6); 2.71 (14) and 2.75 (10); strict 2.43 and 1.33; near misses 2.02 (40) and 1.22 (6); medians 3.9 and 10.6 days; bots 7 of 11 and 4 of 8; people 9 of 13 and 2 of 5; 44 of 105 and 27 of 68 bot-written or merged; leaving out gitpython 1.84, silent 1.13 to 1.68 | The reviewer's at-risk rule is the record's sentence: from the window's start until the pair moves, is removed or is cloned, windows from the release stopping at the advisory |
| AI co-author trailers | 19 of 173, 10 from LH008 and 9 from LH009, 16 naming Claude models | 19 (10 and 9); the nine LH009 trailers all name Claude models, one also Copilot Autofix | |
| The study's own reads | 238 clone attempts, 211 successful; 606 requests; 844 log rows | read_log.csv: 238 clone rows, 211 ok; 213 PyPI, 72 release pages, 12 OSV, 309 Open Source Insights, 606; 844 rows | |
| The figure | articles/fixes-that-said-so.svg | regenerated from scripts/draw_piece_figure.py, byte-identical; well-formed XML; its 24 rate labels match hazard_by_class.csv; its footer's counts match the record | Not rendered by the reviewer |
The piece, sentence by sentence, against the record
| Piece passage | Checked against | As the record has it? |
|---|---|---|
| Line 3: Thursday 21 May 2026; "began the new version with a section headed Security"; GHSA-xgmm-8j9v-c9wx beside the first fix; published 25 days later on the evening of 15 June | Live clone and OSV: the entry's first heading after the version is Security, the first bullet names that id; upload 19:54 UTC 21 May (a Thursday; 15 June is a Monday), published 19:28 UTC 15 June, 24.98 days | Yes |
| Line 5: "ten of its 25 moves"; the summary of the finding | summary_by_event.csv pyjwt 25 moved, 10 before; the Answer | Yes |
| Line 11: 68 advisories, 200 projects, late March to late August; 37 fixed releases across 16 libraries | LH008 as reviewed in R-0008; fix_classes.csv 37 rows, 16 libraries | Yes |
| Line 13: the three sources and the rule | brief.md | Yes |
| Line 15: "19 were announced and 18 silent, and none said so only later"; every announcement in the changelog at the release; "between under a minute and ten hours"; lxml's also in tag message and description; "Eleven of the 19 ... between about a day and 25 days" | fix_classes.csv: 19, 18, none late; basis of all 19 the changelog at the tag; hours_after_upload -0.01 to -9.66; lxml's security_word_in; 11, leads 1.2 to 25.0 days |
Yes |
| Line 17: nine near; the starlette and cryptography quotations and their advisories' subjects; eight with no notes (five litellm, two mcp, one langchain); release pages refused | flaw_judgements.csv (nine near-miss notes naming a change); live clones and OSV summaries; fix_classes.csv; read_log.csv 72 refusals | Yes |
| Line 21: eight and eight; "between a day and seven weeks" | fix_classes.csv events; gaps 1.23 to 48.11 days | Yes |
| Line 23: up to 60, hash order, kept rule, read to 27 September; msgpack and langchain "one of each kind" gave nothing; "only days earlier" | brief.md; screen.csv; releases_*.csv: langchain 1.8 days, msgpack 7.5 days | Changed to "a week or less earlier" (finding 3) |
| Line 25: 257 pairs, 204 repositories, 152 on seven and 105 on seven; 105 and 68 moved | lags.csv | Yes |
| Line 29: 29 of 105 (28 per cent), 17 of 68 (25 per cent) | Recomputed | Yes |
| Line 31: medians 3.9 and 10.6; 2.4 against 1.4, "about 1.7 times" | Recomputed 3.905, 10.625; 2.36, 1.36, ratio 1.74 | Yes |
| Line 33: 3.8 on 11 and 4.0 on 8; bots 7 of 11 and 4 of 8; 3.2 on 13 against 1.5 on 5; people 9, bots 3, one merge; 0.9 on 5 (all pyjwt) and 0.9 on 4 | Recomputed | Yes |
| Line 35: 5.6 on 13 against 3.6 on 6; 2.7 and 2.8 | Recomputed | Yes |
| Line 37 and 39: alt text and caption | hazard_by_class.csv, the regenerated figure | Yes |
| Line 43: 44 of 105 (42 per cent), 27 of 68 (40 per cent); Dependabot and Renovate; "half or more" in the first two days | Recomputed 41.9 and 39.7; by_authorship.csv; bots 7 of 11 and 4 of 8 | Yes |
| Line 45: one security word among 46 moves before an advisory, a person's pyjwt move citing urllib3's advisories | lags.csv; live clone of hermes-agent: "fix(deps): bump urllib3 and PyJWT to clear CVEs", authored and committed 15:19 UTC 9 June | Yes |
| Line 51: gitpython 8 of 29, six in two days, four Renovate's; pyjwt 10; 1.8 against 1.1 to 1.7; 13 against 5, three in one organisation within 46 minutes | Recomputed; live clones: renovate[bot] authored three and a fourth was a person merging a Renovate branch; ocadotechnology 08:00:54, 08:16:45 and 08:46:53 UTC on 14 July | Yes |
| Line 53: mako alone on the flaw named, strict class the same; near misses moved would leave two silent libraries and six moves | hazard_by_class.csv strict and near-miss rows | Yes |
| Line 55 and 59: the limits; "For about half the fixes"; "in eleven cases" | 19 of 37; 11 | Yes |
| Line 63: gitpython nine and two, cryptography one and two | fix_classes.csv | Yes |
| Colophon: 37 releases uploaded 31 March to 4 August; sixteen advisories published 3 April to 3 August; the two GHSA ids and dates; 68 advisories as modified by 10 September; 204 repositories; read windows; 19 of 173 commits, sixteen of the same family; 211 of 238 clones and 606 requests | fix_classes.csv (litellm 31 March, gitpython 3.1.58 4 August; litellm's advisory 3 April, aiohttp's 3 August); live OSV; LH008 data; read_summary.csv; trailers | Yes; the review line and the record version were filled in |
The short form's every figure (37, 19, 18, eleven, 257, fourteen, seven, 28 against 25, 1.7, 13 against 5, 9 of the 13, 46, one) is in the table above. The figure's text carries the same twelve rates and counts and the same footer counts.
The writer's points, and what else was found
-
Must fix, fixed. The class counts in the record's prose. The Answer and Findings said 18 announced and 19 silent, 15 by a security word with 14 in the changelog at the tag, and "eleven of the 18"; brief.md amendments 1 and 2 said the same. data/fix_classes.csv has 19 and 18 (16 by a security word, all in the changelog at the tag; 3 by the flaw named), the Findings' own by-library sentence adds to 19, and the seven entries re-read at their tags agree with the table row by row. The piece and the short form already said 19 and 18. The record's two sentences and the brief's two amendments now carry the table's counts, the brief's with a bracketed dated note, and the record's Corrections entry says what changed.
-
Must fix, fixed. "164 packages in 155 repositories were kept." screen.csv's 164 kept packages sit in 136 distinct repositories; 155 is the number of repository and event pairs after a repository is counted once per frame (lags.csv's 155 LH009 rows, 136 distinct repositories). With LH008's 102 pairs from 88 repositories that gives the 257 pairs from 204 repositories the record and piece state, which reproduce. The sentence now says packages, pairs and distinct repositories separately.
-
Should fix, fixed. "Only days earlier." The piece said msgpack's and langchain's releases before the fix "had come out only days earlier"; langchain's came 1.8 days before, msgpack's 7.5. It now says "a week or less earlier". The record's "days before the fix" for aiohttp (2.25 days) stands.
-
Note. Kept. The judgement calls. mako 1.3.11's entry names the flaw in the terms amendment 1 sets (a bypass of the directory traversal check, arbitrary file reads) with no security word, so "announced (flaw named)" is as the rule says, and the strict class without it is tabulated and reproduces. starlette 1.1.0, python-multipart 0.0.27 and cryptography 48.0.1 name a change and no harm, so "silent, near" is as the rule says, and the record's and piece's "a reader who knew where to look could have guessed" is a fair reading of what the reviewer saw in the files. soupsieve 2.8.4's entry was not re-read from a clone; its quoted words are in fix_texts.csv. gitpython 3.1.48's "untrusted input" is the nearest call among the non-events and is not an event.
-
Note. Kept. The measure. The at-risk rule as the record states it in one sentence reproduces every one of the 60 rate rows exactly, so the scripts do what the record says. The share before the advisory (29 of 105 against 17 of 68) and the day-two-to-seven rates (13 on 403.5 days against 5 on 344.8) are as stated; the "three of the 13 in one organisation within 46 minutes" is codeforlife-package-python, codeforlife-portal and rapid-router, two of them re-read from fresh clones.
-
Note. Kept. The piece states the finding no more strongly than the record. The title and opening say the pace was faster and the share was not, and the opening's last sentence gives the confound; "class is library" has its own section with the leave-one-out figures; "13 against 5" and "no test of significance" are in the piece; "silent in what we could read" and the 403s are stated where the classes are introduced and again in the short form; "a move is a request" and "nothing here says that any project was exposed" are in the limits section; the interpretation is marked "we think" and given its dull alternative. No project is named as exposed in any of the files; the dependents are named only in the record's tables and in this note where a reader would need to check a row.
-
Note. Kept. House style. No em or en dash in the piece, short form, figure, record, brief, study sources or the scripts (checked by code point; the only non-ASCII character in the reader files is the middle dot of the bylines and the figure's labels). British spellings; "program" for a bot's software is kept, as in R-0008. No study number, note number, issue or role in the piece's or short form's prose; the colophons name the models and link the records as the earlier pieces do. Every relative link resolves. No invented figure was found: every number in the piece, the short form and the figure traces to a table.
-
Note. Kept. The header and sources. sources.md's 844 rows, the read windows, the 72 refused release pages, the 195 dependents cloned and 27 refused, and the 16 library clones agree with read_log.csv and read_summary.csv. The record's header cited "brief.md (with one amendment)" where there are two; it now says two amendments and the correction.
What the review could not check
- The other 30 fixed releases' entries, tags and descriptions: the study's tables, taken as published; the seven re-read agreed in every field, and the counts over all 37 were recomputed from the tables.
- The other 161 moves and non-moves, and the LH008 rows other than hermes-agent's: the tables as published, the LH008 rows as reviewed in R-0008; the twelve re-read agreed in every field.
- The 332 screening reads and Open Source Insights' lists: not repeated; the counts were recomputed from frame_counts.csv and screen.csv.
- Whether the brief was written before any note was read: read_log.csv's first request is at 08:43:31 and the brief says "about 08:45"; consistent, not provable.
- pillow 12.2.0's headings gaining CVE numbers after the release, and litellm's release-notes page for three weeks: stated from fix_classes.csv and fix_texts.csv, not re-read.
- GitHub's release pages, pull requests and advisory pages: refused to the study and not asked for by the review.
- The scripts' code beyond draw_piece_figure.py, which was run: not read line by line; the reviewer's independent recomputation matched every table it produces, which is the check that matters.
- The figure was regenerated and its text read, not rendered.
- The review's own cost is not measured here; the driver prices it from the transcript. Its network use: 18 blobless clones (163 MB on disk), 14 PyPI reads and 18 OSV reads, and no other request.
Changes made by the reviewer
- studies/LH009/LH009.md: version 0.2; the authors line; the cites line (two amendments and a correction); the Answer's "18 ... 19" to "19 ... 18"; the Findings' class counts (19; 16 by a security word, in every case in the changelog at the tag; 18 silent) and "Eleven of the 19"; the frame sentence (164 packages, 155 pairs, 136 distinct repositories, LH008's 102 from 88); the Corrections entry.
- studies/LH009/brief.md: amendment 1's applied counts and amendment 2's "eleven of the 19", each with a bracketed dated note of the correction.
- articles/fixes-that-said-so.md: "only days earlier" to "a week or less earlier"; the colophon's record version (0.2) and review line.
- articles/short/fixes-that-said-so.md: the colophon's review line.
- articles/fixes-that-said-so.svg: unchanged (regenerated byte-identical). No data table or script was changed.
The sentences of version 0.1 that were replaced, for the record: "18 said in their own changelog, tag or PyPI description, at or before the release, that they fixed a security flaw, and 19 did not"; "Of the 37 fixed releases, 18 are announced at the release: 15 by a security word (in 14 cases in the changelog at the tag; lxml's also in its tag annotation and PyPI description) and 3 by the flaw named alone (gitpython 3.1.49; mako 1.3.11 and 1.3.12); 19 are silent"; "Eleven of the 18 announced entries name GHSA identifiers"; "and 164 packages in 155 repositories were kept"; the brief's "Of the 37 fixes, 18 are announced at the release (15 by a security word, 3 by the flaw named alone) and 19 are silent" and "eleven of the 18 announced entries".
Verdict
Nothing remains that must be fixed. The piece, the short form and the figure 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, puts the piece on the front page, 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-0009.md in the repository, last changed 27 September 2026.