<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P145-username-registry/ | updated: 2026-10-18 -->

# P145 Username registry

Names folded into keys four ways, from the name as typed to lower case without marks or trailing digits, set against who is who: the people each fold merges and the people it splits.

EML-P project `projects/username-registry` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/username-registry` runs it and `eml project verify projects/username-registry` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: case-fold-collisions (https://efficientnewlanguage.org/cases/218-case-fold-collisions/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
3

3
bad name!
3
seventeen_chars_x
3
Zed

3
Zed
0
3
Zed
11
3
Zed
x
3
Zed
1234
3
zed
n
4
5
```

Screen:

```text
== Username registry ==
Names folded into keys, set against who is who: people merged, and people split.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 0
Pick a number from 1 to 5.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> x
Pick a number from 1 to 5.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> 
Cancelled.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> bad name!
Type a name of 1 to 16 letters, digits, dots, dashes or underscores.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> seventeen_chars_x
Type a name of 1 to 16 letters, digits, dots, dashes or underscores.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> Zed
who is this (1 to 10, or n for someone new)> 
Cancelled.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> Zed
who is this (1 to 10, or n for someone new)> 0
Type a person's number from 1 to 10, or n.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> Zed
who is this (1 to 10, or n for someone new)> 11
Type a person's number from 1 to 10, or n.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> Zed
who is this (1 to 10, or n for someone new)> x
Type a person's number from 1 to 10, or n.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> Zed
who is this (1 to 10, or n for someone new)> 1234
Type a person's number from 1 to 10, or n.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> zed
who is this (1 to 10, or n for someone new)> n
zed, person 11:
  exact         new account 'zed'
  lower         new account 'zed'
  strip         new account 'zed'
  strip+digits  new account 'zed'

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 4
Over 17 sign-ups by 11 people:
  fold          keys  merged  split
  exact           17       0      6
  lower           14       0      3
  strip           10       2      0
  strip+digits     6       7      0
Merged: a person's name lands on another person's key. Split: one person's names land on two keys.
  strip: al.ice (person 2) lands on 'alice', person 1's key
  strip: al-ice (person 2) lands on 'alice', person 1's key
  strip+digits: al.ice (person 2) lands on 'alice', person 1's key
  strip+digits: al-ice (person 2) lands on 'alice', person 1's key
  strip+digits: alice1 (person 3) lands on 'alice', person 1's key
  strip+digits: alice2 (person 4) lands on 'alice', person 1's key
  strip+digits: carol7 (person 8) lands on 'carol', person 7's key
  strip+digits: carol_7 (person 8) lands on 'carol', person 7's key
  strip+digits: Dave2024 (person 10) lands on 'dave', person 9's key
  exact splits, first alice (person 1): 'alice', where the same person has 'Alice'
  lower splits, first al-ice (person 2): 'al-ice', where the same person has 'al.ice'
No fold keeps every person apart and every person's names together, for these names.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
4
3
Bob.Smith
6
3
caro.l
n
3
ALICE2
4
4
5
```

Screen:

```text
== Username registry ==
Names folded into keys, set against who is who: people merged, and people split.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 1
The sign-ups, in order, with who is who:
  Alice             person 1
  alice             person 1
  ALICE             person 1
  al.ice            person 2
  al-ice            person 2
  alice1            person 3
  alice2            person 4
  Bob               person 5
  bob               person 5
  bob.smith         person 6
  bobsmith          person 6
  carol             person 7
  carol7            person 8
  carol_7           person 8
  dave              person 9
  Dave2024          person 10

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 2
  name              lower             strip             strip+digits
  Alice             alice             alice             alice
  alice             alice             alice             alice
  ALICE             alice             alice             alice
  al.ice            al.ice            alice             alice
  al-ice            al-ice            alice             alice
  alice1            alice1            alice1            alice
  alice2            alice2            alice2            alice
  Bob               bob               bob               bob
  bob               bob               bob               bob
  bob.smith         bob.smith         bobsmith          bobsmith
  bobsmith          bobsmith          bobsmith          bobsmith
  carol             carol             carol             carol
  carol7            carol7            carol7            carol
  carol_7           carol_7           carol7            carol
  dave              dave              dave              dave
  Dave2024          dave2024          dave2024          dave

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 4
Over 16 sign-ups by 10 people:
  fold          keys  merged  split
  exact           16       0      6
  lower           13       0      3
  strip            9       2      0
  strip+digits     5       7      0
Merged: a person's name lands on another person's key. Split: one person's names land on two keys.
  strip: al.ice (person 2) lands on 'alice', person 1's key
  strip: al-ice (person 2) lands on 'alice', person 1's key
  strip+digits: al.ice (person 2) lands on 'alice', person 1's key
  strip+digits: al-ice (person 2) lands on 'alice', person 1's key
  strip+digits: alice1 (person 3) lands on 'alice', person 1's key
  strip+digits: alice2 (person 4) lands on 'alice', person 1's key
  strip+digits: carol7 (person 8) lands on 'carol', person 7's key
  strip+digits: carol_7 (person 8) lands on 'carol', person 7's key
  strip+digits: Dave2024 (person 10) lands on 'dave', person 9's key
  exact splits, first alice (person 1): 'alice', where the same person has 'Alice'
  lower splits, first al-ice (person 2): 'al-ice', where the same person has 'al.ice'
No fold keeps every person apart and every person's names together, for these names.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> Bob.Smith
who is this (1 to 10, or n for someone new)> 6
Bob.Smith, person 6:
  exact         new key 'Bob.Smith', though person 6 already has 'bob.smith': a second account (split)
  lower         the account 'bob.smith' of the same person: one account, right
  strip         the account 'bobsmith' of the same person: one account, right
  strip+digits  the account 'bobsmith' of the same person: one account, right

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> caro.l
who is this (1 to 10, or n for someone new)> n
caro.l, person 11:
  exact         new account 'caro.l'
  lower         new account 'caro.l'
  strip         key 'carol' is person 7's: a different person refused (merged)
  strip+digits  key 'carol' is person 7's: a different person refused (merged)

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 3
name> ALICE2
who is this (1 to 11, or n for someone new)> 4
ALICE2, person 4:
  exact         new key 'ALICE2', though person 4 already has 'alice2': a second account (split)
  lower         the account 'alice2' of the same person: one account, right
  strip         the account 'alice2' of the same person: one account, right
  strip+digits  key 'alice' is person 1's: a different person refused (merged)

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 4
Over 19 sign-ups by 11 people:
  fold          keys  merged  split
  exact           19       0      8
  lower           14       0      3
  strip            9       3      0
  strip+digits     5       9      0
Merged: a person's name lands on another person's key. Split: one person's names land on two keys.
  strip: al.ice (person 2) lands on 'alice', person 1's key
  strip: al-ice (person 2) lands on 'alice', person 1's key
  strip: caro.l (person 11) lands on 'carol', person 7's key
  strip+digits: al.ice (person 2) lands on 'alice', person 1's key
  strip+digits: al-ice (person 2) lands on 'alice', person 1's key
  strip+digits: alice1 (person 3) lands on 'alice', person 1's key
  strip+digits: alice2 (person 4) lands on 'alice', person 1's key
  strip+digits: carol7 (person 8) lands on 'carol', person 7's key
  strip+digits: carol_7 (person 8) lands on 'carol', person 7's key
  strip+digits: Dave2024 (person 10) lands on 'dave', person 9's key
  strip+digits: caro.l (person 11) lands on 'carol', person 7's key
  strip+digits: ALICE2 (person 4) lands on 'alice', person 1's key
  exact splits, first alice (person 1): 'alice', where the same person has 'Alice'
  lower splits, first al-ice (person 2): 'al-ice', where the same person has 'al.ice'
No fold keeps every person apart and every person's names together, for these names.

1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P145 username registry: sign-ups with who is who written beside them,
# the key each fold makes of every name, a new sign-up seen by every fold,
# and each fold's merges and splits.
import folds

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def q(s):
    return "'" + s + "'"

def name_ok(t):
    if len(t) == 0 or len(t) > 16:
        return False
    for c in t:
        if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9") or c == "." or c == "-" or c == "_"):
            return False
    return True

def show_signups(rows):
    "The sign-ups, in order, with who is who:" ^0
    for r in rows:
        ("  " + pad(r[0], 18) + "person " + str(r[1])) ^0

def show_keys(rows):
    ("  " + pad("name", 18) + pad("lower", 18) + pad("strip", 18) + "strip+digits") ^0
    for r in rows:
        ("  " + pad(r[0], 18) + pad(folds.fold("lower", r[0]), 18) + pad(folds.fold("strip", r[0]), 18) + folds.fold("strip+digits", r[0])) ^0

def outcome(which, rows, name, who):
    folds.fold(which, name) => k
    folds.owner(which, rows, k) => o
    folds.first_key(which, rows, who) => fk
    if o == 0 and fk == "":
        return "new account " + q(k)
    if o == 0:
        return "new key " + q(k) + ", though person " + str(who) + " already has " + q(fk) + ": a second account (split)"
    if o == who:
        return "the account " + q(k) + " of the same person: one account, right"
    return "key " + q(k) + " is person " + str(o) + "'s: a different person refused (merged)"

def signup(rows):
    plain(input("name> ")) => name
    if name == "":
        "Cancelled." ^0
        return rows
    if not name_ok(name):
        "Type a name of 1 to 16 letters, digits, dots, dashes or underscores." ^0
        return rows
    folds.people(rows) => n
    plain(input("who is this (1 to " + str(n) + ", or n for someone new)> ")) => t
    if t == "":
        "Cancelled." ^0
        return rows
    if t == "n":
        n + 1 => who
    else:
        if len(t) > 3:
            ("Type a person's number from 1 to " + str(n) + ", or n.") ^0
            return rows
        for c in t:
            if c < "0" or c > "9":
                ("Type a person's number from 1 to " + str(n) + ", or n.") ^0
                return rows
        int(t) => who
        if who < 1 or who > n:
            ("Type a person's number from 1 to " + str(n) + ", or n.") ^0
            return rows
    (name + ", person " + str(who) + ":") ^0
    for f in folds.folds():
        ("  " + pad(f, 14) + outcome(f, rows, name, who)) ^0
    return rows + [[name, who]]

def results(rows):
    ("Over " + str(len(rows)) + " sign-ups by " + str(folds.people(rows)) + " people:") ^0
    ("  " + pad("fold", 14) + "keys  merged  split") ^0
    [] => right
    for f in folds.folds():
        folds.measure(f, rows) => m
        ("  " + pad(f, 14) + lpad(str(m[2]), 4) + lpad(str(m[0]), 8) + lpad(str(m[1]), 7)) ^0
        if m[0] == 0 and m[1] == 0:
            right + [f] => right
    "Merged: a person's name lands on another person's key. Split: one person's names land on two keys." ^0
    for f in folds.folds():
        for r in rows:
            folds.fold(f, r[0]) => k
            folds.owner(f, rows, k) => o
            if o != r[1]:
                ("  " + f + ": " + r[0] + " (person " + str(r[1]) + ") lands on " + q(k) + ", person " + str(o) + "'s key") ^0
    for f in folds.folds():
        "" => ex
        for r in rows:
            folds.fold(f, r[0]) => k
            folds.first_key(f, rows, r[1]) => fk
            if fk != k and ex == "":
                "  " + f + " splits, first " + r[0] + " (person " + str(r[1]) + "): " + q(k) + ", where the same person has " + q(fk) => ex
        if ex != "":
            ex ^0
    if len(right) == 0:
        "No fold keeps every person apart and every person's names together, for these names." ^0

folds.opening() => rows

"== Username registry ==" ^0
"Names folded into keys, set against who is who: people merged, and people split." ^0
True => running
while running:
    "" ^0
    "1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_signups(rows)
    elif choice == "2":
        show_keys(rows)
    elif choice == "3":
        signup(rows) => rows
    elif choice == "4":
        results(rows)
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import folds

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def q(s):
    return "'" + s + "'"

def name_ok(t):
    if len(t) == 0 or len(t) > 16:
        return False
    for c in t:
        if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z" or c >= "0" and c <= "9" or c == "." or c == "-" or c == "_"):
            return False
    return True

def show_signups(rows):
    print("The sign-ups, in order, with who is who:")
    for r in rows:
        print("  " + pad(r[0], 18) + "person " + str(r[1]))

def show_keys(rows):
    print("  " + pad("name", 18) + pad("lower", 18) + pad("strip", 18) + "strip+digits")
    for r in rows:
        print("  " + pad(r[0], 18) + pad(folds.fold("lower", r[0]), 18) + pad(folds.fold("strip", r[0]), 18) + folds.fold("strip+digits", r[0]))

def outcome(which, rows, name, who):
    k = folds.fold(which, name)
    o = folds.owner(which, rows, k)
    fk = folds.first_key(which, rows, who)
    if o == 0 and fk == "":
        return "new account " + q(k)
    if o == 0:
        return "new key " + q(k) + ", though person " + str(who) + " already has " + q(fk) + ": a second account (split)"
    if o == who:
        return "the account " + q(k) + " of the same person: one account, right"
    return "key " + q(k) + " is person " + str(o) + "'s: a different person refused (merged)"

def signup(rows):
    name = plain(input("name> "))
    if name == "":
        print("Cancelled.")
        return rows
    if not name_ok(name):
        print("Type a name of 1 to 16 letters, digits, dots, dashes or underscores.")
        return rows
    n = folds.people(rows)
    t = plain(input("who is this (1 to " + str(n) + ", or n for someone new)> "))
    if t == "":
        print("Cancelled.")
        return rows
    if t == "n":
        who = n + 1
    else:
        if len(t) > 3:
            print("Type a person's number from 1 to " + str(n) + ", or n.")
            return rows
        for c in t:
            if c < "0" or c > "9":
                print("Type a person's number from 1 to " + str(n) + ", or n.")
                return rows
        who = int(t)
        if who < 1 or who > n:
            print("Type a person's number from 1 to " + str(n) + ", or n.")
            return rows
    print(name + ", person " + str(who) + ":")
    for f in folds.folds():
        print("  " + pad(f, 14) + outcome(f, rows, name, who))
    return rows + [[name, who]]

def results(rows):
    print("Over " + str(len(rows)) + " sign-ups by " + str(folds.people(rows)) + " people:")
    print("  " + pad("fold", 14) + "keys  merged  split")
    right = []
    for f in folds.folds():
        m = folds.measure(f, rows)
        print("  " + pad(f, 14) + lpad(str(m[2]), 4) + lpad(str(m[0]), 8) + lpad(str(m[1]), 7))
        if m[0] == 0 and m[1] == 0:
            right = right + [f]
    print("Merged: a person's name lands on another person's key. Split: one person's names land on two keys.")
    for f in folds.folds():
        for r in rows:
            k = folds.fold(f, r[0])
            o = folds.owner(f, rows, k)
            if o != r[1]:
                print("  " + f + ": " + r[0] + " (person " + str(r[1]) + ") lands on " + q(k) + ", person " + str(o) + "'s key")
    for f in folds.folds():
        ex = ""
        for r in rows:
            k = folds.fold(f, r[0])
            fk = folds.first_key(f, rows, r[1])
            if fk != k and ex == "":
                ex = "  " + f + " splits, first " + r[0] + " (person " + str(r[1]) + "): " + q(k) + ", where the same person has " + q(fk)
        if ex != "":
            print(ex)
    if len(right) == 0:
        print("No fold keeps every person apart and every person's names together, for these names.")

rows = folds.opening()
print("== Username registry ==")
print("Names folded into keys, set against who is who: people merged, and people split.")
running = True
while running:
    print("")
    print("1) sign-ups  2) keys  3) sign up  4) merged and split  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_signups(rows)
    elif choice == "2":
        show_keys(rows)
    elif choice == "3":
        rows = signup(rows)
    elif choice == "4":
        results(rows)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### folds.eml

```eml
# P145 username registry - names registered under a fold that decides when
# two names are the same account. As in the corpus case
# case-fold-collisions, a fold is many-to-one, and the property that makes
# it a uniqueness key - fold(a) == fold(b) only when a and b are the same
# person - can only be checked against who is who, written apart from every
# fold. Two failures, counted apart: MERGED, a person whose name lands on
# another person's key (refused, or their account lost), and SPLIT, a
# person whose names land on two keys (one spelling no longer reaches the
# account). A sign-up is [name as typed, person].

def folds():
    return ["exact", "lower", "strip", "strip+digits"]

def what():
    return ["the name as typed", "lower case", "lower case, without . - _", "lower case, without . - _ and trailing digits"]

def opening():
    return [["Alice", 1], ["alice", 1], ["ALICE", 1], ["al.ice", 2], ["al-ice", 2], ["alice1", 3], ["alice2", 4], ["Bob", 5], ["bob", 5], ["bob.smith", 6], ["bobsmith", 6], ["carol", 7], ["carol7", 8], ["carol_7", 8], ["dave", 9], ["Dave2024", 10]]

def to_lower(s):
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => up
    "abcdefghijklmnopqrstuvwxyz" => low
    "" => out
    for c in s:
        c => m
        0 => i
        while i < 26:
            if up[i] == c:
                low[i] => m
            i + 1 => i
        out + m => out
    return out

def fold(which, s):
    if which == "exact":
        return s
    to_lower(s) => t
    if which == "lower":
        return t
    "" => u
    for c in t:
        if c != "." and c != "-" and c != "_":
            u + c => u
    if which == "strip":
        return u
    len(u) => i
    while i > 0 and u[i - 1] >= "0" and u[i - 1] <= "9":
        i - 1 => i
    return u[0:i]

def owner(which, rows, k):
    # the person whose name first took key k, or 0
    for r in rows:
        if fold(which, r[0]) == k:
            return r[1]
    return 0

def first_key(which, rows, who):
    # the first key person who has, or ""
    for r in rows:
        if r[1] == who:
            return fold(which, r[0])
    return ""

def measure(which, rows):
    # [merged, split, keys] over the sign-ups in order
    {} => key_to
    {} => person_to
    0 => merged
    0 => split
    for r in rows:
        fold(which, r[0]) => k
        if k in key_to:
            if key_to[k] != r[1]:
                merged + 1 => merged
        else:
            r[1] => key_to[k]
        if r[1] in person_to:
            if person_to[r[1]] != k:
                split + 1 => split
        else:
            k => person_to[r[1]]
    return [merged, split, len(key_to)]

def people(rows):
    0 => n
    for r in rows:
        if r[1] > n:
            r[1] => n
    return n
```

Python projection of folds.eml:

```python
def folds():
    return ["exact", "lower", "strip", "strip+digits"]

def what():
    return ["the name as typed", "lower case", "lower case, without . - _", "lower case, without . - _ and trailing digits"]

def opening():
    return [["Alice", 1], ["alice", 1], ["ALICE", 1], ["al.ice", 2], ["al-ice", 2], ["alice1", 3], ["alice2", 4], ["Bob", 5], ["bob", 5], ["bob.smith", 6], ["bobsmith", 6], ["carol", 7], ["carol7", 8], ["carol_7", 8], ["dave", 9], ["Dave2024", 10]]

def to_lower(s):
    up = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    low = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        m = c
        i = 0
        while i < 26:
            if up[i] == c:
                m = low[i]
            i = i + 1
        out = out + m
    return out

def fold(which, s):
    if which == "exact":
        return s
    t = to_lower(s)
    if which == "lower":
        return t
    u = ""
    for c in t:
        if c != "." and c != "-" and c != "_":
            u = u + c
    if which == "strip":
        return u
    i = len(u)
    while i > 0 and u[i - 1] >= "0" and u[i - 1] <= "9":
        i = i - 1
    return u[0:i]

def owner(which, rows, k):
    for r in rows:
        if fold(which, r[0]) == k:
            return r[1]
    return 0

def first_key(which, rows, who):
    for r in rows:
        if r[1] == who:
            return fold(which, r[0])
    return ""

def measure(which, rows):
    key_to = {}
    person_to = {}
    merged = 0
    split = 0
    for r in rows:
        k = fold(which, r[0])
        if k in key_to:
            if key_to[k] != r[1]:
                merged = merged + 1
        else:
            key_to[k] = r[1]
        if r[1] in person_to:
            if person_to[r[1]] != k:
                split = split + 1
        else:
            person_to[r[1]] = k
    return [merged, split, len(key_to)]

def people(rows):
    n = 0
    for r in rows:
        if r[1] > n:
            n = r[1]
    return n
```

## README

# P145 - Username registry

Names folded into keys that decide when two names are one account, set
against who is who, written apart from every fold.

- `main.eml` - the menu, the sign-ups, the keys each fold makes, a new
  sign-up seen by every fold, and each fold's merges and splits
- `folds.eml` - the four folds, the opening sign-ups, lower case written as
  a table, the owner of a key, a person's first key, and the counts

How each part works:

- Sixteen sign-ups by ten people: Alice, alice and ALICE are one person;
  al.ice and al-ice another; alice1 and alice2 two more; carol7 and carol_7
  one person, and carol another.
- As in the corpus case `case-fold-collisions`, the folds go from the name
  as typed, to lower case, to lower case without dots, dashes and
  underscores, to that without trailing digits. Each is a function of the
  one before, so it only ever joins keys.
- Two failures, counted apart against who is who. Merged: a person's name
  lands on another person's key, so the sign-up is refused or an account is
  lost. Split: one person's names land on two keys, so one spelling no
  longer reaches the account. Merged goes 0, 0, 2, 7 from fold to fold and
  split 6, 3, 0, 0: no fold is right on both for these names.
- A new sign-up is shown under every fold: a new account, one account of
  the same person, a second account for someone who already has a key, or
  a key that is another person's.

What is checked: menu choices 1 to 5; a name of 1 to 16 letters, digits,
dots, dashes or underscores; a person's number that exists, or n for
someone new. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the sign-ups, the keys and the counts;
- Bob.Smith as person 6, caro.l as someone new and ALICE2 as person 4; the
  counts again.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- names: a cancel, one with a space and one of 17 characters;
- Zed with a cancel, 0, 11, x and 1234 for the person; zed as someone new;
  the counts.

Built on the verified corpus case `case-fold-collisions` (three folds of
increasing reach, measured against a separately stated list of who is
who).
