Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,17 @@ jobs:
name: dist
path: dist/

# skip-existing makes a re-run idempotent. Without it, a release that got
# its files onto PyPI some other way -- a manual twine upload, or a first
# attempt that uploaded and then failed later in the job -- can never be
# re-run, because the upload aborts on "file already exists". That took
# 1.0.1, 1.1.0 and 1.2.0 with it: the publish step failed, the release
# job below never ran, and the repository has tags with no releases.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: true

release:
name: Create GitHub Release
Expand Down
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,58 @@

Notable changes, newest first. Dates are ISO 8601.

## Unreleased

### Header typos were swallowed in silence

`tempo: banana` compiled happily at 100. `time: 3-4` compiled at 4/4. `key: Zz`
sounded in C while a chart printed `Zz` over it. None of them said anything —
not on `compile`, not on `check`, and not on `info --verbose`, which documents
itself as showing every diagnostic.

Each still falls back to a default, which is right: a typo in a header should
not cost you the piece. What was wrong is falling back **without saying so**,
the same fault as `Xm9` compiling to a silent bar. Now:

```
warning: tempo 'banana' is not a number; using 100
warning: time '3-4' is not a metre; using 4/4
warning: key 'Zz' is not a key; sounding in C major
```

The key message names the *sounding* key rather than the text, because the
unreadable text is kept for display — so "using Zz major" would have been no
help at all.

The parsers do not raise on nonsense, so these could not simply catch an
exception. `theory.KEY_RE` and `Meter.readable` name each rule once and serve
both the parse and the question "will parsing work", rather than a second copy
of each pattern drifting from the first.

Found by a fresh agent installing from PyPI and typing plausible mistakes — a
test this project cannot run on itself, having long since stopped making
beginner errors. It immediately turned up a real one in the bundled songbook:
`time: 2/4 (Lassan) then 4/4 (Friska)` on the Hungarian Rhapsody, 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.

### The README is the PyPI page, and its links did not work there

`pyproject.toml` sets `readme = "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, which is where a `pip install` user reads them. The page told people to
read a dozen documents none of which they could reach.

All absolute now, images through `raw.githubusercontent.com` because a `blob`
URL serves HTML rather than an image. `tests/test_readme_links.py` buys back the
guarantee absolute links lose: every URL into this repository is mapped to a path
and checked to exist, images must not use `blob`, and the version the README
claims must match the tree — it said "Version 1.0" while PyPI shipped 1.2.0.

The examples section also told pip users to compile `examples/…`, which the wheel
does not ship. It now says so, and points at the bundled library, which it does.

## 1.2.0 — 2026-08-18

A minor rather than a patch: `notation/merge.py`, `notation/lyrics.py` and
Expand Down
26 changes: 19 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,25 @@ famous for came from those rows and are now 2.
- The built-in synthesiser is a preview renderer; timbres are approximations.
Audio is mono.
- The host bridge cannot stream and reports no token usage.
- **`plainsong/mcp/` also exists in `SuperInstance/plainsong-mcp`.** This is the
one open violation of "one of everything" and it is deliberate but temporary:
the extraction happened while this branch was in review. The intended end
state is that the MCP server lives only in the sibling repository and this
package stops carrying it. Until that lands, a change to one copy must be
made to the other or they will drift — which is precisely the failure mode
the rule exists to prevent. Do not build anything new on this copy.
- **`plainsong/mcp/` also exists in `SuperInstance/plainsong-mcp`.** The one
open violation of "one of everything". Do not build anything new on this copy.

**This has now cost something real, so it is no longer a theoretical rule.**
The two copies were measured: 240 lines of difference across seven of eight
files. Most is mechanical — relative versus absolute imports, unavoidable when
the same code lives inside and outside a package. One was not. The HTTP
transport's DNS-rebinding guard (`_host_is_local`, requiring a loopback
`Host` because Origin-against-Host alone is defeated by rebinding) existed
**here and not in the sibling**, which is the copy people `pip install` for
MCP. Fixed there, with tests, but it sat open for months with nothing in
either repository able to notice.

The injection machinery in `mcp/tools.py` and `mcp/resources.py` — the
`ensemble=` parameter and `_default_ensemble()` — is **unused**. It was
written so the sibling could import this `tools.py` and pass its own ensemble;
the sibling never does, having its own copy bound by relative import. Do not
extend it: the end state is that MCP lives only in the sibling and this
directory goes.
- The MCP server has now been driven by a third-party client: the official
`mcp` Python SDK 2.0.0, over stdio. `initialize`, `tools/list`,
`resources/list`, `prompts/list`, `tools/call` and `resources/read` all
Expand Down
Loading
Loading