Proyecto: LPDI SGR (eco.lpdi.co) · Fecha: 23 de julio de 2026 · Estado: PLAN v2 — incorpora tus 10 respuestas; pendiente de tu aprobación antes de ejecutar (tu punto 10)
| # | Tu respuesta | Cómo quedó en el v2 |
|---|---|---|
| 1 | Correos enmascarados; rótulo "Admin en el sistema" | Tarjeta con j***@acme.com y rótulo "Admin en el sistema" (nunca "Admin" a secas). Nombre completo visible. §3.2 |
| 2 | "Crear nueva" siempre permitido, con auditoría; mismo usuario puede tener el mismo nombre en países distintos | Rediseñado el índice único: ahora incluye el país y se ancla al estado real de formalización. Además encontramos que el índice actual está muerto (no protege nada) — ver §1. §3.4 |
| 3 | Código solo al formalizar + regla transversal "nada en borrador recibe código" | Regla documentada para DESIGN.md. Verificado en eventos: el código formateado de eventos SÍ cumple (solo al enviar); el codigo_unico SERIAL de eventos NO cumple (se asigna al guardar el borrador) — señalado como deuda. §5.6 |
| 4 | Código: prefijo telefónico, año a confirmar, tipo granular extensible, consecutivo a 5 dígitos | Estructura reescrita: TTT-PPP-AA-NNNNN con taxonomía de tipos en tabla de catálogo (43 códigos seed, agregar uno = 1 fila). Ejemplos nuevos. §5 |
| 5 | Aprobador asigna permisos + cargo, gestionado en PS/PI | Confirmado: PS/PI es el lugar correcto y consistente con cómo se gestiona hoy el equipo (verificado en el código, ver §4.2). Flujo de aprobación ajustado. §4 |
| 6 | 10 días + recordatorio día 5 + correos a ambas partes en creación y resolución | Matriz de correos actualizada. §4.4 |
| 7 | Barrido de duplicados existentes | Incorporado como informe solo-lectura, y además es prerrequisito técnico del nuevo índice único. §6 |
| 8 | Código visible en PS/PI | Incorporado. §5.5 |
| 9 | Columna de código en las dos consultas | Incorporado con las reglas del DS de tablas anchas. §7 |
| 10 | Revisas el v2 antes de ejecutar | Este documento. Nada se ejecuta sin tu OK. |
Hallazgo nuevo del v2 (importante): al revisar el índice único para incorporar tu punto 2, encontramos que la protección actual contra duplicados del mismo usuario dejó de operar cuando se introdujo el modelo de formalización el 12 de julio. El detalle en §1. La buena noticia: el rediseño que pediste por países lo corrige de paso.
Lo del v1 sigue vigente: nada impide que dos usuarios distintos registren la misma empresa; el aviso del formulario es ignorable; el endpoint /api/check-company-name solo busca coincidencia exacta insensible a mayúsculas (pero sensible a estructura: no cruza startups contra ICGs ni usa el nombre legal).
Hallazgo nuevo — el índice único actual está muerto:
030 + 036) es: UNIQUE (registered_by_id, company_name) WHERE company_name != '' AND status != 'DRAFT' (y su gemelo en ICGs con organization_name).114 (formalización, decisión tuya del 12-jul), el estado real de una entidad es la columna formalized_at (NULL = borrador, con fecha = activa). La columna status quedó como legacy: las entidades se insertan con status='DRAFT' y nada la cambia jamás — el propio código de la consulta admin lo documenta: "status NO SIGNIFICA NADA — no existe proceso de aprobación" (consulta/startups/+page.server.ts).status='DRAFT' para siempre, la condición status != 'DRAFT' hace que el índice único no aplique a ninguna fila. Hoy un mismo usuario puede registrar dos veces "Acme" formalizada y la base no lo impide.psql el estado real en producción (regla nuestra: el estado real ≠ los archivos de migración), pero el código es concluyente.Esto convierte el rediseño del constraint (tu punto 2) de un "ajuste" a una reparación necesaria, y explica por qué el barrido de duplicados (tu punto 7) pasa a ser también un prerrequisito técnico: antes de crear el índice nuevo hay que conocer y resolver las colisiones ya existentes, o la creación del índice falla.
Otros datos verificados que condicionan el diseño:
startups.country / icgs.country guardan el nombre del país en español ("Colombia"), con algunos valores legacy en ISO2 ("CO") que el picker de países resuelve al vuelo. Para que el constraint por país y el código por país sean correctos, hay que normalizar esos valores legacy (data-fix acotado, con snapshot).company_name_norm / organization_name_norm (lower + unaccent, migración 072) con índice — se reutilizan para el matching y para el constraint.pg_trgm (similitud de texto) ya está habilitada (migración 049) — el matching aproximado no necesita extensiones nuevas.La propuesta sigue siendo correcta y ejecutable: convertir el aviso pasivo en una decisión consciente (crear nueva vs. solicitar asociación), con verificación humana de pertenencia por parte de quien administra la entidad existente. Tus 10 respuestas cierran todas las decisiones abiertas del v1 salvo una (el año del código, §9.1), que dejamos como recomendación explícita para tu confirmación.
Igual que el v1: nuevo endpoint que busca contra startups e ICGs en dos niveles — (1) coincidencia exacta normalizada (sin tildes ni mayúsculas, reutilizando company_name_norm/organization_name_norm); (2) coincidencia aproximada por similitud (pg_trgm) que captura "Acme SAS" vs "Acme S.A.S." y errores de tipeo. Compara nombre comercial, nombre legal y nombre de ICG. Solo entidades formalizadas y no eliminadas.
Encontramos entidades parecidas a "Acme"
┌──────────────────────────────────────────────────┐
│ ACME S.A.S. (Startup) · Colombia │
│ Código: STA-057-26-00042 │
│ Nombre legal: Acme Tecnología S.A.S. │
│ Admin en el sistema: Juan Pérez (j***@acme.com) │
│ CEO: María Gómez (m***@acme.com) │
│ Web: acme.com · Coincidencia exacta │
│ [Es mi empresa, solicitar asociación] │
└──────────────────────────────────────────────────┘
[No es ninguna, crear entidad nueva]
*** + dominio completo (j***@acme.com). El enmascarado se hace en el servidor — el correo completo nunca viaja al navegador.Siempre permitido. Cuando el usuario ignora una coincidencia exacta, se deja rastro en action_audit_log con un action_type nuevo (p. ej. entity_created_over_exact_match), incluyendo en metadata el/los ids de las entidades que coincidían. Nota técnica obligatoria (gotcha conocido de este repo): agregar un action_type implica tocar el tipo TS y el CHECK constraint de action_audit_log (la última versión del CHECK está en la migración 128; logAction traga el error si no se hace → falla silenciosa).
Reemplaza al índice muerto. Diseño:
-- startups (gemelo equivalente en icgs con organization_name)
DROP INDEX IF EXISTS startups_registered_by_company_unique;
CREATE UNIQUE INDEX startups_registered_by_company_country_unique
ON startups (registered_by_id, immutable_unaccent(company_name), immutable_unaccent(country))
WHERE company_name != ''
AND formalized_at IS NOT NULL
AND deleted_at IS NULL;
Qué logra, punto por punto:
| Caso | Resultado |
|---|---|
| Mismo usuario, mismo nombre, mismo país | Bloqueado (la protección real que pediste conservar) |
| Mismo usuario, mismo nombre, países distintos | Permitido (tu regla nueva: "Acme Colombia" y "Acme Perú" del mismo usuario conviven) |
| Mismo usuario, N borradores con el mismo nombre | Permitido (los borradores no cuentan — formalized_at IS NULL) |
| Usuarios distintos, mismo nombre | Permitido a nivel de base (como hoy) — lo gobierna el flujo de coincidencias + asociación, no un bloqueo duro |
| "Ácme" vs "acme" | Cuentan como el mismo nombre (normalización con immutable_unaccent, ya existente) |
Prerrequisitos en orden (por eso el barrido va primero):
1. Data-fix de países legacy: convertir los valores ISO2 ("CO") a nombre ("Colombia") para que el país del constraint compare manzanas con manzanas. Acotado, con snapshot previo (Supabase sin PITR) y reversible.
2. Pre-chequeo de colisiones: query que detecta filas que violarían el índice nuevo. Si hay colisiones, van al informe del barrido (§6) y las resuelve el equipo LPDI caso por caso ANTES de crear el índice (si no, el CREATE UNIQUE INDEX falla).
3. Crear el índice nuevo y eliminar el viejo. Todo idempotente y reversible.
Nueva tabla entity_join_requests con token. Quién aprueba = exactamente quien ya puede editar la entidad: en startup, el registrador, el CEO o un miembro verificado con edición; en ICG, solo el registrador (regla dura tuya: los contactos ICG nunca editan). El solicitante debe estar verificado (Opción B). Vigencia 10 días, recordatorio al día 5 — replicando el motor temporal ya probado de las transferencias (RPC tick + cron + correos, patrón de entity_transfer_tick).
Confirmado: sí, el panel de la entidad es el lugar correcto, y es consistente con cómo se gestiona hoy el equipo. Verificado en el código:
startup_founders, que ya tiene las columnas exactas que necesitamos — role (cargo), can_edit_form (permiso de edición) e is_ceo — y hoy se gestionan en el Panel 5C "Equipo" del formulario de startup, al que se llega desde el Panel de Startup (/dashboard/perfil-startup). La vista personal "Gestión de permisos" del PU muestra estos mismos roles (Admin / CEO / Miembro / Miembro + Edición).icg_contacts (con campo position = cargo y verification_status), gestionados desde el formulario ICG al que se llega por el Panel de ICG (/dashboard/perfil-icg).Flujo ajustado: las solicitudes pendientes aparecen en el PS (startup) o PI (ICG) de la entidad. Al aprobar, en esa misma pantalla el aprobador asigna de una vez:
role) y los permisos (can_edit_form sí/no; default: sin edición). El aprobado entra como miembro verificado (ambas partes consintieron: él solicitó, el aprobador aceptó).position). No hay permiso de edición que asignar — los contactos ICG nunca editan (tu regla dura se mantiene intacta); el aprobado entra como contacto verificado.pending → (approved | rejected | expired a los 10 días). Recordatorio al aprobador el día 5. Todo transiciona vía RPC atómica (sin dobles aprobaciones) y deja rastro en action_audit_log (action_types nuevos: join_request_created, join_request_approved, join_request_rejected, join_request_expired — mismo gotcha del CHECK, §3.3).
| Evento | Al solicitante | Al aprobador / aprobadores |
|---|---|---|
| Solicitud creada | ✅ constancia "tu solicitud fue enviada a los administradores de X" | ✅ con la tarjeta del solicitante y el enlace de decisión |
| Día 5 sin respuesta | — | ✅ recordatorio |
| Aprobada | ✅ "ya haces parte de X" (con su cargo) | ✅ confirmación de lo que aprobó |
| Rechazada | ✅ notificación (sin exponer quién rechazó) | ✅ constancia |
| Expirada (día 10) | ✅ "venció sin respuesta; puedes crear la entidad o volver a intentar" | ✅ constancia informativa |
(La fila "expirada → ambas partes" es una extensión menor de tu regla, por simetría; puedes recortarla — §9.4.)
Los renders de correo siguen el patrón del repo (script de render separado, mismo layout de los correos de entidad existentes).
TTT-PPP-AA-NNNNN — ejemplos: STA-057-26-00042, VCF-051-26-00007.
| Segmento | Qué es | Detalle |
|---|---|---|
TTT |
Tipo granular (3 letras) | Subcategoría de segundo nivel, desde tabla de catálogo (§5.2). Reemplaza al ST/IC plano del v1 |
PPP |
Prefijo telefónico del país (3 dígitos) | El MISMO criterio del código de eventos, verificado en el código: se replica getPhonePrefix3 — Colombia → 057, Perú → 051, México → 052; sin país → 000. (Eventos usa el prefijo telefónico rellenado a 3 dígitos, no el ISO numérico) |
AA |
Año (2 dígitos) | Recomendación: año de registro en el sistema (fecha de formalización) — pendiente de tu confirmación, §9.1 |
NNNNN |
Consecutivo a 5 dígitos | Contador atómico en base de datos por año, réplica exacta del patrón de eventos (evento_codigo_counters + RPC next_evento_consecutivo): tabla entidad_codigo_counters + RPC propia. Sin colisiones por concurrencia |
El consecutivo se propone por año, compartido entre startups e ICGs (igual que eventos, que llevan un solo contador por año): mantiene los números cortos y el código ya distingue el tipo en TTT. Si prefieres contadores separados por tipo, es un cambio de una línea — §9.2.
Nueva tabla de catálogo catalogo_codigo_tipo_entidad: mapea cada subcategoría de segundo nivel a su código corto de 3 letras. Agregar una categoría o subcategoría nueva = insertar 1 fila (no tocar código de aplicación).
CREATE TABLE catalogo_codigo_tipo_entidad (
id SERIAL PRIMARY KEY,
rol_codigo VARCHAR(100) NOT NULL UNIQUE, -- referencia al codigo de catalogo_rol_ecosistema
codigo_corto CHAR(3) NOT NULL UNIQUE, -- el TTT del código de entidad
activo BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Cómo conecta con lo que ya existe (verificado en el código):
catalogo_rol_ecosistema (43 filas: codigo, label, categoria) ya contiene el segundo nivel completo, y ya cubre a las startups: tiene las filas Startup, Scaleup y PyME (categoría "Startup"). Es la columna vertebral perfecta para colgar el código corto.company_type (enum STARTUP/PYME/SCALEUP) → fila del catálogo (Startup/PyME/Scaleup) → codigo_corto.icg_role guarda exactamente el codigo del catálogo (p. ej. Venture Capital Fund - VC) → codigo_corto directo. Fallback para ICGs legacy sin icg_role: código OTR.Seed propuesto (los 43 códigos — puedes ajustar cualquier letra, §9.3):
| Categoría | Subcategoría (2.º nivel) | Código |
|---|---|---|
| Inversionista Individual | Ángel Inversionista | ANG |
| Inversionista Individual | Limited Partner | LIP |
| Inversionista Individual | General Partner | GEP |
| Inversionista Individual | Lead Investor | LIN |
| Inversionista Institucional | Family Office | FAM |
| Inversionista Institucional | Fondo de Inversión | FDI |
| Inversionista Institucional | Banca de Inversión | BAN |
| Inversionista Institucional | Venture Capital Fund – VC | VCF |
| Inversionista Institucional | Corporate Venture Capital – CVC | CVC |
| Inversionista Institucional | Government Venture Capital – GVC | GVC |
| Gestor especializado | Incubadora | INC |
| Gestor especializado | Aceleradora | ACE |
| Gestor especializado | Cámara de Comercio | CAM |
| Gestor especializado | Caja de Compensación | CDC |
| Gestor especializado | Company Builder | CBU |
| Gestor especializado | Startup Studio | SST |
| Corporativo | Empresa Privada | EMP |
| Corporativo | Empresa Mixta | EMX |
| Consultora | Gran consultora | GCO |
| Consultora | Consultora boutique | CBO |
| Consultora | Consultor independiente | COI |
| Startup | Startup | STA |
| Startup | Scaleup | SCA |
| Startup | PyME | PYM |
| Gobierno | Entidad Pública Municipal | GPM |
| Gobierno | Entidad Pública Regional | GPR |
| Gobierno | Entidad Pública Nacional | GPN |
| Gobierno | Organismo Multilateral | OMU |
| Academia e Innovación | Universidad | UNI |
| Academia e Innovación | Centro de Innovación | CEI |
| Academia e Innovación | Centro de Investigación | CEV |
| Academia e Innovación | Otros centros de formación | OCF |
| Hub y Redes | Hub de Innovación | HUB |
| Hub y Redes | Red de Innovación | RDI |
| Hub y Redes | Red de Emprendimiento | RDE |
| Hub y Redes | Clúster Tecnológico | CLU |
| Hub y Redes | Comunidad | COM |
| Otros | Asociación | ASO |
| Otros | Gremio | GRE |
| Otros | Fundación | FUN |
| Otros | Medio de comunicación | MED |
| Otros | ONG | ONG |
| Otros | Otro | OTR |
| Entidad | Código | Lectura |
|---|---|---|
| Startup en Colombia, formalizada en 2026, consecutivo 41 | STA-057-26-00041 |
Startup · Colombia (+57) · 2026 · #41 |
| PyME en México, 2026, consecutivo 87 | PYM-052-26-00087 |
PyME · México (+52) · 2026 · #87 |
| ICG fondo VC en Perú, 2026, consecutivo 12 | VCF-051-26-00012 |
Venture Capital Fund · Perú (+51) · 2026 · #12 |
| ICG Cámara de Comercio en Colombia, 2026, consecutivo 104 | CAM-057-26-00104 |
Cámara de Comercio · Colombia (+57) · 2026 · #104 |
Nota: el código es inmutable desde su asignación. Si la entidad luego cambia de tipo o de país, el código no se regenera (es un identificador, no un atributo vivo) — igual criterio que eventos.
formalizeEntity() — el único punto del código por donde pasa toda formalización (público y dashboard), ya idempotente a nivel de base. Los borradores abandonados no queman números.formalized_at ascendente (las más antiguas reciben los números más bajos), con el año de su propia formalización en AA.Nada que esté en BORRADOR recibe código. Los códigos únicos del sistema (entidades, eventos, convocatorias, noticias y cualquier módulo futuro) se asignan únicamente al formalizar/enviar, nunca al crear o autoguardar un borrador. Un borrador abandonado no debe quemar números ni dejar identificadores huérfanos.
Verificación de cómo se comportan hoy los eventos (lo pediste explícito):
evento_codigo, formato M-PPP-TTT-AA-NNNNN) cumple la regla: se genera únicamente en el submit final; el autosave de borradores lo deja en NULL (está documentado en el propio código: "evento_codigo NO se genera en drafts").codigo_unico, columna SERIAL que la base asigna al insertar la fila — incluidos los borradores del autosave. Ese número se usa en los enlaces públicos de compartir (/eventos/{codigo}). No cumple la regla nueva: un borrador abandonado quema un número. Lo señalamos como deuda a corregir para cumplir la regla (no la corregimos dentro de esta tarea salvo que lo pidas — §9.5); quedará listada en DESIGN.md junto a la regla.El código de entidades nace ya conforme a la regla (solo en formalizeEntity).
Informe solo lectura sobre la base actual (sin tocar ni un dato), con el mismo matching del §3.1:
Verificado el estado actual de ambas páginas:
/dashboard/consulta/startups: columnas Compañía · Tipo · Estado · Transferida · Ubicación · TRL · CRL · Base tec. · Completitud · Sitio web · Acciones (sticky)./dashboard/consulta/icg: Organización · Tipo · Estado · Transferida · Perfil · Ubicación · Convocatoria · Completitud · Sitio web · Acciones (sticky).Se agrega Código inmediatamente después del nombre (Compañía/Organización), ordenable, con — para entidades sin código (borradores). Cumpliendo las reglas del DS de tablas anchas (DESIGN.md): mismos valores fijos de toda tabla ancha del dashboard, columna Acciones sticky a la derecha intacta (con su sombra de separación y su skipClass en la medición de anchos), scroll horizontal en el contenedor — prohibido ajustar una tabla por su cuenta.
Todas las migraciones: aditivas, idempotentes y reversibles, con snapshot previo (Supabase sin PITR, prohibido DELETE sin snapshot). Numeración de migraciones desde 130_. Zonas de alto riesgo (migraciones + data-fix + RLS) con manos Opus y verificación con evidencia (psql real, no reporte en prosa); las tareas mecánicas con Sonnet.
Waves:
| Wave | Tasks | Depende de | Parallelizable | Riesgo |
|---|---|---|---|---|
| 0 | 1, 2 | — | Sí (solo lectura + doc) | Bajo |
| 1 | 3, 4, 5 | Wave 0 (T3 requiere colisiones resueltas del T1) | Sí | ALTO — Opus (migraciones) |
| 2 | 6, 7, 8 | Wave 1 | Sí | Medio — Opus |
| 3 | 9, 10, 11, 12 | Wave 2 | Sí | Bajo-medio |
| 4 | 13 | Waves 1–3 | No (integración) | Medio — Opus |
Files: - Create: script de informe solo-lectura (queries de matching exacto/aproximado + colisiones duras) - Create: informe entregable para el equipo LPDI
Done when:
- [ ] Informe generado con grupos de duplicados (exactos y aproximados) y sección "colisiones duras" del índice nuevo
- [ ] Verificado con psql el estado real: pg_indexes de los índices únicos actuales + conteo de filas con status != 'DRAFT' (confirmar el hallazgo del índice muerto)
- [ ] Cero escrituras a la base: el script solo hace SELECT (revisión del código del script)
Files:
- Edit: DESIGN.md (regla "nada en borrador recibe código" + deuda codigo_unico SERIAL de eventos)
Done when: - [ ] DESIGN.md contiene la regla transversal con la redacción del §5.6 y la deuda de eventos documentada - [ ] No se tocó ningún archivo de código (solo documentación)
Files:
- Create: supabase/migrations/130_dedup_unique_index_country.sql (+ rollback)
Done when:
- [ ] Snapshot previo tomado y verificado (conteo de filas coincide)
- [ ] Data-fix ISO2→nombre aplicado solo a los valores legacy detectados; SELECT de control devuelve 0 países en formato ISO2
- [ ] Índice nuevo creado y viejo eliminado; probado con psql: inserción duplicada (mismo usuario+nombre+país formalizada) falla con 23505, y mismo nombre en país distinto pasa
- [ ] Idempotencia probada: la migración corre dos veces sin error
- [ ] Rollback probado en local: restaura índice viejo y no pierde datos
entity_join_requests + RPC tick (Wave 1) — ALTO RIESGO, manos OpusFiles:
- Create: supabase/migrations/131_entity_join_requests.sql (+ rollback)
Done when:
- [ ] Tabla con estados, token, expiración a 10 días y RLS deny-by-default (acceso vía service role, patrón icg_contacts)
- [ ] RPC de transición atómica probada con psql: dos aprobaciones concurrentes → solo una gana
- [ ] RPC tick (expiración + recordatorio día 5) probada con filas de prueba retro-fechadas y limpiadas después
- [ ] Idempotencia y rollback probados
Files:
- Create: supabase/migrations/132_entity_code.sql (+ rollback)
Done when:
- [ ] catalogo_codigo_tipo_entidad creada y sembrada con las 43 filas del §5.2; codigo_corto UNIQUE
- [ ] Columna entity_code (UNIQUE, nullable) en startups e icgs + tabla entidad_codigo_counters + RPC consecutivo atómica (réplica del patrón de eventos)
- [ ] Backfill completo: 100% de entidades formalizadas con código, orden por formalized_at asc verificado con psql (muestra de 10 antiguas/nuevas); borradores en NULL
- [ ] CHECK de action_audit_log reescrito con los 5 action_types nuevos; INSERT de prueba de cada tipo pasa (y se limpia)
- [ ] Idempotencia y rollback probados
Files:
- Create: src/routes/api/entidades/similares/+server.ts (nombre final según convención del repo)
- Create: tests del enmascarado y del matching
Done when:
- [ ] Responde exactos + aproximados cruzando startups/ICGs con los campos de la tarjeta §3.2
- [ ] Correos enmascarados EN EL SERVIDOR (test: la respuesta JSON nunca contiene un correo completo) y rótulo "Admin en el sistema"
- [ ] Solo entidades formalizadas y no eliminadas; rate-limit/min-length para no ser un oráculo de scraping
- [ ] npm run test y npm run lint sin errores nuevos
Files:
- Create: src/lib/server/join-requests.ts + renders de correo (script de render separado, patrón del repo)
- Create: src/routes/api/cron/join-request-tick/+server.ts (patrón entity-transfer-tick, protegido por CRON_SECRET)
- Edit: vercel.json (cron nuevo — recordar: los crons de Vercel disparan por GET)
Done when:
- [ ] Los 5 correos de la matriz §4.4 se envían a las partes correctas (test por evento: creada→2, aprobada→2, rechazada→2, expirada→2, recordatorio→1)
- [ ] Aprobación crea la fila en startup_founders (con role + can_edit_form elegidos) o icg_contacts (con position), verificada
- [ ] Solicitante no verificado (Opción B) recibe 403; auditoría registrada en cada transición
- [ ] Tests pasan; render de correos generado y revisado visualmente
Files:
- Edit: src/lib/server/entities.ts (formalizeEntity) + helper de composición del código
- Create: tests del compositor (TTT/PPP/AA/NNNNN, fallbacks OTR/000)
Done when:
- [ ] Toda formalización (público y dashboard) asigna código exactamente una vez; idempotente (segunda llamada no regenera)
- [ ] Fallbacks probados: ICG sin icg_role → OTR; sin país → 000; país legacy ISO2 → resuelve el prefijo correcto
- [ ] PPP replica el criterio de eventos (CO→057, PE→051) con test que lo compara contra getPhonePrefix3
- [ ] Tests y lint sin errores nuevos
Files: - Create: componente de tarjetas de coincidencia (consumiendo el DS) - Edit: FSS/FSD/FIS/FID (los 4 shells de formulario)
Done when:
- [ ] Al salir del campo nombre con coincidencias, aparece el panel §3.2 en los 4 formularios (capturas Playwright de cada uno)
- [ ] "Crear de todas formas" sobre coincidencia exacta deja el rastro de auditoría (verificado en action_audit_log con psql)
- [ ] "Solicitar asociación" crea la solicitud y muestra la confirmación
- [ ] UI consume el DS (sin estilos nuevos fuera de tema); build sin errores
Files:
- Edit: /dashboard/perfil-startup y /dashboard/perfil-icg (sección de solicitudes pendientes + pantalla de decisión)
Done when: - [ ] El aprobador ve la tarjeta del solicitante y al aprobar asigna cargo + permisos (startup) o cargo (ICG) en la misma pantalla (capturas Playwright) - [ ] En ICG NO se ofrece permiso de edición (regla dura verificada en la UI y en el server) - [ ] Rechazo pide confirmación y notifica; estados vacíos correctos - [ ] Build y lint sin errores
Files:
- Edit: PS, PI y fichas de entidad (mostrar entity_code con acción copiar)
Done when: - [ ] Código visible en PS, PI y ficha; borradores muestran el estado "se asignará al formalizar" (capturas) - [ ] Sin regresiones visuales en los paneles (comparación antes/después)
Files:
- Edit: /dashboard/consulta/startups/+page.svelte (+ server) y /dashboard/consulta/icg/+page.svelte (+ server)
Done when:
- [ ] Columna "Código" tras el nombre, ordenable, — en vacíos, en ambas consultas (capturas Playwright)
- [ ] Reglas del DS intactas: Acciones sticky derecha con sombra y medición con skipClass sin romper (scroll horizontal verificado en viewport angosto)
- [ ] Build y lint sin errores
Done when: - [ ] Flujo real completo en preview, sin caveats pendientes: registro con nombre duplicado → tarjeta → solicitar asociación → correos a ambas partes → aprobar con cargo+permisos → miembro verificado visible; y el camino "crear de todas formas" → auditoría + código asignado al formalizar - [ ] Recordatorio día 5 y expiración día 10 probados con filas retro-fechadas (y limpiadas) - [ ] Smoke test de las páginas tocadas + deploy verificado por commit SHA (no solo READY) - [ ] Códigos en producción: muestra aleatoria de 10 entidades cumple el formato y la unicidad (psql)
AA del código = año de REGISTRO EN EL SISTEMA (fecha de formalización), no año de creación de la empresa. Justificación: (a) siempre se conoce y nunca cambia — el año de fundación puede ser desconocido, disputado o muy antiguo ("1987" en un identificador de sistema no aporta); (b) hace el código verificable contra la base (coincide con formalized_at); (c) es el mismo criterio del código de eventos (año del evento en el sistema); (d) el dato de fundación ni siquiera es un campo confiable/obligatorio hoy. Recomendación: año de registro. ¿Confirmas?NNNNN por año, compartido entre startups e ICGs (un solo contador anual, como eventos). Alternativa: contador separado por tipo. Recomendamos compartido: números más informativos ("cuántas entidades entraron este año") y el tipo ya está en TTT. ¿Confirmas?codigo_unico SERIAL de eventos (§5.6): viola la regla nueva (se asigna al guardar borrador). Recomendamos registrarla como deuda y corregirla en una tarea aparte (tocarla implica revisar los enlaces públicos de compartir eventos); no la incluimos en el alcance de la 67. ¿De acuerdo?Resumen: el v2 incorpora tus 10 respuestas y además repara un hueco que encontramos al revisarlas: el índice único actual quedó inoperante desde el modelo de formalización de julio, así que el rediseño por países que pediste es también la reparación de la protección base. El código único queda TTT-PPP-AA-NNNNN con prefijo telefónico (criterio de eventos), tipo granular extensible por catálogo (agregar subcategoría = 1 fila) y consecutivo a 5 dígitos, asignado solo al formalizar bajo la regla transversal nueva "nada en borrador recibe código". La aprobación de miembros vive en el PS/PI con cargo + permisos en una sola pantalla, con correos a ambas partes en cada hito. 13 tareas en 5 olas, migraciones aditivas y reversibles con snapshot, alto riesgo con manos Opus y verificación con evidencia. Con tu OK (y tu confirmación de los 6 puntos del §9) arrancamos por la Ola 0.