A name that two nodes use resolves to no key, so it is grey
A name is a label any key can claim, and a channel message carries only its sender's name. When two nodes use one name, the resolver used to let the last contact win, which handed one node's hue, map, and page to a message that nothing tied to it: a second "LOG.IC 3R" with a new key took the first one's green and location. Such a name now resolves to None, like any unplaced name. A contact's full key and the stored id of the same node still count as one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2d2128718a
commit
ad8792f34b
4 files changed
+61
-11
No files matched your search
@@ -32,6 +32,11 @@ can change how things behave. It does not only add new things.
|
||||
keys hold what only one screen has: `Retry` or `Resend`, and `Log in` on a room. The
|
||||
`Day ↑` and `Day ↓` chips are gone. On the desktop, ^PgUp and ^PgDn still move from one
|
||||
day to the next.
|
||||
- **A name that two nodes use is grey.** A channel message names its sender only by the
|
||||
name that the sender typed, and any node can use any name. When two nodes use the same
|
||||
name, MeshTerm cannot know which one wrote the message. Thus the name is grey, as an
|
||||
unknown node is, and ^L and ^O do not open anything for it. Before, MeshTerm chose one
|
||||
of the two nodes, and the name got the colour, the map, and the page of that node.
|
||||
- **Chips show where a route starts and where it ends.** Where your terminal draws path
|
||||
chips, the author of a message has a point on the right of its chip. A mention is a chip
|
||||
with a notch on its left. The packet viewer, the live feed, the Messages list, and the
|
||||
|
||||
@@ -496,8 +496,9 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
|
||||
node's key (its first byte — any known prefix agrees) so a rename keeps the colour.
|
||||
One rule on **every** platform; only the resolution changes (see **Platforms**).
|
||||
A surface holding only a name resolves it first (`make_name_key_resolver`); a node no
|
||||
key can place — an unresolved sender, a bare hash standing in as a name, an `○` ring —
|
||||
takes `node.unknown`, THE light grey for an unidentified node, everywhere and on both
|
||||
key can place — an unresolved sender, a name two or more keys use (a name is a claim
|
||||
anyone can make, so MeshTerm never picks between them; JP, 2026-10-11), a bare hash
|
||||
standing in as a name, an `○` ring — takes `node.unknown`, THE light grey for an unidentified node, everywhere and on both
|
||||
platforms. Colour is reserved for keyed identities, never seeded from a name's
|
||||
characters. `node.unknown` is deliberately not `muted`: muted is chrome and may sit a
|
||||
step darker, while an unidentified node is content you can still act on. Our own node
|
||||
|
||||
@@ -188,11 +188,22 @@ def make_name_key_resolver(
|
||||
The app-wide colour rule takes the hue of each name from the key of the node. Some
|
||||
places in the UI (the inline sender of a channel message, an ``@mention``, the origin
|
||||
of a route graph) have only a *name*. This resolver changes that name (casefolded)
|
||||
back into a key. The contacts of the device win (their keys are canonical). Then the
|
||||
back into a key. The contacts of the device give the canonical key of a node. Then the
|
||||
stored names of the recorder add the nodes that the companion never added as
|
||||
contacts. Thus a known node that is not a contact still gets its own hue. A name that
|
||||
no node has resolves to ``None``. The caller renders it muted, because colour is only
|
||||
for identities with a key.
|
||||
no node has resolves to ``None``. The caller renders it in the grey of an unknown node,
|
||||
because colour is only for identities with a key.
|
||||
|
||||
**A name that two or more nodes use also resolves to** ``None``. A name is a label that
|
||||
the owner of a key chooses, and nothing stops a second key from taking it. A channel
|
||||
message carries only the name of its sender, as text that each holder of the channel
|
||||
secret can write. Thus, when two keys use one name, MeshTerm cannot know which one
|
||||
sent the message, and a choice between the two is a guess that the hue, the map (^L),
|
||||
and the node page (^O) then show as a fact. Before, the last contact in the list won.
|
||||
A second "LOG.IC 3R" with a new key then took the hue and the map of the first, which
|
||||
nothing proved (JP, 2026-10-11). Two ids are one node when one is a prefix of the
|
||||
other: the full key of a contact and the stored id of the same node are one claim, not
|
||||
two.
|
||||
|
||||
Args:
|
||||
contacts: The known contacts to resolve against (name → public key/key prefix).
|
||||
@@ -202,17 +213,29 @@ def make_name_key_resolver(
|
||||
|
||||
Returns:
|
||||
A callable that takes a display name and returns the best key hex that we have
|
||||
for it, or ``None`` when the name matches no known node.
|
||||
for it, or ``None`` when the name matches no known node, or more than one.
|
||||
"""
|
||||
keys: dict[str, str] = {}
|
||||
# The ids of the nodes that use each name, one entry for each node.
|
||||
claims: dict[str, list[str]] = {}
|
||||
|
||||
def claim(name: str, ident: str, *, canonical: bool) -> None:
|
||||
held = claims.setdefault(name.casefold(), [])
|
||||
for i, other in enumerate(held):
|
||||
if other.startswith(ident) or ident.startswith(other):
|
||||
if canonical:
|
||||
held[i] = ident # the key of a contact replaces the stored id
|
||||
return
|
||||
held.append(ident)
|
||||
|
||||
for node, name in (stored_names or {}).items():
|
||||
ident = node.lower().removeprefix("0x")
|
||||
if name and ident:
|
||||
keys.setdefault(name.casefold(), ident)
|
||||
claim(name, ident, canonical=False)
|
||||
for c in contacts or []:
|
||||
ident = (c.public_key or c.key_prefix or "").lower().removeprefix("0x")
|
||||
if c.name and ident:
|
||||
keys[c.name.casefold()] = ident # contacts win over stored names
|
||||
claim(c.name, ident, canonical=True)
|
||||
keys = {name: ids[0] for name, ids in claims.items() if len(ids) == 1}
|
||||
|
||||
def key_of(name: str) -> str | None:
|
||||
if not name:
|
||||
|
||||
+23
-2
@@ -162,12 +162,12 @@ def test_path_text_dims_the_tail_from_dim_from() -> None:
|
||||
|
||||
|
||||
def test_make_name_key_resolver_contacts_win_over_stored_names() -> None:
|
||||
"""Contacts resolve first, stored advert names resolve strangers, and misses are None."""
|
||||
"""Contacts give the canonical key, stored advert names resolve strangers, misses are None."""
|
||||
from meshterm.core.models import Contact
|
||||
from meshterm.services.trace_runner import make_name_key_resolver
|
||||
|
||||
contacts = [Contact(name="Alice", public_key="d4" + "0" * 62)]
|
||||
stored = {"60aabbccdd11": "Bob", "77ee00112233": "Alice"} # an old stored Alice
|
||||
stored = {"60aabbccdd11": "Bob", "d40000000000": "Alice"} # Alice's own stored id
|
||||
key_of = make_name_key_resolver(contacts, stored)
|
||||
assert key_of("Alice") == "d4" + "0" * 62 # the key of the contact, not the stored id
|
||||
assert key_of("alice") == "d4" + "0" * 62 # case-folded
|
||||
@@ -175,6 +175,27 @@ def test_make_name_key_resolver_contacts_win_over_stored_names() -> None:
|
||||
assert key_of("Zed") is None # no node has the name
|
||||
|
||||
|
||||
def test_a_name_that_two_keys_use_resolves_to_no_key() -> None:
|
||||
"""Two nodes with one name: MeshTerm cannot know which one a name means, so it is grey.
|
||||
|
||||
A channel message carries only the name of its sender. A second key that takes a name
|
||||
must not get the hue, the map, and the page of the first (JP, 2026-10-11).
|
||||
"""
|
||||
from meshterm.core.models import Contact
|
||||
from meshterm.services.trace_runner import make_name_key_resolver
|
||||
|
||||
old = Contact(name="LOG.IC 3R", public_key="5d" + "0" * 62)
|
||||
new = Contact(name="LOG.IC 3R", public_key="47" + "0" * 62)
|
||||
assert make_name_key_resolver([old, new])("LOG.IC 3R") is None
|
||||
assert make_name_key_resolver([new, old])("log.ic 3r") is None # the order does not matter
|
||||
|
||||
# A stranger that we only heard is a claim too, and it makes the name of a contact unsure.
|
||||
stranger = {"77ee00112233": "Alice"}
|
||||
alice = Contact(name="Alice", public_key="d4" + "0" * 62)
|
||||
assert make_name_key_resolver([alice], stranger)("Alice") is None
|
||||
assert make_name_key_resolver([alice])("Alice") == "d4" + "0" * 62
|
||||
|
||||
|
||||
def test_name_style_without_a_key_is_the_unknown_grey() -> None:
|
||||
"""A keyless name has no hue, because colour is only for identities that have a key."""
|
||||
from meshterm.ui.theme import name_style, node_style
|
||||
|
||||
Reference in new issue
Block a user