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>
2026-09-13 11:57:48 +02:00
#!/usr/bin/env python3
""" Land rewritten Curated commits on a Mantra branch, one cherry-pick at a time.
The companion of curated - unbrand . py , and the second half of docs / curated - to - mantra . md ' s
pipeline as it was actually run . The rewrite ( filter - branch with curated - unbrand . py as the
tree filter and a ` Pulled - From : curated / curated @ < sha > ` trailer stamped by its message
filter ) leaves a branch ` tmp / curated - src ` in Mantra ' s names; this replays commits off it
by their * original * sha , in the order you give , onto the current branch of < worktree > .
curated - replay . py pick < worktree > < orig - sha > . . . cherry - pick each , resolving by the rules below
curated - replay . py check < worktree > < orig - cut - sha > files differing from the rewritten cut
Resolutions are rules , and anything the rules do not cover stops the run with the commit
aborted , so a new conflict is a decision rather than a guess . What the rules encode :
* a branch join - - the first commit off one side of an upstream merge landing on top of
the other side - - is resolved by taking the join files exactly as upstream ' s own merge
left them ( TAKE_FROM ) . Not a union : a union orders the two appended blocks its own way ,
and every later commit that edits the block then misses its base and its deletions
silently fail .
* files whose lines are unique by construction ( string keys , imports , route
registrations , table rows ) get a line - set three - way merge over diff3 hunks ( UNION ) :
ours , minus what theirs deleted from base , plus what theirs added that the file does
not already hold anywhere .
* the library pin follows the commit being replayed ( PIN ) , because each upstream commit
compiled against the pin it recorded ; ` 5 efeae76 ` named a branch commit that no longer
exists and is mapped to the master commit with the identical tree .
* a handful of ( commit , file ) pairs where one side simply wins ( THEIRS , DELETE ) , each
with the reason written into the commit .
Every resolution is appended to the landed commit ' s message as a paragraph, ahead of the
trailers , so the branch explains itself .
"""
import os , re , subprocess , sys
STRINGS = ' composeApp/src/commonMain/composeResources/values/strings.xml '
NAVHOST = ' composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt '
UI = ' composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ '
GITLINK = ' lightning-kmp-app '
UNION = { STRINGS , NAVHOST , ' docs/README.md ' }
PIN = {
' 366b0177 ' : ' 59c11ed926ac05c820b98afb4cc961a5a3506310 ' , # the nsec line was written against 59c11ed's NostrKeyManager
' 5efeae76 ' : ' 84cc44c855e3d173e26016b807c449153a3af597 ' , # upstream: 01962f3, a rebased-away branch commit with this tree
' 00c36ec5 ' : ' 84cc44c855e3d173e26016b807c449153a3af597 ' ,
}
TAKE_FROM = { # (orig, path) -> the original merge commit whose rewritten tree supplies the file
( ' 1e52fc8f ' , STRINGS ) : ' c8de3a1f ' , ( ' 1e52fc8f ' , NAVHOST ) : ' c8de3a1f ' ,
( ' 0634487e ' , UI + ' CuratedSuggestionListScreen.kt ' ) : ' fedbe724 ' ,
( ' 0634487e ' , STRINGS ) : ' fedbe724 ' , ( ' 0634487e ' , NAVHOST ) : ' fedbe724 ' ,
}
AFTER = { # orig -> files folded in after the pick, from the merge that edited them outside its conflicts
' 0634487e ' : [ ( UI + ' AcceptCuratedSuggestionScreen.kt ' , ' fedbe724 ' ) , ( UI + ' BroadcastGroupSignedEventScreen.kt ' , ' fedbe724 ' ) ] ,
}
THEIRS = {
( ' 42f3a697 ' , ' composeApp/src/commonMain/kotlin/press/mantra/compose/network/relays/RelaysSocketManager.kt ' ) ,
( ' 42f3a697 ' , ' composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.kt ' ) ,
( ' e3cc23ae ' , UI + ' KeyRecoveryScreen.kt ' ) ,
}
THEIRS_WHY = {
' 42f3a697 ' : " this commit ' s import block taken whole -- Mantra had already moved the nostrPublicKey() import to the library ' s (39fb64b6), and this commit removes the call it served " ,
' e3cc23ae ' : ' the class comment this commit rewrites is taken whole; the only base difference was the dropped rebrand capitalising the brand in one word of it ' ,
}
DELETE = { ( ' 55664cc7 ' , ' composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/GroupNostrProfileSectionJvmTest.kt ' ) }
def main ( ) :
mode , wt , args = sys . argv [ 1 ] , sys . argv [ 2 ] , sys . argv [ 3 : ]
def git ( * a , check = True , * * kw ) :
return subprocess . run ( [ ' git ' , * a ] , cwd = wt , capture_output = True , text = True , check = check , * * kw )
rewritten = { }
for line in git ( ' log ' , ' --format= % H % (trailers:key=Pulled-From,valueonly) ' , ' origin/mantra..tmp/curated-src ' ) . stdout . splitlines ( ) :
parts = line . split ( )
if len ( parts ) == 2 : rewritten [ parts [ 1 ] . split ( ' @ ' ) [ - 1 ] [ : 8 ] ] = parts [ 0 ]
rw = lambda orig : rewritten [ orig [ : 8 ] ]
if mode == ' check ' :
print ( git ( ' diff ' , ' --name-only ' , rw ( args [ 0 ] ) , ' HEAD ' ) . stdout , end = ' ' ) ; return
def union_resolve ( path ) :
p = os . path . join ( wt , path ) ; lines = open ( p , encoding = ' utf-8 ' ) . read ( ) . split ( ' \n ' )
state = None ; outside = set ( )
for l in lines :
if l . startswith ( ' <<<<<<< ' ) : state = ' in ' ; continue
if l . startswith ( ' >>>>>>> ' ) : state = None ; continue
if state is None : outside . add ( l )
out = [ ] ; state = None ; ours = base = theirs = None
for l in lines :
if l . startswith ( ' <<<<<<< ' ) : state = ' ours ' ; ours , base , theirs = [ ] , [ ] , [ ] ; continue
if l . startswith ( ' ||||||| ' ) and state == ' ours ' : state = ' base ' ; continue
if l == ' ======= ' and state in ( ' ours ' , ' base ' ) : state = ' theirs ' ; continue
if l . startswith ( ' >>>>>>> ' ) and state == ' theirs ' :
removed = { b for b in base if b not in theirs and b . strip ( ) }
added = [ t for t in theirs if t not in base ]
kept = [ o for o in ours if o not in removed ]
have = outside | set ( kept )
out . extend ( kept ) ; out . extend ( a for a in added if a not in have or a . strip ( ) == ' ' ) ; state = None ; continue
if state == ' ours ' : ours . append ( l )
elif state == ' base ' : base . append ( l )
elif state == ' theirs ' : theirs . append ( l )
else : out . append ( l )
open ( p , ' w ' , encoding = ' utf-8 ' ) . write ( ' \n ' . join ( out ) )
def amend_note ( note ) :
msg = git ( ' log ' , ' -1 ' , ' --format= % B ' ) . stdout . rstrip ( ' \n ' ) . split ( ' \n ' ) ; i = len ( msg )
while i > 0 and re . match ( r ' ^[A-Za-z-]+: ' , msg [ i - 1 ] ) : i - = 1
body , trailers = msg [ : i ] , msg [ i : ]
while body and body [ - 1 ] == ' ' : body . pop ( )
subprocess . run ( [ ' git ' , ' commit ' , ' -q ' , ' --amend ' , ' -F ' , ' - ' ] , cwd = wt , input = ' \n ' . join ( body + [ ' ' , note , ' ' ] + trailers ) + ' \n ' , text = True , check = True )
for orig in args :
o8 = orig [ : 8 ] ; sha = rw ( o8 ) ; subject = git ( ' log ' , ' -1 ' , ' --format= %s ' , sha ) . stdout . strip ( ) ; notes = [ ]
r = git ( ' -c ' , ' merge.conflictStyle=diff3 ' , ' cherry-pick ' , ' --allow-empty ' , sha , check = False )
if r . returncode != 0 :
stuck = [ ]
for f in git ( ' diff ' , ' --name-only ' , ' --diff-filter=U ' ) . stdout . split ( ) :
if f == GITLINK and o8 in PIN :
git ( ' update-index ' , ' --cacheinfo ' , f ' 160000, { PIN [ o8 ] } , { GITLINK } ' ) ; notes . append ( f ' gitlink -> { PIN [ o8 ] [ : 8 ] } , the pin this commit compiles against ' )
elif ( o8 , f ) in TAKE_FROM :
src = TAKE_FROM [ ( o8 , f ) ] ; open ( os . path . join ( wt , f ) , ' w ' ) . write ( git ( ' show ' , f ' { rw ( src ) } : { f } ' ) . stdout ) ; git ( ' add ' , f )
notes . append ( f ' { os . path . basename ( f ) } : taken as the original merge { src } left it, this being the branch join ' )
elif ( o8 , f ) in THEIRS :
git ( ' checkout ' , ' --theirs ' , ' -- ' , f ) ; git ( ' add ' , f ) ; notes . append ( f ' { os . path . basename ( f ) } : { THEIRS_WHY [ o8 ] } ' )
elif ( o8 , f ) in DELETE :
git ( ' rm ' , ' -q ' , ' -f ' , f ) ; notes . append ( f ' { os . path . basename ( f ) } : deleted, as this commit deletes it upstream ' )
elif f in UNION :
union_resolve ( f ) ; git ( ' add ' , f ) ; notes . append ( f ' { os . path . basename ( f ) } : line-set three-way merge, both sides \' additions kept and this commit \' s deletions applied ' )
else :
stuck . append ( f )
if stuck :
git ( ' cherry-pick ' , ' --abort ' , check = False ) ; sys . exit ( f ' STUCK at { o8 } { subject } : no rule for { stuck } ' )
r2 = subprocess . run ( [ ' git ' , ' -c ' , ' core.editor=true ' , ' cherry-pick ' , ' --continue ' ] , cwd = wt , capture_output = True , text = True )
if r2 . returncode != 0 : sys . exit ( f ' STUCK continuing { o8 } : { r2 . stderr . strip ( ) [ : 300 ] } ' )
for path , src in AFTER . get ( o8 , [ ] ) :
open ( os . path . join ( wt , path ) , ' w ' ) . write ( git ( ' show ' , f ' { rw ( src ) } : { path } ' ) . stdout ) ; git ( ' add ' , path )
notes . append ( f ' { os . path . basename ( path ) } : brought to the state the original merge { src } left it in, an edit that merge made outside its conflicts ' )
docs(scripts): the pin follows the commit even when git fast-forwards the gitlink
Found landing Phase 3 for real, one commit after the trial had passed it. In
the trial worktree the submodule directory was an empty placeholder, so when
366b0177 moved the pin from 01489b8 to 59c11ed against a branch already at
84cc44c, git could not compare the three and reported a conflict, and the pin
rule set 59c11ed as intended. On the real branch the submodule is populated,
git can see that 59c11ed is an ancestor of 84cc44c, and it resolves the
gitlink by fast-forward -- cleanly, silently, and to the newer pin, which is
the one the commit does not compile against.
So the pin rule no longer waits for a conflict: after any pick of a commit
the table names, the gitlink is set to what the table says, and the note says
git had fast-forwarded it. The same code path now covers 5efeae76's mapping of
the unfetchable 01962f3 onto 84cc44c, which used to be a special case.
Phase 3 was reset to the Phase 2 tip and is landed again with this in place;
nothing else about those eight commits changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:00:07 +02:00
if o8 in PIN and git ( ' ls-tree ' , ' HEAD ' , GITLINK ) . stdout . split ( ) [ 2 ] != PIN [ o8 ] :
# a populated submodule lets git resolve the gitlink by fast-forward and keep the newer pin;
# the pin has to follow the commit, since each compiles only against the library it named
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>
2026-09-13 11:57:48 +02:00
git ( ' update-index ' , ' --cacheinfo ' , f " 160000, { PIN [ o8 ] } , { GITLINK } " )
docs(scripts): the pin follows the commit even when git fast-forwards the gitlink
Found landing Phase 3 for real, one commit after the trial had passed it. In
the trial worktree the submodule directory was an empty placeholder, so when
366b0177 moved the pin from 01489b8 to 59c11ed against a branch already at
84cc44c, git could not compare the three and reported a conflict, and the pin
rule set 59c11ed as intended. On the real branch the submodule is populated,
git can see that 59c11ed is an ancestor of 84cc44c, and it resolves the
gitlink by fast-forward -- cleanly, silently, and to the newer pin, which is
the one the commit does not compile against.
So the pin rule no longer waits for a conflict: after any pick of a commit
the table names, the gitlink is set to what the table says, and the note says
git had fast-forwarded it. The same code path now covers 5efeae76's mapping of
the unfetchable 01962f3 onto 84cc44c, which used to be a special case.
Phase 3 was reset to the Phase 2 tip and is landed again with this in place;
nothing else about those eight commits changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:00:07 +02:00
notes . append ( ' gitlink -> 84cc44c: upstream pinned 01962f3, a branch commit since rebased onto the library \' s master as 84cc44c with an identical tree ' if o8 == ' 5efeae76 '
else f ' gitlink -> { PIN [ o8 ] [ : 8 ] } , the pin this commit compiles against (git had fast-forwarded it to the newer one) ' )
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>
2026-09-13 11:57:48 +02:00
if notes :
git ( ' commit ' , ' -q ' , ' --amend ' , ' --no-edit ' , check = False )
amend_note ( ' Replayed onto Mantra by docs/curated-to-mantra.md: ' + ' ; ' . join ( notes ) + ' . ' )
print ( f " { o8 } { ' resolved ' if notes else ' clean ' : 8s } { subject [ : 80 ] } " )
if __name__ == ' __main__ ' :
if len ( sys . argv ) < 4 or sys . argv [ 1 ] not in ( ' pick ' , ' check ' ) : sys . exit ( __doc__ )
main ( )