docs: record what the multiple-profiles plan built, and the places it chose differently

Phases 1–8 are implemented, in order, one commit each; Phase 9 is the
rollout and stays as written. The plan's phases are kept as the reasoning,
and the table at the top says where the build chose differently: the
repair reads before it writes and hands the listing its result; one
WalletAttached outcome with two ways in; the node stop and the relay scope
injected for their tests; the default save that cannot crash; a
ProfilesViewModel over flows; a NewProfileWriter and a route flag where the
plan expected the create screen's existing writer to serve; the colliding
id on the outcome rather than on the enum; the DAO's own requests left
unowned because they fetch public kinds; the inbox reopened by re-indexing
each wrap in its own transaction rather than by lifting the unseal branch
out; and the round trip's A made through the create view model with a
never-started PhoenixBusiness for the switch to stop.

Three things found on the way and in no phase are recorded beside the
table: the library's unsynchronised global-preferences cache, reached by
two threads at once for the first time, now behind JvmGlobalPrefs on the
jvm target; the same cache's consequence for tests that construct the
sovereign view model; and the gift wrap seal's link to its wrap, a foreign
key that existed and was never written until the inbox sweep asked the
question it answers.

The README's row and reading order say the note is built, and name
switchToIdentity where they named switchToWallet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulled-From: curated/curated@29027f2b92
This commit is contained in:
Kgothatso Ngako
2026-09-13 01:57:05 +02:00
parent 72b9d3d2ed
commit ba5ef72385
2 changed files with 46 additions and 7 deletions

View File

@@ -58,8 +58,9 @@ The npub sign-in note is a phased plan that has been built, and reads as that
plan's out-of-scope note answered: it takes the `Identity` type and the sign-in
machine as given and asks what an identity with no secret is for, before it asks
how to build one; read its table of where the build chose differently first.
The multiple-profiles note is a phased plan that has not been built, and reads as
the third of the sign-in notes: it asks what happens when the device holds two
The multiple-profiles note is a phased plan that has been built, and reads as the
third of the sign-in notes: it asks what happens when the device holds two
identities, and its first phase changes one rule the other two share — a seed's
key becomes a credential like any other — so read it with `StoredIdentity.merge`,
`SovereignWalletViewModel.switchToWallet` and the startup screen open.
`SovereignWalletViewModel.switchToIdentity` and the startup screen open, and read
its table of where the build chose differently first.

View File

@@ -11,10 +11,41 @@ stores, the sign-in screen and the forget sequence as given, changes one rule th
two of them share, and asks the question both deferred without naming it: what
happens when the device holds two of them.
**Not built.** Phases 1–8 are one commit each, in the order given; Phase 9 is the
rollout. The phases are written as reasoning rather than as a checklist, for the
reason the two sign-in plans give: the code reads better against the argument it
came from.
**Built**, phases 1–8, one commit each, in the order given; Phase 9 is the rollout
and is process rather than code. The phases are kept as written because they are
the reasoning, and the code reads better against the argument it came from than
against a summary of itself. Where the implementation chose differently the table
below says so:
| what the plan said | what it turned out to be |
|---|---|
| the repair runs "before the credentials file is read for the listing" | after both files are read, and the listing is built from what it returns — the repaired map, or the one read if the write failed — so the file is decrypted once |
| `NotACredential` becomes `WalletAttached` for a key a seed derives | and for a key not in the file at all, which since the repair can only be a seed's key the repair could not write; one outcome, two ways in |
| `stopPlatformBusiness` called from `switchToIdentity` | injected into the view model as a function, defaulting to the `expect`, so the branch can be pinned with a recorder — a node cannot be started in a test, but a `PhoenixBusiness` can be built, since everything in it is lazy |
| the relay observer restructured | as planned, plus an injectable scope and a `relayUrls` read for the test that fails against the old observer |
| `setActiveIdentity` saves the default | wrapped: a default that could not be written is a selector on the next boot, not a crash now. The forget tails call `forgetDefaultIdentity()` on the view model rather than `clearDefaultWallet()` inline |
| "Initializing…" among the four that become *opening profile* | it and "Preparing wallet…" both became *preparing profiles*; they are the same wait |
| `ProfilesViewModel` "owns the four states" | takes the four flows it joins rather than the view model that holds them, so a compose test can drive it with `MutableStateFlow`s and a fake repository; the fourth state, empty, is not modelled, as the plan said it could not happen |
| a row's fallback "the npub and the metadata's emoji" | the npub as the title over the emoji avatar, with no second line; a key with a kind 0 gets the name over the picture with the npub under it |
| "`createAccount` already takes its writer as a function" | a `NewProfileWriter` interface returning the key it derived and the id it filed it under, with `seedProfileWriter` and `bareKeyProfileWriter` on the view model and `CreateProfileRoute(withWallet)` choosing; the view model plants the six events *after* the write and calls the tail after that, so the account is in the database before the identity is activated — an ordering the create flow used to get away with by luck |
| `CredentialProblem.AlreadyOnThisDevice` "carries it" | the id rides on `Outcome.Failed` and the `Error` state instead; `CredentialProblem` stays an enum and the parser's tests stay as they were |
| the DAO's own requests "stamped from the `activeKeyPair` it is already handed" | left unowned, on purpose: the placeholder-profile syncs and a Marmot join's participant syncs fetch public kinds that need no key to open, and any profile that is open may as well fetch them. Only what goes through the repository is stamped |
| `openGiftWrap` lifted out of `indexNostrEvent` | not lifted: the branch is a screenful of room and participant handling, and running the stored wrap through `indexNostrEvent` again, in a transaction of its own per wrap, gives the same result with one code path. `reopenGiftWrap` is that |
| `ForgetIdentityJvmTest` gains "forgetting B while A remains" | the round trip covers it, forgetting B and C and listing A alone; the per-outcome tests were done in Phase 1 |
| the round trip "creates A from a phrase" | through the create view model with the seed writer, as Landing does, rather than through a pasted phrase; and A's node is a `PhoenixBusiness` that was never started, which is enough for the switch to have something to stop |
Three things found on the way that are in no phase. `DataStoreManager` caches one
`GlobalPrefs` per process behind an unsynchronised check-then-set, and DataStore
refuses a second instance over the same file: two first calls at once — the listing
on IO and a sign-in's write — could both build one, and the loser threw at first
use. `JvmGlobalPrefs` is now the one way the jvm target reaches the prefs, under a
lock; Android goes through the `Application`'s single instance and never raced. The
same cache means a test that constructs `SovereignWalletViewModel` must not build a
`GlobalPrefs` of its own. And `GiftWrapSeal.giftWrapMessageId`, a foreign key to the
wrap a seal came out of, existed and was never written — so nothing in the database
could say which wraps had been opened, which is the question the inbox sweep asks;
`decryptGiftWrapSeal` now sets it, and a seal from before is swept once and comes
back with the link.
## The vocabulary
@@ -1049,6 +1080,13 @@ is a test to rewrite, not to delete. Phase 6's unseal branch of `indexNostrEvent
has never been called from anywhere but `indexNostrEvent`, and lifting it out is
where a transaction boundary will turn out to have been load-bearing.
Phases 1–8 are now implemented, in one sitting and in order. What each turned out
to require, as against what was predicted here, is in the table at the top and in
the commit messages on this branch. The Phase 6 uncertainty resolved itself by not
lifting the branch at all; the uncertainty that did bite was in no phase — a
process-wide cache in the library, reached by two threads at once for the first
time, which the tests for Phase 5 hit one run in three.
## Out of scope
- **A lazy node.** Phase 1 puts the key in the credential, so a profile with a