From d74b8cebd61bf1849cd165991f75047008261d3f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 01:20:10 +0000 Subject: [PATCH] Housekeeping: two rename leftovers, and documents that had drifted from the tree Two files still carried `tap` from before the rename: examples/plainsong-4-tap-closing-time.song and docs/traditions/03-the-tap-songbook.md. The songbook's own hits on that string -- jarabe tapatio, patapan, tapestry -- are song titles and were left alone. The fingerprint is keyed on path, so its baseline had to be re-recorded. Proved inert the way the earlier renames were: the note-hash multiset across all 6,321 files is b850e4729399e7e5068a13c0 before and after. Not one note moved, only a path. The documentation had drifted from the thing it describes. - The README claimed nine complete examples. There are eight. - SHIPPING.md said 527 tests and 6,333 sources; it is 667 and 6,340. That document opens by saying every claim in it was checked against the working tree, so stale numbers make it lie about itself specifically. - It still listed the four-note chord cap as an open limitation -- "D9 sounds like D7" -- which 1.0.1 fixed. That is the second time that same stale claim has been found in a document; the first was the 1.0.0 changelog. - Its claim about the browser demo was the pre-fix one, describing note counts as the guard. Counting is exactly what failed to notice `.` and `-` being read as rests. It now describes what actually checks it. - chart and merge reached CLAUDE.md, AGENTS.md and docs/architecture.md, which described neither. The architecture doc had grown two lists of the same three projections; they are one list now. - proposals/02-the-voyage.md's status table said 578 tests and "wheel built in a clean venv", which undersells it -- it is on PyPI. And the MCP caveat is closed, by doing it rather than by arguing. The server was driven with the official mcp Python SDK 2.0.0 over stdio, as a third-party client would: initialize returns plainsong 1.1.0 on protocol 2025-06-18, 27 tools, 9 resources and 2 prompts enumerate, compile_score round-trips inline notation, a resource reads back, and a call with a missing required argument returns isError: true. One finding recorded rather than fixed. Notation the compiler cannot read comes back isError: false with the failure in the content -- "error: no sections found". The tool ran; the music did not compile. That is defensible under the specification, which reserves isError for execution failures, but it means an agent client has to read the diagnostics rather than the flag. That is the "success is not evidence" trap AGENTS.md warns about, served over the wire. Changing isError semantics is a decision about the protocol surface rather than a tidy-up, so it is written down and left. Verified: 667 tests, 7 specs, ruff clean, 6,340 sources checked, 6,321 files compiling to exactly the music they did. Every relative link and image across 35 documents resolves. All 24 subcommands the documentation names exist, and the 18 that take a file were run end to end. The demo differential agrees on all eight cases. A wheel was built and exercised from outside the source tree, including the three new modules. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc --- AGENTS.md | 2 + CHANGELOG.md | 39 +++++++++++++++ CLAUDE.md | 15 ++++-- README.md | 4 +- SHIPPING.md | 48 ++++++++++++------- docs/architecture.md | 28 ++++++----- ...the-tap-songbook.md => 03-the-songbook.md} | 2 +- ...ime.song => plainsong-4-closing-time.song} | 0 proposals/02-the-voyage.md | 7 +-- tests/corpus-fingerprint.txt | 2 +- 10 files changed, 108 insertions(+), 39 deletions(-) rename docs/traditions/{03-the-tap-songbook.md => 03-the-songbook.md} (99%) rename examples/{plainsong-4-tap-closing-time.song => plainsong-4-closing-time.song} (100%) diff --git a/AGENTS.md b/AGENTS.md index 2da0e45d..d9be61dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,7 @@ Every command takes `--json`. Use it. plainsong chord "G7alt" --explain # what a symbol means, degree by degree plainsong voicing "C13" # which notes actually sound, and why those plainsong lyrics song.song # which note each syllable is sung on +plainsong chart song.song -o out.svg # a chord chart you can embed in a document plainsong info song.song --verbose # every diagnostic available plainsong check docs README.md # compiles fenced blocks out of markdown too plainsong fingerprint mysongs --check baseline.txt # did anything change? @@ -213,6 +214,7 @@ refused and handed the current state to rebase onto. See | [docs/chords.md](docs/chords.md) | Every chord spelling, and the rules that derive the notes | | [docs/voicing.md](docs/voicing.md) | Which notes sound when a chord names more than fit | | [docs/lyrics.md](docs/lyrics.md) | Binding syllables to notes, and why a column is not a promise | +| [docs/chart.md](docs/chart.md) | Drawing a chord chart, and what it deliberately is not | | [docs/integration.md](docs/integration.md) | `--json`, the Python API, HTTP, MCP | | [docs/performance.md](docs/performance.md) | Arrival-centric timing and the `[Stage]` block | | [CLAUDE.md](CLAUDE.md) | How the codebase is organised, and why each rule exists | diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c4cc9f..64ea54d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ Notable changes, newest first. Dates are ISO 8601. ## Unreleased +### Housekeeping: two rename leftovers, and documentation that had drifted + +- **`examples/plainsong-4-tap-closing-time.song`** still carried `tap` from + before the rename, as did `docs/traditions/03-the-tap-songbook.md`. Both + renamed. The fingerprint is keyed on path, so its baseline was re-recorded — + and proved inert the way the earlier renames were, by comparing the note-hash + multiset across all 6,321 files: `b850e4729399e7e5068a13c0` before and after. + Not one note moved. +- **The README claimed nine complete examples and ships eight.** +- **`SHIPPING.md` said 527 tests and 6,333 sources**; it is 667 and 6,340. It + opens by saying every claim in it was checked against the working tree, so + stale numbers make it lie about itself. +- **It also still listed the four-note chord cap as an open limitation** — + `D9` sounds like `D7` — which 1.0.1 fixed. Corrected, with the measurement and + the `core.voicing = "stack"` escape hatch. +- **Its claim about the browser demo was the pre-fix one**, describing note + counts as the guard. Counting was exactly what failed to catch `.` and `-` + being read as rests. Rewritten to describe what actually checks it now. +- `chart` and `merge` reached `CLAUDE.md`, `AGENTS.md` and + `docs/architecture.md`, which described neither. + +### A third-party MCP client has now connected + +The long-standing caveat — *"the MCP server has never had a real MCP client +connect to it"* — is closed. Driven with the official `mcp` Python SDK 2.0.0 +over stdio, as any client would: `initialize` returns `plainsong 1.1.0` on +protocol `2025-06-18`, and 27 tools, 9 resources and 2 prompts enumerate. +`compile_score` round-trips inline notation, a resource reads back, and a call +missing a required argument correctly returns `isError: true`. + +**One thing worth knowing.** Notation the compiler cannot read returns +`isError: false`, with the failure in the *content* (`error: no sections +found`). The tool ran; the music did not compile. That is defensible under the +specification, which reserves `isError` for execution failures — but it means an +agent client has to read the diagnostics rather than the flag, which is the +"success is not evidence" trap `AGENTS.md` warns about, served over the wire. +Recorded rather than changed: altering `isError` semantics is a decision about +the protocol surface, not a tidy-up. + ### A conflict between two agents is now decidable `notation/merge.py` three-way merges two edits of one score. An edit occupies a diff --git a/CLAUDE.md b/CLAUDE.md index dda42b65..9898e3ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,12 +38,17 @@ python3 -m pytest tests -q # works too, if pytest is installe python3 -m plainsong spec # exits non-zero on failure python3 -m plainsong doctor --specs python3 -m plainsong check docs examples plainsong/songbook README.md # every source, prose included +python3 -m plainsong fingerprint plainsong/songbook examples docs --check tests/corpus-fingerprint.txt + +# Checks CI cannot run -- no browser there. Run by hand after touching either side. +python3 tools/demo_differential.py # the browser demo against the compiler # Working with notation python3 -m plainsong new "Title" -o song.song python3 -m plainsong compile song.song -o out.mid --audio out.wav python3 -m plainsong info song.song --verbose # every diagnostic, parser and arranger python3 -m plainsong lyrics song.song # which note each syllable is sung on +python3 -m plainsong chart song.song -o out.svg # a chord chart, embeddable as python3 -m plainsong transpose song.song Dm # The browser demo -- open it, no server needed @@ -298,9 +303,13 @@ famous for came from those rows and are now 2. 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. -- The MCP server has never had a third-party MCP client connect to it. Its - protocol behaviour is verified by hand-driven JSON-RPC against the - specification, which is strong evidence and not the same thing. +- 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 + behave, and a missing required argument returns `isError: true`. Notation the + compiler cannot read returns `isError: false` with the error in the content, + which is defensible — the tool ran — but means a client has to read the + diagnostics rather than the flag. ## Emitting notation must round-trip diff --git a/README.md b/README.md index 1b29972d..8f35ad0f 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ Every command takes `--json`. Use it when parsing output. ## Examples to start from -Nine complete pieces ship in the repository, written to be read rather than +Eight complete pieces ship in the repository, written to be read rather than generated. Each one is a template: copy it, change the chords, recompile. ```bash @@ -389,7 +389,7 @@ plainsong compile examples/plainsong-1-creatures-of-interval.song --audio out.wa | [Creatures of Interval](examples/plainsong-1-creatures-of-interval.song) | Four voices, plain 4/4 — the simplest complete piece | | [The Room Is Safe](examples/plainsong-2-the-room-is-safe.song) | Lyrics and melody together | | [Hermes Blues](examples/plainsong-3-hermes-blues.song) | Twelve-bar blues with swing | -| [Closing Time](examples/plainsong-4-tap-closing-time.song) | The relative dialect — roman numerals and scale degrees | +| [Closing Time](examples/plainsong-4-closing-time.song) | The relative dialect — roman numerals and scale degrees | | [Deck Work](examples/plainsong-5-deck-work.song) | Several named players | | [Spacing: melody](examples/plainsong-6-spacing-melody.song) · [chords](examples/plainsong-7-spacing-chords.song) · [dashes](examples/plainsong-8-spacing-dashes.song) | How duration-by-spacing behaves | diff --git a/SHIPPING.md b/SHIPPING.md index c49c131a..034b6f65 100644 --- a/SHIPPING.md +++ b/SHIPPING.md @@ -15,16 +15,20 @@ These have been proven, not assumed. | Works on Python 3.10 | Hid `tomllib` entirely and ran the CLI (`info`, `compile`) and all specs through the fallback reader. | | The TOML fallback matches the real one | Differential test: every TOML file in the repo plus 17 edge cases parsed by both readers and compared. | | Arrival timing is arithmetically right | Hand-checked a three-voice stage: 14 m at 343.21 m/s is 40.8 ms, plus 140 ms pipe speech and 60 ms perceptual attack gives the organ's -241 ms. Spread is 0 ms at the podium and non-zero at every player's desk. | -| Notation compatibility held | 6,333 sources compile with 0 errors, including every fenced `plainsong` block in the prose. | +| Notation compatibility held | 6,340 sources compile with 0 errors, including every fenced `plainsong` block in the prose, and 6,321 files compile to exactly the music they did -- which `check` cannot see and `fingerprint` can. | | No hidden dependencies | CI installs nothing on 4 Python versions x 3 operating systems. | | No hardcoded paths | A test greps the package for `~/.openclaw`, `/home/eileen`, `/Users/`. | | Nothing half-written | No TODO, FIXME, XXX, HACK or `NotImplementedError` anywhere in `plainsong/`. | -| The browser demo agrees with the compiler | `docs/demo/index.html` carries its own parser and arranger in JavaScript. It states the note count it produces for each preset, and `tests/test_demo.py` compiles the same notation with the real compiler and requires the same answer -- so a change to the arranger that moves a count fails CI rather than making the page quietly lie. | +| The browser demo agrees with the compiler | Checked by running both implementations over the same notation and comparing **pitch, start and duration** of every note (`tools/demo_differential.py`). Counting alone was not enough and this is not hypothetical: `.` and `-` sat in the demo's REST set where the compiler has them in SUSTAIN, so every held note was cut to a single subdivision while the note *counts* stayed identical and CI stayed green. A second implementation checked by counting is not checked. | | A wheel actually works | Built, installed into a clean venv outside the source tree, and exercised there. This is the only way the two packaging faults were visible: `plainsong spec` and `plainsong library` both did nothing for anyone who installed rather than cloned. | +| A chart renders without a font | `plainsong chart` emits a standalone SVG; inspected in headless Chromium at three viewport sizes, light and dark. Text carries `textLength` so layout survives font substitution, and the flat sign is folded to ASCII because Liberation Sans has U+266F and lacks U+266D. | +| A merge conflict is decidable | Two edits occupy sets of `(section, row, bar)` cells and collide exactly when those intersect. Three rules -- run-on bar numbering, removal counting as a change, and requiring the base -- were each mutated and the suite confirmed red. | +| The rename moved no music | `examples/plainsong-4-tap-closing-time.song` became `plainsong-4-closing-time.song`. Proved inert by comparing the note-hash multiset across all 6,321 files before and after: `b850e4729399e7e5068a13c0`, both sides. | | The new tests would catch a regression | Each guard was removed and the suite confirmed to go red: retryable statuses, the `[DONE]` stream terminator, the User-Agent and header merge, the same-origin refusal, the `/files/` basename and containment guards together, the null-byte guard, percent-decoding, the `MAX_BODY` limit, `ConnectorResult.__bool__`, the broken-connector skip, and the availability check in `run()`. | -527 tests, 7 specs, 6,333 sources checked including every fenced example in the -prose, ruff clean. +667 tests, 7 specs, 6,340 sources checked including every fenced example in the +prose, ruff clean. Published: `pip install plainsong` installs 1.1.0 from PyPI +and its specs pass on a machine that has never seen this repository. ## Blockers @@ -201,19 +205,27 @@ passes on Windows across 3.10 through 3.13. - The TUI needs `curses`, which stock Python on Windows does not ship. It reports this and points at `windows-curses` or the web interface. Found by the new TUI tests running on Windows CI for the first time. -- The MCP server has never had a real MCP client connect to it. The protocol is - verified by hand-driven JSON-RPC, which is strong evidence but not the same - thing as Claude Desktop or an SDK client connecting. -- **The renderer discards chord extensions.** `arrange.Options.max_chord_notes` - is 4 and the notes are taken from the bottom, so a five-note chord keeps - root-third-fifth-seventh and drops what sat above. `D9` sounds like `D7`; - `E7#9` sounds like `E7`; `G7alt` renders as four notes that are not a chord - anybody would name. 459 occurrences, 0.3% of the corpus, concentrated - entirely in the chords where the dropped note is the whole point. The cap is - defensible; taking the bottom four is not — a player drops the fifth first, - then the root, because the third and seventh carry the identity. Left alone - because fixing it changes how existing files sound, and that wants its own - reviewed diff rather than a ride along with a parser change. +- ~~The MCP server has never had a real MCP client connect to it.~~ **Closed.** + Driven with the official `mcp` Python SDK 2.0.0 as any third-party client + would: `initialize` returns `plainsong 1.1.0` on protocol `2025-06-18`, and + 27 tools, 9 resources and 2 prompts enumerate. `compile_score` round-trips + inline notation, a resource reads back, and a call with a missing required + argument correctly returns `isError: true`. + **One thing to know:** notation the compiler cannot read returns + `isError: false` with the failure in the *content* (`error: no sections + found`). The tool ran; the music did not compile. That is defensible under the + specification, which reserves `isError` for execution failures, but it means + an agent client must read the diagnostics rather than the flag — the same + "success is not evidence" trap `AGENTS.md` warns humans about, presented over + the wire. +- ~~**The renderer discards chord extensions.**~~ **Fixed in 1.0.1.** The + four-note cap took the lowest four notes, so `D9` sounded like `D7` and + `G7alt` rendered as four notes nobody would name. A player drops the fifth + first and the root second, because the third and seventh carry the identity; + taking the bottom four does the opposite. Chosen by measurement over the 277 + occurrences where the cap actually bites: keeping the symbol's defining note + went from 50.1% to 94.4%. It changed how seventy-two bundled files sound, so + `core.voicing = "stack"` restores the old rendering exactly. - **`plainsong/mcp/` also exists in `SuperInstance/plainsong-mcp`.** The one open violation of "one of everything", and it is temporary: the extraction happened while this branch was in review. Until the copy here is removed, a @@ -225,7 +237,7 @@ passes on Windows across 3.10 through 3.13. - `master` has been red on its own older workflow since before this branch, for reasons unrelated to it. - **Dialect auto-detection fails on a relative-dialect file, and it looks like - corrupt source.** `examples/plainsong-4-tap-closing-time.song` is written in + corrupt source.** `examples/plainsong-4-closing-time.song` is written in the relative dialect -- `I`, `IV`, `vi` as roman-numeral chords and `1 . 3 | 5 . . |` as scale degrees. `dialect: auto` reads it as absolute, so every scale degree becomes an unreadable token: 42 notes and 51 warnings. Told diff --git a/docs/architecture.md b/docs/architecture.md index c101faa9..09f13273 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,8 +21,8 @@ Around that core: | Package | Holds | |---|---| -| `notation/` | theory, parser, arranger, the intermediate representation | -| `render/` | MIDI writer, synthesiser, voices, optional backends | +| `notation/` | theory, parser, arranger, the intermediate representation, the time grid, lyric binding, merge | +| `render/` | MIDI writer, synthesiser, voices, the SVG chart and its font metrics, optional backends | | `runtime/` | paths, layered configuration, host capability probing | | `llm/` | provider-neutral types, transport, catalogue, adapters | | `agent/` | the loop, the tool registry, prompts | @@ -56,15 +56,21 @@ computed by one function, so a lyric and a note are placed by the same arithmetic. `Arrangement.grid` carries it. `unit` is the load-bearing field: a token's position within its own bar, from 0.0 to just under 1.0. -Three separate problems become the same projection: - -- rendering is `x = unit * bar_width`, a coordinate transform rather than a - layout engine; -- merging is set intersection on `(row, bar, unit)`, so two agents editing - different rows provably cannot collide — the row axis is disjoint, and player - rows are keyed by name; -- alignment linting can finally be *expressed*: `grid.disagreements()` names - each bar whose rows divide it differently. +Three separate problems become the same projection, and two of them are built: + +- **Rendering.** `render/chart.py` places a chord at `x = unit * bar_width` — a + coordinate transform rather than a layout engine, so a chart cannot disagree + with the audio about when a chord arrives. +- **Merging.** `notation/merge.py` decides a conflict by set intersection on + `(section, row, bar)`: two edits collide exactly when those sets overlap. The + row axis is disjoint and player rows are keyed by name, so two agents on + `@bass` and `Melody:` cannot collide — and, less obviously, neither can two + agents rewriting bars 1–4 and 5–8 of *one* melody. That second case is what a + coordinate per bar buys over a file per voice. +- **Linting**, not yet built. `grid.disagreements()` names each bar whose rows + divide it differently, which is the `came`/`C5` lie stated as data. Nothing + raises a diagnostic from it: uneven subdivision is legal and usually + deliberate, so what to warn about is still an open question. The grid observes; it does not steer. The arranger populates it from positions it has already computed, so if building it ever moved a note, the grid would be diff --git a/docs/traditions/03-the-tap-songbook.md b/docs/traditions/03-the-songbook.md similarity index 99% rename from docs/traditions/03-the-tap-songbook.md rename to docs/traditions/03-the-songbook.md index 70326575..620da0cb 100644 --- a/docs/traditions/03-the-tap-songbook.md +++ b/docs/traditions/03-the-songbook.md @@ -1,4 +1,4 @@ -# The Tap Songbook +# The Songbook *(Shared, stolen, and sung slightly off-key)* --- diff --git a/examples/plainsong-4-tap-closing-time.song b/examples/plainsong-4-closing-time.song similarity index 100% rename from examples/plainsong-4-tap-closing-time.song rename to examples/plainsong-4-closing-time.song diff --git a/proposals/02-the-voyage.md b/proposals/02-the-voyage.md index b44014f4..72cd5239 100644 --- a/proposals/02-the-voyage.md +++ b/proposals/02-the-voyage.md @@ -11,12 +11,13 @@ Not claims — things this repository can demonstrate on demand. | | | |---|---| -| Notation compiles | 6,333 sources, 0 errors, including every fenced example in the prose | +| Notation compiles | 6,340 sources, 0 errors, including every fenced example in the prose | | The music is pinned | 6,321 files hashed; CI fails if any file's *notes* change | | Chord symbols | An open grammar. `C7b9#11` works without being enumerated | | Voicing | Decided by measurement: 94.4% of named notes survive, against 50.1% | -| Tests | 578, plus 7 specs, on 3.10–3.13 across three operating systems, nothing installed | -| Packaging | Wheel built and exercised in a clean venv outside the tree | +| Tests | 667, plus 7 specs, on 3.10–3.13 across three operating systems, nothing installed | +| Packaging | On PyPI. `pip install plainsong` and the specs pass on a machine that never saw this repository | +| Rendering | `plainsong chart` draws an SVG chord chart with no font and no dependency | Two properties matter more than the numbers. **Parsing is already whitespace invariant** — tight, loose, ragged and tab-separated layouts of the same bar diff --git a/tests/corpus-fingerprint.txt b/tests/corpus-fingerprint.txt index b74e8f20..4a8674f3 100644 --- a/tests/corpus-fingerprint.txt +++ b/tests/corpus-fingerprint.txt @@ -2490,7 +2490,7 @@ b1ef7007b7a0f712 34 examples/edge-cases/edge-4-players-only.song 73ff48fe72d8bf12 157 examples/plainsong-1-creatures-of-interval.song a8567c72abd8ec82 143 examples/plainsong-2-the-room-is-safe.song 87a9033c4c06bcce 224 examples/plainsong-3-hermes-blues.song -c4040d29fc2a8835 111 examples/plainsong-4-tap-closing-time.song +c4040d29fc2a8835 111 examples/plainsong-4-closing-time.song 1c2186b5371e33f7 101 examples/plainsong-5-deck-work.song d9419c3c1bc05fc0 29 examples/plainsong-6-spacing-melody.song a351ccbd50ff2d4e 23 examples/plainsong-7-spacing-chords.song