refactor(realism): single source of truth for persona→login

decnet/realism/naming._home and decnet/canary/cultivator._persona_login
both normalised "John Smith"→"johnsmith" with identical logic. Lift
to decnet.realism.personas.login_for(persona) and have both consumers
import it. Drift between the two would have left canary placement and
realism path naming using different login derivations.
This commit is contained in:
2026-04-27 17:39:04 -04:00
parent 7e9bc6d49a
commit 49da15823f
6 changed files with 51 additions and 22 deletions

View File

@@ -24,6 +24,7 @@ import secrets
import string
from typing import Callable, Optional
from decnet.realism.personas import login_for
from decnet.realism.taxonomy import ContentClass
@@ -32,17 +33,8 @@ from decnet.realism.taxonomy import ContentClass
# paths (out of scope until per-OS personas land). For now everything
# is POSIX.
def _home(persona: str) -> str:
"""Return the canonical home directory for *persona*.
The persona's ``name`` is used as the linux username when it's a
plausible login (lowercase, no spaces); otherwise we fall back to
a generic ``user`` so the path doesn't reveal a persona display
name on the decky filesystem.
"""
candidate = persona.lower().replace(" ", "")
if candidate.isalnum() and candidate.isascii() and candidate:
return f"/home/{candidate}"
return "/home/user"
"""Return the canonical home directory for *persona*."""
return f"/home/{login_for(persona)}"
def _random_token(rng: secrets.SystemRandom, length: int = 6) -> str:

View File

@@ -117,6 +117,21 @@ def parse_personas(
return out
def login_for(persona: str) -> str:
"""Return the linux login derived from a persona's display name.
Lowercase, strip spaces; if the result isn't a plausible POSIX
login (alnum ASCII), fall back to ``user`` so the path doesn't
leak the persona's display name onto the decky filesystem.
Shared by realism path naming (``decnet/realism/naming.py``) and
canary cultivation (``decnet/canary/cultivator.py``).
"""
candidate = persona.lower().replace(" ", "")
if candidate.isalnum() and candidate.isascii() and candidate:
return candidate
return "user"
def in_active_hours(persona: EmailPersona, now_hour: int) -> bool:
"""Return True if *now_hour* (023) falls in the persona's window.