Handoff for 8 October: random words, the word lists and their decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 20 hours ago
parent 56a2edb970
commit 52a441a96f

@ -1,6 +1,6 @@
# Handoff DateKeys
*Estado al 7 de octubre de 2026. Lo que pasó hasta aquí, sesión a sesión, está en [HANDOFF_historial.md](HANDOFF_historial.md).*
*Estado al 7 de octubre de 2026 por la noche. Lo que pasó hasta aquí, sesión a sesión, está en [HANDOFF_historial.md](HANDOFF_historial.md).*
Sirve para retomar sin contexto previo, sea una persona o una sesión de Claude.
@ -15,7 +15,7 @@ Sirve para retomar sin contexto previo, sea una persona o una sesión de Claude.
| Repo | Rama y commit | Tags | Qué es |
|---|---|---|---|
| `datekeys-go` | `v0.15` y `main` en `fe50885` | `spec-v0.15` en `fe50885`, y los de las versiones anteriores | Spec v0.15 aprobada, implementación de referencia, `testdata` compartido |
| `datekeys-go` | `v0.15` en `c49c67c`; `main` en `fe50885` | `spec-v0.15` en `fe50885`, y los de las versiones anteriores | Spec v0.15 aprobada, implementación de referencia, `testdata` compartido; `c49c67c` añade las palabras al azar |
| `App` (`datekeys-ts`) | `v0.10` y `main` en `7650418` | `v0.4.0` en `7650418`, `v0.3.0`, `v0.2.0`, `v0.1.0` | Librería TypeScript 0.4.0, de la spec 0.15, y páginas `/inspect` y `/create` |
| `datekeys-dart` | `v0.15` en `faa2c4c` | ninguno | Librería Dart completa, de la spec 0.15; las ramas `v0.11` a `v0.14` se quedan atrás |
| `docs` | `main` | — | Este repo |
@ -27,13 +27,14 @@ Sirve para retomar sin contexto previo, sea una persona o una sesión de Claude.
- Go: `scripts/check.sh` entero en `fe50885`, con el anexo de recuperación abriendo fixtures sin código de DateKeys, y `scripts/fuzz.sh 20s` en `fe50885` con sus 27 objetivos, sin fallos;
- TypeScript: `npm run verify` en `7650418`, con 8 106 pruebas;
- Dart: `tool/check.sh` en `faa2c4c`, con 2 211 pruebas en la VM y 694 en Node.
- **Palabras al azar (7-10, después de cerrar la v0.15):** `datekeys-go` `c49c67c` añade `wordkey.Generate`, `List` y `CheckList`, y `datekeys encrypt -new-words FICHERO [-dic es] [-word-count 7]`. Es el SHOULD de §38.1 de ofrecer palabras generadas; no cambia ningún formato ni la derivación. `scripts/check.sh` pasa entero. La lista española, `wordkey/lists/es.txt` (7 776 palabras, SHA-256 `ff77b487…34fe`), es un **borrador sin revisar**, CC BY-SA 4.0, sacado de las frecuencias de subtítulos de FrequencyWords filtradas con el diccionario de LibreOffice; el método está en `wordkey/lists/README.md` y el script en [wordlists/build_es.py](wordlists/build_es.py).
- **La revisión externa:** el paquete de [revision_externa/](revision_externa/NOTA_PARA_EL_AUTOR.md) está al día con la v0.15. Su nota dice qué decidió el autor y qué falta: elegir revisor, alcance y presupuesto, el NDA, los bundles y el envío.
---
## Qué queda
La recuperación a largo plazo quedó hecha con la v0.15 (7-10). Lo siguiente que recomendó la sesión del 6-10 son las recomendaciones de la v0.14 en los clientes.
La recuperación a largo plazo quedó hecha con la v0.15 (7-10). **Para empezar mañana**, la recomendación de la sesión del 7-10: llevar las palabras al azar a TypeScript y a `/create` (Librerías, primer punto), y, si la web de Leipzig ha vuelto, rehacer la lista española con su corpus.
### Protocolo
@ -47,9 +48,14 @@ La recuperación a largo plazo quedó hecha con la v0.15 (7-10). Lo siguiente qu
### Librerías y clientes
- **Las palabras al azar en el resto** (§38.1). Go ya las tiene (`c49c67c`). Falta:
- `Generate` en TypeScript y Dart, con la misma lista, su SHA-256 y `CheckList`; los dos copian la lista de `datekeys-go`, como el `testdata`;
- que `/create` ofrezca por defecto palabras generadas, con su aviso de que las elegidas son más débiles;
- la lista inglesa de la EFF (7 776 palabras, CC BY): descargarla necesita permiso del autor. Con ella, `-dic` pasa a `en` por defecto, como decidió el autor;
- la lista española definitiva: el autor espera a que vuelva la web de la Universidad de Leipzig (el 7-10 daba un error de servidor), cuyos corpus de unos 256 idiomas se descargan con CC BY. El script sirve cambiando la fuente de frecuencias. Su web bloquea a los agentes (Anubis): descarga el autor. Cada lista la revisa alguien que hable el idioma antes de fijar su hash;
- el servicio `words.datekeys.com`, cuando haya sitio propio: ver las decisiones.
- **Las recomendaciones de la v0.14 y la v0.15 al SDK oficial**, que no aplican ni `/create` ni la CLI de Go:
- recomendar `time_and_key` para horizontes largos (§7.6);
- ofrecer por defecto palabras al azar (§38.1). El autor tiene que elegir la lista: cuál, en qué idioma y con qué licencia;
- avisar del estado de un perfil (§71);
- explicar al crear cómo se abrirá la cápsula dentro de veinte años: el anexo de recuperación (§79) y los archivos de releases (§50).
- **La página web, cuando se publique** (§59): builds reproducibles con sus hashes, Subresource Integrity y un cliente sin conexión.
@ -73,6 +79,11 @@ La recuperación a largo plazo quedó hecha con la v0.15 (7-10). Lo siguiente qu
- código entero en bundles de git cifrados con `age`;
- un NDA mutuo;
- la versión siguiente, después del informe. La v0.15 se adelantó a petición del autor (7-10).
- **Las palabras al azar** (7-10):
- listas de 7 776 palabras por idioma y 7 palabras por defecto; BIP-39 (2 048) se le queda pequeña al autor;
- la app descarga la lista del idioma del usuario; la CLI las lleva dentro, inglés por defecto, y otra con `-dic es`, `-dic fr`;
- **las palabras se sortean siempre en el dispositivo y nunca viajan**: el servidor no las genera ni las ve, ni cifradas. El autor propuso un servicio que las mandara cifradas con la clave de DateKeys y aceptó esto en su lugar;
- `words.datekeys.com` solo servirá las listas públicas: ficheros estáticos con el hash en la URL, el SHA-256 fijado en la app o un índice firmado por DateKeys. La clave de DateKeys firma; no cifra nada.
- **Sin fichero `.dkr`** (7-10): el objeto release no se guarda junto a la cápsula ni tiene extensión propia.
- **`locator.Open` sigue leyendo como mucho 1 MiB** del localizador (6-10).
- **El cifrado tlock queda en `BigInt`, sin tiempo constante**, documentado en Dart y TypeScript («continúa», 6-10).

@ -54,4 +54,5 @@ Para retomar el trabajo, lee [HANDOFF.md](HANDOFF.md): el estado de cada repo, l
| [spec_v0.9/](spec_v0.9/README.md) | Papeles de trabajo del borrador v0.9: diseño, revisiones y correcciones pendientes |
| [spec_v0.10/](spec_v0.10/formato3_diseno.md) | Formato de cápsula 3 en diseño, en tres entregas: varios ficheros, firma de autor y sello de tiempo. El diseño, con sus revisiones incorporadas, la revisión de Fable y la revisión final del borrador del spec |
| [spec_v0.11/](spec_v0.11/llave_palabras.md) | Papeles de trabajo de la v0.11, ya etiquetada. Contiene: <br>la [llave de palabras](spec_v0.11/llave_palabras.md); <br>la [consulta a Fable y Astra sobre el tamaño del área de firmas y sellos](spec_v0.11/area_firmas_para_revision.md) y [su propuesta](spec_v0.11/revision_fable_astra.md); <br>la [revisión del borrador](spec_v0.11/revision_borrador.md); <br>la [revisión de lo hecho en la sesión del 1 y 2 de octubre](spec_v0.11/revision_sesion_1_2_octubre.md), con los fallos pendientes de arreglar |
| [wordlists/](wordlists/README.md) | El script del borrador de la lista española de palabras al azar de la llave de palabras (7-10) |
| [ideas/](ideas/interruptor_sapphire.md) | Ideas de producto fuera de la cápsula del tiempo: el [interruptor de liberación condicional con Sapphire](ideas/interruptor_sapphire.md) y el [almacén cifrado de la app móvil, con PIN de coacción](ideas/almacen_cifrado_movil.md) |

@ -0,0 +1,10 @@
# Listas de palabras
El script que hizo el borrador de la lista española de `datekeys-go/wordkey/lists/es.txt` (7-10). Lee, en su carpeta de trabajo:
- `es_50k.txt`, de [FrequencyWords](https://github.com/hermitdave/FrequencyWords), `content/2018/es/es_50k.txt` (CC BY-SA 4.0);
- `es_ES.dic` y `es_ES.aff`, del repo de diccionarios de LibreOffice, `es/` (GPL, LGPL o MPL; solo como filtro).
Escribe `es_7776.txt`, la lista ordenada; `es_7776_rank.txt`, con la posición y la frecuencia de cada palabra; y `descartes.txt`, lo que quitó cada filtro. Los ficheros de entrada y de salida del 7-10 están en `G:\tmp\wordlist-es`.
Para otro idioma o para el corpus de Leipzig, se cambian la fuente de frecuencias, el diccionario, la regla de la marca `G` (los femeninos, propia del diccionario español) y la lista de palabras ofensivas.

@ -0,0 +1,129 @@
import re
import unicodedata
from collections import Counter
N = 7776
LETTERS = re.compile(r'^[a-záéíóúüñ]+$')
# Clitic pronouns glued to a verb: "démela", "dennos", "dígame".
CLITIC = re.compile(r'(me|te|se|nos|os|lo|la|le|los|las|les)$')
KEEP = {'miércoles'}
# A small seed blocklist; the author reviews the final list by hand anyway.
BLOCK = set('''
puta puto putas mierda joder coño cojones polla follar culo culos pene
vagina teta tetas zorra cabrón cabron maricón maricon marica gilipollas
imbécil imbecil idiota estúpido estupido estúpida subnormal retrasado
mamada chupar chupa pajero paja orgasmo semen sexo sexual violar violación
violador nazi negro negrata gitano moro sudaca puticlub prostituta
mear cagar caca pedo pis pito chocho concha verga pija pinche pendejo
chingar chingada cabrona bastardo perra cerdo cerda asesinar asesino
suicidio suicida matar muerte muerto cadáver cáncer droga drogas cocaína
heroína porno pornografía ramera furcia hostia hostias
manolo pal
'''.split())
def norm(w):
# §38.1: NFD, drop U+0300..U+036F, lowercase.
d = unicodedata.normalize('NFD', w)
d = ''.join(c for c in d if not (0x300 <= ord(c) <= 0x36F))
return d.lower()
# Rules of the suffix G (masculine to feminine) from the .aff.
grules = []
for line in open('es_ES.aff', encoding='utf-8'):
p = line.split()
if len(p) >= 5 and p[0] == 'SFX' and p[1] == 'G':
strip = '' if p[2] == '0' else p[2]
add = p[3].split('/')[0]
add = '' if add == '0' else add
grules.append((strip, add, re.compile(p[4] + '$')))
stems = set()
group = {} # word -> its masculine stem, so a gender pair keeps one word
unflagged = set()
for i, line in enumerate(open('es_ES.dic', encoding='utf-8')):
if i == 0:
continue
entry = line.strip().split()[0] if line.strip() else ''
if not entry:
continue
w, _, flags = entry.partition('/')
stems.add(w)
group.setdefault(w, w)
if not flags:
unflagged.add(w)
if 'G' in flags:
for strip, add, cond in grules:
if cond.search(w) and w.endswith(strip):
f = w[:len(w) - len(strip)] + add
stems.add(f)
group.setdefault(f, w)
break
freq = []
for line in open('es_50k.txt', encoding='utf-8'):
w, c = line.split()
freq.append((w, int(c)))
drop = {k: [] for k in ('not_letters', 'length', 'not_stem', 'clitic', 'blocked', 'gender_pair', 'collision')}
seen = {}
pairs = {}
kept = []
last = None
for w, c in freq:
if not LETTERS.match(w):
drop['not_letters'].append(w)
continue
if not 3 <= len(w) <= 9:
drop['length'].append(w)
continue
if w not in stems:
drop['not_stem'].append(w)
continue
# An entry without flags that ends in a clitic is a verb form, not a word to learn.
# Entries without flags are conjugations, plurals and pieces of names.
if w in unflagged and w not in KEEP:
drop['clitic'].append(w)
continue
if w in BLOCK:
drop['blocked'].append(w)
continue
g = group[w]
if norm(g)[-1] in 'oa':
g = norm(g)[:-1] + '·'
if g in pairs:
drop['gender_pair'].append(f'{w} (pareja de {pairs[g]})')
continue
k = norm(w)
if k in seen:
drop['collision'].append(f'{w} (choca con {seen[k]})')
continue
seen[k] = w
pairs[g] = w
kept.append((w, c))
if len(kept) == N:
last = (w, c)
break
print('kept', len(kept), 'last', last)
if len(kept) < N:
raise SystemExit('not enough words')
words = [w for w, _ in kept]
open('es_7776_rank.txt', 'w', encoding='utf-8', newline='\n').write(
''.join(f'{i + 1}\t{w}\t{c}\n' for i, (w, c) in enumerate(kept)))
open('es_7776.txt', 'w', encoding='utf-8', newline='\n').write(''.join(w + '\n' for w in sorted(words)))
with open('descartes.txt', 'w', encoding='utf-8', newline='\n') as f:
for k, v in drop.items():
f.write(f'== {k}: {len(v)}\n')
f.write('\n'.join(v) + '\n\n')
for k, v in drop.items():
print(k, len(v), v[:20])
lens = Counter(len(w) for w in words)
print('lengths', sorted(lens.items()))
print('with ñ', sum('ñ' in w for w in words), 'with accent', sum(norm(w) != w for w in words))
u = [w for w in words if w in unflagged]
print('unflagged kept', len(u), u[:60])
print('tail', words[-30:])
Loading…
Cancel
Save

Powered by TurnKey Linux.