Verify releases from outside the tree, and extract the arrival-time solver - #15
Merged
Conversation
`plainsong spec` printed "no specs found" and exited 0. That is the exact shape of the fault it exists to catch: the spec files once sat outside the package, so every pip install shipped without them, and every install and CI job that ran `plainsong spec` read that zero as a pass. The self-verification the design leans on was doing nothing, loudly enough to warn and quietly enough that no exit status moved. It exits 1 now and says which of the two cases happened. Found while writing tools/verify_release.py, which exists because everything in tests/ runs with the repository on sys.path and is therefore blind to packaging. It builds a wheel, installs it into a throwaway venv outside the source tree, drives the console script from /tmp with PYTHONPATH stripped, then does the same against PyPI -- including a real JSON-RPC stdio session against the MCP server and an identity check that the sibling's loopback re-export is the compiler's own function. Two things it had to learn, both by being wrong first. setuptools reuses whatever is already in build/lib, so a data file that has stopped being packaged still reaches the wheel from the last build that included it; the script clears build/ and *.egg-info first. And the packaging canary cannot trust an exit status alone, so it now requires that specs were found as well as that none failed. CI gains a packaging job running --stage wheel, so a data file that stops being packaged fails a pull request rather than a release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
A written time is an arrival time, and the solver works backwards from it to when each participant must act. That idea is the one genuinely novel thing in this repository and none of it is about music: the equation is six lines of float arithmetic wearing instrument names. `speech` is actuation latency, `p_center` is systematic bias, and the reference/observed propagation split is "the conditions this was tuned for" against "the conditions it is in". coordinate/ is that equation with the music vocabulary removed, plus interval division and a medium that is a parameter rather than an assumption. One file, standard library only, Apache-2.0 so it can sit beside the robotics ecosystem, which is Apache-2.0 throughout. It is staged here because this is where the proof can run; it belongs in its own repository and nothing under plainsong/ imports it. The proof is what matters. test_equivalence.py drives coordinate and plainsong.perform.solve with the same inputs over ~4,000 combinations and requires bit-identical results -- assertEqual, not assertAlmostEqual, because a reordered sum is a different implementation even when it is close, and close is what accumulates. Reordering a single addition in solve_one, same terms and same mathematical value, fails 1,803 of those cases. So the extraction is inert by measurement rather than by reading. It also pins the four claims that make the thing general rather than decorative: the effect lands where written at the reference point and does not away from it, spread is zero at the tuning point and positive elsewhere, and intent survives compensation while lead does not. One test assertion was wrong on the first run and is worth keeping the lesson from: accumulating a twelfth twelve times lands on exactly 1.0, while a seventh does not. Which divisors drift is not guessable, so the test now measures them instead of asserting a favourite example. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
This file says its rules are faults that were actually paid for, which only works if the file is true. Each of these was checked by running the thing, not by reading around it. `EbMaj7`, `G7alt` and `CM7` were described as spellings the chord parser does not accept, "still open". All three parse. The entry is kept, because the lesson generalises and the fix does not: the warning on an unrecognised token is what found them, and before it an unreadable chord and a deliberate rest were the same silence with the compiler reporting ok for both. The songbook's bar-count warnings were given as 2. There is 1 -- the Hungarian Rhapsody's `time: 2/4 (Lassan) then 4/4 (Friska)`, a human annotation the metre field cannot express, left alone deliberately because changing the metre would change the music. Naming it is more useful than counting it. "Every command takes --json" is true only for the global position. Written after the subcommand, argparse refuses the whole invocation with `unrecognized arguments: --json`. A reader following that sentence literally gets an error, which is how tools/verify_release.py came to report two false failures. The totals also live under `arrangement`, so the note count is `arrangement.notes` rather than a top-level key. Verified unchanged: songbook 3,824 charts, fakebook archive 2,484, 27 MCP tools. 693 tests pass, check reports 6,336 files, and the corpus fingerprint is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc
I chose Apache-2.0 for the patent grant and to match the robotics ecosystem. The stated priority is distribution over credit, and that inverts the trade. Apache-2.0's obligations run against exactly this file's purpose. It is one file meant to be copied next to whatever needs it -- onto a Pi beside a steering pump, with no packaging step -- and Apache-2.0 asks that vendored copy to carry a NOTICE and to state that files were changed. MIT asks it to keep one paragraph. When the goal is that the code travels, the licence with fewer conditions on travelling is the right one. It also makes the stack one licence rather than two. plainsong is MIT; a single licence across the ecosystem means no compatibility matrix for anyone assembling pieces, and MIT is the text every legal review already has a stamp for. Apache-2.0 and GPL-3.0 can both still consume MIT, so nothing downstream is foreclosed -- including MAGDA. The patent grant is the real thing given up. It is worth less here than it looks: this is arithmetic rather than a mechanism, and Apache's grant protects users from *contributors'* patents, which matters most in projects with many corporate contributors. This has none yet. If attribution should be dropped entirely, 0BSD is the next step and is a one-file change from here. MIT is the more conservative choice and the more widely recognised one, so it is where this starts.
The lessons from this week were living in commit messages, which the next person does not read. docs/verification.md collects them, deliberately written so nothing in it is about music -- the same faults turned up in five sibling repositories and the file should be portable to any of them. Six rules, each with the incident that paid for it. Success is not evidence: `plainsong spec` printed "no specs found" and exited 0, and a sibling's CI suppresses its own build with `|| true`, so neither has ever demonstrated anything. A guard you cannot fail is not a guard: two TimeGrid guards were redundant and no mutation could fail a test until one was removed. Two copies drift into the same bug, not different ones -- both loopback copies accepted `127.evil.example` and both mangled `[::1]`. Verify by doing: PyPI's API reported a stale version twice, a suite on sys.path cannot see a packaging bug, setuptools reuses build/lib so a broken package verifies perfectly, and a pipe eats the exit code. A name is not a description and a README is not a fact. Report what happened, including when the two failures in your own verification run turn out to be your harness rather than the product. CLAUDE.md points at it from a new section and its command list now carries verify_release.py beside the demo differential, both being checks the test suite structurally cannot perform.
plainsong-mcp 1.0.0 is on PyPI, so for the first time there is somewhere to send people. The command still works and will until 2.0; it now warns and names the replacement. The duplication has already cost twice. A DNS-rebinding fix existed in this copy and not the sibling for months -- in the copy people pip install for MCP. Then the same eight lines got the same two things wrong in both, because the second was a copy of the first. Neither repository could notice either time. The notice goes to stderr, and that is not a detail. In stdio mode stdout *is* the protocol: a deprecation line printed there would desynchronise every client, turning a courtesy into an outage. PLAINSONG_NO_DEPRECATION=1 silences it for anyone scripting against the old command. The rule is enforced rather than remembered. TestDeprecationNoticeStaysOffTheWire drives a real subprocess and parses every line of its stdout as JSON, so a future edit that prints the notice to the wrong stream fails the suite instead of breaking clients quietly.
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.
Two commits, one about proving what ships and one about what the solver actually is.
plainsong speccalled finding nothing a passIt printed
no specs foundand exited 0. That is the exact shape of the fault it exists to catch: the spec files once sat outside the package, so everypip installshipped without them — and every install, and every CI job that ranplainsong spec, read that zero as a pass. The self-verification the whole design leans on was doing nothing, loudly enough to print a warning and quietly enough that no exit status moved.It exits 1 now and says which of the two cases happened: an install missing its spec files, or a
--tagnothing carries. This is a behaviour change — a script treating 0 as success against a spec-less install will now see a failure, which is the point.A release is now verified from outside the tree
Everything in
tests/runs with the repository onsys.path, which is structurally blind to packaging.tools/verify_release.pynever imports plainsong: it builds a wheel, installs it into a throwaway virtualenv outside the source tree, drives the console script from/tmpwithPYTHONPATHstripped, then repeats against what is actually on PyPI — including a real JSON-RPC stdio session against the MCP server and a check that the sibling's loopback re-export is the compiler's own function.CI gains a
packagingjob running--stage wheel, so a data file that stops being packaged fails a pull request rather than a release.Two things it had to learn by being wrong first:
build/liband reuses whatever is already there, so a data file that has stopped being packaged still reaches the wheel from the last build that did include it. A broken package then verifies perfectly. The script clearsbuild/and*.egg-infofirst.Run against 1.4.0 on PyPI: 26 checks, 0 failures.
Extract the arrival-time solver as a domain-neutral core
A written time is an arrival time, and the solver works backwards to when each participant must act. None of that is about music — the equation is six lines of float arithmetic wearing instrument names.
speechis actuation latency,p_centeris systematic bias, and the reference/observed propagation split is "the conditions this was tuned for" against "the conditions it is in".coordinate/is that equation with the vocabulary removed, plus interval division and a medium that is a parameter rather than an assumption. One file, standard library only, Apache-2.0 so it can sit beside the robotics ecosystem, which is Apache-2.0 throughout.It is staged here, not adopted. Nothing under
plainsong/imports it and it is not in the wheel. It lives here because this is the only place the proof can run; it belongs in its own repository.The proof is the point
coordinate/test_equivalence.pydrivescoordinateandplainsong.perform.solvewith the same inputs over ~4,000 combinations and requires bit-identical results —assertEqual, notassertAlmostEqual, because a reordered sum is a different implementation even when it is close, and close is what accumulates.Reordering a single addition in
solve_one— same terms, same mathematical value — fails 1,803 of those cases. So the extraction is inert by measurement rather than by reading.It also pins the four claims that make it general rather than decorative: the effect lands where written at the reference point and does not away from it,
spreadis zero at the tuning point and positive elsewhere, andintentsurvives compensation whileleaddoes not.Checks
spec7/7;check6,336 files / 4 warnings; fingerprint 6,321 files unchangedruff check plainsong tests tools coordinatecleanspecpasses, a real compile producesarrangement.notes=28, and the DNS-rebinding guard is correct in the shipped packageGenerated by Claude Code