Header typos, the PyPI page, and a release that can be re-run - #10
Merged
Conversation
Three releases in a row -- 1.0.1, 1.1.0 and 1.2.0 -- are tags with no GitHub Release. Each publish step failed, and the release job depends on publish, so the notes were never created even though the artifacts are on PyPI. The publish step now passes skip-existing. Without it a release whose files reached PyPI any other way can never be re-run: the upload aborts on "file already exists" and everything after it is unreachable. With it, a re-run skips the upload and goes on to create the release, which is what re-running is for. docs/releasing.md gains the two traps, both of which cost real time today: Tag the commit that carries the bump, not whatever master was when you started. v1.2.0 was created on the commit before the bump merged, so the tree there still said 1.1.0. The workflow's tag check refused it -- exactly what that guard is for -- and a local build in the same clone quietly produced 1.1.0 artifacts that PyPI rejected as duplicates. A one-line `grep __version__` before tagging catches it, and the recovery is written out, including that a --depth 1 clone cannot delete and re-push a tag. And what skip-existing is protecting against, so nobody removes it as noise. The workflow was parsed to confirm the key landed where intended rather than trusting the diff. 667 tests, ruff clean, 2,499 sources checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
A fresh agent installed 1.2.0 from PyPI and typed the mistakes a beginner makes. Two findings, both real. Header typos were swallowed in silence. `tempo: banana` compiled at 100, `time: 3-4` at 4/4, and `key: Zz` sounded in C while keeping "Zz" for display, so a chart would print one key over music in another. Nothing said anything -- not compile, not check, and not `info --verbose`, which documents itself as showing every diagnostic. Each still falls back to a default, which is right; falling back without saying so is the same fault as Xm9 compiling to a silent bar. The parsers do not raise on nonsense, so this could not be done by catching an exception. theory.KEY_RE and Meter.readable name each rule once and serve both the parse and the question "will parsing work", instead of a second copy of each pattern drifting from the first. The key warning names the sounding key rather than the text, because "using Zz major" would have been no help. It found a real one on the first run over the corpus: the Hungarian Rhapsody carries `time: 2/4 (Lassan) then 4/4 (Friska)`, a human annotation the metre field cannot express, silently becoming 4/4. The file is left alone -- changing the metre would change the music -- and the warning now says what was ignored. And the README is the PyPI page. pyproject sets readme = "README.md", so forty-four relative links and one relative image resolved on GitHub and 404'd on PyPI, which is where a pip user reads them: the page told people to read a dozen documents none of which they could reach, and to compile examples/ files the wheel does not ship. All absolute now, images through raw.githubusercontent.com because a blob URL serves HTML rather than an image, and the examples section says where the examples actually are. tests/test_readme_links.py buys back what absolute links lose: every URL into this repository is mapped to a path and checked to exist, images may not use blob, and the version the README claims must match the tree. It said "Version 1.0" while PyPI shipped 1.2.0. One process note. The first attempt at this changelog entry was a str.replace on "## Unreleased", which no longer exists since 1.2.0 was cut. It matched nothing and wrote the file unchanged -- a silent no-op of exactly the kind this commit is about. Caught because git status did not list the file. The second attempt asserts its anchor. 671 tests, 7 specs, ruff clean, 6,321 files compiling to exactly the music they did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A fresh agent installed 1.2.0 from PyPI and typed the mistakes a beginner makes.
It found two real things, and the release pipeline had a third.
Header typos were swallowed in silence
compiled happily — C major, 100 bpm, 4/4 — and said nothing. Not on
compile, not oncheck, and not oninfo --verbose, which documents itself asshowing every diagnostic.
Each still falls back to a default, which is right: a typo in a header should not
cost you the piece. Falling back without saying so is the same fault as
Xm9compiling to a silent bar.
The key warning names the sounding key rather than the text, because the
unreadable text is kept for display — a chart would print
Zzover music in C,so "using Zz major" would have been no help.
The parsers do not raise on nonsense, so this could not be done by catching an
exception.
theory.KEY_REandMeter.readablename each rule once and serveboth the parse and the question "will parsing work", rather than a second copy of
each pattern drifting from the first.
It found a real one immediately. The bundled Hungarian Rhapsody carries
time: 2/4 (Lassan) then 4/4 (Friska)— a human annotation the metre fieldcannot express, silently becoming 4/4. The file is left alone (changing the metre
would change the music) and the warning now says what was ignored.
The README is the PyPI page, and its links did not work there
pyproject.tomlsetsreadme = "README.md", so that file is the project page.Forty-four relative links and one relative image resolved on GitHub and 404'd
on PyPI — where a
pip installuser reads them. The page told people to read adozen documents none of which they could reach, and to compile
examples/…filesthe wheel does not ship.
All absolute now; images through
raw.githubusercontent.com, because ablobURL serves HTML rather than an image. The examples section says where the
examples actually are, and points pip users at the bundled library, which is
shipped.
tests/test_readme_links.pybuys back what absolute links lose: every URL intothis repository is mapped to a path and checked to exist, images may not use
blob, and the version the README claims must match the tree — it said"Version 1.0" while PyPI shipped 1.2.0.
A release that can be re-run
Three releases in a row are tags with no GitHub Release: each publish step
failed, and the release-notes job depends on it. Worse, it was a dead end —
without
skip-existing, a release whose files reached PyPI another way can neverbe re-run, because the upload aborts on "file already exists".
skip-existing: truefixes that, anddocs/releasing.mdnow records both trapsthat cost real time today: tagging the commit before the version bump merged
(which the workflow's tag check caught, correctly), and what
skip-existingguards, so nobody removes it as noise.
Also
CLAUDE.mdnow records the measured drift betweenplainsong/mcp/and thesibling: 240 lines across seven of eight files. Most is mechanical, but the
DNS-rebinding guard existed here and not in the published sibling — fixed
separately in
plainsong-mcp#2. Theensemble=injection machinery here isdocumented as unused, since the sibling never imports this
tools.py.Verification
671 tests, 7 specs, ruff clean,
6339 file(s) checked,6321 file(s) compile exactly as recorded.One process note worth keeping: my first attempt at the changelog entry was a
str.replaceon## Unreleased, which no longer exists since 1.2.0 was cut. Itmatched nothing and wrote the file unchanged — a silent no-op of exactly the kind
this PR is about. Caught because
git statusdid not list the file. The secondattempt asserts its anchor.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc