docs: record the day's dry run, and the three things it changed in the plan

Phase 1 of docs/curated-to-mantra.md, run on 2026-09-13 on top of Phase 0, on a
scratch worktree that landed nothing. The plan asked for exactly this -- redo
the dry run the day you land, because the numbers drift -- and it was right to:
the upstream had moved, the pipeline as written could not do phased landings,
and the library pin turned out to have to move backwards. All three are now in
the plan, in the Phase 1 section, as a record beside the plan rather than a
rewrite of it.

**The replay lands by cherry-pick in topological order, not by
`rebase --rebase-merges`, and the reason is a property of git rather than a
preference.** The plan's phases are cuts through a graph with four merges in
it. Rebasing a cut whose merges have one parent in an earlier cut recreates
each such merge against the *pre*-replay parent -- the commit in the rewritten
source branch, not the one already landed -- and drags the unrebased lineage in
beside the rebased one. A single whole-range rebase, which is what the plan's
dry run measured, never meets this because every parent is inside the range.
So: the ordinary commits are picked one at a time, the merge commits land as
nothing, and their content, which is only ever conflict resolutions, is folded
in where the conflicts actually surface. The driver that does it is
docs/scripts/curated-replay.py, added here; every commit it lands carries a
`Pulled-From: curated/curated@<sha>` trailer stamped by the rewrite, and a
paragraph naming any resolution made on the way in.

**At a branch join the join files are taken exactly as upstream's own merge
left them, and a union was tried and rejected three times before that rule
was reached.** A union -- keep both sides of the conflict -- is the obvious
resolution for two branches appending strings to the same file, and it was
wrong three ways, each caught by the exactness check against the rewritten
tip and none of them visible in a passing build. It duplicates lines both
sides already hold when a conflict hunk widens (thirteen string keys, twice).
It never applies the other side's deletions, so the two copy-suggestion
strings that fcc19f95 removes survived. And, the one that took longest to see,
a join resolved in a different *order* from upstream's merge leaves every later
commit that edits the block unable to find its base, so its deletions fail
silently while its additions land -- which is why the second fix still left
the same two strings behind. The rule that survives: files whose lines are
unique by construction (string keys, imports, route registrations, table rows)
get a line-set three-way merge over diff3 hunks -- ours, minus what theirs
deleted from base, plus what theirs added that the file does not already hold
anywhere -- and never at a join, where the file upstream's merge produced is
the answer. The driver's docstring says all of this so the next reader does
not rediscover it.

**The library pin goes back to 59c11ed for Phases 3 and 4 and forward again
in Phase 5, and the compiler was asked before deciding.** The nsec line was
written against lightning-kmp-app 59c11ed and writes through its
`NostrKeyManager`; 84cc44c, Mantra's pin, renamed that class to a read-only
`LegacyNostrKeysFile`. Reasoning said the Phase 3 cut would not compile against
84cc44c; the trial worktree was put at that cut and built against it, and
produced eight `Unresolved reference 'NostrKeyManager'` errors in
IdentityWriter.kt. So 366b0177 moves the pin to 59c11ed, whose nested
lightning-kmp -> bitcoin-kmp -> secp256k1 chain is the same commit as
84cc44c's and rebuilds nothing native, and 5efeae76 brings it to 84cc44c --
not to the 01962f3 it named upstream, which is a branch commit since rebased
onto the library's master and no longer fetchable, but whose tree 84cc44c
reproduces exactly. Every commit on the branch will compile against the pin it
records, which is what upstream's history had and a fixed pin would have
thrown away. Alternatives rejected: adapting the Phase 3 commits to the
renamed class (rewriting upstream's work on the way in, and inventing an
intermediate state nobody built) and landing Phases 3 to 5 as one unit whose
inner commits do not build (bisect would hate it, and so would review).

**The upstream moved, and the new line is the next pull rather than part of
this one.** curated/curated went from 86cb876b, which the plan was written
against, to 29027f2b: ten commits for several profiles on one device, one of
which (759199f2) adds schema version 20, the fork's first migration. They are
recorded and left out on purpose; a migration landing on Mantra's database
deserves its own decision, and the plan's own principle -- pull the whole
non-brand tree so the next pull is a replay -- says how that decision goes
once it is made.

The trial, with the committed driver: 30 commits replayed (4, 8, 6, 12), 21
clean and 9 with a resolution note, 0 stuck; the driver's own rerun from the
Phase 0 tip reproduces the tree and improves on the hand-run trial by the one
README line the old union had wrongly kept; residual diff against the
rewritten 86cb876b exactly the known set -- Phase 0's prose, 39fb64b6's files,
808a3459's three, this document and its scripts, the logo.
:composeApp:compileDebugKotlinAndroid and :composeApp:compileKotlinJvm clean;
:composeApp:jvmTest 1,039 tests, 0 failures; :composeApp:testDebugUnitTest
530 tests, 0 failures; :composeApp:m3Audit all budgets met.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso NgakoandClaude Opus 5 committed 2026-09-13 11:57:48 +02:00
1 parent ba0830a3d4
commit 2a02fbc14f
2 files changed
+196

No files matched your search

+48
View File
@@ -456,6 +456,54 @@ touched a seam; a non-empty exactness diff means a new brand token the normalise
not know. Delete the worktree and the `tmp/` branches when done; the script recreates
them in under two minutes.
**Run on 2026-09-13, after Phase 0, and it changed the plan in three places.** The
record, so the next pull starts from what happened rather than from what was planned:
*The upstream moved first.* `curated/curated` was `86cb876b` when the plan was written
and `29027f2b` when Phase 1 ran: ten more commits, a "multiple profiles on a device"
line, one of which (`759199f2`) adds schema version 20 — the fork's first migration.
They are outside this plan's scope and are the next pull; the schema change is why
they get a decision of their own rather than a ride on this one.
*Landing is by cherry-pick in topological order, not by `rebase --rebase-merges`.* The
phased cuts cannot be expressed as rebases: a recreated merge whose other parent was
replayed in an earlier phase would be pointed at the *pre*-replay commit, dragging the
unrebased lineage in beside the rebased one. So the ordinary commits are picked one by
one, the four merge commits land as nothing, and their content — the resolutions — is
folded in where the conflicts actually surface: at each branch join the join files
(`strings.xml`, `MantraNavHost.kt`, `CuratedSuggestionListScreen.kt`) are taken exactly
as upstream's own merge left them, and the two files the merges edited outside their
conflicts are folded into the back-button commit. Every landed commit carries a
`Pulled-From: curated/curated@<sha>` trailer, stamped by the rewrite, and a paragraph
naming any resolution made on the way in. The driver is
[`docs/scripts/curated-replay.py`](./scripts/curated-replay.py).
*The library pin goes backwards for Phases 3 and 4, on purpose.* The nsec line was
written against lightning-kmp-app `59c11ed`, whose `NostrKeyManager` it writes through;
`84cc44c` — Mantra's pin — renamed that class to a read-only `LegacyNostrKeysFile`, and
the compiler says so at the Phase 3 cut (`Unresolved reference 'NostrKeyManager'` in
`IdentityWriter.kt`, eight errors). So `366b0177` moves the pin back to `59c11ed`, whose
nested chain is identical, and `5efeae76` brings it forward again — to `84cc44c` rather
than the `01962f3` it named upstream, which is an unfetchable branch commit whose tree
`84cc44c` reproduces exactly. Every commit on the branch compiles against the pin it
records, which is the property upstream's history had and a fixed pin would have lost.
*Three ways a "keep both sides" resolution is wrong,* each caught by the exactness
check and each now handled: it duplicates lines both sides already hold when a hunk
widens; it never applies the other side's deletions (the copy-suggestion strings
`fcc19f95` removes survived one attempt); and, subtlest, a join resolved in a different
*order* from upstream's merge leaves later commits unable to find the block they edit,
so their deletions silently miss. Hence: conflicts are picked with `diff3` markers and
resolved as a line-set three-way merge for files whose lines are unique by
construction, and never at a join, where upstream's merge is the answer.
The numbers, on the trial worktree: 30 commits replayed (4, 8, 6, 12), 0 stuck, 9
carrying a resolution note; residual diff against the rewritten `86cb876b` exactly
the known set (Phase 0's prose, `39fb64b6`'s files, `808a3459`'s three, this document
and its scripts); `compileDebugKotlinAndroid` and `compileKotlinJvm` clean;
`jvmTest` 1,039 tests, `testDebugUnitTest` 530, both 0 failures; `m3Audit` all budgets
met.
### Phase 2 — The group's identity, and the lists' read side
Cut at `392b90b8'`: `4c0ed0c1`, `334e6dd1`, `930d37c8`, `392b90b8`, with `808a3459`