Skip to content

Phase 4's merge predicate, plus a housekeeping pass that found six documents lying about the tree - #7

Merged
SuperInstance merged 1 commit into
masterfrom
claude/init-1wgqn0
Aug 17, 2026
Merged

Phase 4's merge predicate, plus a housekeeping pass that found six documents lying about the tree#7
SuperInstance merged 1 commit into
masterfrom
claude/init-1wgqn0

Conversation

@SuperInstance

@SuperInstance SuperInstance commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Two things: the merge predicate, and a documentation audit that turned up more
than expected.

A conflict between two agents is decidable

from plainsong.notation.merge import merge

result = merge(base, mine, theirs)
result.ok           # False only when both sides wrote the same bar differently
result.conflicts    # [section 1, melody, bar 1] -- a coordinate, not a diff hunk

An edit occupies a set of (section, row, bar) cells, and two edits conflict
exactly when those sets intersect.

What the matrix actually bought was not what the plan claimed.
Row-disjointness — two agents on @bass and Melody: — is something a
file-per-voice layout already has, and plainsong-mcp already had it. What only
a 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:

  • Bars numbered the way the arranger counts them — a repeated row continues,
    so two Melody: rows are bars 0–3, not 0–1 twice. (fails 1 test)
  • A removal is a change — otherwise a deleted row reads as no edit and the
    other side's work is resurrected. (fails 2)
  • The base is required — without it an untouched copy cannot be told from a
    deliberate revert. (fails 6)

plainsong-mcp's _FileLock is deliberately untouched: that is live
concurrency 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 mcp
Python SDK 2.0.0 over stdio: initialize returns plainsong 1.1.0 on protocol
2025-06-18; 27 tools, 9 resources, 2 prompts enumerate; compile_score
round-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: false with the failure in the content (error: no sections found). The tool ran; the music did not compile. Defensible under the
specification, which reserves isError for execution failures — but it means an
agent client must read the diagnostics rather than the flag, which is exactly the
"success is not evidence" trap AGENTS.md warns about, served over the wire.
Changing isError semantics is a decision about the protocol surface, not a
tidy-up, so it is written down and left for you.

The housekeeping found six documents disagreeing with the tree

  • Two rename leftovers. examples/plainsong-4-tap-closing-time.song and
    docs/traditions/03-the-tap-songbook.md still carried tap. Renamed. The
    songbook's own hits on that string — jarabe tapatío, patapan, tapestry
    are song titles and were left alone.
  • The README claimed nine complete examples. There are eight.
  • SHIPPING.md said 527 tests and 6,333 sources. It is 667 and 6,340 — and
    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 open"D9 sounds like
    D7"
    — which 1.0.1 fixed. That is the second document found carrying
    that same stale claim; the first was the 1.0.0 changelog.
  • Its browser-demo claim was the pre-fix one, describing note counts as the
    guard. Counting is precisely what failed to notice . and - being read as
    rests.
  • chart and merge appeared in no reference doc. Now in CLAUDE.md,
    AGENTS.md and docs/architecture.md — which had also grown two lists of the
    same 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 b850e4729399e7e5068a13c0 before and after. Not
one note moved, only a path.

Verification

  • 667 tests, 7 specs, ruff clean, 6340 file(s) checked, 6321 file(s) compile exactly as recorded.
  • Every relative link and image across 35 documents resolves.
  • All 24 subcommands the documentation names exist, and the 18 that operate
    on a file were run end to end.
  • The demo differential agrees on all eight cases — the check CI cannot run.
  • A wheel was built and exercised from outside the source tree, including the
    three new modules.
  • No TODO/FIXME/XXX/HACK in the package; no tracked build artifacts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PBAjxy7cD6DzJ72NX8TJEc

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
@SuperInstance
SuperInstance merged commit 7a53d1f into master Aug 17, 2026
16 checks passed
@SuperInstance SuperInstance changed the title Phase 4: make a conflict between two agents decidable instead of guessed Phase 4's merge predicate, plus a housekeeping pass that found six documents lying about the tree Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants