docs: plan the second pull from the fork, with its dry run already done
The upstream moved the day the first pull landed: ten commits on curated/curated, 86cb876b..29027f2b, that let a device hold several profiles and move between them, and with them the fork's first schema migration, version 20. The first plan named them the next pull and said the schema change earned a decision of its own. This is that plan, and unlike the first it was written after the dry run rather than before it, so its numbers are measured: rewrite from the fork point in 103 s, ten commits replayed onto776455ecwith nine clean and one resolved by rule, both compilers clean, jvmTest 826 -> 868 and testDebugUnitTest 413 -> 420 with no failures, every audit budget met, and 20.json regenerated byte-identical after a full build. The verdict is all ten, because the line is one feature and seven fixes braided together and four of the fixes are live on Mantra today with one profile: the relay observer that is never all cancelled, the read-only inbox that stays closed after its nsec is pasted, the DataStore race on desktop, and the startup screen's wallet-worded literals. Four decisions: take the whole line rather than carve the fixes out of five commits; take upstream's version 20 verbatim and adopt the rule that whichever tree migrates first owns the number; the one conflict is Mantra's own import from39fb64b6, resolved theirs, which closes one of the first plan's owed reverse-pulls; and two of the device's profiles in one group is a Mantra follow-up before release, since ChatRoom is keyed by the group id alone. What changed in the method: Mantra's tree is no longer a superset of the fork's, so the exactness check becomes "the residual between the trees is the same before and after the pull, file for file", and the plan gives the commands. The replay driver gains the one rule the dry run needed, with its reason. The README gets the row and a reading-order sentence, and the first plan points forward from the paragraph that predicted this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ silent, or a decision that looked arbitrary and was not.
|
||||
| [jvm-target.md](./jvm-target.md) | what desktop support cost, phased — why the native chain was already done, why an empty source set in our phoenix fork was the real blocker, and why DAO tests need none of it |
|
||||
| [material-design-conformance.md](./material-design-conformance.md) | what the M3 foundations actually require, measured against all 43 screens — the colour pairing that renders the app's own proposals invisible, and eight phases that put the decisions back in the theme |
|
||||
| [curated-to-mantra.md](./curated-to-mantra.md) | pulling the Curated fork's thirty-nine commits back under Mantra's names — which lines of work to take, the three decisions, and a measured way to replay a twice-rebranded history without touching seven hundred files by hand |
|
||||
| [curated-to-mantra-profiles.md](./curated-to-mantra-profiles.md) | the second pull: the fork's several-profiles line, ten commits and its first schema migration — which of it is a fix Mantra has today, who allocates a schema version number when two trees share one history, and the one conflict, which was Mantra's own |
|
||||
|
||||
Start with the ceremony if you are new to this area; the Marmot notes all assume it.
|
||||
Read the skipped-keys note before debugging any "the other device never got it"
|
||||
@@ -43,7 +44,11 @@ phase, which is a decision: it is about the repository rather than the app, and
|
||||
alone, except that its first decision leans on the derivation note's one rule. Two of
|
||||
the lines it pulled -- the group's nostr identity and the curated lists -- were taken
|
||||
out again the same day, and its record says what went and what stayed. The nsec and
|
||||
npub sign-in notes arrived with it and record what they built.
|
||||
npub sign-in notes arrived with it and record what they built. The profiles pull note
|
||||
is the same exercise a second time, planned with its dry run already done; read it
|
||||
after the first, because it assumes the method and the three decisions and only says
|
||||
what changed — chiefly that the tree is no longer a superset, and what the check for an
|
||||
exact pull becomes when it is not.
|
||||
The nsec sign-in note is a phased plan that has been built; it inherits the
|
||||
key-storage decision from the jvm-target note and drives the navigation state
|
||||
machine `NavigationViewModel.processLocalAccount` implements, so read it with the
|
||||
|
||||
527
docs/curated-to-mantra-profiles.md
Normal file
527
docs/curated-to-mantra-profiles.md
Normal file
@@ -0,0 +1,527 @@
|
||||
# Pulling Curare's several-profiles line back into Mantra
|
||||
|
||||
The [first pull](./curated-to-mantra.md) landed on 2026-09-13 and the upstream moved
|
||||
the same day: `curated/curated` went from `86cb876b` to `29027f2b`, ten commits that
|
||||
let a device hold several profiles and move between them — and, with them, the fork's
|
||||
first schema migration, version 20. That plan named them the next pull and said the
|
||||
schema change earned them a decision of their own rather than a ride. This is that
|
||||
decision, which of the ten Mantra wants, in what order, and how they land — with the
|
||||
dry run already done, so the numbers below are measured rather than predicted.
|
||||
|
||||
Read [multiple-profiles.md](./multiple-profiles.md) for the line's own reasoning once
|
||||
Phase 2 brings it; nothing here repeats it. Read the first pull's
|
||||
[three decisions](./curated-to-mantra.md#three-decisions) and its
|
||||
[Phase 1 record](./curated-to-mantra.md#phase-1--the-dry-run) for the method and the
|
||||
lessons; this document assumes both and adds what changed. The one rule everything
|
||||
still rests on is the derivation note's: two strings in this tree are hash inputs and
|
||||
are never renamed, and the normaliser cannot touch them by construction.
|
||||
|
||||
Written against `mantra` at `776455ec` — `origin/mantra` at `0fa807a6` plus the
|
||||
removal commit, which is where this plan starts and which `origin` does not yet carry
|
||||
— and `curated/curated` at `29027f2b`. Dry run executed 2026-09-13.
|
||||
|
||||
## The shape of the fork now
|
||||
|
||||
Since the first pull, Mantra's tree has stopped being a superset of Curare's non-brand
|
||||
tree. It took thirty of the fork's thirty-nine commits, then took three lines out again
|
||||
— the group's nostr identity, the curated lists, and the row-per-kind group screen — by a
|
||||
product decision recorded in the first plan. So the two trees now differ in 135 files:
|
||||
83 that Curare has and Mantra does not (the twelve removed screens and their view
|
||||
models, `nostr/curated/`, the seven group readings, and `WalletManagerExtension.kt`,
|
||||
which `39fb64b6` deleted on Mantra's side), 4 that Mantra has and Curare does not (the
|
||||
first plan, its two scripts, and `NostrPublicKeyJvmTest`), and 48 that both have with
|
||||
different content — `strings.xml`, `MantraNavHost.kt`, `ChatRoomDetailScreen.kt` and
|
||||
the rest of the removal's footprint. That residual is the seam every future pull has to
|
||||
cross, and the first plan predicted that anything upstream touching the identity block
|
||||
would conflict in it.
|
||||
|
||||
The ten commits, all of 2026-09-13, all on one line with no merges:
|
||||
|
||||
```
|
||||
* 29027f2b docs: record what the multiple-profiles plan built (record)
|
||||
* 88577bb5 test(identity): several profiles, both ways, and a boot (Phase 8)
|
||||
* 9bb34004 feat(ui): leaving one profile among several (Phase 7)
|
||||
* 759199f2 feat(sync): a fetch queue per identity, the inbox reopened (Phase 6, schema v20)
|
||||
* 2f420230 feat(identity): adding a profile from inside (Phase 5)
|
||||
* 25464595 feat(ui): the switcher (Phase 4)
|
||||
* 00fc9969 feat(identity): which profile opens (Phase 3)
|
||||
* 9b3a1927 feat(identity): a switch that tears down what it should (Phase 2)
|
||||
* 3008137e feat(identity): a seed's key is a credential (Phase 1)
|
||||
* a5ff2641 docs: plan several profiles on a device (plan)
|
||||
```
|
||||
|
||||
Three facts about them decide most of what follows.
|
||||
|
||||
**They touch none of what Mantra removed.** The ten change 69 files. Of those, six
|
||||
already differ between the trees, and in only four of the six is the difference the
|
||||
removal's — `strings.xml`, `MantraNavHost.kt`, `DatabaseNostrRepository.kt`,
|
||||
`NostrRepository.kt` — and in all four the removal's hunks and this line's hunks are far
|
||||
apart. The seam the first plan predicted is real and this line does not cross it: nine of
|
||||
the ten replay without a conflict, and the tenth conflicts on a line that is Mantra's own
|
||||
([the third decision](#3-the-one-conflict-is-mantras-and-it-closes-a-loop)).
|
||||
|
||||
**No library change.** The pin is `84cc44c` on both sides, unchanged by the ten;
|
||||
`BusinessManager.stopBusiness`, `GlobalPrefs.saveDefaultWallet` and the credentials
|
||||
file's format all existed already. This is the first pull from the fork in which the pin
|
||||
does not have to follow any commit.
|
||||
|
||||
**One schema migration**, `759199f2`: version 20, two nullable columns by
|
||||
`AutoMigration`, the export committed beside its predecessors. Mantra has no version 20
|
||||
of its own on any branch — measured: `curated/curated` is the only ref in this
|
||||
repository carrying a `20.json` — so the number is free, and after the pull Room
|
||||
regenerates nothing: the built tree's `20.json` is byte-identical to the fork's. The
|
||||
[second decision](#2-schema-version-20-and-who-allocates-the-number) is about the
|
||||
number, not the columns.
|
||||
|
||||
## The inventory
|
||||
|
||||
Every commit, what it is, what it does to Mantra *as it is today*, and the verdict.
|
||||
Sizes are `git show --shortstat`; `759199f2`'s is 5,698 lines of generated schema plus
|
||||
395 of code.
|
||||
|
||||
| commit | phase | size | what Mantra gets | verdict |
|
||||
|---|---|---|---|---|
|
||||
| `a5ff2641` | plan | 2 files, +1,192 | `docs/multiple-profiles.md` and its README row | **pull** |
|
||||
| `3008137e` | 1 | 16 files, +649 −111 | a seed's nostr key written to `nostr-credentials.dat` as a `Secret`, repaired in for every seed already on the device; `StoredIdentity.merge` inverted so the credentials file is the list of profiles; the identity's key read from the credential rather than from the running node; `forgetNostrCredential` refusing a key a seed derives | **pull** — the on-disk change; see [Phase 2](#phase-2--the-credential-alone) |
|
||||
| `9b3a1927` | 2 | 9 files, +490 −31 | `switchToWallet` becomes `switchToIdentity` and stops the node it leaves; `RelaysSocketManager.observeActiveUserId` restructured under `collectLatest` so a switch cancels the previous identity's relay observer; `stopPlatformBusiness` as three one-line actuals | **pull** — a fix Mantra has today, see below |
|
||||
| `00fc9969` | 3 | 7 files, +233 −22 | `StartupChoice.resolve`, the startup precedence lifted out of the screen and reordered; `setActiveIdentity` saves the default, the forget tails clear it; the startup screen's six literals into the catalogue as *profile* words | **pull** |
|
||||
| `25464595` | 4 | 13 files, +547 −24 | `ProfilesRoute`/`ProfilesScreen`, reached from the profile tab's row that today goes to `ImplementationPendingRoute`; `WalletsSelector` rows showing the nostr profile instead of "Default name" over a random emoji, and "Wallet" beside a seed | **pull** |
|
||||
| `2f420230` | 5 | 24 files, +624 −201 | *sign in with a key* and *create a new profile* under both lists; a profile created from inside is a bare key through `NewProfileWriter`, a seed only from Landing; *end this* and its `wipeDatabase` gone from the create screen; a pasted key already on the device offers *switch to it*; `JvmGlobalPrefs` closing a DataStore race on desktop | **pull** — two fixes Mantra has today, see below |
|
||||
| `759199f2` | 6 | 17 files, +6,093 −23 | `ownerPublicKey` on the two fetch queues, stamped by the repository, drained by the owner's pump; `NostrDao.reopenInbox` opening every wrap addressed to the active key that no seal names, once per activation; `GiftWrapSeal.giftWrapMessageId` written on the path that never wrote it; **schema v20** | **pull** — a fix Mantra has today, and the decision |
|
||||
| `9bb34004` | 7 | 3 files, +61 −1 | *switch profile* on the not-found screen when another profile exists | **pull** |
|
||||
| `88577bb5` | 8 | 1 file, +282 | `MultipleProfilesRoundTripJvmTest`: two profiles, both ways, and a cold boot | **pull** |
|
||||
| `29027f2b` | record | 2 files, +46 −7 | the built-record table in the plan; the README's reading order | **pull** |
|
||||
|
||||
Nothing is dropped. The line is one feature and seven fixes braided together — four of
|
||||
them live on Mantra today — and the
|
||||
[first decision](#1-take-the-whole-line-and-what-taking-less-would-cost) is why the
|
||||
braid is not unpicked.
|
||||
|
||||
### What is a fix for Mantra today, before any second profile exists
|
||||
|
||||
Four of the ten close bugs a Mantra install has now, with one profile, and each is
|
||||
verifiable against the current tree:
|
||||
|
||||
- **The relay observer leaks across sign-outs** (`9b3a1927`).
|
||||
[RelaysSocketManager.kt:65](../composeApp/src/commonMain/kotlin/press/mantra/compose/network/relays/RelaysSocketManager.kt)
|
||||
still reads `// TODO: Cancel all pending jobs?` above a map of one relay-list job per
|
||||
pubkey, of which only the job for the pubkey being *started* is ever cancelled. Sign out
|
||||
of a read-only identity, sign in as another, and two observers feed `updateRelayPools`
|
||||
with the one pool following whichever list emitted last. The sign-out exits that make
|
||||
this reachable arrived in the first pull.
|
||||
- **A read-only identity's inbox stays closed after its nsec is pasted** (`759199f2`).
|
||||
[NostrDao.kt:451](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NostrDao.kt)
|
||||
says of a wrap it cannot open that "the key that opens this one may be signed in later",
|
||||
and nothing runs when it is: `storeNostrEvent` never re-indexes an id it holds, so the
|
||||
wraps an npub preview fetched are never opened by the upgrade the first pull built. And
|
||||
`GiftWrapSeal.giftWrapMessageId` is written on the NIP-17 DAO's path and not on the one
|
||||
`indexNostrEvent` takes, so nothing in the database can say which wraps were opened —
|
||||
the question `reopenInbox` asks.
|
||||
- **Desktop can throw "multiple DataStores active for the same file"** (`2f420230`).
|
||||
`Phoenix.jvm.kt` builds a `DataStoreManager` per call, and the library caches one
|
||||
`GlobalPrefs` per process behind an unsynchronised check-then-set; the listing on IO and
|
||||
a sign-in's write racing at first use both build one, and the loser throws. Upstream's
|
||||
tests hit it one run in three. `JvmGlobalPrefs` is the one way the jvm target reaches
|
||||
the prefs, under a lock; Android goes through the `Application`'s single instance and
|
||||
never raced.
|
||||
- **The startup screen's words** (`00fc9969`). `SovereignWalletStartupScreen` carries six
|
||||
string literals — "Initializing...", "Preparing wallet...", "Opening wallet", "Starting
|
||||
wallet" — outside the catalogue and in the wallet's vocabulary; the M3 audit counts them
|
||||
among the 40 literals it reports without a budget, and after the pull it reports 30.
|
||||
"Starting wallet" survives, in the one branch where a wallet is what is starting.
|
||||
|
||||
The other three fixes are latent until a device holds two profiles — the precedence
|
||||
`when` that puts "show me the list" above "open this one", the default that is read at
|
||||
every boot and saved by nothing, *end this* wiping every profile's rooms from the screen
|
||||
that adds one — and the line makes two profiles possible, so they ship with it.
|
||||
|
||||
### What is a feature, and whether Mantra wants it
|
||||
|
||||
The profile tab has had a *Change account* row since the first pull, and it routes to
|
||||
`ImplementationPendingRoute`
|
||||
([ActiveProfileScreen.kt:324](../composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ActiveProfileScreen.kt)).
|
||||
The switch behind it — `switchToWallet`, `resetToSelector`, the null identity sent to
|
||||
startup with `popUpTo(0)` — has been in the tree since Phoenix and is reachable from
|
||||
nowhere; Landing shows only when the device holds nothing, so a second profile cannot be
|
||||
added. Mantra took the nsec and npub sign-in lines without qualification; this is the
|
||||
line that answers the question both of them deferred, and it is wanted for the same
|
||||
reason they were: a translator who reviews as one key and signs a chapter as another, or
|
||||
who holds a collective's read-only preview beside their own profile, is the ordinary
|
||||
case, and the row that promises it is already on the screen.
|
||||
|
||||
The one feature choice inside the line that is a Mantra question rather than a Curare
|
||||
one is that **a profile created from inside is a key, not a second wallet**. Mantra's
|
||||
wallet is the Phoenix fork's; nothing in the translation product asks for a second
|
||||
Lightning node, and a user who wants another profile has not asked for a second phrase
|
||||
with funds behind it. The seed stays the first profile's, from Landing. That is the
|
||||
right answer here too.
|
||||
|
||||
## Four decisions
|
||||
|
||||
### 1. Take the whole line, and what taking less would cost
|
||||
|
||||
The alternative is to take the four fixes and leave the feature: `9b3a1927`'s observer,
|
||||
`759199f2`'s inbox, `2f420230`'s `JvmGlobalPrefs`, `00fc9969`'s words. Measured
|
||||
against the commits, that is not four cherry-picks; it is editing five commits by hand.
|
||||
`9b3a1927` renames `switchToWallet` in the same hunk that stops the node; `759199f2`'s
|
||||
owner column exists so that a *switch* cannot lose a message, and its `reopenInbox`
|
||||
runs from the same activation collector; `2f420230` fixes the DataStore race in the
|
||||
jvm actuals it rewrites for the bare-key writer; `00fc9969`'s strings are the
|
||||
selector's. Every carve-out leaves a Mantra `SovereignWalletViewModel`, nav host and
|
||||
startup screen that differ from upstream's in the same places, and the first plan's
|
||||
principle stands: every feature left out is a permanent conflict seam, paid for at every
|
||||
later pull. The removal of lines B, D and H was that principle's one exception, made on
|
||||
product grounds and measured at 135 files of residual; this line touches none of it,
|
||||
and adding a second exception for a feature the product wants would be paying the price
|
||||
twice for nothing.
|
||||
|
||||
So: all ten, in upstream's order, with the phases cut where upstream's own rollout
|
||||
section says the line can ship in pieces.
|
||||
|
||||
### 2. Schema version 20, and who allocates the number
|
||||
|
||||
Two apps, two `applicationId`s, two databases that never meet on one device: nothing
|
||||
stops Mantra from having a version 20 of its own shape and Curare another. What it would
|
||||
cost is the method. The first plan's pull was mechanical because the nineteen schema
|
||||
files were byte-identical on both sides; a Mantra 20 that is not Curare's 20 makes every
|
||||
later migration a hand-merge of `MantraDatabase.kt`'s `autoMigrations` list and a renumbering
|
||||
of every schema file after it, in both directions, forever.
|
||||
|
||||
So the rule, stated once: **a schema version is allocated by whichever tree migrates
|
||||
first, and the other tree pulls that migration before it adds its own.** Never two
|
||||
different 20s. Today that is trivially satisfiable — Mantra has none pending; the check
|
||||
that it stays satisfiable is a `git ls-tree` over every ref for the next number before
|
||||
a migration is written on either side.
|
||||
|
||||
Taking upstream's 20 verbatim is safe on the evidence: the columns are nullable
|
||||
additions to two request-queue tables, `AutoMigration` with no data rewrite, and the
|
||||
built tree regenerates nothing (`git status` clean under `composeApp/schemas/` after a
|
||||
full compile; identity hash `4414c373…`). The downgrade story is the one every
|
||||
migration in this database has had — an older build cannot open the file — and
|
||||
`PlatformDatabaseBuilder` has the same answer it always had. The two-column change
|
||||
carries no risk of its own; what it carries is the number, and the rule above is what
|
||||
makes the number cheap.
|
||||
|
||||
### 3. The one conflict is Mantra's, and it closes a loop
|
||||
|
||||
The dry run's single conflict is `CreateProfileViewModel.kt` at `2f420230`, and it is
|
||||
not the removal's seam. Mantra's `39fb64b6` moved the `nostrPublicKey()` import from the
|
||||
app's `WalletManagerExtension` to the library's; upstream never took that change and
|
||||
still imported the app's — one line of residual, the last of the first plan's Phase 6
|
||||
reverse-pulls that was never done. `2f420230` rewrites the import block wholesale, and
|
||||
removes the seed generation the import served: the new view model asks a
|
||||
`NewProfileWriter` for a key and derives nothing itself.
|
||||
|
||||
Resolution: take the file as upstream leaves it. Not a union — the previous run's lesson
|
||||
about joins holds for import blocks too — but *theirs*, because the base difference is
|
||||
one import this commit deletes the use of. Afterwards the file is identical on both
|
||||
sides, and the residual between the trees goes from 135 files to 134. The
|
||||
[replay driver](./scripts/curated-replay.py) carries the rule as a `THEIRS` entry with
|
||||
that reason, beside the one `42f3a697` needed for the same import in two other files.
|
||||
|
||||
What is left of `39fb64b6` upstream after this: `WalletManagerExtension.kt` itself,
|
||||
now with zero callers on either side. The reverse-pull the first plan owed shrinks to
|
||||
deleting a dead file; see [Phase 6](#phase-6--close-the-loop).
|
||||
|
||||
### 4. Two of the device's own profiles in one group
|
||||
|
||||
Upstream's out-of-scope list names it and Mantra should name it louder, because groups
|
||||
are Mantra's product. `ChatRoom`'s primary key is the group id and `userPublicKey` says
|
||||
whose the row is; `DkgSession`, `GroupKeyState` and every FROST share hang off the room.
|
||||
Today one device holds one profile and the collision cannot happen. After this line a
|
||||
user can invite their second profile into their own collective, and the second member's
|
||||
MLS state overwrites the first's — silently, in the shape of "the other device never got
|
||||
it". The library, dialects and projects are keyed by `chatRoomId` and reached through
|
||||
the room, so they scope per profile for free; the room itself does not.
|
||||
|
||||
Decision: **not part of the pull, and a Mantra-native follow-up before the line ships
|
||||
to users.** The cheap fix upstream names — refuse a Marmot welcome for a room the device
|
||||
already holds under another profile, with a notice saying which — is one branch in the
|
||||
inbound manager and a string. The real fix, keying the room by `(id, userPublicKey)`,
|
||||
is a migration of every table that references a room, and it is not this plan's. Write
|
||||
the cheap one natively, under Mantra's names, and offer it upstream by cherry-pick; it
|
||||
is the kind of commit the fork should take.
|
||||
|
||||
## How it lands
|
||||
|
||||
The first plan's pipeline, unchanged in method, with three things that differ.
|
||||
|
||||
**Rewrite from the fork point, not from the last pull.** The rewritten history is
|
||||
rebuilt from `ba26c0b1` every time — 49 commits now, 103 seconds — because a
|
||||
`filter-branch` over only `86cb876b..curated/curated` would leave the first new commit's
|
||||
parent in Curare's names, and the cherry-pick of that commit would be a seven-hundred-file
|
||||
rename. The normaliser has no new rules to learn: the rewritten tip holds no `curare`,
|
||||
`Curare` or `com.it.curated` token outside the first plan's own prose, checked by
|
||||
`git grep`.
|
||||
|
||||
```bash
|
||||
git fetch curated
|
||||
git worktree add -b tmp/pull2 <scratch> mantra # from 776455ec, the removal included
|
||||
cd <scratch>
|
||||
git branch -f tmp/curated-src curated/curated
|
||||
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f -d <tmpdir> \
|
||||
--tree-filter 'python3 /abs/path/docs/scripts/curated-unbrand.py tree .' \
|
||||
--msg-filter 'cat; echo "Pulled-From: curated/curated@$GIT_COMMIT"' \
|
||||
-- ba26c0b1..tmp/curated-src
|
||||
python3 docs/scripts/curated-replay.py pick <scratch> \
|
||||
a5ff2641 3008137e 9b3a1927 00fc9969 25464595 2f420230 759199f2 9bb34004 88577bb5 29027f2b
|
||||
```
|
||||
|
||||
A clean worktree is required — `filter-branch` refuses unstaged changes, and a
|
||||
populated `lightning-kmp-app` at a pin other than `84cc44c` counts. A fresh worktree
|
||||
has no submodule and is clean; for the build afterwards, symlink a populated checkout
|
||||
at `84cc44c` in (`git status` then shows ` T lightning-kmp-app`, which the ten never
|
||||
touch) and pass `ANDROID_HOME` on the command line, as the first plan's appendix did.
|
||||
|
||||
**The exactness check is restated**, because `git diff tmp/curated-src HEAD` is no
|
||||
longer "the commits you dropped" — it is 135 files of the removal. The property that
|
||||
holds instead: **the pull moves Mantra by exactly the ten commits, so the residual
|
||||
between the trees is the same before and after**, file for file and hunk for hunk,
|
||||
except where a resolution deliberately changed it.
|
||||
|
||||
```bash
|
||||
U0=<rewritten 86cb876b>; U1=<rewritten 29027f2b>; M0=776455ec; M1=HEAD
|
||||
diff <(git diff --name-only $U0 $M0 | sort) <(git diff --name-only $U1 $M1 | sort)
|
||||
# expected: exactly CreateProfileViewModel.kt, gone from the residual
|
||||
for f in $(comm -12 <(git diff --name-only $U0 $M0 | sort) <(git diff --name-only $U1 $M1 | sort)); do
|
||||
cmp -s <(git diff $U0 $M0 -- $f | grep '^[-+]' | grep -v '^[-+][-+]') \
|
||||
<(git diff $U1 $M1 -- $f | grep '^[-+]' | grep -v '^[-+][-+]') || echo "residual moved: $f"
|
||||
done
|
||||
# expected: nothing
|
||||
```
|
||||
|
||||
Anything else in either list is a normaliser rule that is wrong, a resolution that is,
|
||||
or an upstream commit that crossed the seam — and it is found here.
|
||||
|
||||
**The rules the driver needs**: one. `2f420230` × `CreateProfileViewModel.kt` → theirs,
|
||||
for the reason in the third decision. The `UNION` rule for `strings.xml`,
|
||||
`MantraNavHost.kt` and `docs/README.md` stays in place and did not fire — git merged all
|
||||
three cleanly at every one of the ten — but a future upstream commit that appends
|
||||
strings where the removal deleted them will need it, and the first plan's lesson about
|
||||
deletions is already encoded in it.
|
||||
|
||||
Then the four checks, per phase, as before: `:composeApp:compileDebugKotlinAndroid`,
|
||||
`:composeApp:compileKotlinJvm`, `:composeApp:jvmTest`, `:composeApp:testDebugUnitTest`,
|
||||
`:composeApp:m3Audit`, and — new for this pull — `git status` under `composeApp/schemas/`
|
||||
after the compile, which is the check that Room agrees with the committed `20.json`.
|
||||
|
||||
## The phases
|
||||
|
||||
Each phase ends the same way: both compilers, both test suites and the audit green on
|
||||
the branch, the exactness check as above against the phase's cut, and one review of the
|
||||
diff against the phase's stated contents. Upstream's rollout section says which pieces
|
||||
can ship apart — Phase 1 alone, 2 with 3, 4 with 5, 6 whenever, 7 after — and the cuts
|
||||
below follow it, so that each phase is also a shippable state.
|
||||
|
||||
### Phase 0 — the base
|
||||
|
||||
Nothing from Curare lands. Two things to settle first:
|
||||
|
||||
1. **The removal is on `mantra` and not on `origin/mantra`.** `776455ec` is the local
|
||||
`mantra` tip and `origin` is at `0fa807a6`. Every measurement in this document is
|
||||
against `776455ec`; a pull onto `0fa807a6` would land the ten on a tree that still
|
||||
holds lines B, D and H, and would then have to be removed from under them. Push the
|
||||
removal, or merge it, before anything else.
|
||||
2. **A worktree with the library at `84cc44c`.** The pin does not move in this line, so
|
||||
there is no `PIN` entry to add; but a worktree whose submodule sits at another commit
|
||||
(this session's was at `01489b8`, an older master) makes `filter-branch` refuse and
|
||||
the compile fail on `NostrCredentialManager`. `git submodule update --init` fixes it
|
||||
online; offline, fetch the commit from a populated sibling checkout —
|
||||
`git -C lightning-kmp-app fetch <checkout>/lightning-kmp-app 84cc44c && git -C lightning-kmp-app checkout --detach 84cc44c`
|
||||
— since worktrees keep separate submodule git directories.
|
||||
|
||||
Exit: `git status` clean in the scratch worktree; `origin/mantra` at `776455ec` or later.
|
||||
|
||||
### Phase 1 — the dry run
|
||||
|
||||
**Run on 2026-09-13, against `curated/curated` at `29027f2b`, on a scratch worktree of
|
||||
`776455ec`.** The record, so the next reader starts from what happened:
|
||||
|
||||
| measurement | value |
|
||||
|---|---|
|
||||
| `filter-branch` over `ba26c0b1..curated/curated` | 49 commits, 103 s; 834 files rewritten, 828 paths moved at the tip; 0 brand tokens left in code |
|
||||
| files the ten touch | 69 (+10,184 −387; without `20.json`, 68 files +4,486 −387) |
|
||||
| of those, already differing between the trees | 6: `strings.xml` (−234), `MantraNavHost.kt` (−264), `DatabaseNostrRepository.kt` (−6), `NostrRepository.kt` (−14), `CreateProfileViewModel.kt` (±1), `docs/README.md` (+7) |
|
||||
| replay | 10 picked: 9 clean, 1 resolved by rule, 0 stuck |
|
||||
| the resolution | `2f420230` × `CreateProfileViewModel.kt`: theirs |
|
||||
| exactness | residual 135 → 134 files; the one gone is `CreateProfileViewModel.kt`; no other file's residual moved |
|
||||
| `compileDebugKotlinAndroid` + `compileKotlinJvm` | clean |
|
||||
| `:composeApp:jvmTest` | 868 tests, 0 failures (Mantra before: 826) |
|
||||
| `:composeApp:testDebugUnitTest` | 420 tests, 0 failures (before: 413; the seven are `StartupChoiceTest`) |
|
||||
| `:composeApp:m3Audit` | all budgets met; 12 adaptive uses, 2 navigation components; composable literals 40 → 30 |
|
||||
| Room after the compile | `git status` clean under `composeApp/schemas/`; `20.json` byte-identical to the fork's |
|
||||
| `strings.xml` | 444 → 455 strings; 14 added, 3 removed (`change_account`, `end_this`, `select_a_wallet`) |
|
||||
| library pin | `84cc44c` before and after; the ten do not touch the gitlink or `.gitmodules` |
|
||||
|
||||
If the upstream moves before Phases 2–5 land, redo this on the day: a number that
|
||||
drifts is an upstream commit that crossed the seam, and the driver stops on it rather
|
||||
than guessing.
|
||||
|
||||
### Phase 2 — the credential, alone
|
||||
|
||||
`a5ff2641`, `3008137e`. The plan and its first phase, on their own, because this is the
|
||||
one commit in the line that **changes what is on disk**: the first launch after it writes
|
||||
a `Secret` credential into `nostr-credentials.dat` for every seed the device holds, and a
|
||||
build from before it, reached by a downgrade after that write, lists each seed-backed
|
||||
profile twice — once as a wallet with a derived key, once as a bare key under a different
|
||||
id. Nothing is lost, both rows sign as the same key, and the next upgrade lists them as
|
||||
one; but it is the one line of this pull worth a release note, and shipping it first
|
||||
means that a rollback of any later phase rolls back to a tree that already knows the new
|
||||
file.
|
||||
|
||||
What to look at in review: `writeMnemonic` writes the credential *before* the seed, so a
|
||||
crash between the two leaves a profile the phrase completes rather than a seed with no
|
||||
listing; `SeedCredentials.reconcile` runs after both files are read and hands the listing
|
||||
what it returns, so a repair that could not write still lists the seed by derivation — a
|
||||
failed write must never hide a wallet; `setActiveWallet` reads the key from the
|
||||
credential and `check()`s it against the node's, since a node disagreeing with the
|
||||
credentials file is the one corruption worth refusing to run under;
|
||||
`forgetNostrCredential` answers `WalletAttached` for a key a seed derives, because the
|
||||
next repair would write it back.
|
||||
|
||||
Exit: a device with a seed lists once, from the credential, with the node's key as a
|
||||
cross-check; `SeedCredentialsJvmTest` and the rewritten halves of `IdentityWriterJvmTest`
|
||||
and `StoredIdentityJvmTest` green; release note drafted.
|
||||
|
||||
### Phase 3 — the switch, the precedence, the switcher, the entrances
|
||||
|
||||
`9b3a1927`, `00fc9969`, `25464595`, `2f420230`. Upstream ships 2 with 3 and 4 with 5;
|
||||
they go together here because a switch nobody can reach and a switcher nobody can add to
|
||||
are each half a feature, and the four are 53 files of which the one resolution above is
|
||||
the whole of the conflict.
|
||||
|
||||
What to look at: `observeActiveUserId` is a child of `collectLatest` and the per-pubkey
|
||||
map is gone — the test that fails against the old observer is
|
||||
`RelaysSocketManagerSwitchJvmTest`'s re-emission of A's list changing nothing;
|
||||
`switchToIdentity` clears the identity *before* stopping the node, so every collector
|
||||
that could reach the business is cancelled first; `StartupChoice.resolve` puts what a
|
||||
switch or sign-in named above the user's earlier "show me the list"; `ProfilesScreen`
|
||||
has a back button and one column at every width; `CreateProfileRoute(withWallet = true)`
|
||||
only from Landing; the create view model writes the secret, then the six bootstrap
|
||||
events, then calls the tail — the order the sign-in screen documents as load-bearing;
|
||||
`JvmGlobalPrefs` is the only place the jvm target reaches `loadGlobalPrefsForWallet`.
|
||||
|
||||
Exit: on desktop and Android, a second profile can be signed in or created from the
|
||||
switcher and switched to; the previous node is stopped once; a cold boot opens the
|
||||
last-used profile without asking; `IdentitySwitchJvmTest`, `ProfilesScreenJvmTest`,
|
||||
`AddProfileFromInsideJvmTest`, `CreateProfileScreenJvmTest` green.
|
||||
|
||||
### Phase 4 — the queue owner and the inbox, with the migration
|
||||
|
||||
`759199f2`, alone, because it is the schema change and upstream's rollout says it can
|
||||
ship early or late independently of everything else. It fixes a case Mantra has today —
|
||||
the closed inbox of an upgraded read-only identity — and the case the switch would
|
||||
create: a wrap fetched under the other profile's key, stored, and never opened again.
|
||||
|
||||
What to look at: the `AutoMigration(from = 19, to = 20)` comment says what the columns
|
||||
mean and that the broadcast queue deliberately has none; `DatabaseNostrRepository`
|
||||
stamps where the caller stamped nothing and keeps an owner a caller named; the DAO's
|
||||
own requests — placeholder-profile syncs, a Marmot join's participant syncs — stay
|
||||
unowned on purpose, since they fetch public kinds; `reopenInbox` runs from the sync
|
||||
pumps' collector once per activation, for an identity that can sign, *before* the live
|
||||
inbox and the broadcast pump are launched; one transaction per wrap, so one that
|
||||
cannot be opened rolls back its own changes and the next is still tried;
|
||||
`GiftWrapMessage.toGiftWrapSeal` sets `giftWrapMessageId`. And the schema check: a
|
||||
full compile, then `git status` — nothing under `composeApp/schemas/`.
|
||||
|
||||
Exit: `OwnedRequestQueueJvmTest` and `ReadOnlyGiftWrapDaoJvmTest` green; an npub
|
||||
signed in, sent a DM by someone else, then upgraded with its nsec reads the DM on the
|
||||
next activation;
|
||||
schema files 1–20 committed and regenerated identically.
|
||||
|
||||
### Phase 5 — the exits, the round trip, the record
|
||||
|
||||
`9bb34004`, `88577bb5`, `29027f2b`. Small, and the last: the not-found screen's third
|
||||
action when another profile exists, the eight-phase round trip through the real view
|
||||
models, and the built-record table at the top of `multiple-profiles.md` with the
|
||||
README's reading-order sentence.
|
||||
|
||||
What to look at: `MultipleProfilesRoundTripJvmTest` constructs no `GlobalPrefs` of its
|
||||
own — the library's per-process cache means a test that builds a
|
||||
`SovereignWalletViewModel` must read the default through the view model's instance, the
|
||||
trap `00fc9969` recorded — and Mantra's own future tests inherit that rule.
|
||||
|
||||
Exit: the full suite at the dry run's numbers or above; the audit green; the README's
|
||||
table has the new row between the npub note and the jvm-target note.
|
||||
|
||||
### Phase 6 — close the loop
|
||||
|
||||
Five things, none of them a merge.
|
||||
|
||||
1. **The follow-up in the fourth decision**, natively: refuse a welcome for a room the
|
||||
device already holds under another profile. Before the line reaches users, not
|
||||
after the first report.
|
||||
2. **Prose that is now off by a removal.** `multiple-profiles.md` anchors three links to
|
||||
line numbers — `MantraNavHost.kt:418` and `:746`, `DatabaseNostrRepository.kt:111` —
|
||||
that are upstream's, in a file Mantra's removal shortened by 264 lines. The anchors
|
||||
describe the tree *before* the line was built and were already approximate upstream;
|
||||
either drop the numbers or re-point them, in the same commit as item 1 or alone.
|
||||
3. **Reverse-pull what Mantra owes the fork**, shrunk by the third decision:
|
||||
`WalletManagerExtension.kt` is dead on both sides and Curare can delete it; the Torch
|
||||
retirement is unchanged from the first plan's list. Two small commits, both
|
||||
hand-cherry-picks in the other repository.
|
||||
4. **Offer item 1 upstream.** It is written under Mantra's names against a file both
|
||||
trees hold identically, so it applies to the fork as an ordinary cherry-pick — the
|
||||
direction this whole exercise exists to make cheap.
|
||||
5. **The fork decision, restated with a second data point.** The first plan estimated
|
||||
half a day per pull; this one cost the machine about ten minutes and the reading about an hour,
|
||||
because the tooling was in place and the line crossed no seam. That is the good case.
|
||||
The bad case is any upstream commit that edits `ChatRoomDetailScreen.kt` or the nav
|
||||
host's identity block, and the driver will stop on it rather than resolve it. The
|
||||
three options the first plan listed stand in the same order — converge the package
|
||||
name, invert the relationship, or keep pulling — and nothing about this pull changes
|
||||
which is best; it only shows that the third is affordable for as long as the fork's
|
||||
work stays clear of what Mantra took out.
|
||||
|
||||
## Risks, and the silent ones in particular
|
||||
|
||||
**The write on first launch.** Phase 2's repair is the first time a pull has changed a
|
||||
file in the node-data directory of an existing install. It is idempotent, one write for
|
||||
however many seeds are missing, nothing at all on a device with none or one already
|
||||
repaired, and a failed write is a result rather than a throw; the listing goes on with
|
||||
what it read. Test it on a real install with a seed from before the first pull, not
|
||||
only on the in-memory harness.
|
||||
|
||||
**The downgrade double-listing** is a release note, not a bug, and Phase 2 shipping
|
||||
alone is what keeps it to one line.
|
||||
|
||||
**Room 20.** An older build cannot open the file; the same story as versions 2–19.
|
||||
Anyone carrying a Mantra branch that adds a table must rebase onto this before numbering
|
||||
it 20 — the second decision's rule, and the `ls-tree` check that enforces it.
|
||||
|
||||
**iOS is unverified**, again. `stopPlatformBusiness` has an ios actual that calls
|
||||
`BusinessManager.stopBusiness`, one line, compiled by nobody on linux; the ios target is
|
||||
gated behind macOS. Build it on a Mac before believing it — and before it, the
|
||||
first plan's `Config.xcconfig` change, which has still not been.
|
||||
|
||||
**Every switch passes the social precondition gate.** Upstream names it as out of
|
||||
scope: every `ProfileLoaded` routes through the onboarding gate, on every launch and so
|
||||
on every switch. For Mantra, whose users will switch between a collective's preview and
|
||||
their own profile more often than Curare's, this is the first thing a user will complain
|
||||
about; the fix is one branch in `processLocalAccount` for an account the machine has
|
||||
seen before, and it is Mantra's to write.
|
||||
|
||||
**The per-process `GlobalPrefs` cache.** A test that constructs `SovereignWalletViewModel`
|
||||
must not build a `GlobalPrefs` of its own, or the second instance over the same file
|
||||
throws — recorded upstream, and true of every Mantra test written after this.
|
||||
|
||||
**`strings.xml` loses three keys.** `change_account`, `end_this` and `select_a_wallet`
|
||||
go; nothing on Mantra references them outside the files the same commits rewrite, and
|
||||
the compile is the check. The `UNION` rule applies a deletion correctly since the first
|
||||
plan's fix; it did not need to.
|
||||
|
||||
**The upstream moves.** Between this document being written and the pull landing,
|
||||
`curated/curated` may gain commits. Redo Phase 1; the driver stops on anything new.
|
||||
|
||||
## Appendix — the residual, for the record
|
||||
|
||||
The 135 files by which Mantra at `776455ec` differs from the rewritten `86cb876b`, and
|
||||
which this pull leaves alone, so that the next reader can tell a seam from a surprise:
|
||||
|
||||
| kind | count | what |
|
||||
|---|---|---|
|
||||
| Curare has, Mantra does not | 83 | 35 under `ui/composable`, 28 under `ui/view`, `nostr/curated/` (5), the seven `nostr/Group*` readings and `CuratedSuggestion` with their tests, `WalletManagerExtension.kt` |
|
||||
| Mantra has, Curare does not | 4 | `docs/curated-to-mantra.md`, its two scripts, `NostrPublicKeyJvmTest.kt` |
|
||||
| both have, differently | 48 | the logo's thirteen files and one `build.gradle.kts` comment; the seven places where Phase 0's prose about the protected names is worded for a tree that had no rebrand (`Relays.kt`, `SharedKeyDerivation.kt`, `ChillDkgRitualManager.kt`, `PlatformContext.jvm.kt`, `HomeScreen.kt`, `shared-key-derivation.md`, `material-design-conformance.md`); the removal's footprint of twenty-six — `m3-title-case.py`'s two sample titles, `strings.xml`, `MantraNavHost.kt`, `ChatRoomDetailScreen.kt` with its view model and state, `ChatMessage.kt`'s `applyInnerEvent` arms, `ProposedEvent` and its test, the four repositories, `ChatTranscript`, `ProfileAvatar`, `Member.kt`, `NostrEvent.kt`, `NostrEventDao`, `LocalChatRoom`, the broadcast screen, button, view model, state and two tests, `docs/README.md`; and, until this pull, `CreateProfileViewModel.kt` |
|
||||
|
||||
After the pull: 134, the same list less one.
|
||||
@@ -60,7 +60,9 @@ resolution is to drop it.
|
||||
The upstream moved before this landed: `curated/curated` went from `86cb876b` to
|
||||
`29027f2b`, ten commits for several profiles on one device, one of them the fork's
|
||||
first schema migration (version 20). They are the next pull, and the schema change is
|
||||
why they get their own decision rather than a ride on this one.
|
||||
why they get their own decision rather than a ride on this one — which is
|
||||
[curated-to-mantra-profiles.md](./curated-to-mantra-profiles.md), planned and dry-run the
|
||||
same day.
|
||||
|
||||
Written against `origin/mantra` at `ba26c0b1` and `curated/curated` at `86cb876b`;
|
||||
executed on 2026-09-13.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"""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
|
||||
pipeline as it was actually run; docs/curated-to-mantra-profiles.md is the second pull it
|
||||
served, and added the one rule that pull needed. 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>.
|
||||
@@ -56,10 +57,12 @@ 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'),
|
||||
('2f420230', 'composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/CreateProfileViewModel.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',
|
||||
'2f420230': "this commit's import block taken whole -- Mantra had already moved the nostrPublicKey() import to the library's (39fb64b6), and this commit removes the seed derivation that import served; the file is identical on both sides afterwards (docs/curated-to-mantra-profiles.md, the third decision)",
|
||||
}
|
||||
DELETE = {('55664cc7', 'composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/GroupNostrProfileSectionJvmTest.kt')}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user