iterations on /goal

This commit is contained in:
Alain Brenzikofer
2026-09-30 15:15:54 +02:00
parent 13e3d5a6e9
commit 22e89573aa
4 changed files with 70 additions and 80 deletions
+34 -34
View File
@@ -24,17 +24,17 @@ The only UI work here is removing the name cache both apps kept (§9). This is t
## Executive summary
#7525 is a declaration-only sketch: it adds `nameRegistration_`, `CPNameNotConnectable` and `PRMAll` to `Controller.hs` and changes two arities, but no producer was written and no consumer was updated, so **the branch does not compile**. `resolveNameRecord` (`Commands.hs:5082-5088`) collapses every non-`NRRegistered` answer into `NAME NOT_FOUND`, so expiry, price and reserved-reason — the substance of nine of the sixteen states — never leave core.
As of `db779eff4`, #7525 was a declaration-only sketch: it added `nameRegistration_`, `CPSimplexName` (renamed `CPNameNotConnectable` in `54bc83d80`) and `PRMAll` to `Controller.hs` and changed two arities, but no producer was written and no consumer was updated, so **the branch did not compile**. `resolveNameRecord` (`Commands.hs:5082-5088`) collapsed every non-`NRRegistered` answer into `NAME NOT_FOUND`, so expiry, price and reserved-reason — the substance of nine of the sixteen states — never left core.
The declared types are close to right. Twelve states map onto them as they stand, and of the four that do not, three are settled by wording or by a client-side reading rather than by the API (§4, §7). The work is therefore mostly **producer, not type**: stop discarding the registration, consult the existing by-name store lookups when the registry yields no usable link, and attach the registration to whichever plan comes out. One field is genuinely missing (`addressChanged`, for 3c) and one constructor needs its domain (`CPNameNotConnectable`, so the UI can print the bare name the canvas shows).
The declared types were close to right. Twelve states mapped onto them as they stood, and of the four that did not, three are settled by wording or by `NameWarning` (§4, §7). The work was therefore mostly **producer**: stop discarding the registration, consult the existing by-name store lookups when the registry yields no usable link, and attach a warning to whichever plan comes out, as `nameWarning_ :: Maybe NameWarning` in place of the declared `nameRegistration_`. Two fields were genuinely missing (`addressChanged` and `existingChat_`, for 3c) and one constructor needed its domain (`CPNameNotConnectable`, so the UI can print the bare name the canvas shows).
`getContactToConnect` / `getGroupToConnect` (`Direct.hs:802`, `Groups.hs:1090`) and `getUserContactLinkViaTarget` already accept `CTName` and query by domain. The by-name lookup that `CPNameNotConnectable`'s own precondition needs therefore exists — it is simply never reached, because the `CTDomain` branch throws first.
`getContactToConnect` / `getGroupToConnect` (`Direct.hs:802`, `Groups.hs:1090`) and `getUserContactLinkViaTarget` already accept `CTName` and query by domain. The by-name lookup that `CPNameNotConnectable`'s own precondition needs therefore exists — the `CTDomain` branch reached it, but when it found nothing and the registry gave no usable link, it threw instead of answering.
---
## 1. What #7525 declares, and what it does not produce
The diff against `fea9f482c` is eleven added lines in one file. It gives `PlanResolveMode` a `PRMAll` (`Controller.hs:714`, parser `:725`), makes `CRConnectionPlan.connLink` a `Maybe` (`:887`), hangs `nameRegistration_ :: Maybe NameRegistration` off `CPContactAddress` and `CPGroupLink`, adds `CPNameNotConnectable`, and updates `connectionPlanProceed` (`:1212-1233`).
This section records the sketch as of `db779eff4`, under today's constructor name: `CPNameNotConnectable` was then `CPSimplexName`. The diff against `fea9f482c` is eleven added lines in one file. It gives `PlanResolveMode` a `PRMAll` (`Controller.hs:714`, parser `:725`), makes `CRConnectionPlan.connLink` a `Maybe` (`:887`), hangs `nameRegistration_ :: Maybe NameRegistration` off `CPContactAddress` and `CPGroupLink`, adds `CPNameNotConnectable`, and updates `connectionPlanProceed` (`:1212-1233`).
None of it is reachable:
@@ -52,7 +52,7 @@ None of it is reachable:
## 2. Type delta
Four changes: two to the types in `Controller.hs`, two to `connectPlan` in `Commands.hs`.
The changes are to the types in `Controller.hs`, including `PlanResolveMode`, and to `connectPlan` in `Commands.hs`.
**`CPNameNotConnectable` carries its domain.**
@@ -62,14 +62,14 @@ Four changes: two to the types in `Controller.hs`, two to `connectPlan` in `Comm
`planSimplexName` cannot serve here. It is a `SimplexNameInfo`, which needs a `nameType`, and an unregistered name has none — today's code invents one by trying `NTPublicGroup` then `NTContact` (`Commands.hs:4432-4434`), which is arbitrary and becomes visible the moment the UI renders it. The canvas writes every band-2 body as a bare name (`sunflower.simplex is available…`, `bakery.simplex expired on…`), never `@`/`#`, so the UI wants `fullDomainName`, not `shortStr`.
**`CAPOk` and `GLPOk` gain `addressChanged :: Bool`.**
**`CAPOk` and `GLPOk` gain `addressChanged :: Bool` and `existingChat_ :: Maybe AChatInfo`.**
```haskell
| CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification, addressChanged :: Bool}
| GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification, addressChanged :: Bool}
| CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification, addressChanged :: Bool, existingChat_ :: Maybe AChatInfo}
| GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification, addressChanged :: Bool, existingChat_ :: Maybe AChatInfo}
```
True when the name resolved to a link that differs from the one held by the local chat that claims this name. This is state 3c and nothing else expresses it: both "a link you do not have" and "a link that replaced yours" are `CAPOk` today. It states that the address moved and not who moved it, which is exactly what the canvas claims ("Only the address change is known, not who made it") and is the narrow form of the `ownerChanged` field dropped in `f3bcd4a16` — no owner identity is carried, stored or compared.
`addressChanged` is true when the name resolved to a link that differs from the one held by the local chat, own address or own channel that claims this name. This is state 3c and nothing else expresses it: both "a link you do not have" and "a link that replaced yours" are `CAPOk` today. It states that the address moved and not who moved it, which is exactly what the canvas claims ("Only the address change is known, not who made it") and is the narrow form of the `nameOwnerChanged` field dropped in `f3bcd4a16` — no owner identity is carried, stored or compared.
**`connectPlan` returns an optional link and takes the registration already resolved.**
@@ -81,7 +81,7 @@ connectPlan :: User -> AConnectTarget -> PlanResolveMode -> Maybe LinkOwnerSig
The fifth parameter is the registration the bare name path resolved, so the path of the kind it plans does not resolve the name again. `CPContactAddress` and `CPGroupLink` have `nameWarning_ :: Maybe NameWarning` rather than a registration: see `plans/2026-09-28-name-warnings.md` §5.
**`PRMAll` gets its meaning, and replaces `PRMAllGroups`.** It re-resolves a chat that is already known, instead of short-circuiting in `knownLinkPlans`. `PRMAllGroups` did this for groups only, so it is removed and the directory service uses `PRMAll`.
**`PRMAll` gets its meaning, and replaces `PRMAllGroups`.** It re-resolves a chat that is already known by name and, as `PRMAllGroups` did, a group known by link, instead of answering from the local lookup (`knownContactPlans`, `knownGroupPlans`); so `PRMAllGroups` is removed and the directory service uses `PRMAll`.
Nothing else is removed. `SimplexDomainError` keeps both constructors (§6).
@@ -103,11 +103,11 @@ resolveNameRecord user nm domain =
_ -> throwError $ chatErrorAgent $ SMP "" (NAME SMP.NOT_FOUND)
```
**The plan logic** is described in `plans/2026-09-28-name-warnings.md` §6. For each kind, the path compares the local chat with the name's link. The bare name path looks up both kinds locally, resolves the name once, and plans one kind. When nothing is local and the name has no link of the kind, the answer is `CPNameNotConnectable d warning` with `connLink = Nothing`, which is why `connLink` had to become optional. The local lookups (`getUserContactLinkViaTarget`, `getContactToConnect`, `getGroupToConnect`) match on `cp.contact_domain` / `gp.group_domain` with `*_verified = 1`.
**The plan logic** is described in `plans/2026-09-28-name-warnings.md` §6. For each kind, the path compares the local chat with the name's link. The bare name path looks up both kinds locally, resolves the name once, and plans one kind. When nothing is local and the name has no link of the kind, the answer is `CPNameNotConnectable d warning` with `connLink = Nothing`, which is why `connLink` had to become optional. The chat lookups (`getContactToConnect`, `getGroupToConnect`) match on `cp.contact_domain` / `gp.group_domain` with `*_verified = 1`; the own address and own channel lookups (`getUserContactLinkViaTarget`, `getGroupInfoViaUserTarget`) match the user's profile claim and `gp.group_domain`.
**Expiry is a producer rule, not a UI rule.** An expired name must not yield a connectable plan — "It does not connect while only its owner can renew it". `expires` absent (a v20/v21 router sent the record alone) means expiry is unknown, so the name is treated as live — the only safe reading. The dateless fallback in §4 covers the other case: `expires` known and past, `graceUntil` absent.
**`addressChanged`.** Under `PRMAll`, or under `PRMUnknown` when the stored resolution is stale (§9), when the target is a `CTName` and the local lookup returns a known contact, a group, or the user's own address or channel, resolve the link anyway and compare it with the stored one. Equal, or fresh under `PRMUnknown`: today's `CAPKnown` / `GLPKnown`, which is 3a. Different: return the `Ok` plan **for the new link**, with `addressChanged = True` — the canvas draws 3c as a profile card for the new address with *Open new chat*, not as a known chat. Every other construction site passes `False`.
**`addressChanged`.** Under `PRMAll`, or under `PRMUnknown` when the stored resolution is stale (§9), when the target is a `CTName` and the local lookup returns a known contact, a group, or the user's own address or channel, resolve the link anyway and compare it with the stored one. Equal, or fresh under `PRMUnknown`: today's `CAPKnown` / `GLPKnown` (3a), or the own link plan (4a). Different: return the `Ok` plan **for the new link**, with `addressChanged = True` — the canvas draws 3c as a profile card for the new address with *Open new chat*, not as a known chat — unless that link's profile does not claim the name or, for a chat, its data cannot be fetched, when the local plan is returned (`plans/2026-09-28-name-warnings.md`, N12 and N19). Every other construction site passes `False`.
**Warnings, not registrations.** `nameWarning_` is `Just` exactly when the canvas shows an alert. The registration stays in core (`plans/2026-09-28-name-warnings.md` §5).
@@ -129,9 +129,9 @@ The canvas asks that a name you have a chat for resolve at most once a day, or w
| caller state | mode | result |
|---|---|---|
| known chat, resolved within a day and not past expiry | `PRMUnknown` | `CAPKnown` from the store, no network — 3a |
| known chat, stale or past expiry | `PRMUnknown` | registry + link resolve + compare — 3b, 3c, 3d |
| no known chat | `PRMUnknown` | full resolution — band 2 |
| known chat, resolved within a day and not past expiry | `PRMUnknown` | `CAPKnown` / `GLPKnown` from the store, no network — 3a |
| known chat, stale or past expiry | `PRMUnknown` | registry + link resolve + compare — 3a, 3b, 3c, 3d |
| no known chat, or own address or channel | `PRMUnknown` | full resolution — band 2, or 3c, 4a, 4c, 4d for own |
| forced refresh (directory service) | `PRMAll` | always resolves |
| per keystroke in search | `PRMNever` | local hit, or `CENotResolvedLocally` swallowed |
@@ -145,18 +145,18 @@ Link targets keep today's `PRMUnknown` meaning: a known chat is answered from th
`SDEUnknownDomain` stays, for 2g and 4b. It is a mismatch between a resolved link's profile and the name asked for, not a property of the registry answer, so it cannot become a `NameWarning`. Per the review thread it stays nullary — which name was claimed is not carried, because it is not shown.
`SDENoValidLink` stays for `verifyEntityDomain` and `/_set domain`. The plan path no longer raises it: 2f is `CPNameNotConnectable` with `NWNoValidLink`, and a chat or own name is answered without a warning.
`SDENoValidLink` stays for `/_set domain`, `APISetPublicGroupAccess` and service requests by name. The plan path no longer raises it: 2f is `CPNameNotConnectable` with `NWNoValidLink`, and a chat or own name is answered without a warning.
Registry and network failures stay `CPError` (2h). The canvas shows the resolver's own text, which `chatErrorAgent` already carries.
Registry and network failures stay command errors (2h). The canvas shows the resolver's own text, which `chatErrorAgent` already carries.
---
## 7. Decisions taken
1. **Reversed on 2026-09-28: the price is the 2-year term, computed in core.** `NamePrice {amount, years}` is the registry's per-year price for the label's length times `years = 2`, so the term is in one place and no client hardcodes it (`plans/2026-09-28-name-warnings.md`, N3).
2. **3c is a `Bool` on the `Ok` plans, not a revived `ownerChanged`.** It carries no owner and needs nothing persisted, so it does not reopen `f3bcd4a16`, and it is all the canvas claims.
3. **Dateless alerts rather than suppressed ones.** A user on an old router still learns the name expired; only the two dates go.
4. **Registration does not go through `ConnectionPlan`.** On the sibling canvas, 5a and 5b are the only registration states that would need one — 5a is `CPContactAddress (CAPKnown ct) (Just NRRegistered)`, 5b is `CPContactAddress (CAPOk …) (Just NRRegistered)` — and both are proposed for dropping (`b898b991d`, "suggest to drop 5a & 5b"). With them gone the registration check is a plain name-status call, this API keeps one consumer, and the two surfaces stop competing. Nothing here becomes removable as a result: every field 5a and 5b would have used is independently required by 3b and 3d.
2. **3c is a `Bool` on the `Ok` plans, not a revived `nameOwnerChanged`.** It carries no owner and needs nothing persisted, so it does not reopen `f3bcd4a16`, and it is all the canvas claims.
3. **Dateless alerts rather than suppressed ones.** A registration whose `expires` has passed but has no `graceUntil` still gets the expiry alert; only the renew-by date goes (`plans/2026-09-28-name-warnings.md`, N24). An absent `expires` is treated as live.
4. **Registration does not go through `ConnectionPlan`.** On the sibling canvas, 5a and 5b are the only registration states that would need one, and `nameWarning_` cannot express them: a live name has no warning. Both are proposed for dropping (`b898b991d`, "suggest to drop 5a & 5b"). With them gone the registration check is a plain name-status call, this API keeps one consumer, and the two surfaces stop competing.
5. **Consequently the lookup canvas's footer line "registration will always resolve" is obsolete** and should come off the sketch with this change.
6. **Reversed on review (2026-09-24): the freshness rule is in core, not the UIs.** Kept in the apps it was duplicated, lost on export, and keyed by domain name alone, so it was shared across user profiles and, under remote access, across hosts. §9.
7. **The constructor is `CPNameNotConnectable`, renamed from `CPSimplexName`.** All five states it carries share one invariant — you cannot connect — and the attached `NameWarning` says why. Rejected, with reasons, so they are not re-litigated: *`CPUnregisteredSimplexName`* is false for 2b and 2f, which are registered; *`CPNonResolvingName`* is false for 2b, where both `resolveNameRecord` and `resolveNameLink` succeed and the refusal is policy, and it collides with `CENotResolvedLocally` and with 2h, the cases that genuinely do not resolve; *`CPSimplexName`* reads as a sibling of `CPContactAddress` / `CPGroupLink` naming the target kind, but a name that resolves produces those instead. `CPSimplexDomain`, asked for in review on `ea721d33f`, is vague rather than wrong and remains the fallback if the thread is reopened. The ordering follows the file's dominant negation pattern — `<Noun>Not<Predicate>`, 20 constructors including the close sibling `CESimplexDomainNotReady`; a `Non` prefix appears nowhere in `src/`.
@@ -165,11 +165,11 @@ Registry and network failures stay `CPError` (2h). The canvas shows the resolver
## 8. Compile fixes and regeneration
- `View.hs:2234`, `:2252` — match the new arities; `viewConnectionPlan` (`:2214`) takes `Maybe ACreatedConnLink` and gains a `CPNameNotConnectable` case rendering domain, kind, and expiry or price under `testView`.
- `View.hs:2234`, `:2252` — match the new arities; `viewConnectionPlan` (`:2214`) takes `Maybe ACreatedConnLink` and gains a `CPNameNotConnectable` case rendering the domain; the warning line is `viewNameWarning` (`plans/2026-09-28-name-warnings.md` §7).
- `View.hs:217` — pass the now-optional `connLink` through.
- `Commands.hs` — the 26 `CPContactAddress` / `CPGroupLink` occurrences take the second argument; `:2178` and `:4586` build `CRConnectionPlan` with `Maybe`.
- `CAPOk` / `GLPOk` construction sites take `addressChanged`.
- Regenerate the client types: `bots/api/TYPES.md:1903-1922`, `packages/simplex-chat-client/types/typescript/src/types.ts`, `packages/simplex-chat-python/src/simplex_chat/types/_types.py` all still describe the three-constructor plan. Note that `bots/src/API/Docs/Commands.hs:142` and both generated clients never emit `resolve=`, which is fine and stays.
- Regenerate the client types: `bots/api/TYPES.md:1903-1922`, `packages/simplex-chat-client/types/typescript/src/types.ts`, `packages/simplex-chat-python/src/simplex_chat/types/_types.py` all described the four-constructor plan before regeneration. Note that `bots/src/API/Docs/Commands.hs:142` and both generated clients never emit `resolve=`, which is fine and stays.
**Encoding note.** Superseded on 2026-09-28: the plan has `NameWarning`, which is chat's own type and derives through `sumTypeJSON`, so the Swift decoder is synthesized like its neighbours and the hand-written `NameRegistration` decoder is gone.
@@ -179,18 +179,18 @@ Registry and network failures stay `CPError` (2h). The canvas shows the resolver
Review on 2026-09-24 reversed decision 6. Each decision below was taken by the author, not the implementer.
1. **Storage.** `contact_profiles.contact_domain_resolved_at` and `contact_domain_expires_at`; `groups.group_domain_resolved_at` and `group_domain_expires_at` — beside `contact_domain_verified` and `group_domain_verified`, which record whether the name checked out; these record when it was last resolved and when its registration expires. `TEXT` in SQLite, `TIMESTAMPTZ` in Postgres, `_at` as in `badge_purchases.expires_at`. One migration per backend.
2. **Rule.** Under `PRMUnknown`, a known chat reached by a name is re-resolved when `resolved_at` is `NULL` or over a day old, or `expires_at` has passed. `PRMAll` always resolves; `PRMNever` never does. A name with no chat is resolved on every call and nothing is stored; the user's own address is not a chat, so it too is resolved on every call.
1. **Storage.** `contact_profiles.contact_domain_resolved_at` and `contact_domain_expires_at`; `groups.group_domain_resolved_at` and `group_domain_expires_at` — beside `contact_domain_verified` and `group_domain_verified`, which record whether the name checked out; these record when it was last resolved and when its registration expires. `TEXT` in SQLite, `TIMESTAMPTZ` in Postgres, `_at` as in `invoices.expires_at`. One migration per backend.
2. **Rule.** Under `PRMUnknown`, a known chat reached by a name is re-resolved when `resolved_at` is `NULL` or over a day old, or `expires_at` has passed. `PRMAll` always resolves; `PRMNever` never does. A name with no chat is resolved on every call and nothing is stored, except that a contact or channel the name leads to but not yet verified for the name is verified and stored as resolved (a business chat reached this way is not); the user's own address is not a chat, so it too is resolved on every call.
3. **Writes.** Wherever core sets a verification flag after a resolution, it also sets `resolved_at` to now and `expires_at` to the registration's expiry, or `NULL` when the caller does not have it — then only the one-day limit applies until the next resolution. That is `setContactDomainVerified`, `setGroupDomainVerified`, and `createPreparedContact`, which inserts a chat created by name already verified, only when the link's profile claims the name the app passes. The plan passes the expiry of the registration it resolved, and `updateGroupFromLinkData` takes it from its callers, since it must not resolve; `/_verify domain` and `APISetPublicGroupAccess` pass `NULL`. Two paths gain a write: a re-resolution confirming the name still resolves to the known chat, and one that answers the chat without a warning because the name no longer has a link of its kind or is not registered (`plans/2026-09-28-name-warnings.md`, N7 and N11). Both re-set the flag to `True`, a no-op, since only verified chats are found by name. Setting the flag to `True` clears it on the user's other chats of the name's kind (contacts and business chats for a contact name, channels for a channel name), so after "Open new chat" in 3c the name finds the new chat.
4. **Moved name.** When re-resolution finds the name resolves elsewhere (3c), nothing is written to the old chat. It stays stale, so each default lookup re-resolves and reports the new address; `resolve=never` still returns the old chat.
5. **Reading.** A store function reads the two columns for the one chat being planned. `LocalProfile` and `GroupInfo` do not change, so the columns never reach the UIs; loading them there would touch 19 queries in 6 store files and both types' JSON.
5. **Reading.** Two store functions, `getContactDomainResolution` and `getGroupDomainResolution`, read the two columns for the chat each local lookup finds. `LocalProfile` and `GroupInfo` do not change, so the columns never reach the UIs; loading them there would touch 19 queries in 6 store files and both types' JSON.
6. **UIs.** Both apps lose the cache — the preference, `SimplexNameResolved`, the local probe before resolving, and the invalidation in `UserAddressView` — and plan a name with the default mode.
7. **iOS decoding.** Superseded, per the note in §8.
**Tests**, in core. A name re-pointed with `registerName` shows whether core queried the registry; a stored time is backdated with `withCCTransaction … DB.execute "UPDATE …"`, as `tests/ChatTests/Groups.hs:8825` does:
- a fresh known chat is answered from the store: after re-pointing, the default plan still returns the old contact;
**Tests**, in core. A name re-pointed with `registerName`, or registered as expired with `registerExpiredName`, shows whether core queried the registry; a stored time is backdated with `withCCTransaction … DB.execute "UPDATE …"`, as `tests/ChatTests/Groups.hs:8825` does:
- a fresh known chat is answered from the store: with the name expired, the default plan returns the contact without a warning;
- with `resolved_at` backdated, the default plan re-resolves and reports the new address;
- with `expires_at` in the past, likewise;
- with `expires_at` in the past, the default plan re-resolves and reports the expiry;
- a name with no chat resolves on every call and stores nothing;
- a moved name stays stale: two default lookups both report the new address;
- `resolve=all` and `resolve=never` are unchanged, and existing tests using them stay as they are.
@@ -201,15 +201,15 @@ Review on 2026-09-24 reversed decision 6. Each decision below was taken by the a
**A. Types.** The changes in §2, plus the `connectionPlanProceed` and derivation updates. Done when the constructors, fields and `deriveJSON` calls are in place and `Controller.hs` has no remaining arity error.
**B. Producer.** `resolveNameRegistration`, the rewritten `CTDomain` branch, the `knownLinkPlans` fall-through, expiry gating, and `addressChanged` under `PRMAll`. Done when every row of §4 can be produced.
**B. Producer.** `resolveNameRegistration`, the rewritten `CTDomain` branch, the local lookups (`knownContactPlans`, `knownGroupPlans`), expiry gating, and `addressChanged` under `PRMAll`. Done when every row of §4 can be produced.
**C. Consumers.** `View.hs`, the `Commands.hs` construction sites, the response builders. Done when `cabal build` is clean.
**D. Tests.** The harness comes first: `tests/NameResolver.hs:49-50` can only answer `NRRegistered` with `expires = Nothing`, or `NRAvailable` at a fixed price. Give `registerName` a way to set `expires`, `graceUntil` and `reservedReason_`, and add a way to register a name as `NRReserved` — without it, nine of the sixteen rows cannot be reached at all. Then extend `tests/ChatTests/Names.hs` (which already drives `/_connect plan` at `:236`, `:261`, `:282`, `:292`): one case per §4 row, plus a `PRMNever` hit and miss, plus `addressChanged` both ways. Done when all sixteen rows are asserted.
**D. Tests.** The harness comes first: `tests/NameResolver.hs:49-50` could only answer `NRRegistered` with `expires = Nothing`, or `NRAvailable` at a fixed price. Add `registerExpiredName` (expired a day ago, renewable for 30 days), `registerReservedName`, `unregisterName` and `failNameResolution`; the dateless and live-community cases are unit tests of `nameLinkOrWarning` — without these, nine of the sixteen rows cannot be reached at all. Then extend `tests/ChatTests/Names.hs` (which already drives `/_connect plan` at `:236`, `:261`, `:282`, `:292`): one case per §4 row, plus a `PRMNever` hit and miss, plus `addressChanged` both ways. Done when all sixteen rows are asserted.
**E. Regeneration.** §8. Done when the generated types describe five constructors.
**F. Re-resolution in core.** §9: the migration and store functions, the rule and its writes, the core tests, then removing the cache from both apps and aligning the Swift decoder with `MsgChatLink` (§8). Done when the §9 tests and the full names suite pass and neither app keeps a name cache.
**F. Re-resolution in core.** §9: the migration and store functions, the rule and its writes, the core tests, then removing the cache from both apps. Done when the §9 tests and the full names suite pass and neither app keeps a name cache.
---
@@ -217,10 +217,10 @@ Review on 2026-09-24 reversed decision 6. Each decision below was taken by the a
- `tests/NameResolver.hs` can answer expired, reserved and available, not only registered-without-dates
- every row of §4 is produced by core and asserted by a test
- `CPNameNotConnectable` carries a domain and is returned only when no local chat claims the name
- `CPNameNotConnectable` carries a domain and is returned only when nothing of the planned kind is local for the name
- `nameWarning_` is `Just` exactly when the lookup canvas shows an alert (`plans/2026-09-28-name-warnings.md` §3)
- `PRMAll` re-resolves a known chat and replaces `PRMAllGroups` (`allGroups` still parses), `PRMNever` is unchanged, and `PRMUnknown` applies the rule in §9
- `PRMAll` re-resolves a chat known by name and a group known by link, and replaces `PRMAllGroups` (`allGroups` still parses), `PRMNever` is unchanged, and `PRMUnknown` applies the rule in §9
- an expired name never yields a connectable plan, and an absent `expires` is treated as live
- `resolve=all` on a known chat whose name moved returns an `Ok` plan with `addressChanged = True`
- `resolve=all` on a known chat whose name moved to a link that claims it returns an `Ok` plan with `addressChanged = True`
- `cabal build` and `cabal test` are clean, and the generated client types match
- one migration per backend, no new chat command, and no name cache left in either app
+25 -25
View File
@@ -26,14 +26,14 @@ The canvas was reviewed against this model on 2026-09-28, story by story (§4).
- **`NameWarning` replaces `NameRegistration` in the plan.** `CPContactAddress` and `CPGroupLink` have `nameWarning_ :: Maybe NameWarning`, and `CPNameNotConnectable` has `nameWarning :: NameWarning`. An app shows an alert exactly when the plan has a warning. It does not compare dates, lengths or registrations.
- **Two pure functions decide the warning.** One computes a registration's link, or the warning when nothing is local. The other maps that warning to the one for the user's own name or for a chat. Both are tested directly.
- **A typed name (`@d`, `#d`) is planned for its kind only.**
- A chat at the name's link is confirmed.
- A chat, own address or own channel at another link gives the new link's plan with `addressChanged` (3c).
- Nothing local gives the new link's plan (2a).
- **A bare name (`d`) is planned for both kinds.**
- A chat at the name's live link is confirmed.
- A chat, own address or own channel at another link gives the new link's plan with `addressChanged` (3c), unless the new link does not claim the name or, for a chat, cannot be fetched (N12, N19).
- Nothing local gives the plan for the name's live link (2a), or `SDEUnknownDomain` if that link's profile does not claim the name (2g).
- **A bare name (`d`) is looked up for both kinds and planned for one.**
- It looks up both kinds locally and resolves the name once.
- It plans the kind that matches locally (the channel first), otherwise the kind the name has a link for.
- It offers the other kind when the name has a link of it that the user does not have.
- **The local lookup of each kind also returns whether the chat's name was resolved within a day.** It reads this where it finds the chat: the contact's resolution for a contact, the group's for a business chat or channel. No function checks freshness for both kinds.
- It plans the kind that matches locally (the channel first), otherwise the kind the name has a live link for.
- It offers the other kind when the name has a live link of it at which nothing is local, except after the channel failed (N22).
- **The local lookup of each kind also returns whether the chat's name was resolved within a day.** It reads this where it finds the chat: the contact's resolution for a contact, the group's for a business chat or channel. No function reads a contact's and a group's freshness together; the two lookups share `gPlan`, which reads the group's resolution for a business chat or channel, and the `resolvedRecently` predicate.
- **Accepted:**
- An answer for a chat can be up to a day old.
- Removing the link of a chat's kind from the name is not reported.
@@ -113,11 +113,11 @@ The match is what the channel kind's local lookup found, else what the contact k
| `resolve=never` | the match, or `CENotResolvedLocally` |
| default mode, and the match is a fresh chat | the chat, from the store |
| any other match: own, a chat that is not fresh, or `resolve=all` | the resolved registration, planned as the typed name of the match's kind (the rows above) |
| nothing local | the channel's plan if the name has a live channel link, falling back to the contact kind if it fails; otherwise the contact kind's plan if the name has a live contact link; otherwise not connectable with the "nothing" row's warning; a failed request is an error |
| nothing local | the channel's plan if the name has a live channel link, falling back to the contact kind if it fails and the name has a live contact link; otherwise the contact kind's plan if the name has a live contact link; otherwise not connectable with the "nothing" row's warning; a failed request is an error |
When the name is resolved and has a live link of the kind not planned, `otherSimplexName` is that kind's name, unless the user has a chat, own address or own channel of that kind at that link. The plan's screen shows it:
- the second button of 2a, 3c and 4a;
- 3e for a chat or own channel (new).
When the name is resolved and has a live link of the kind not planned, `otherSimplexName` is that kind's name, unless the user has a chat, own address or own channel of that kind at that link, or the channel was planned and failed (N22). The plan's screen shows it:
- the second button of 2a, 3c and 4a, and, from a message, of a chat's or own channel's alert;
- 3e for a chat or own channel, from search (new).
## 4. Changes against today
@@ -126,12 +126,12 @@ When the name is resolved and has a live link of the kind not planned, `otherSim
| # | Story | Today | New |
|---|---|---|---|
| 1 | The name is live and also reserved for community, whatever is local | the community alert instead of the plan | the plan, as for any live name |
| 4 | Bare name; a chat that is not fresh; the name also leads to the other kind at a link the user has no chat at (e.g. channel `#bakery`, the name now has only a contact link; or contact `@bakery`, the name has both) | the channel's plan if the name has a channel link, else the contact's; the local chat is shown only as the other kind's button, if at all | the local chat, and 3e: "bakery.simplex also leads to …", with Join channel or Connect |
| 4 | Bare name; a chat that is not fresh; the name also leads to the other kind at a link the user has no chat at (e.g. channel `#bakery`, the name now has only a contact link; or contact `@bakery`, the name has both) | the channel's plan if the name has a channel link, else the contact's; the local chat is shown only as the other kind's button, if at all | the local chat; from search, 3e: "bakery.simplex also leads to …", with Join channel or Connect; from a message, the chat's alert with that button |
| 7 | A chat; the name leads to a new link whose profile does not claim the name | the "Unconfirmed name" error alert | the chat, no alert |
| 8 | A chat that is not fresh; the request fails | the "SimpleX name error" alert | the chat, no alert |
| 9 | A chat; the name leads to a new link; from a message | 3c with Open new chat, Cancel | 3c with Open new chat, Open existing chat (opens the plan's `existingChat_`), and no Cancel |
| 10 | A chat; the name is available | "Name no longer registered", "from $X per year" | the same alert, "$Y for 2 years" |
| 11 | Own address or channel; the name leads to another link | "Connect to yourself?", or the own channel | 3c: "alice.simplex now leads to a new address", as for a chat, including 9 |
| 11 | Own address or channel; the name leads to another link | "Connect to yourself?", or the own channel | 3c: "alice.simplex now leads to a new address", as for a chat, including 9 for the own channel (N20) |
| 14 | Own; the name is available | "Your name has expired", "from $X per year" | the same alert, "$Y for 2 years" |
| 15 | Bare name; own address; the name also has a channel link the user has no chat at | the channel's join sheet | "Connect to yourself?" (4a) with Join channel |
@@ -204,11 +204,11 @@ Each returns the plan for what it finds, and whether it is fresh. It reads the c
4. Resolve the registration once. If the request fails, answer with the match if it is a chat, or fail.
5. Plan the kind:
- the match's kind, if there is a match;
- otherwise the channel if the name has a live channel link, falling back to the contact kind if that fails;
- otherwise the channel if the name has a live channel link, falling back to the contact kind if that fails and the name has a live contact link;
- otherwise the contact kind if the name has a live contact link.
It is planned as the typed name, passing the registration. With no kind to plan, answer `CPNameNotConnectable d` with the "nothing" warning.
6. Set `otherSimplexName` to the other kind's name if the name has a live link of it, unless the local lookup of that kind found something at that link.
6. Set `otherSimplexName` to the other kind's name if the name has a live link of it, unless the local lookup of that kind found something at that link, or the channel failed (N22).
## 7. CLI
@@ -227,29 +227,29 @@ A missing `graceUntil` drops the second clause of the expiry lines. `otherSimple
## 8. Apps
- **Alert.** `showNameRegistrationAlert` becomes a plain `case` from `NameWarning` to title, message and action (Renew, Register, Re-register, Connect to SimpleX team). It keeps "Open existing chat" when the plan has a chat (1d).
- **Alert.** `showNameRegistrationAlert` becomes `showNameWarningAlert`, a plain `case` from `NameWarning` to title, message and action (Renew, Register, Re-register, Connect to SimpleX team). It keeps "Open existing chat" when the plan has a chat (1d).
- **Flow.** `planAndConnect` shows the alert when the plan has a warning. Otherwise:
- a plan for a chat or own channel (`CAPKnown`, `GLPKnown`, `GLPOwnLink`) with `otherSimplexName` shows 3e (N14);
- a plan for a chat or own channel (`CAPKnown`, `GLPKnown`, `GLPOwnLink`) with `otherSimplexName` shows 3e from search (N14, N17); from a message, the chat's or own channel's alert carries the other kind's button;
- every other plan proceeds as today.
There is no `isOwn`, `notConnectable`, `hasLocalChat`, expiry or length logic in either app.
- **3c from a message.** The buttons are Open new chat (Open new channel) and Open existing chat, with no Cancel. Open existing chat opens the plan's `existingChat_`, which core leaves empty for the own address, so it gets Cancel (N20).
- **Name search.** The "Connect to" row passes the filters, as a pasted link does (N17).
- **Name search.** The chat list's "Connect to" row passes the filters, as a pasted link does (N17). The new chat sheet's row passes none, so it behaves as from a message.
- **Types.** Kotlin and Swift get `NameWarning` and `NamePrice` in place of `NameRegistration` and `NamePricing`. The hand-written Swift decoder for `NameRegistration` goes away: `NameWarning` is chat's own type and derives like its neighbours.
- **Strings.** The price strings change from "from %s per year" to "%s for %d years". 3e needs a title, a message and its buttons.
- **Strings.** The price strings change from "from %s per year" to "%s for %d years". 3e needs a title; its buttons reuse "Join channel %s" / "Connect to %s" and OK.
## 9. Canvas changes
- **2a, 3c, 4a:** the other kind's button is shown for bare names only.
- **3c:** also applies to the own address and channel. From a message, it shows Open new chat and Open existing chat, with no Cancel.
- **3e (new):** a bare name matches a chat or own channel, and the name also leads to the other kind: "bakery.simplex also leads to channel #bakery.simplex", Join channel, OK. From a message it also shows Open existing chat.
- **3c:** also applies to the own address and channel. From a message, it shows Open new chat and Open existing chat, with no Cancel; for the own address, Cancel (N20).
- **3e (new):** from search, a bare name matches a chat or own channel, and the name also leads to the other kind: "bakery.simplex also leads to channel #bakery", Join channel #bakery, OK. From a message, the chat's or own channel's alert shows the other kind's button instead.
- **Prices:** "$X for 2 years", computed from the registry's price. The amounts on the canvas are examples.
- **3a:** unchanged: "Still leads to your chat, or not found, no valid link, another name, or the request failed" matches §3.
- **3a:** "Still leads to your chat, or not found, no valid link, another name, its new link fails, or the request failed" matches §3 and N19.
## 10. What goes away
- `nameRegistration_` on `CPContactAddress`/`CPGroupLink`, `nameRegistration` on `CPNameNotConnectable`, and `setPlanRegistration`.
- The `PRMUnknown` equation with `resolvedRecently`.
- The `PRMUnknown` equation; `resolvedRecently` stays as the local lookups' predicate.
- The pre-resolve branch of `CTShortContact`, `resolveNameLink`, `nameHasLink` and `nameExpired`.
- `viewNameRegistration`, replaced by `viewNameWarning`.
- In both apps: the decisions in `showNameRegistrationAlert`, `NameRegistration.expired`, `reservedForCommunity`, `centsPerYear`, `nameCentsPerYear`, and the Swift `NameRegistration` decoder.
@@ -274,7 +274,7 @@ Decided:
| N12 | The new link does not claim the name, with a chat or own | the local one, no alert |
| N15 | The request failed | with a chat: the chat, no alert; with own or nothing: the error alert |
| N16 | Where the bare name's lookups and freshness are | the two local lookups move to the equation's `where` and return freshness; the bare name path uses both |
| N17 | Name search ("Connect to" row) | behaves as the canvas's search (1c): it passes the filters, so found chats stay filtered, dismissing keeps the search, 3c shows Cancel and 3e is the alert |
| N17 | Name search (the chat list's "Connect to" row) | behaves as the canvas's search (1c): it passes the filters, so found chats stay filtered, dismissing keeps the search, 3c shows Cancel and 3e is the alert |
| N18 | `/c` when the name moved (3c) or the own name has a warning | the plan is shown instead of connecting (`connectionPlanProceed`) |
| N19 | A chat, and the name's link data cannot be fetched | the chat, no alert, as N15 |
| N20 | 3c for the own address from a message | Cancel: there is no chat to open |
@@ -300,7 +300,7 @@ Decided:
- a chat and own with a name not registered (no warning);
- a business chat, fresh and not fresh;
- a bare name with a fresh chat (no resolution).
- **Unchanged:** the freshness tests (§9 of the lookup plan) pass as they are.
- **Changed:** the freshness tests (§9 of the lookup plan) assert the warning lines; `testPlanKnownNameStale` detects re-resolution by an expired registration.
## 13. Order of work
@@ -152,7 +152,7 @@
{"id":"re965099215536","type":"rectangle","x":1535,"y":1281,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"index":"b2o","strokeWidth":2},
{"id":"te1033495344727","type":"text","x":1541.95,"y":1283.8,"width":21.1,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"new","fontSize":10,"fontFamily":9,"index":"b2p","strokeWidth":2,"lineHeight":1.25},
{"id":"te208733714710","type":"text","x":1360,"y":1518.14,"width":191.47,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"3c. Leads to a new address","fontSize":13,"fontFamily":9,"index":"b2q","strokeWidth":2,"lineHeight":1.25},
{"id":"te377882734292","type":"text","x":1360,"y":1534.78,"width":224.53,"height":55.0,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"From search your chat stays filtered in the list;\nfrom a message Cancel is Open existing chat.\nAlso for your own address or channel. A\nchannel reads \"now leads to a new channel\".","fontSize":11,"fontFamily":9,"index":"b2r","strokeWidth":2,"lineHeight":1.25},
{"id":"te377882734292","type":"text","x":1360,"y":1534.78,"width":247.4,"height":55.0,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"From search your chat stays filtered in the list;\nfrom a message Cancel is Open existing chat, except\nfor your own address. Also for your own address or\nchannel. A channel reads \"now leads to a new channel\".","fontSize":11,"fontFamily":9,"index":"b2r","strokeWidth":2,"lineHeight":1.25},
{"id":"re819241109796","type":"rectangle","x":1650,"y":1300,"width":209.99,"height":145.08,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"boundElements":[{"id":"ar26133587117","type":"arrow"}],"index":"b2t","strokeWidth":2},
{"id":"te1029996043274","type":"text","x":1678.36,"y":1316.38,"width":153.33,"height":14.72,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"Name no longer registered","fontSize":12,"fontFamily":9,"index":"b2u","strokeWidth":2,"lineHeight":1.25},
{"id":"te364627884637","type":"text","x":1683.96,"y":1339.49,"width":142.14,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"bakery.simplex is available for","fontSize":9,"fontFamily":9,"index":"b2v","strokeWidth":2,"lineHeight":1.25},
+10 -20
View File
@@ -53,7 +53,7 @@ chatNamesTests = do
it "known chat and own name, name without link or reserved, stored as resolved" testPlanKnownNameReserved
it "known chat and own name, the request failed" testPlanKnownNameResolverFailed
it "known chat, the name's new link cannot be fetched" testPlanKnownNameLinkFailed
it "own channel expired, joined channel moved to a new channel" testPlanChannelNameMoved
it "own channel expired, joined channel moved to a new channel, new channel joined" testPlanChannelNameMoved
it "known chat, resolved over a day ago or past expiry" testPlanKnownNameStale
it "no local chat, resolved on every call" testPlanNameResolvedEveryCall
it "own name, expired" testPlanOwnNameExpired
@@ -396,18 +396,17 @@ withAliceName test ps = withSmpServerAndNames $ \reg ->
where
setup reg alice bob = do
mapM_ enableNamesRole [alice, bob]
aliceRecord <- setAliceName reg alice
test reg aliceRecord alice bob
(shortLink, _) <- setAliceName reg alice
test reg (contactNameRecord "alice.simplex" (T.pack shortLink)) alice bob
setAliceName :: HasCallStack => NameRegistry -> TestCC -> IO NameRecord
setAliceName :: HasCallStack => NameRegistry -> TestCC -> IO (String, String)
setAliceName reg alice = do
alice ##> "/ad"
(shortLink, _) <- getContactLinks alice True
let aliceRecord = contactNameRecord "alice.simplex" (T.pack shortLink)
registerName reg aliceSimplexName aliceRecord
links@(shortLink, _) <- getContactLinks alice True
registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack shortLink))
alice ##> "/_set domain 1 alice.simplex"
alice <## "new contact address set"
pure aliceRecord
pure links
knownAlicePlan :: HasCallStack => TestCC -> IO ()
knownAlicePlan bob = do
@@ -454,11 +453,7 @@ testPrepareNameNotClaimed ps = withSmpServerAndNames $ \reg ->
where
test reg alice bob = do
mapM_ enableNamesRole [alice, bob]
alice ##> "/ad"
(shortLink, fullLink) <- getContactLinks alice True
registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack shortLink))
alice ##> "/_set domain 1 alice.simplex"
alice <## "new contact address set"
(shortLink, fullLink) <- setAliceName reg alice
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "contact address: ok to connect"
contactSLinkData <- getTermLine bob
@@ -635,18 +630,13 @@ testPlanKnownNameNewChatOpened ps = withSmpServerAndNames $ \reg ->
mapM_ enableNamesRole [alice, bob, cath]
_ <- setAliceName reg alice
connectBobByName alice bob
cath ##> "/ad"
(cathLink, cathFullLink) <- getContactLinks cath True
registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack cathLink))
cath ##> "/_set domain 1 alice.simplex"
cath <## "new contact address set"
(cathLink, cathFullLink) <- setAliceName reg cath
bob ##> "/_connect plan 1 @alice.simplex resolve=all"
bob <## "contact address: ok to connect, address changed"
contactSLinkData <- getTermLine bob
bob ##> ("/_prepare contact 1 " <> cathFullLink <> " " <> cathLink <> " domain=alice.simplex " <> contactSLinkData)
bob <## "cath: contact is prepared"
failNameResolution reg aliceSimplexName
bob ##> "/_connect plan 1 @alice.simplex"
bob ##> "/_connect plan 1 @alice.simplex resolve=never"
bob <## "contact address: known prepared contact cath"
bob <## "SimpleX name: @alice.simplex (verified)"