Callsigns
A random identifier you can read aloud, remember for the length of a standup, and paste anywhere without escaping. Every token is word-word-hash (e.g. sunny-champion-8h3kq7): two human-readable words plus a six-character disambiguating hash. The point is that all three parts are ref-, path-, URL-, and shell-safe by construction — not “usually fine,” but safe as a proven property of the alphabet each part draws from, so a callsign drops straight into a git branch, a directory name, a URL segment, or a shell argument with no quoting. The hash alphabet is confusable-free (digits + a-z minus i/l/o/u) and lowercase-only, so there are no case-fold collisions. Seed it and the same seed yields the same token on any machine, forever.
The honest edge
A callsign is a memorable, SAFE identifier — not a guaranteed-unique one. The hash makes an accidental collision astronomically unlikely, but “unlikely” is not “impossible”: if your correctness depends on uniqueness, pair a callsign with a real uniqueness source (a timestamp, a sequence, a registry that rejects duplicates). It buys memorability and paste-safety, not a uniqueness authority.
Run it
python3 callsigns.py --demo
test_callsigns.py (2061 checks / 10 tests, mutation-bitten, pinned golden batch hash)
Python standard library only, deterministic under --seed, headless
The code — every file that ships
callsigns.py203 lineson GitHub →
#!/usr/bin/env python3
"""callsigns — memorable IDs that are safe by construction.
A random unique identifier you can read aloud, remember for the length of a
standup, and paste anywhere without escaping. Every token has the shape
word-word-hash e.g. sunny-champion-8h3kq7
Two human-readable words drawn from a curated pool, then a six-character
disambiguating hash. The point is that all three parts are *ref-, path-, URL-,
and shell-safe by construction* — not "usually fine," but safe as a proven
property of the alphabet each part draws from, so a callsign drops straight into
a git branch name, a directory name, a URL segment, or a shell argument with no
quoting and no surprises.
WHY IT'S HONEST
- **Safe by construction, not by hope.** The words pass a lowercase-ASCII
allowlist; the hash draws from a confusable-free, case-safe base32 alphabet
(digits + a-z minus i/l/o/u). Every character clears git refs, Windows and
macOS filenames, RFC-3986 URL segments, and the shell. There is no escaping
step to forget because there is nothing to escape.
- **No case-fold collisions.** Everything is lowercase, so two callsigns can
never collide only because a filesystem folded their case.
- **The namespace is a stated number, not a vibe.** 64 x 64 word-pairs = 4096
memorable prefixes; the six-char hash adds 32^6 (~1.07e9) per prefix, for
~4.4e12 total. You can reason about collision odds because the size is exact.
- **No degenerate pairs.** A token is never `word-word` with the two words
equal; the draw rejects and redraws, so every callsign reads as two distinct
words.
- **Seed it and it's deterministic.** Pass a seed and the same seed yields the
same callsign on any machine, forever — so a demo, a test, or a reproducible
fixture is byte-identical. Leave the seed off and it draws from the system CSPRNG.
THE HONEST EDGE
A callsign is a *memorable, safe* identifier, not a *guaranteed-unique* one.
The hash makes an accidental collision astronomically unlikely, but "unlikely"
is not "impossible": if your system's correctness depends on uniqueness, pair a
callsign with a real uniqueness source (a timestamp prefix, a sequence, a
registry that rejects duplicates) — exactly as the Loop MMT session floor does,
joining a callsign to a UTC timestamp. Callsigns buy you memorability and
paste-safety; they do not replace a uniqueness authority.
USAGE
python3 callsigns.py # one callsign
python3 callsigns.py --n 5 # five callsigns, one per line
python3 callsigns.py --seed 42 # deterministic: same seed, same token
python3 callsigns.py --demo # a short, reproducible demonstration
MIT licensed. Python standard library only. Deterministic under --seed; headless.
"""
from __future__ import annotations
import argparse
import json
import os
import random
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
WORDLIST = os.path.join(HERE, "wordlist.json")
# A drawn word must be ref-/path-/URL-/shell-safe by construction.
SAFE = frozenset("abcdefghijklmnopqrstuvwxyz")
# Hash field: confusable-free, case-safe base32 — digits + a-z minus i,l,o,u.
# Lowercase-only (no case-fold collision); every char clears git refs,
# Windows/macOS filenames, RFC-3986 unreserved URL chars, and the shell.
# Length is the lever, not the alphabet: 32^6 ~= 1.07e9 per word-pair.
HASH_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz" # 32 chars: a-z drop i,l,o,u
HASH_LEN = 6
_MAX_REDRAW = 64 # attempts to avoid a degenerate equal-word pair before forcing
def _is_safe(word: str) -> bool:
"""True iff word is non-empty and every char is in the safe allowlist."""
return bool(word) and all(c in SAFE for c in word)
def draw_hash(rng: random.Random, length: int = HASH_LEN) -> str:
"""Draw a `length`-char hash from the case-safe base32 alphabet."""
if length < 0:
raise ValueError("hash length must be non-negative")
return "".join(rng.choice(HASH_ALPHABET) for _ in range(length))
def load_pools(path: str = WORDLIST) -> tuple[list[str], list[str]]:
"""Load and safety-filter the two word pools from the wordlist JSON.
Raises ValueError if either pool is empty after the safety filter — a
wordlist that can't produce a safe word is a loud failure, never a silent
empty draw.
"""
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
pool_one = [w for w in data["pool_one"] if _is_safe(w)]
pool_two = [w for w in data["pool_two"] if _is_safe(w)]
if not pool_one or not pool_two:
raise ValueError("wordlist pools are empty after the safety filter")
return pool_one, pool_two
def draw(
rng: random.Random | None = None,
path: str = WORDLIST,
pools: tuple[list[str], list[str]] | None = None,
) -> str:
"""Draw one callsign: `word-word-hash`, the two words always distinct.
`rng` — a random.Random (seed it for determinism); defaults to the CSPRNG.
`pools` — optional pre-loaded (pool_one, pool_two); loaded from `path` if omitted.
"""
rng = rng or random.SystemRandom()
pool_one, pool_two = pools if pools is not None else load_pools(path)
# Reject the degenerate identical-word pair and redraw.
for _ in range(_MAX_REDRAW):
a, b = rng.choice(pool_one), rng.choice(pool_two)
if a != b:
return f"{a}-{b}-{draw_hash(rng)}"
# Vanishingly unlikely fallthrough: force a distinct pair.
a = rng.choice(pool_one)
alt = [w for w in pool_two if w != a]
if not alt:
raise ValueError("cannot form a distinct word pair from these pools")
b = rng.choice(alt)
return f"{a}-{b}-{draw_hash(rng)}"
def draw_many(
count: int,
rng: random.Random | None = None,
path: str = WORDLIST,
) -> list[str]:
"""Draw `count` callsigns, reusing one loaded pool and one RNG."""
if count < 0:
raise ValueError("count must be non-negative")
rng = rng or random.SystemRandom()
pools = load_pools(path)
return [draw(rng=rng, pools=pools) for _ in range(count)]
def demo(rng: random.Random | None = None) -> str:
"""A short, reproducible demonstration rendered as text.
Deterministic when `rng` is seeded — the same seed prints the same block.
"""
rng = rng or random.Random(42)
pools = load_pools()
lines = [
"callsigns — memorable IDs safe by construction",
"",
"Five draws (seed=42):",
]
for _ in range(5):
lines.append(" " + draw(rng=rng, pools=pools))
lines += [
"",
"Same seed, same tokens — reproducible anywhere.",
f"Namespace: {len(pools[0])} x {len(pools[1])} pairs x {len(HASH_ALPHABET)}^{HASH_LEN}"
f" ~= {len(pools[0]) * len(pools[1]) * len(HASH_ALPHABET) ** HASH_LEN:.2e} total.",
]
return "\n".join(lines)
def _build_arg_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="callsigns",
description="Draw memorable, safe-by-construction identifiers (word-word-hash).",
)
p.add_argument("--n", type=int, default=1, help="how many callsigns to draw (default 1)")
p.add_argument("--seed", type=int, default=None, help="seed for deterministic output")
p.add_argument("--demo", action="store_true", help="print a short reproducible demonstration")
return p
def main(argv: list[str] | None = None) -> int:
args = _build_arg_parser().parse_args(argv)
try:
if args.demo:
rng = random.Random(args.seed) if args.seed is not None else random.Random(42)
sys.stdout.write(demo(rng=rng) + "\n")
return 0
if args.n < 0:
sys.stderr.write("callsigns: FATAL: --n must be non-negative\n")
return 1
rng = random.Random(args.seed) if args.seed is not None else random.SystemRandom()
pools = load_pools()
for _ in range(args.n):
sys.stdout.write(draw(rng=rng, pools=pools) + "\n")
return 0
except Exception as exc: # loud, never silent
sys.stderr.write(f"callsigns: FATAL: {exc}\n")
return 1
if __name__ == "__main__":
raise SystemExit(main())
wordlist.json35 lineson GitHub →
{
"_meta": {
"name": "callsign-wordlist",
"version": "v1",
"produced": "S05.1342 (Mature Maple)",
"product_of": "Wes (FWW(C) vocabulary) + Tamar (structural guarantee)",
"purpose": "Source vocabulary for The Callsign Convention. A session callsign is one word drawn from pool_one and one from pool_two, joined by a hyphen, e.g. sunny-champion, taco-disco, feral-biscuit.",
"structure": "Cartesian product pool_one x pool_two. Draw is uniform-random over the product; reject and redraw the degenerate case where word_one == word_two (Tamar's exclusion rule).",
"namespace": 4096,
"namespace_note": "|pool_one| (64) x |pool_two| (64) = 4096 distinct callsigns, minus the handful of identical-word pairs the draw rejects. Same-minute twin-collision probability is 1/namespace ~ 0.024%; scaling is additive (add words to either pool).",
"token_rule": "Every token is lowercase ASCII [a-z] only: no spaces, no punctuation, no path/ref-hostile characters. The drawn callsign 'word-word' is safe as a git branch segment and a directory name by construction. (Nyx: the callsign is never free user input, so there is no injection surface — the wordlist IS the allowlist.)",
"display_form": "Stored/path form is lowercase-hyphenated (sunny-champion). Spoken/display form is Title-Case (Sunny-Champion).",
"fwwc": "Pools are curated HEAVY with FWW(C) per operator directive: Taco-Disco energy, not Server-Node-7. Both pools individually vetted clean; the product of two clean pools reads clean (no slur, no unfortunate phrase) for this vocabulary."
},
"pool_one": [
"sunny", "turbo", "velvet", "feral", "cosmic", "disco", "rowdy", "mellow",
"plucky", "saucy", "zesty", "wobbly", "snazzy", "brisk", "crispy", "nimble",
"goblin", "thunder", "mossy", "taco", "rascal", "dapper", "jazzy", "breezy",
"fuzzy", "sneaky", "peppy", "glossy", "rugged", "swift", "gilded", "frosty",
"hasty", "lucky", "moody", "nifty", "prickly", "quirky", "rusty", "spunky",
"tidal", "vivid", "witty", "amber", "bouncy", "chunky", "drowsy", "eager",
"frantic", "gallant", "husky", "ironic", "jolly", "keen", "lanky", "merry",
"noble", "oaken", "pesky", "quiet", "ruddy", "stout", "twangy", "untamed"
],
"pool_two": [
"champion", "disco", "taco", "walrus", "comet", "biscuit", "bandit", "lighthouse",
"mango", "otter", "tuba", "cyclone", "pickle", "falcon", "wizard", "noodle",
"dynamo", "llama", "kraken", "sprocket", "pumpkin", "raccoon", "satchel", "thunder",
"vortex", "whistle", "anvil", "beacon", "cactus", "domino", "ember", "fjord",
"gadget", "harpoon", "igloo", "jukebox", "kazoo", "lantern", "marmot", "nugget",
"octopus", "pelican", "quartz", "rocket", "saxophone", "tangerine", "ukulele", "viper",
"waffle", "xylophone", "yeti", "zeppelin", "badger", "conch", "dragon", "ferret",
"gizmo", "hatchet", "jamboree", "koala", "lobster", "mammoth", "narwhal", "oyster"
]
}
test_callsigns.py166 lineson GitHub →
#!/usr/bin/env python3
"""test_callsigns.py — the certifying properties of the callsigns gift.
Run: python3 test_callsigns.py (exit 0 = all pass, 1 = a failure)
The tests are chosen to be MUTATION-BITTEN: each one is here because a plausible
mutation of callsigns.py makes it fail loud. In particular the determinism test
pins a GOLDEN sha256 of a seeded batch rather than checking self-equality — a
weak self-equality test passes benign reorders (the Loop MMT sudoku lesson), a
pinned golden does not.
"""
import hashlib
import random
import sys
import callsigns as c
# --- pinned golden: a seeded batch must reproduce this exact signature -------
# Regenerate ONLY on an intentional change:
# python3 -c "import hashlib,random,callsigns as c; \
# rng=random.Random(1234); p=c.load_pools(); \
# b=[c.draw(rng=rng,pools=p) for _ in range(50)]; \
# print(hashlib.sha256('\n'.join(b).encode()).hexdigest())"
GOLDEN_SEED = 1234
GOLDEN_N = 50
GOLDEN_SHA256 = "815a0dc83fa371ebb1df70e9a82961aeeea11cd3ad05b80e9e73a90c2e10448f"
_FAILURES: list[str] = []
_PASSES = 0
def check(cond: bool, msg: str) -> None:
global _PASSES
if cond:
_PASSES += 1
else:
_FAILURES.append(msg)
def _batch(seed: int, n: int) -> list[str]:
rng = random.Random(seed)
pools = c.load_pools()
return [c.draw(rng=rng, pools=pools) for _ in range(n)]
# --- 1. determinism: seeded draw reproduces the pinned golden ----------------
def test_golden_signature():
batch = _batch(GOLDEN_SEED, GOLDEN_N)
sig = hashlib.sha256("\n".join(batch).encode()).hexdigest()
check(
sig == GOLDEN_SHA256,
f"golden signature drifted: got {sig[:16]}... expected {GOLDEN_SHA256[:16]}...",
)
# kills a mutation that drops the redraw and lets equal-word pairs through:
for tok in batch:
a, b, _ = tok.split("-")
check(a != b, f"golden batch contains a degenerate equal-word pair: {tok}")
# --- 2. same seed => same first token; different seed => (almost surely) not --
def test_seed_repeatability():
r1 = random.Random(7)
r2 = random.Random(7)
p = c.load_pools()
check(c.draw(rng=r1, pools=p) == c.draw(rng=r2, pools=p), "same seed produced different tokens")
r3 = random.Random(8)
# different seeds SHOULD differ (namespace ~4.4e12, collision astronomically rare)
p2 = c.load_pools()
check(
c.draw(rng=random.Random(7), pools=p) != c.draw(rng=r3, pools=p2),
"different seeds produced identical tokens (namespace or seeding is broken)",
)
# --- 3. shape: exactly three dash-parts, word-word-hash ----------------------
def test_shape():
for tok in _batch(99, 200):
parts = tok.split("-")
check(len(parts) == 3, f"token is not word-word-hash: {tok!r}")
# --- 4. every char of the words is in the safe allowlist ---------------------
def test_words_safe():
for tok in _batch(101, 200):
a, b, _ = tok.split("-")
check(all(ch in c.SAFE for ch in a), f"word 1 has an unsafe char: {a!r}")
check(all(ch in c.SAFE for ch in b), f"word 2 has an unsafe char: {b!r}")
# --- 5. the hash never contains a confusable (i/l/o/u) or an out-of-alphabet char
def test_hash_alphabet():
banned = set("ilou")
for tok in _batch(202, 300):
h = tok.split("-")[2]
check(len(h) == c.HASH_LEN, f"hash wrong length: {h!r}")
check(all(ch in c.HASH_ALPHABET for ch in h), f"hash has out-of-alphabet char: {h!r}")
check(not (set(h) & banned), f"hash contains a confusable char (i/l/o/u): {h!r}")
# kills a mutation that swaps HASH_ALPHABET for a plain base32 including i/l/o/u
check(not (set(c.HASH_ALPHABET) & banned), "HASH_ALPHABET must not contain i/l/o/u")
# --- 6. no degenerate equal-word pair, ever, across a large sample -----------
def test_no_equal_pairs():
for tok in _batch(303, 500):
a, b, _ = tok.split("-")
check(a != b, f"degenerate equal-word pair produced: {tok}")
# --- 7. empty-pool wordlist is a loud failure, not a silent empty draw -------
def test_empty_pool_is_loud():
import json
import os
import tempfile
with tempfile.TemporaryDirectory() as d:
bad = os.path.join(d, "wordlist.json")
with open(bad, "w") as fh:
json.dump({"pool_one": [], "pool_two": ["x"]}, fh)
raised = False
try:
c.load_pools(bad)
except ValueError:
raised = True
check(raised, "empty pool did not raise ValueError (silent-empty-draw regression)")
# --- 8. namespace math is the stated exact number ---------------------------
def test_namespace_is_exact():
p1, p2 = c.load_pools()
total = len(p1) * len(p2) * len(c.HASH_ALPHABET) ** c.HASH_LEN
check(len(p1) == 64 and len(p2) == 64, f"pools not 64x64: {len(p1)}x{len(p2)}")
check(len(c.HASH_ALPHABET) == 32, f"hash alphabet not 32 chars: {len(c.HASH_ALPHABET)}")
check(total == 64 * 64 * 32 ** 6, f"namespace math changed: {total}")
# --- 9. draw_many honors count and reuses one pool --------------------------
def test_draw_many():
got = c.draw_many(0, rng=random.Random(1))
check(got == [], "draw_many(0) should be empty")
got5 = c.draw_many(5, rng=random.Random(1))
check(len(got5) == 5, f"draw_many(5) returned {len(got5)}")
# --- 10. demo is deterministic under a seed ---------------------------------
def test_demo_deterministic():
d1 = c.demo(rng=random.Random(42))
d2 = c.demo(rng=random.Random(42))
check(d1 == d2, "demo(seed=42) is not reproducible")
def run() -> int:
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for t in tests:
t()
total = _PASSES + len(_FAILURES)
if _FAILURES:
print(f"FAIL — {len(_FAILURES)} of {total} checks failed:")
for f in _FAILURES:
print(f" ✗ {f}")
return 1
print(f"OK — {_PASSES}/{total} checks passed ({len(tests)} tests).")
return 0
if __name__ == "__main__":
sys.exit(run())