Phase 4's merge predicate, plus a housekeeping pass that found six documents lying about the tree - #7
Merged
Merged
Conversation
notation/merge.py three-way merges two edits of one score. An edit occupies a set of (section, row, bar) cells, and two edits conflict exactly when those sets intersect. That is a decision procedure: if the sets are disjoint the merge is defined, and no amount of locking would have made it more correct. The matrix bought more than the plan claimed. Row-disjointness is something a file-per-voice layout already has, and plainsong-mcp already had it -- parts are separate files with their own versions. What only a coordinate *per bar* can decide is that two edits to the same row do not overlap: bars 1-4 against bars 5-8 of one melody. That is the case the lock was actually serialising, and it is the one this makes answerable. Three rules earned their place by failing the suite when removed: - Bars are numbered the way the arranger counts them. A repeated row continues rather than restarting, so two `Melody:` rows are bars 0-3 and not 0-1 twice. Number them per row and two agents editing different bars look like a single collision. - A removal is a change. Otherwise a deleted row reads as no edit at all, and the other side's work on it is resurrected by a merge that believed nobody had objected. - The base is required. Without it, an untouched copy of a row cannot be told from a deliberate revert, and a two-way diff silently undoes the other agent. Writing the same thing on both sides is agreement, not collision. The merge reasons about written tokens rather than arranged notes, so it cannot change anyone's music by rounding -- and it does not claim the result is any good. Two agents can write compatible bars that make poor harmony together, which is a musical judgement and not a merge conflict. What is deliberately not here: plainsong-mcp's write_part still takes a file lock over the whole session directory. Replacing that is a change to live concurrency control in another repository and wants its own diff, its own review, and a test that runs two writers at once rather than reasoning about them. The predicate it needs now exists. 667 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.
Two things: the merge predicate, and a documentation audit that turned up more
than expected.
A conflict between two agents is decidable
An edit occupies a set of
(section, row, bar)cells, and two edits conflictexactly when those sets intersect.
What the matrix actually bought was not what the plan claimed.
Row-disjointness — two agents on
@bassandMelody:— is something afile-per-voice layout already has, and
plainsong-mcpalready had it. What onlya coordinate per bar can decide is that two edits to the same row don't
overlap: bars 1–4 against bars 5–8 of one melody. That is the case the lock was
actually serialising.
Three rules, each mutated and confirmed to turn the suite red:
so two
Melody:rows are bars 0–3, not 0–1 twice. (fails 1 test)other side's work is resurrected. (fails 2)
deliberate revert. (fails 6)
plainsong-mcp's_FileLockis deliberately untouched: that is liveconcurrency control in another repository and wants its own diff plus a test
that runs two writers, not one that reasons about them. The plan says "the
predicate is done; the ensemble is not" rather than rounding up.
A third-party MCP client has now connected
The caveat that had stood since the server was written — "never had a real MCP
client connect to it" — is closed by doing it. Driven with the official
mcpPython SDK 2.0.0 over stdio:
initializereturnsplainsong 1.1.0on protocol2025-06-18; 27 tools, 9 resources, 2 prompts enumerate;compile_scoreround-trips inline notation; a resource reads back; a call missing a required
argument returns
isError: true.One finding recorded rather than fixed. Notation the compiler cannot read
returns
isError: falsewith the failure in the content (error: no sections found). The tool ran; the music did not compile. Defensible under thespecification, which reserves
isErrorfor execution failures — but it means anagent client must read the diagnostics rather than the flag, which is exactly the
"success is not evidence" trap
AGENTS.mdwarns about, served over the wire.Changing
isErrorsemantics is a decision about the protocol surface, not atidy-up, so it is written down and left for you.
The housekeeping found six documents disagreeing with the tree
examples/plainsong-4-tap-closing-time.songanddocs/traditions/03-the-tap-songbook.mdstill carriedtap. Renamed. Thesongbook's own hits on that string — jarabe tapatío, patapan, tapestry —
are song titles and were left alone.
SHIPPING.mdsaid 527 tests and 6,333 sources. It is 667 and 6,340 — andthat document opens by saying every claim in it was checked against the
working tree, so stale numbers make it lie about itself specifically.
D9sounds likeD7" — which 1.0.1 fixed. That is the second document found carryingthat same stale claim; the first was the 1.0.0 changelog.
guard. Counting is precisely what failed to notice
.and-being read asrests.
chartandmergeappeared in no reference doc. Now inCLAUDE.md,AGENTS.mdanddocs/architecture.md— which had also grown two lists of thesame three projections, now one.
The fingerprint is keyed on path, so the rename required re-recording its
baseline. Proved inert the way earlier renames were: the note-hash multiset
across all 6,321 files is
b850e4729399e7e5068a13c0before and after. Notone note moved, only a path.
Verification
6340 file(s) checked,6321 file(s) compile exactly as recorded.on a file were run end to end.
three new modules.
TODO/FIXME/XXX/HACKin the package; no tracked build artifacts.🤖 Generated with Claude Code
https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc