2026-09-08

2026-09-08 — la numeración de sprints y artefactos necesita un asignador con autoridad en la base de datos, no un contador de archivos

Estado: investigación / instrucción para una sesión futura. Escrito durante una consolidación de varios árboles de trabajo (/consolidate-worktrees; la bitácora de esa sesión no la lleva directamente este repositorio — ver los commits que citan «2026-09-08 multi-worktree consolidation» para los casos concretos). No es un documento de Gate 1; es el punto de entrada para quien tome el arreglo de verdad. Directiva del operador, textual en sustancia: «la base de datos manda. El UUID manda. El ID.»

Qué se encontró, en concreto

Consolidar 9 árboles de trabajo dispersos de este repositorio sacó a la luz tres colisiones reales de número de sprint en una sola pasada: dos documentos independientes reclamando cada uno el 4.54, el 4.60 y el 4.61 en ramas distintas. Uno de los documentos en colisión ya se había renumerado una vez antes (4.56 → 4.60) el mismo día en que se redactó, y su propia nota de cabecera ya nombra la clase de falla que está en docs/POSTMORTEMS.md: bug-id-allocated-twice-across-branches — «un número asignado desde un listado del que el asignador tiene una vista vieja». No es un modo de falla nuevo; es uno conocido, con nombre, recurrente, y que nunca se ha arreglado de raíz.

Los dos asignadores que existen hoy, y por qué los dos fallan bajo concurrencia:

1. `docs/reference/sprint-number-registry.txt` — una sola línea, un contador monótono en archivo plano que tools-artifact-scaffold lee e incrementa. Cada árbol de trabajo tiene su propia copia. Dos sesiones en dos árboles leen el mismo número, cada una incrementa su copia local, las dos hacen commit — colisión, descubierta solo después (si acaso) al fusionar. 2. `sprint_identity` (base corpus_sourcecode, migración crates/operations/control-plane/migrations/0016_story_harness_language.sql y enmiendas posteriores) — a prueba de colisiones por diseño: UNIQUE (number) WHERE is_primary, un disparador de admisión de solo añadir (sprint_identity_admit, que prohíbe cambiar number/slug después de la creación) y la exigencia de que exista antes un renglón work_product(kind='sprint', revision=1). Pero nada escribe en ella. Comprobado directamente: SELECT number, slug FROM sprint_identity devuelve exactamente 2 renglones (4.63, 5.1) frente a más de 60 archivos de sprint reales. Ni story-new, ni compile-story, ni tools-artifact-scaffold crean un renglón de work_product o sprint_identity. La única tabla construida para resolver este problema está casi por completo sin usar.

Por qué la numeración por archivo no se puede arreglar sola

Cualquier contador en archivo plano bajo control de versiones tiene el mismo problema estructural por cuidadoso que sea el incremento: dos árboles, ramas o sesiones trabajando a la vez ven cada uno una vista LOCAL consistente y cada uno cree que su lectura era la última. Solo un sistema con una restricción transaccional real de unicidad — es decir, la base de datos, vía INSERT ... UNIQUE o equivalente — puede hacer que dos reclamos simultáneos del mismo número fallen en voz alta en vez de que los dos tengan éxito en silencio hasta que alguien se dé cuenta al fusionar (que es lo que pasó aquí, tres veces por separado, en una sola pasada de consolidación).

Qué tiene que hacer el arreglo

1. Construir el escritor que falta. Un equivalente de sprint-new (o una extensión de story-new) que, antes de escribir el archivo markdown, haga la secuencia completa de nacimiento: repo_artifact (o reutilice el renglón de repo_artifact que el documento acabará teniendo) → work_product(kind='sprint', revision=1)sprint_identity(number, slug, artifact_id). El INSERT sobre sprint_identity es la comprobación de colisión real: si falla (UNIQUE (number) WHERE is_primary), el número ya estaba tomado y la herramienta rechaza y elige o reporta el siguiente libre, de forma transaccional, sin ventana de carrera. 2. Hacer que el número sea derivado, no elegido. Según el propio planteamiento del operador y el precedente ya establecido en la ronda de Gate 1.5 del sprint 4.65 (este repositorio, docs/userstories/sprint-4.65-*.md, la decisión de «numeración secuencial por producto»): el UUID (sprint_identity.artifact_id o equivalente) es la identidad real; el número que ve una persona es presentación, idealmente derivado por MAX(number) + 1 calculado dentro de la misma transacción que inserta el renglón nuevo, no leído-y-luego-escrito desde un archivo con el que dos sesiones pueden competir. 3. Retirar (o degradar con claridad) `sprint-number-registry.txt` en cuanto exista el escritor de base de datos — conservarlo solo como una vista generada y en caché del máximo actual de la base, regenerada, nunca como fuente de verdad, igual que el patrón ya establecido en este repositorio para el léxico exportado y docs/reference/story-template.tsv (los dos declarados explícitamente «representaciones de renglones» en sus propias cabeceras). [Nota de la traducción: el original nombra un solo archivo de léxico bajo docs/reference que desde entonces se partió en glossary.tsv y term-canonical.tsv. El patrón que cita es el mismo.] 4. Rellenar la historia. Una vez que exista el escritor, registrar en sprint_identity el número de cada documento de sprint que ya existe (un script de migración de una sola vez, no un barrido a mano), para que la restricción de unicidad tenga de verdad contra qué comparar los reclamos nuevos. Hasta que eso corra, lo casi vacía que está sprint_identity significa que no puede atrapar una colisión contra los más de 60 números que hoy solo existen como archivos.

Qué NO se está pidiendo

No es un rediseño del formato del documento de sprint, no es un cambio en cómo funciona la planeación de Gate 1, y no es un esquema de numeración nuevo (los números decimales de sprint se quedan exactamente como están). Únicamente: mover la asignación del siguiente número de «lee un archivo y ojalá nadie lo haya leído antes» a «pregúntale a la base de datos, que no puede mentir sobre si un número está tomado».

Pistas para quien tome esto

posteriores — el esquema real de sprint_identity y su disparador.

cercano (la secuencia de nacimiento de work_item, aunque hoy no toca sprint_identity ni work_product en absoluto; de B-092 a B-097 en esta misma sesión ya se endurecieron otros errores de esta función — lee esos registros de error primero, esta función tiene historial de sorpresas sin prueba en el primer uso).

de falla con nombre que esto arregla, con (presumiblemente) incidentes previos que leer para entender por qué se le puso nombre en primer lugar.

2026-09-08) — las tres colisiones concretas encontradas en esta pasada, por si una prueba de regresión quiere datos de campo reales.

Toda la investigación