Update to Skynet v7 - Complete documentation restructure

- Nueva estructura de carpetas según Skynet v7
- Añadidos schemas SQL completos
- Documentación de entidades, componentes e integraciones
- Modelo de seguridad actualizado
- Infraestructura y operaciones reorganizadas
This commit is contained in:
ARCHITECT
2025-12-29 18:23:41 +00:00
parent ac481fe266
commit 6ea70bd34f
76 changed files with 13029 additions and 4340 deletions

View File

@@ -0,0 +1,347 @@
# Estructura CONTENEDOR
Esquema de datos completo para un registro en el sistema TZZR.
## Esquema JSON
```json
{
"id": "uuid-contenedor",
"h_instancia": "sha256-instancia",
"archivo_hash": "sha256-archivo",
"origen": {
"dispositivo": "uuid-dispositivo",
"app_version": "1.0.0",
"timestamp_captura": "2025-01-15T10:30:00Z",
"geolocalizacion": {
"lat": 41.3851,
"lon": 2.1734,
"precision_m": 10
}
},
"archivo": {
"tipo": "image/jpeg",
"categoria": "imagen",
"size_bytes": 2048576,
"nombre_original": "IMG_2024.jpg",
"dimensiones": {
"ancho": 1920,
"alto": 1080
},
"duracion_seg": null
},
"tags": [
{
"h_maestro": "sha256-tag",
"grupo": "hst",
"nombre": "Factura",
"confianza": 1.0
}
],
"extraccion": {
"servicio": "openai-vision",
"version": "gpt-4o",
"timestamp": "2025-01-15T10:30:05Z",
"coste_cents": 2,
"texto": "FACTURA N 2024-001...",
"resumen": "Factura de servicios por 1.500EUR",
"entidades": {
"fechas": ["2025-01-15"],
"importes": [{"valor": 1500.00, "moneda": "EUR"}],
"personas": [],
"organizaciones": ["Acme Corp"],
"ubicaciones": [],
"documentos": [{"tipo": "factura", "numero": "2024-001"}]
},
"clasificacion": {
"categoria": "finanzas",
"subcategoria": "factura_recibida",
"confianza": 0.95
},
"tags_sugeridos": [
{"h_maestro": "sha256", "nombre": "Factura", "confianza": 0.95}
],
"especifico": {}
},
"enriquecimiento": {
"notas": "Pendiente de pago",
"campos_personalizados": {
"proyecto": "Proyecto Alpha",
"responsable": "Juan Garcia"
},
"tags_confirmados": ["sha256-tag1", "sha256-tag2"],
"tags_rechazados": ["sha256-tag3"],
"correcciones": {
"texto": null,
"entidades": null
},
"editado_por": "usuario-id",
"editado_at": "2025-01-15T11:00:00Z"
},
"estado": {
"actual": "en_feldman_cola",
"historial": [
{"paso": "clara", "entrada": "...", "salida": "...", "procesado_por": "clara-service"},
{"paso": "mason", "entrada": "...", "salida": "...", "procesado_por": "usuario-id"},
{"paso": "feldman_cola", "entrada": "...", "procesado_por": "feldman-service"}
]
},
"bloque": {
"id": "sha256-bloque",
"numero": 42,
"registro_hash": "sha256-este-registro",
"merkle_proof": ["sha256-1", "sha256-2", "sha256-3"]
}
}
```
---
## Secciones
### IDENTIFICACION
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `id` | UUID | Identificador unico del contenedor, generado por PACKET |
| `h_instancia` | SHA-256 | Identifica servidor DECK/CORP de origen |
| `archivo_hash` | SHA-256 | Hash del archivo, sirve como ruta en R2 |
**Ruta R2**: `{endpoint}/{archivo_hash}`
---
### ORIGEN
Informacion del momento de captura. Generada automaticamente por PACKET.
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `dispositivo` | UUID | ID del dispositivo que capturo |
| `app_version` | String | Version de PACKET |
| `timestamp_captura` | ISO 8601 | Momento exacto de captura |
| `geolocalizacion` | Object | Ubicacion GPS (opcional) |
| `geolocalizacion.lat` | Float | Latitud |
| `geolocalizacion.lon` | Float | Longitud |
| `geolocalizacion.precision_m` | Int | Precision en metros |
**Inmutable**: Esta seccion nunca se modifica despues de la captura.
---
### ARCHIVO
Metadata tecnica del archivo capturado.
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `tipo` | String | MIME type (image/jpeg, audio/mp3, etc.) |
| `categoria` | Enum | imagen, audio, video, documento |
| `size_bytes` | Int | Tamano en bytes |
| `nombre_original` | String | Nombre original del archivo (opcional) |
| `dimensiones.ancho` | Int | Ancho en pixels (imagen/video) |
| `dimensiones.alto` | Int | Alto en pixels (imagen/video) |
| `duracion_seg` | Float | Duracion en segundos (audio/video) |
**Inmutable**: Esta seccion nunca se modifica.
---
### TAGS
Etiquetas aplicadas por el usuario en PACKET al momento de captura.
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `h_maestro` | SHA-256 | Referencia a la biblioteca de tags |
| `grupo` | Enum | hst, emp, hsu, pjt, sys |
| `nombre` | String | Nombre visible del tag |
| `confianza` | Float | 1.0 = usuario, <1.0 = sugerido por IA |
**Grupos de tags**:
- `hst`: Biblioteca publica HST
- `emp`: Biblioteca de empresa
- `hsu`: Biblioteca personal de usuario
- `pjt`: Biblioteca de proyecto
- `sys`: Tags de sistema (automaticos)
**Mutable**: Se pueden anadir tags en MASON.
---
### EXTRACCION
Resultado del servicio de extraccion (OCR, transcripcion, analisis IA).
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `servicio` | String | Identificador del servicio usado |
| `version` | String | Version del modelo/servicio |
| `timestamp` | ISO 8601 | Momento de la extraccion |
| `coste_cents` | Int | Coste en centavos (obligatorio para auditoria) |
| `texto` | String | Contenido textual extraido |
| `resumen` | String | Resumen generado por IA |
| `entidades` | Object | Datos estructurados detectados |
| `clasificacion` | Object | Categoria sugerida automaticamente |
| `tags_sugeridos` | Array | Tags de biblioteca que podrian aplicar |
| `especifico` | Object | Campos segun tipo de archivo |
#### Entidades detectadas
```json
{
"fechas": ["2025-01-15"],
"importes": [{"valor": 1500.00, "moneda": "EUR"}],
"personas": ["Juan Garcia"],
"organizaciones": ["Acme Corp"],
"ubicaciones": ["Barcelona"],
"documentos": [{"tipo": "factura", "numero": "2024-001"}]
}
```
#### Campos especificos por tipo
**Imagen**:
```json
{
"descripcion_visual": "Documento escaneado...",
"objetos_detectados": ["documento", "texto", "logo"],
"texto_ocr_bruto": "..."
}
```
**Audio**:
```json
{
"transcripcion": "...",
"idioma_detectado": "es",
"hablantes": 2
}
```
**Video**:
```json
{
"transcripcion": "...",
"escenas": [...]
}
```
**Documento (PDF)**:
```json
{
"paginas": 3,
"texto_por_pagina": ["...", "...", "..."]
}
```
**Inmutable**: Esta seccion no se modifica una vez generada.
---
### ENRIQUECIMIENTO
Lo que el usuario anade o modifica en MASON.
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `notas` | String | Texto libre del usuario |
| `campos_personalizados` | Object | Campos key-value definidos por usuario |
| `tags_confirmados` | Array | h_maestro de tags sugeridos aceptados |
| `tags_rechazados` | Array | h_maestro de tags sugeridos descartados |
| `correcciones.texto` | String | Texto corregido si OCR fallo |
| `correcciones.entidades` | Object | Entidades corregidas |
| `editado_por` | String | ID del usuario que edito |
| `editado_at` | ISO 8601 | Ultima edicion |
**Mutable**: Hasta que se consolida en FELDMAN.
---
### ESTADO
Trazabilidad del registro a traves del sistema.
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `actual` | String | Ubicacion actual del registro |
| `historial` | Array | Pasos por los que ha pasado |
| `historial[].paso` | String | Nombre del paso (clara, mason, feldman_cola, feldman_bloque) |
| `historial[].entrada` | ISO 8601 | Cuando entro |
| `historial[].salida` | ISO 8601 | Cuando salio |
| `historial[].procesado_por` | String | Servicio o usuario que proceso |
**Estados posibles**:
- `en_clara` / `en_margaret`: Recien capturado
- `en_mason`: En enriquecimiento
- `en_feldman_cola`: Esperando consolidacion
- `en_feldman_bloque`: Consolidado (inmutable)
---
### BLOQUE
Solo se rellena cuando FELDMAN consolida el registro.
| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `id` | SHA-256 | Hash del bloque |
| `numero` | Int | Numero secuencial del bloque |
| `registro_hash` | SHA-256 | Hash de este registro |
| `merkle_proof` | Array | Prueba de inclusion en el arbol Merkle |
**Inmutable**: Una vez consolidado, nunca cambia.
---
## Mutabilidad por Seccion
| Seccion | Quien escribe | Inmutable |
|---------|---------------|-----------|
| identificacion | PACKET | Si |
| origen | PACKET | Si |
| archivo | PACKET | Si |
| tags | PACKET (usuario) | No (se puede anadir) |
| extraccion | Servicio externo | Si |
| enriquecimiento | Usuario en MASON | No (hasta consolidar) |
| estado | Sistema | Se acumula |
| bloque | FELDMAN | Si (al consolidar) |
---
## Flujo de datos
```
PACKET genera:
- identificacion
- origen
- archivo
- tags (iniciales)
CLARA/MARGARET registra:
- estado.historial += {paso: "clara/margaret"}
Servicio extraccion anade:
- extraccion (completa)
MASON permite editar:
- enriquecimiento
- tags (anadir)
- estado.historial += {paso: "mason"}
FELDMAN consolida:
- bloque (completo)
- estado.actual = "en_feldman_bloque"
- estado.historial += {paso: "feldman_bloque"}
```
---
*Actualizado: 2025-12-22*

View File

@@ -0,0 +1,262 @@
# Tablas de Contexto
**Estado:** Implementado
**Base de datos:** PostgreSQL (ARCHITECT)
---
## Descripción
Tablas que proporcionan contexto a las instancias Claude.
---
## Diagrama de Relaciones
```
instancias ──┬── memoria
└── conversaciones ──── mensajes_v2
└── memoria (conversacion_origen)
conocimiento (independiente, compartido)
contexto_ambiental (independiente, periódico)
```
---
## instancias
Identidad de cada instancia Claude.
```sql
CREATE TABLE instancias (
id VARCHAR(50) PRIMARY KEY,
nombre VARCHAR(100),
system_prompt TEXT NOT NULL,
personalidad JSONB DEFAULT '{
"tono": "profesional",
"idioma": "es",
"verbosidad": "conciso"
}',
permisos JSONB DEFAULT '{
"puede_ejecutar_bash": false,
"puede_acceder_red": false,
"puede_modificar_archivos": false,
"servidores_permitidos": []
}',
modelo VARCHAR(50) DEFAULT 'sonnet',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**Uso:** Define quién es cada Claude, su personalidad, y qué puede hacer.
### Instancias Activas
| ID | Nombre | Modelo | Servidor |
|----|--------|--------|----------|
| architect | Architect | sonnet | ARCHITECT |
| hst | HST | sonnet | HST |
| deck | Deck | sonnet | DECK |
| corp | Corp | sonnet | CORP |
| runpod | Runpod | sonnet | RunPod |
| locker | Locker | haiku | R2/Storage |
---
## memoria
Memoria a largo plazo por instancia.
```sql
CREATE TABLE memoria (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
instancia_id VARCHAR(50) REFERENCES instancias(id),
tipo VARCHAR(50) NOT NULL,
contenido TEXT NOT NULL,
importancia INT DEFAULT 5,
usos INT DEFAULT 0,
ultimo_uso TIMESTAMP,
conversacion_origen UUID REFERENCES conversaciones(id),
expira_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_memoria_instancia ON memoria(instancia_id, importancia DESC);
CREATE INDEX idx_memoria_tipo ON memoria(instancia_id, tipo);
```
### Tipos de Memoria
| Tipo | Descripción |
|------|-------------|
| preferencia | Preferencias del usuario |
| hecho | Hechos aprendidos |
| decision | Decisiones tomadas |
| aprendizaje | Lecciones aprendidas |
| procedimiento | Procedimientos aprendidos |
---
## conocimiento
Base de conocimiento compartida (RAG).
```sql
CREATE TABLE conocimiento (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
categoria VARCHAR(50) NOT NULL,
titulo VARCHAR(200),
contenido TEXT NOT NULL,
tags TEXT[],
instancias_permitidas VARCHAR(50)[],
prioridad INT DEFAULT 0,
fuente VARCHAR(200),
hash_contenido VARCHAR(64),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_conocimiento_categoria ON conocimiento(categoria);
CREATE INDEX idx_conocimiento_prioridad ON conocimiento(prioridad DESC);
CREATE INDEX idx_conocimiento_tags ON conocimiento USING GIN(tags);
```
### Categorías
| Categoría | Descripción |
|-----------|-------------|
| infraestructura | Documentación de infra |
| proyecto | Información de proyectos |
| personal | Datos personales |
| procedimiento | Procedimientos operativos |
---
## conversaciones
Sesiones de chat.
```sql
CREATE TABLE conversaciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
instancia_id VARCHAR(50) REFERENCES instancias(id),
usuario VARCHAR(50) NOT NULL,
titulo VARCHAR(200),
activa BOOLEAN DEFAULT TRUE,
total_tokens INT DEFAULT 0,
total_mensajes INT DEFAULT 0,
resumen TEXT,
resumen_updated_at TIMESTAMP,
contexto_ambiental JSONB DEFAULT '{
"proyecto_activo": null,
"archivos_abiertos": [],
"ultimo_comando": null,
"hora_local": null
}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_conversaciones_instancia ON conversaciones(instancia_id, activa);
CREATE INDEX idx_conversaciones_usuario ON conversaciones(usuario, activa);
```
---
## mensajes_v2
Mensajes con soporte para compactación.
```sql
CREATE TABLE mensajes_v2 (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversacion_id UUID REFERENCES conversaciones(id),
role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
contenido TEXT NOT NULL,
archivos JSONB DEFAULT '[]',
tokens_estimados INT,
compactado BOOLEAN DEFAULT FALSE,
resumen_compactado TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_mensajes_v2_conversacion ON mensajes_v2(conversacion_id, created_at);
CREATE INDEX idx_mensajes_v2_no_compactados ON mensajes_v2(conversacion_id, compactado)
WHERE compactado = FALSE;
```
**Nota:** Los mensajes antiguos se compactan (resumen) para ahorrar tokens.
---
## contexto_ambiental
Estado actual del sistema (captura periódica).
```sql
CREATE TABLE contexto_ambiental (
id SERIAL PRIMARY KEY,
capturado_at TIMESTAMP DEFAULT NOW(),
servidores JSONB,
servicios JSONB,
tareas_pendientes JSONB,
alertas JSONB,
git_estado JSONB,
expira_at TIMESTAMP DEFAULT NOW() + INTERVAL '1 hour'
);
CREATE INDEX idx_contexto_ambiental_tiempo ON contexto_ambiental(capturado_at DESC);
```
### Estructura JSONB
```json
{
"servidores": {
"architect": {"status": "online", "uptime": "5d"},
"deck": {"status": "online", "uptime": "3d"}
},
"servicios": {
"gitea": "running",
"windmill": "running",
"directus": "running"
},
"tareas_pendientes": [
{"id": 1, "descripcion": "...", "prioridad": "alta"}
],
"alertas": [
{"tipo": "warning", "mensaje": "...", "severidad": "medium"}
],
"git_estado": {
"branch": "main",
"commits_ahead": 0,
"uncommitted_changes": false
}
}
```
---
## Flujo de Contexto
Cuando Claude recibe un mensaje:
```
1. System Prompt ← instancias.system_prompt
2. Personalidad ← instancias.personalidad
3. Memorias ← memoria WHERE instancia_id = X
ORDER BY importancia DESC LIMIT 20
4. Conocimiento ← conocimiento WHERE instancias_permitidas
IS NULL OR X = ANY(instancias_permitidas)
5. Contexto Ambiental ← contexto_ambiental
ORDER BY capturado_at DESC LIMIT 1
6. Historial ← mensajes_v2 WHERE compactado = FALSE
ORDER BY created_at
7. Resumen ← conversaciones.resumen (mensajes compactados)
```

View File

@@ -0,0 +1,139 @@
# Entidades
**Versión:** 1.0
**Estado:** Definición
---
## Visión General
Las entidades base son los bloques fundamentales del sistema. Cada una tiene un hash único SHA-256 que la identifica de forma unívoca.
```
DEFINICIÓN ORIGINAL → SHA-256 → h_{tipo} (64 chars)
```
---
## Entidades del Sistema
| Entidad | Hash | Descripción | Estado |
|---------|------|-------------|--------|
| **HST** | h_maestro | Tags semánticos | Implementado |
| **ITM** | h_item | Plano ideal | Planificado |
| **PLY** | h_player | Identidad | Planificado |
| **LOC** | h_loc | Ubicaciones | Planificado |
| **FLG** | h_flag | Marcos jurídicos | Planificado |
---
## HST (Hash Semantic Tagging)
Sistema de etiquetas semánticas visuales.
### Fórmula
```
h_maestro = SHA-256(grupo:ref)
```
### Grupos
| Grupo | Cantidad |
|-------|----------|
| hst | 639 |
| spe | 145 |
| vsn | 84 |
| flg | 65 |
| vue | 21 |
---
## ITM (Items)
Plano ideal - definiciones abstractas.
### Tipos
| Tipo | Descripción |
|------|-------------|
| accion_ideal | Acción que debería ejecutarse |
| objetivo | Meta a alcanzar |
| requerimiento | Requisito a cumplir |
---
## PLY (Players)
Identidad de actores en el sistema.
### Tipos
| Tipo | Descripción |
|------|-------------|
| persona | Usuario individual |
| empresa | Organización |
| agente | Sistema automatizado |
---
## LOC (Locations)
Ubicaciones geográficas.
### Tipos
| Tipo | Descripción |
|------|-------------|
| punto | Coordenadas exactas |
| area | Zona delimitada |
| ruta | Trayecto |
---
## FLG (Flags)
Marcos jurídicos y normativas.
### Uso
- Países
- Normativas (RGPD, SOX, ISO)
- Jurisdicciones
---
## Relaciones Entre Entidades
```
ITM (ideal)
├──► h_maestro (HST tags)
├──► h_player (PLY responsable)
├──► h_loc (LOC ubicación)
└──► h_flag (FLG normativa)
MST (milestone)
├──► item_asociado (ITM)
└──► h_maestro (HST tags)
BCK (bloque)
├──► item_asociado (ITM)
├──► milestone_asociado (MST)
└──► h_maestro (HST tags)
```
---
## Extensiones de Usuario
| Tabla | Descripción |
|-------|-------------|
| hsu | HST Usuario |
| spu | SPE Usuario |
| vsu | VSN Usuario |
| vuu | VUE Usuario |
| pju | Proyectos Usuario |
| flu | FLG Usuario |

188
03_MODELO_DATOS/flujos.md Normal file
View File

@@ -0,0 +1,188 @@
# Flujos
**Versión:** 1.0
**Estado:** Definición
---
## Visión General
El sistema tiene tres flujos principales según la naturaleza de la información entrante.
---
## Flujo Estándar (entrada manual)
**Condición:** La información no encaja (entrada manual, incidencia, improvisación). Requiere enriquecimiento o validación manual.
```
Secretaría (Clara/Margaret)
│ registro inmutable
Administración (Mason)
│ enriquecimiento (24h)
Contable (Feldman)
│ consolidación
Inmutable
```
---
## Flujo de Producción (procesos predefinidos)
**Condición:** La información encaja (viene de Producción, proceso predefinido completo). La información ya está completa y estructurada.
```
Producción (Alfred/Jared)
│ flujo predefinido
Secretaría (Clara/Margaret)
│ registro inmutable
Contable (Feldman)
│ consolidación directa
Inmutable
```
**Nota:** Este flujo salta Administración porque no hay nada que enriquecer.
---
## Flujo con Incidencia
**Condición:** Durante un flujo de producción, el usuario improvisa o se produce un cambio.
**Comportamiento:** La improvisación marca el punto de quiebre. Todo lo anterior registrado se mantiene, pero lo que viene después requiere el paso por Administración.
```
Producción (Alfred/Jared)
│ flujo predefinido
Secretaría (Clara/Margaret)
│ ⚠️ incidencia detectada
Administración (Mason)
│ enriquecimiento
Contable (Feldman)
```
---
## Regla de Decisión
| Condición | Flujo |
|-----------|-------|
| **Encaja** | Secretaría → Feldman |
| **No encaja** | Secretaría → Mason → Feldman |
---
## Diagrama de Decisión
```
┌─────────────────┐
│ Entrada │
└────────┬────────┘
┌────────▼────────┐
│ ¿Viene de │
│ Producción? │
└────────┬────────┘
┌────────┴────────┐
│ │
SÍ NO
│ │
┌────────▼────────┐ │
│ ¿Encaja │ │
│ completo? │ │
└────────┬────────┘ │
│ │
┌────────┴────────┐ │
│ │ │
SÍ NO │
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────────────┐
│ Directo │ │ Mason │
│ Feldman │ │ (enriquecer) │
└──────────┘ └──────────────────┘
```
---
## Mecanismo de "Encaja"
**Pendiente de definir:** Sistema de hashes que determina automáticamente si la información encaja con un flujo predefinido.
Concepto propuesto:
```python
def encaja(entrada, flujo_esperado):
# Comparar estructura de datos
# Verificar campos requeridos
# Validar tipos
return estructura_coincide and campos_completos
```
---
## Estados del Flujo
| Estado | Ubicación | Descripción |
|--------|-----------|-------------|
| **recibido** | Secretaría | Entrada registrada |
| **en_edicion** | Mason | Usuario enriqueciendo |
| **listo** | Mason | Preparado para Feldman |
| **pendiente** | Feldman cola | En espera de consolidación |
| **consolidado** | Feldman bloques | Registro final inmutable |
---
## Estructura de Feldman
Feldman tiene **dos tablas internas**:
```
┌─────────────────────────────────────────────────────────────────┐
│ FELDMAN │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ COLA (24h configurable) │ │
│ │ • Registros esperando consolidación │ │
│ │ • Usuario puede: DEVOLVER a Mason │ │
│ │ • Si expira → pasa a BLOQUE │ │
│ └─────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ BLOQUES INMUTABLES │ │
│ │ GÉNESIS ─▶ BLOQUE 1 ─▶ BLOQUE 2 ─▶ ... │ │
│ │ (hash anterior, merkle root, timestamp) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## Ventanas Temporales
| Etapa | Default | Configurable | Descripción |
|-------|---------|--------------|-------------|
| Mason | 24h | ✓ | Tiempo para enriquecer antes de auto-envío |
| Feldman cola | 24h | ✓ | Tiempo en cola antes de cerrar en bloque |

134
03_MODELO_DATOS/hashes.md Normal file
View File

@@ -0,0 +1,134 @@
# Hashes e Identificadores
**Versión:** 1.0
**Estado:** Definición
---
## Visión General
El sistema usa SHA-256 para identificación única e inmutable de todos los elementos.
```
CONTENIDO → SHA-256 → 64 caracteres hexadecimales
```
---
## Tipos de Hash
| Prefijo | Tipo | Descripción |
|---------|------|-------------|
| **h_maestro** | Tag HST | Hash de etiqueta semántica |
| **h_instancia** | Contexto | Identidad del servidor/instancia |
| **h_entrada** | Contenedor | Hash de ingesta en secretaría |
| **h_bloque** | BCK | Hash de bloque consolidado |
| **h_milestone** | MST | Hash de milestone |
| **h_item** | ITM | Hash de ítem (plano ideal) |
| **h_player** | PLY | Hash de identidad |
| **h_loc** | LOC | Hash de ubicación |
| **h_flag** | FLG | Hash de marco jurídico |
---
## Fórmulas
### h_maestro (HST)
```
h_maestro = SHA-256(grupo:ref)
Ejemplo:
h_maestro = SHA-256("hst:person")
```
### h_entrada (Secretaría)
```
h_entrada = SHA-256(timestamp:origen:contenido)
```
### h_bloque (BCK)
```
h_bloque = SHA-256(h_instancia:secuencia:hash_previo:hash_contenido)
hash_contenido = SHA-256(contenido_serializado)
```
### h_milestone (MST)
```
h_milestone = SHA-256(h_instancia:secuencia:tipo:contenido)
```
### h_item (ITM)
```
h_item = SHA-256(h_instancia:secuencia:tipo:contenido)
```
### h_loc (LOC)
```
h_loc = SHA-256(lat:lon:precision)
```
---
## Encadenamiento
```
Bloque 1 Bloque 2 Bloque 3
┌────────────┐ ┌────────────┐ ┌────────────┐
│h_bloque: A │──────►│hash_prev: A│──────►│hash_prev: B│
│ │ │h_bloque: B │ │h_bloque: C │
└────────────┘ └────────────┘ └────────────┘
```
---
## Verificación
### Integridad de Contenido
```python
import hashlib
import json
def verificar_integridad(bloque):
contenido_serializado = json.dumps(bloque['contenido'], sort_keys=True)
hash_calculado = hashlib.sha256(contenido_serializado.encode()).hexdigest()
return hash_calculado == bloque['hash_contenido']
```
### Encadenamiento
```python
def verificar_cadena(bloque, bloque_previo):
return bloque['hash_previo'] == bloque_previo['h_bloque']
```
---
## Propiedades
| Propiedad | Descripción |
|-----------|-------------|
| **Determinista** | Mismo input → mismo hash |
| **Único** | Colisión prácticamente imposible |
| **Irreversible** | No se puede obtener contenido desde hash |
| **Fijo** | Siempre 64 caracteres |
---
## Uso en Trazabilidad
```
Bloque (Feldman)
└── h_entrada ──► Contenedor (Secretaría)
└── archivos_hashes ──► R2 Storage
```
Cualquier registro final puede rastrearse hasta su origen.

141
03_MODELO_DATOS/planos.md Normal file
View File

@@ -0,0 +1,141 @@
# Planos de Datos
**Versión:** 1.0
**Estado:** Definición
---
## Visión General
El sistema utiliza tres planos conceptuales para organizar la información:
```
┌─────────────────────────────────────────────────────────────────┐
│ T0 - PLANO IDEAL │
│ ITM (Items): Objetivos, acciones ideales, requerimientos │
└─────────────────────────────────────────────────────────────────┘
▼ materializa
┌─────────────────────────────────────────────────────────────────┐
│ MST - MILESTONES │
│ Compromisos futuros con fecha de vencimiento │
└─────────────────────────────────────────────────────────────────┘
▼ cumple/genera
┌─────────────────────────────────────────────────────────────────┐
│ BCK - BLOQUES │
│ Hechos inmutables del pasado (blockchain-ready) │
└─────────────────────────────────────────────────────────────────┘
```
---
## T0 - Plano Ideal (ITM)
### Concepto
El plano T0 contiene los Items que representan el estado ideal futuro. Son la "partitura" que guía las acciones.
### Naturaleza
| Aspecto | Valor |
|---------|-------|
| Temporalidad | T-N → T-1 → T0 |
| Energía | No consume (es definición) |
| Mutabilidad | Versionable |
### Estado
**PLANIFICADO** - Schema definido pero sin tablas creadas.
---
## MST - Milestones
### Concepto
Compromisos con fecha futura. Tienen un período flotante de 24 horas antes de consolidarse.
### Tipos
| Tipo | Descripción |
|------|-------------|
| compromiso | Compromiso de entrega |
| deadline | Fecha límite |
| evento | Evento programado |
| recordatorio | Recordatorio futuro |
### Período Flotante
| Parámetro | Valor |
|-----------|-------|
| Duración | 24 horas |
| Durante | Modificable vía Mason |
| Después | Inmutable |
### Estado
**IMPLEMENTADO** en CORP.
---
## BCK - Bloques
### Concepto
Registros inmutables de hechos pasados. Cada bloque está encadenado al anterior mediante hash.
### Tipos
| Tipo | Descripción |
|------|-------------|
| transaccion | Transacción económica |
| documento | Documento consolidado |
| evento | Evento registrado |
| milestone_cumplido | Milestone que se cumplió |
### Encadenamiento
```
Bloque N-1 Bloque N
┌──────────────────┐ ┌──────────────────┐
│ h_bloque: abc123 │ ──────► │ hash_previo: │
│ hash_contenido: │ │ abc123 │
│ def456 │ │ h_bloque: ghi789 │
│ secuencia: 1 │ │ secuencia: 2 │
└──────────────────┘ └──────────────────┘
```
### Estado
**IMPLEMENTADO** en CORP.
---
## Flujo Entre Planos
```
Usuario crea ITM
ITM (T0) "Objetivo: Completar proyecto X"
│ materializa
MST "Milestone: Entrega módulo 1 - 2024-01-15"
│ [período flotante 24h]
MASON (enriquecimiento)
FELDMAN (validación)
│ cumple/genera
BCK "Bloque: Módulo 1 entregado - 2024-01-14"
Inmutable ✓
```

View File

@@ -0,0 +1,101 @@
-- ============================================
-- TIPOS ENUMERADOS COMUNES
-- Sistema TZZR - contratos-comunes
-- ============================================
-- Aplicar primero antes de cualquier otro schema
-- Estados de tarea
DO $$ BEGIN
CREATE TYPE task_status AS ENUM (
'draft', -- Borrador, no iniciada
'pending', -- Pendiente de inicio
'in_progress', -- En progreso
'blocked', -- Bloqueada por dependencia
'review', -- En revisión
'completed', -- Completada
'cancelled' -- Cancelada
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Prioridad de tarea
DO $$ BEGIN
CREATE TYPE task_priority AS ENUM (
'critical', -- Crítica, atención inmediata
'high', -- Alta prioridad
'medium', -- Media (default)
'low', -- Baja prioridad
'someday' -- Algún día / sin fecha
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Dirección de archivo en work log
DO $$ BEGIN
CREATE TYPE file_direction AS ENUM (
'inbound', -- Archivo recibido
'outbound', -- Archivo enviado
'internal', -- Archivo interno/generado
'reference' -- Archivo de referencia
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Servicios de IA del ecosistema
DO $$ BEGIN
CREATE TYPE ai_service AS ENUM (
'grace', -- GRACE - Capa de procesamiento
'penny', -- PENNY - Asistente de voz
'factory' -- THE FACTORY - Procesamiento documental
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Grupos de etiquetas HST
DO $$ BEGIN
CREATE TYPE hst_grupo AS ENUM (
'hst', -- Sistema (sync tzrtech.org)
'emp', -- Empresa
'hsu', -- Usuario
'pjt' -- Proyecto
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Modo de despliegue S-CONTRACT
DO $$ BEGIN
CREATE TYPE deployment_mode AS ENUM (
'EXTERNAL', -- Solo APIs externas
'SELF_HOSTED', -- Solo infraestructura propia
'SEMI' -- Híbrido con fallback
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tier de proveedor
DO $$ BEGIN
CREATE TYPE provider_tier AS ENUM (
'SELF_HOSTED', -- Infraestructura propia
'EXTERNAL' -- API externa
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- ============================================
-- FUNCIÓN: Actualizar updated_at automáticamente
-- ============================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';

View File

@@ -0,0 +1,227 @@
-- ============================================
-- TABLAS HST (Sistema de Etiquetas)
-- Sistema TZZR - contratos-comunes
-- ============================================
-- Requiere: 00_types.sql
--
-- SISTEMA DUAL DE HASHES:
-- ┌─────────────────────────────────────────────────────────────┐
-- │ h_maestro = SHA-256(grupo || ':' || ref) │
-- │ → Identidad SEMÁNTICA (determinista, para S-CONTRACT) │
-- │ → Ejemplo: SHA-256("hst:finanzas") = "a7b3c9..." │
-- │ │
-- │ mrf = SHA-256(bytes_imagen) │
-- │ → Identidad de ARCHIVO (para servir imágenes) │
-- │ → URL: https://tzrtech.org/{mrf}.png │
-- └─────────────────────────────────────────────────────────────┘
-- ============================================
-- TABLA BASE: hst_tags
-- Tabla unificada de etiquetas HST
-- ============================================
CREATE TABLE IF NOT EXISTS hst_tags (
id SERIAL PRIMARY KEY,
-- Identificadores duales
h_maestro VARCHAR(64) UNIQUE NOT NULL, -- SHA-256(grupo:ref), identidad semántica
ref VARCHAR(50) NOT NULL, -- Referencia corta (ej: "finanzas")
mrf VARCHAR(64), -- SHA-256(imagen_bytes), hash de imagen
-- Nombres
nombre VARCHAR(100) NOT NULL, -- Nombre legible en español
nombre_en VARCHAR(100), -- Nombre en inglés
descripcion TEXT,
-- Visual
imagen_url TEXT, -- URL completa (https://tzrtech.org/{mrf}.png)
color VARCHAR(7), -- Color hex (#RRGGBB)
icono VARCHAR(50), -- Nombre de icono (ej: lucide:tag)
-- Clasificación
grupo hst_grupo NOT NULL, -- hst, emp, hsu, pjt
categoria VARCHAR(100), -- Categoría dentro del grupo
-- Jerarquía
padre_h_maestro VARCHAR(64) REFERENCES hst_tags(h_maestro),
rootref VARCHAR(50), -- Ref del ancestro raíz
nivel INTEGER DEFAULT 0, -- Nivel de profundidad (0 = raíz)
path_h_maestros TEXT[], -- Array de ancestros para queries rápidas
-- Estado
activo BOOLEAN DEFAULT true,
visible BOOLEAN DEFAULT true, -- Visible en UI
-- Sync con tzrtech.org
source VARCHAR(50) DEFAULT 'local', -- local, tzrtech, empresa
source_id VARCHAR(100), -- ID en el sistema origen
synced_at TIMESTAMP, -- Última sincronización
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Constraints
CONSTRAINT unique_ref_grupo UNIQUE(ref, grupo)
);
-- Índices principales
CREATE INDEX IF NOT EXISTS idx_hst_tags_h_maestro ON hst_tags(h_maestro);
CREATE INDEX IF NOT EXISTS idx_hst_tags_ref ON hst_tags(ref);
CREATE INDEX IF NOT EXISTS idx_hst_tags_mrf ON hst_tags(mrf);
CREATE INDEX IF NOT EXISTS idx_hst_tags_grupo ON hst_tags(grupo);
CREATE INDEX IF NOT EXISTS idx_hst_tags_categoria ON hst_tags(categoria);
CREATE INDEX IF NOT EXISTS idx_hst_tags_padre ON hst_tags(padre_h_maestro);
CREATE INDEX IF NOT EXISTS idx_hst_tags_rootref ON hst_tags(rootref);
CREATE INDEX IF NOT EXISTS idx_hst_tags_activo ON hst_tags(activo);
CREATE INDEX IF NOT EXISTS idx_hst_tags_path ON hst_tags USING GIN(path_h_maestros);
-- Trigger para updated_at
DROP TRIGGER IF EXISTS update_hst_tags_updated_at ON hst_tags;
CREATE TRIGGER update_hst_tags_updated_at BEFORE UPDATE ON hst_tags
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- FUNCIÓN: Calcular path de ancestros
-- ============================================
CREATE OR REPLACE FUNCTION calculate_hst_path()
RETURNS TRIGGER AS $$
DECLARE
parent_path TEXT[];
BEGIN
IF NEW.padre_h_maestro IS NULL THEN
NEW.path_h_maestros := ARRAY[]::TEXT[];
NEW.nivel := 0;
ELSE
SELECT path_h_maestros, nivel + 1
INTO parent_path, NEW.nivel
FROM hst_tags
WHERE h_maestro = NEW.padre_h_maestro;
NEW.path_h_maestros := array_append(parent_path, NEW.padre_h_maestro);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_calculate_hst_path ON hst_tags;
CREATE TRIGGER trigger_calculate_hst_path
BEFORE INSERT OR UPDATE OF padre_h_maestro ON hst_tags
FOR EACH ROW EXECUTE FUNCTION calculate_hst_path();
-- ============================================
-- VISTAS POR GRUPO (Compatibilidad con v2)
-- ============================================
-- Vista: Etiquetas del sistema (HST)
CREATE OR REPLACE VIEW hst_tags_sistema AS
SELECT * FROM hst_tags WHERE grupo = 'hst';
-- Vista: Etiquetas de empresa (EMP)
CREATE OR REPLACE VIEW hst_tags_empresa AS
SELECT * FROM hst_tags WHERE grupo = 'emp';
-- Vista: Etiquetas de usuario (HSU)
CREATE OR REPLACE VIEW hst_tags_usuario AS
SELECT * FROM hst_tags WHERE grupo = 'hsu';
-- Vista: Etiquetas de proyecto (PJT)
CREATE OR REPLACE VIEW hst_tags_proyecto AS
SELECT * FROM hst_tags WHERE grupo = 'pjt';
-- ============================================
-- TABLA: hst_tag_relations
-- Relaciones entre etiquetas (sinónimos, relacionados)
-- ============================================
CREATE TABLE IF NOT EXISTS hst_tag_relations (
id SERIAL PRIMARY KEY,
tag_a_h_maestro VARCHAR(64) NOT NULL REFERENCES hst_tags(h_maestro),
tag_b_h_maestro VARCHAR(64) NOT NULL REFERENCES hst_tags(h_maestro),
-- Tipo de relación
relation_type VARCHAR(50) NOT NULL, -- synonym, related, opposite, child_of
strength DECIMAL(3,2) DEFAULT 1.0, -- Fuerza de la relación (0-1)
bidirectional BOOLEAN DEFAULT true,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_tag_relation UNIQUE(tag_a_h_maestro, tag_b_h_maestro, relation_type),
CONSTRAINT chk_different_tags CHECK (tag_a_h_maestro != tag_b_h_maestro)
);
CREATE INDEX IF NOT EXISTS idx_tag_relations_a ON hst_tag_relations(tag_a_h_maestro);
CREATE INDEX IF NOT EXISTS idx_tag_relations_b ON hst_tag_relations(tag_b_h_maestro);
CREATE INDEX IF NOT EXISTS idx_tag_relations_type ON hst_tag_relations(relation_type);
-- ============================================
-- FUNCIÓN: Generar h_maestro (DETERMINISTA)
-- SHA-256(grupo || ':' || ref)
-- ============================================
CREATE OR REPLACE FUNCTION generate_h_maestro(p_grupo TEXT, p_ref TEXT)
RETURNS VARCHAR(64) AS $$
BEGIN
-- DETERMINISTA: mismo input = mismo output SIEMPRE
RETURN encode(sha256((p_grupo || ':' || p_ref)::bytea), 'hex');
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- ============================================
-- FUNCIÓN: Generar imagen_url desde mrf
-- ============================================
CREATE OR REPLACE FUNCTION generate_imagen_url(p_mrf VARCHAR(64))
RETURNS TEXT AS $$
BEGIN
IF p_mrf IS NULL THEN
RETURN NULL;
END IF;
RETURN 'https://tzrtech.org/' || p_mrf || '.png';
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- ============================================
-- DATOS INICIALES: Categorías base
-- Nota: Los datos reales vienen de tzrtech.org/api/index.json
-- ============================================
INSERT INTO hst_tags (h_maestro, ref, nombre, nombre_en, grupo, categoria, color) VALUES
(generate_h_maestro('hst', 'trabajo'), 'trabajo', 'Trabajo', 'Work', 'hst', 'actividad', '#3498db'),
(generate_h_maestro('hst', 'personal'), 'personal', 'Personal', 'Personal', 'hst', 'actividad', '#2ecc71'),
(generate_h_maestro('hst', 'urgente'), 'urgente', 'Urgente', 'Urgent', 'hst', 'prioridad', '#e74c3c'),
(generate_h_maestro('hst', 'proyecto'), 'proyecto', 'Proyecto', 'Project', 'hst', 'organizacion', '#9b59b6'),
(generate_h_maestro('hst', 'factura'), 'factura', 'Factura', 'Invoice', 'hst', 'documento', '#f39c12'),
(generate_h_maestro('hst', 'contrato'), 'contrato', 'Contrato', 'Contract', 'hst', 'documento', '#1abc9c'),
(generate_h_maestro('hst', 'email'), 'email', 'Email', 'Email', 'hst', 'comunicacion', '#34495e'),
(generate_h_maestro('hst', 'reunion'), 'reunion', 'Reunión', 'Meeting', 'hst', 'evento', '#e67e22')
ON CONFLICT (h_maestro) DO NOTHING;
-- ============================================
-- VISTA: API JSON compatible con tzrtech.org
-- ============================================
CREATE OR REPLACE VIEW hst_api_index AS
SELECT
ref,
h_maestro,
mrf,
nombre AS nombre_es,
nombre_en,
grupo,
categoria,
color,
imagen_url,
padre_h_maestro,
rootref,
nivel,
activo
FROM hst_tags
WHERE activo = true
ORDER BY grupo, nivel, nombre;

View File

@@ -0,0 +1,290 @@
-- ============================================
-- GESTOR DE TAREAS
-- Sistema TZZR - contratos-comunes
-- ============================================
-- Requiere: 00_types.sql, 01_hst_tags.sql
-- ============================================
-- TABLA: task_projects
-- Proyectos que agrupan tareas
-- ============================================
CREATE TABLE IF NOT EXISTS task_projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
codigo VARCHAR(50) UNIQUE NOT NULL, -- Código corto (ej: DECK-2024)
nombre VARCHAR(255) NOT NULL,
descripcion TEXT,
-- Estado
activo BOOLEAN DEFAULT true,
archivado BOOLEAN DEFAULT false,
fecha_inicio DATE,
fecha_fin_estimada DATE,
fecha_fin_real DATE,
-- Etiquetas (array de h_maestro)
tags JSONB DEFAULT '[]',
-- Configuración de contexto por defecto
default_context_id UUID, -- FK a task_contexts (añadido después)
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_task_projects_codigo ON task_projects(codigo);
CREATE INDEX IF NOT EXISTS idx_task_projects_activo ON task_projects(activo);
CREATE INDEX IF NOT EXISTS idx_task_projects_tags ON task_projects USING GIN(tags);
DROP TRIGGER IF EXISTS update_task_projects_updated_at ON task_projects;
CREATE TRIGGER update_task_projects_updated_at BEFORE UPDATE ON task_projects
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- TABLA: task_milestones
-- Hitos dentro de un proyecto
-- ============================================
CREATE TABLE IF NOT EXISTS task_milestones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES task_projects(id) ON DELETE CASCADE,
codigo VARCHAR(50) NOT NULL, -- Código dentro del proyecto
nombre VARCHAR(255) NOT NULL,
descripcion TEXT,
-- Orden y jerarquía
orden INTEGER DEFAULT 0,
parent_milestone_id UUID REFERENCES task_milestones(id),
-- Fechas
fecha_objetivo DATE,
fecha_completado DATE,
-- Progreso (calculado automáticamente)
progreso_percent DECIMAL(5,2) DEFAULT 0,
-- Etiquetas
tags JSONB DEFAULT '[]',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_milestone_code UNIQUE(project_id, codigo)
);
CREATE INDEX IF NOT EXISTS idx_task_milestones_project ON task_milestones(project_id);
CREATE INDEX IF NOT EXISTS idx_task_milestones_parent ON task_milestones(parent_milestone_id);
CREATE INDEX IF NOT EXISTS idx_task_milestones_orden ON task_milestones(orden);
DROP TRIGGER IF EXISTS update_task_milestones_updated_at ON task_milestones;
CREATE TRIGGER update_task_milestones_updated_at BEFORE UPDATE ON task_milestones
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- TABLA: task_blocks
-- Bloques de trabajo dentro de milestones
-- ============================================
CREATE TABLE IF NOT EXISTS task_blocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
milestone_id UUID REFERENCES task_milestones(id) ON DELETE CASCADE,
codigo VARCHAR(50) NOT NULL,
nombre VARCHAR(255) NOT NULL,
descripcion TEXT,
-- Orden
orden INTEGER DEFAULT 0,
-- Estimación
horas_estimadas DECIMAL(6,2),
horas_reales DECIMAL(6,2),
-- Estado
status task_status DEFAULT 'pending',
-- Etiquetas
tags JSONB DEFAULT '[]',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_block_code UNIQUE(milestone_id, codigo)
);
CREATE INDEX IF NOT EXISTS idx_task_blocks_milestone ON task_blocks(milestone_id);
CREATE INDEX IF NOT EXISTS idx_task_blocks_status ON task_blocks(status);
DROP TRIGGER IF EXISTS update_task_blocks_updated_at ON task_blocks;
CREATE TRIGGER update_task_blocks_updated_at BEFORE UPDATE ON task_blocks
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- TABLA: task_manager
-- Tareas individuales
-- ============================================
CREATE TABLE IF NOT EXISTS task_manager (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Jerarquía
project_id UUID REFERENCES task_projects(id) ON DELETE SET NULL,
milestone_id UUID REFERENCES task_milestones(id) ON DELETE SET NULL,
block_id UUID REFERENCES task_blocks(id) ON DELETE SET NULL,
parent_task_id UUID REFERENCES task_manager(id) ON DELETE SET NULL,
-- Identificación
codigo VARCHAR(100), -- Código único generado
titulo VARCHAR(500) NOT NULL,
descripcion TEXT,
-- Estado y prioridad
status task_status DEFAULT 'pending',
priority task_priority DEFAULT 'medium',
-- Fechas
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
fecha_inicio DATE,
fecha_vencimiento DATE,
fecha_completado TIMESTAMP,
-- Estimación
horas_estimadas DECIMAL(6,2),
horas_reales DECIMAL(6,2),
-- Asignación
asignado_a VARCHAR(100), -- Usuario o servicio
creado_por VARCHAR(100) DEFAULT 'system',
-- Etiquetas HST (separadas por grupo para queries eficientes)
tags_hst JSONB DEFAULT '[]', -- h_maestros de grupo hst
tags_hsu JSONB DEFAULT '[]', -- h_maestros de grupo hsu
tags_emp JSONB DEFAULT '[]', -- h_maestros de grupo emp
tags_pjt JSONB DEFAULT '[]', -- h_maestros de grupo pjt
-- Contexto IA
context_id UUID, -- FK a task_contexts (añadido después)
-- Archivos relacionados (array de UUIDs de work_log)
archivos_ref JSONB DEFAULT '[]',
-- Trazabilidad S-CONTRACT
trace_id VARCHAR(64),
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_task_manager_project ON task_manager(project_id);
CREATE INDEX IF NOT EXISTS idx_task_manager_milestone ON task_manager(milestone_id);
CREATE INDEX IF NOT EXISTS idx_task_manager_block ON task_manager(block_id);
CREATE INDEX IF NOT EXISTS idx_task_manager_parent ON task_manager(parent_task_id);
CREATE INDEX IF NOT EXISTS idx_task_manager_status ON task_manager(status);
CREATE INDEX IF NOT EXISTS idx_task_manager_priority ON task_manager(priority);
CREATE INDEX IF NOT EXISTS idx_task_manager_fecha_venc ON task_manager(fecha_vencimiento);
CREATE INDEX IF NOT EXISTS idx_task_manager_asignado ON task_manager(asignado_a);
CREATE INDEX IF NOT EXISTS idx_task_manager_tags_hst ON task_manager USING GIN(tags_hst);
CREATE INDEX IF NOT EXISTS idx_task_manager_tags_hsu ON task_manager USING GIN(tags_hsu);
CREATE INDEX IF NOT EXISTS idx_task_manager_trace ON task_manager(trace_id);
DROP TRIGGER IF EXISTS update_task_manager_updated_at ON task_manager;
CREATE TRIGGER update_task_manager_updated_at BEFORE UPDATE ON task_manager
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- TABLA: task_dependencies
-- Dependencias entre tareas
-- ============================================
CREATE TABLE IF NOT EXISTS task_dependencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES task_manager(id) ON DELETE CASCADE,
depends_on_task_id UUID NOT NULL REFERENCES task_manager(id) ON DELETE CASCADE,
-- Tipo de dependencia
tipo VARCHAR(50) DEFAULT 'finish_to_start',
-- finish_to_start: B empieza cuando A termina
-- start_to_start: B empieza cuando A empieza
-- finish_to_finish: B termina cuando A termina
-- start_to_finish: B termina cuando A empieza
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_dependency UNIQUE(task_id, depends_on_task_id),
CONSTRAINT chk_no_self_dep CHECK (task_id != depends_on_task_id)
);
CREATE INDEX IF NOT EXISTS idx_task_deps_task ON task_dependencies(task_id);
CREATE INDEX IF NOT EXISTS idx_task_deps_depends ON task_dependencies(depends_on_task_id);
-- ============================================
-- FUNCIÓN: Actualizar progreso de milestone
-- ============================================
CREATE OR REPLACE FUNCTION update_milestone_progress()
RETURNS TRIGGER AS $$
BEGIN
UPDATE task_milestones
SET progreso_percent = (
SELECT COALESCE(
(COUNT(*) FILTER (WHERE status = 'completed')::DECIMAL /
NULLIF(COUNT(*), 0) * 100),
0
)
FROM task_manager
WHERE milestone_id = COALESCE(NEW.milestone_id, OLD.milestone_id)
),
updated_at = CURRENT_TIMESTAMP
WHERE id = COALESCE(NEW.milestone_id, OLD.milestone_id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_update_milestone_progress ON task_manager;
CREATE TRIGGER trigger_update_milestone_progress
AFTER INSERT OR UPDATE OF status OR DELETE ON task_manager
FOR EACH ROW EXECUTE FUNCTION update_milestone_progress();
-- ============================================
-- FUNCIÓN: Generar código de tarea
-- ============================================
CREATE OR REPLACE FUNCTION generate_task_codigo()
RETURNS TRIGGER AS $$
DECLARE
prefix VARCHAR(20);
seq INTEGER;
BEGIN
IF NEW.codigo IS NULL OR NEW.codigo = '' THEN
IF NEW.project_id IS NOT NULL THEN
SELECT codigo INTO prefix FROM task_projects WHERE id = NEW.project_id;
ELSE
prefix := 'TASK';
END IF;
SELECT COALESCE(MAX(
CAST(SUBSTRING(codigo FROM '[0-9]+$') AS INTEGER)
), 0) + 1
INTO seq
FROM task_manager
WHERE codigo LIKE prefix || '-%';
NEW.codigo := prefix || '-' || LPAD(seq::TEXT, 4, '0');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_generate_task_codigo ON task_manager;
CREATE TRIGGER trigger_generate_task_codigo
BEFORE INSERT ON task_manager
FOR EACH ROW EXECUTE FUNCTION generate_task_codigo();

View File

@@ -0,0 +1,168 @@
-- ============================================
-- LOG DE TRABAJO (Work Log)
-- Sistema TZZR - contratos-comunes
-- ============================================
-- Requiere: 00_types.sql, 02_task_manager.sql
-- ============================================
-- TABLA: task_work_log
-- Registro de archivos entrantes/salientes
-- ============================================
CREATE TABLE IF NOT EXISTS task_work_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Relación con tarea (opcional)
task_id UUID REFERENCES task_manager(id) ON DELETE SET NULL,
project_id UUID REFERENCES task_projects(id) ON DELETE SET NULL,
-- Dirección
direction file_direction NOT NULL,
-- Información del archivo
filename VARCHAR(500) NOT NULL,
filepath TEXT, -- Ruta en FileBrowser o URL
file_hash VARCHAR(64), -- SHA-256 del contenido
file_size_bytes BIGINT,
mime_type VARCHAR(100),
file_extension VARCHAR(20),
-- Origen/Destino
source VARCHAR(255), -- email, upload, api, scrape, etc.
source_ref VARCHAR(500), -- Referencia específica (message_id, url, etc.)
destination VARCHAR(255), -- grace, penny, factory, export, storage
destination_ref VARCHAR(500),
-- Procesamiento IA
processed_by_ia BOOLEAN DEFAULT false,
ai_service ai_service,
ai_module VARCHAR(50), -- Módulo específico (ASR_ENGINE, OCR_CORE, etc.)
ai_trace_id VARCHAR(64), -- trace_id del S-CONTRACT
ai_status VARCHAR(20), -- SUCCESS, ERROR, PARTIAL
ai_result_summary JSONB, -- Resumen del resultado
-- Descripción
titulo VARCHAR(255),
descripcion TEXT,
-- Etiquetas (h_maestros)
tags JSONB DEFAULT '[]',
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_work_log_task ON task_work_log(task_id);
CREATE INDEX IF NOT EXISTS idx_work_log_project ON task_work_log(project_id);
CREATE INDEX IF NOT EXISTS idx_work_log_direction ON task_work_log(direction);
CREATE INDEX IF NOT EXISTS idx_work_log_hash ON task_work_log(file_hash);
CREATE INDEX IF NOT EXISTS idx_work_log_source ON task_work_log(source);
CREATE INDEX IF NOT EXISTS idx_work_log_destination ON task_work_log(destination);
CREATE INDEX IF NOT EXISTS idx_work_log_ai_service ON task_work_log(ai_service);
CREATE INDEX IF NOT EXISTS idx_work_log_ai_trace ON task_work_log(ai_trace_id);
CREATE INDEX IF NOT EXISTS idx_work_log_fecha ON task_work_log(created_at);
CREATE INDEX IF NOT EXISTS idx_work_log_tags ON task_work_log USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_work_log_mime ON task_work_log(mime_type);
-- ============================================
-- TABLA: task_work_log_versions
-- Versiones de archivos (tracking de cambios)
-- ============================================
CREATE TABLE IF NOT EXISTS task_work_log_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
work_log_id UUID NOT NULL REFERENCES task_work_log(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
filepath TEXT NOT NULL,
file_hash VARCHAR(64) NOT NULL,
file_size_bytes BIGINT,
-- Cambio
change_type VARCHAR(50), -- created, modified, processed, exported
change_description TEXT,
changed_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_version UNIQUE(work_log_id, version)
);
CREATE INDEX IF NOT EXISTS idx_work_log_versions_log ON task_work_log_versions(work_log_id);
CREATE INDEX IF NOT EXISTS idx_work_log_versions_hash ON task_work_log_versions(file_hash);
-- ============================================
-- FUNCIÓN: Auto-incrementar versión
-- ============================================
CREATE OR REPLACE FUNCTION auto_version_work_log()
RETURNS TRIGGER AS $$
DECLARE
next_version INTEGER;
BEGIN
SELECT COALESCE(MAX(version), 0) + 1
INTO next_version
FROM task_work_log_versions
WHERE work_log_id = NEW.work_log_id;
NEW.version := next_version;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_auto_version_work_log ON task_work_log_versions;
CREATE TRIGGER trigger_auto_version_work_log
BEFORE INSERT ON task_work_log_versions
FOR EACH ROW EXECUTE FUNCTION auto_version_work_log();
-- ============================================
-- VISTA: Work log reciente
-- ============================================
CREATE OR REPLACE VIEW v_work_log_recent AS
SELECT
w.id,
w.filename,
w.direction,
w.source,
w.destination,
w.ai_service,
w.ai_module,
w.ai_status,
w.processed_by_ia,
t.titulo AS tarea,
t.codigo AS tarea_codigo,
p.nombre AS proyecto,
p.codigo AS proyecto_codigo,
w.file_size_bytes,
w.mime_type,
w.created_at,
w.processed_at
FROM task_work_log w
LEFT JOIN task_manager t ON w.task_id = t.id
LEFT JOIN task_projects p ON w.project_id = p.id
ORDER BY w.created_at DESC;
-- ============================================
-- VISTA: Archivos pendientes de procesar
-- ============================================
CREATE OR REPLACE VIEW v_work_log_pending AS
SELECT
w.id,
w.filename,
w.mime_type,
w.file_size_bytes,
w.source,
w.direction,
p.nombre AS proyecto,
w.created_at
FROM task_work_log w
LEFT JOIN task_projects p ON w.project_id = p.id
WHERE w.processed_by_ia = false
AND w.direction IN ('inbound', 'internal')
ORDER BY w.created_at ASC;

View File

@@ -0,0 +1,354 @@
-- ============================================
-- CONTEXTO PARA SERVICIOS IA
-- Sistema TZZR - contratos-comunes
-- ============================================
-- Requiere: 00_types.sql, 01_hst_tags.sql, 02_task_manager.sql
--
-- IMPORTANTE: Las estructuras aquí almacenadas corresponden
-- exactamente al bloque "context" del S-CONTRACT v2.1
-- (schemas/s-contract-request.json)
-- ============================================
-- TABLA: s_contract_contexts
-- Contextos reutilizables en formato S-CONTRACT
-- ============================================
CREATE TABLE IF NOT EXISTS s_contract_contexts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identificación
codigo VARCHAR(100) UNIQUE NOT NULL,
nombre VARCHAR(255) NOT NULL,
descripcion TEXT,
-- ============================================
-- BLOQUE context (S-CONTRACT v2.1)
-- Almacenado como JSONB para garantizar formato exacto
-- ============================================
context JSONB NOT NULL DEFAULT '{
"lang": "es",
"mode": "strict",
"pii_filter": false,
"bandera_id": null,
"player_id": null,
"method_hash": null,
"human_readable": null,
"system_instruction": null,
"datasets": [],
"tags": {"hst": [], "hsu": [], "emp": [], "pjt": []},
"ambiente": {"timezone": "Europe/Madrid", "locale": "es-ES"}
}'::jsonb,
-- ============================================
-- BLOQUE deployment (S-CONTRACT v2.1)
-- Configuración de despliegue asociada
-- ============================================
deployment JSONB DEFAULT '{
"mode": "SEMI",
"tier_preference": ["SELF_HOSTED", "EXTERNAL"]
}'::jsonb,
-- Alcance
scope VARCHAR(50) DEFAULT 'global', -- global, project, task
project_id UUID REFERENCES task_projects(id) ON DELETE SET NULL,
-- Estado
activo BOOLEAN DEFAULT true,
-- Metadata (campos no S-CONTRACT, solo para gestión interna)
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_scontract_ctx_codigo ON s_contract_contexts(codigo);
CREATE INDEX IF NOT EXISTS idx_scontract_ctx_scope ON s_contract_contexts(scope);
CREATE INDEX IF NOT EXISTS idx_scontract_ctx_project ON s_contract_contexts(project_id);
CREATE INDEX IF NOT EXISTS idx_scontract_ctx_activo ON s_contract_contexts(activo);
CREATE INDEX IF NOT EXISTS idx_scontract_ctx_context ON s_contract_contexts USING GIN(context);
CREATE INDEX IF NOT EXISTS idx_scontract_ctx_deployment ON s_contract_contexts USING GIN(deployment);
DROP TRIGGER IF EXISTS update_scontract_ctx_updated_at ON s_contract_contexts;
CREATE TRIGGER update_scontract_ctx_updated_at BEFORE UPDATE ON s_contract_contexts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- TABLA: s_contract_datasets
-- Datasets reutilizables (formato S-CONTRACT context.datasets[])
-- ============================================
CREATE TABLE IF NOT EXISTS s_contract_datasets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Campos que mapean a context.datasets[] en S-CONTRACT
codigo VARCHAR(100) UNIQUE NOT NULL,
tipo VARCHAR(50) NOT NULL, -- knowledge, examples, rules, vocabulary, context, persona
contenido TEXT, -- Contenido inline
contenido_ref TEXT, -- URI a contenido externo
-- Campos adicionales de gestión (no van en S-CONTRACT)
nombre VARCHAR(255) NOT NULL,
descripcion TEXT,
version VARCHAR(20) DEFAULT '1.0',
scope VARCHAR(50) DEFAULT 'global',
project_id UUID REFERENCES task_projects(id) ON DELETE SET NULL,
activo BOOLEAN DEFAULT true,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_dataset_tipo CHECK (tipo IN ('knowledge', 'examples', 'rules', 'vocabulary', 'context', 'persona'))
);
CREATE INDEX IF NOT EXISTS idx_scontract_ds_codigo ON s_contract_datasets(codigo);
CREATE INDEX IF NOT EXISTS idx_scontract_ds_tipo ON s_contract_datasets(tipo);
CREATE INDEX IF NOT EXISTS idx_scontract_ds_activo ON s_contract_datasets(activo);
DROP TRIGGER IF EXISTS update_scontract_ds_updated_at ON s_contract_datasets;
CREATE TRIGGER update_scontract_ds_updated_at BEFORE UPDATE ON s_contract_datasets
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- FUNCIÓN: Convertir dataset a formato S-CONTRACT
-- ============================================
CREATE OR REPLACE FUNCTION dataset_to_scontract(p_dataset_id UUID)
RETURNS JSONB AS $$
SELECT jsonb_build_object(
'codigo', codigo,
'tipo', tipo,
'contenido', contenido,
'contenido_ref', contenido_ref
)
FROM s_contract_datasets
WHERE id = p_dataset_id AND activo = true;
$$ LANGUAGE SQL STABLE;
-- ============================================
-- FUNCIÓN: Obtener context block para S-CONTRACT
-- Devuelve EXACTAMENTE el formato de context en S-CONTRACT
-- ============================================
CREATE OR REPLACE FUNCTION get_scontract_context(p_context_codigo VARCHAR)
RETURNS JSONB AS $$
SELECT context
FROM s_contract_contexts
WHERE codigo = p_context_codigo AND activo = true;
$$ LANGUAGE SQL STABLE;
-- ============================================
-- FUNCIÓN: Obtener deployment block para S-CONTRACT
-- ============================================
CREATE OR REPLACE FUNCTION get_scontract_deployment(p_context_codigo VARCHAR)
RETURNS JSONB AS $$
SELECT deployment
FROM s_contract_contexts
WHERE codigo = p_context_codigo AND activo = true;
$$ LANGUAGE SQL STABLE;
-- ============================================
-- FUNCIÓN: Construir context con datasets expandidos
-- Útil cuando se quiere incluir datasets por código
-- ============================================
CREATE OR REPLACE FUNCTION build_scontract_context(
p_context_codigo VARCHAR,
p_dataset_codigos VARCHAR[] DEFAULT NULL
)
RETURNS JSONB AS $$
DECLARE
base_context JSONB;
datasets_array JSONB;
BEGIN
-- Obtener contexto base
SELECT context INTO base_context
FROM s_contract_contexts
WHERE codigo = p_context_codigo AND activo = true;
IF base_context IS NULL THEN
RETURN NULL;
END IF;
-- Si se especifican datasets, construir el array
IF p_dataset_codigos IS NOT NULL AND array_length(p_dataset_codigos, 1) > 0 THEN
SELECT jsonb_agg(
jsonb_build_object(
'codigo', d.codigo,
'tipo', d.tipo,
'contenido', d.contenido,
'contenido_ref', d.contenido_ref
)
)
INTO datasets_array
FROM s_contract_datasets d
WHERE d.codigo = ANY(p_dataset_codigos)
AND d.activo = true;
-- Reemplazar datasets en el contexto
base_context := jsonb_set(base_context, '{datasets}', COALESCE(datasets_array, '[]'::jsonb));
END IF;
RETURN base_context;
END;
$$ LANGUAGE plpgsql STABLE;
-- ============================================
-- FUNCIÓN: Actualizar campo específico del context
-- ============================================
CREATE OR REPLACE FUNCTION update_scontract_context_field(
p_context_codigo VARCHAR,
p_field_path TEXT[],
p_value JSONB
)
RETURNS BOOLEAN AS $$
BEGIN
UPDATE s_contract_contexts
SET context = jsonb_set(context, p_field_path, p_value),
updated_at = CURRENT_TIMESTAMP
WHERE codigo = p_context_codigo;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql;
-- ============================================
-- Añadir FK en task_manager
-- ============================================
-- Eliminar columna antigua si existe
ALTER TABLE task_manager DROP COLUMN IF EXISTS context_id;
-- Añadir referencia al contexto S-CONTRACT
ALTER TABLE task_manager
ADD COLUMN IF NOT EXISTS scontract_context_codigo VARCHAR(100);
-- Añadir FK en task_projects
ALTER TABLE task_projects DROP COLUMN IF EXISTS default_context_id;
ALTER TABLE task_projects
ADD COLUMN IF NOT EXISTS default_scontract_context VARCHAR(100);
-- ============================================
-- DATOS INICIALES
-- ============================================
-- Contexto por defecto
INSERT INTO s_contract_contexts (codigo, nombre, descripcion, context, deployment) VALUES
(
'CTX_DEFAULT',
'Contexto por defecto',
'Contexto base para operaciones sin configuración específica',
'{
"lang": "es",
"mode": "strict",
"pii_filter": false,
"bandera_id": null,
"player_id": null,
"method_hash": null,
"human_readable": null,
"system_instruction": "Procesa la información recibida de forma precisa. Responde en español.",
"datasets": [],
"tags": {"hst": [], "hsu": [], "emp": [], "pjt": []},
"ambiente": {
"timezone": "Europe/Madrid",
"locale": "es-ES",
"currency": "EUR"
}
}'::jsonb,
'{
"mode": "SEMI",
"tier_preference": ["SELF_HOSTED", "EXTERNAL"]
}'::jsonb
),
(
'CTX_GRACE',
'Contexto para GRACE',
'Contexto optimizado para procesamiento GRACE',
'{
"lang": "es",
"mode": "strict",
"pii_filter": false,
"system_instruction": "Eres un asistente de procesamiento del sistema GRACE. Procesa los datos de forma estructurada y precisa. Si hay incertidumbre, indica el nivel de confianza.",
"datasets": [],
"tags": {"hst": [], "hsu": [], "emp": [], "pjt": []},
"ambiente": {
"timezone": "Europe/Madrid",
"locale": "es-ES"
}
}'::jsonb,
'{"mode": "SEMI", "tier_preference": ["SELF_HOSTED", "EXTERNAL"]}'::jsonb
),
(
'CTX_PENNY',
'Contexto para PENNY',
'Contexto optimizado para asistente de voz PENNY',
'{
"lang": "es",
"mode": "lenient",
"pii_filter": false,
"system_instruction": "Eres PENNY, un asistente de voz amable y eficiente. Responde de forma concisa (máximo 2-3 oraciones). Usa tono conversacional pero profesional.",
"datasets": [],
"tags": {"hst": [], "hsu": [], "emp": [], "pjt": []},
"ambiente": {
"timezone": "Europe/Madrid",
"locale": "es-ES",
"session_type": "interactive"
}
}'::jsonb,
'{"mode": "SEMI", "tier_preference": ["SELF_HOSTED", "EXTERNAL"]}'::jsonb
),
(
'CTX_FACTORY',
'Contexto para THE FACTORY',
'Contexto optimizado para procesamiento documental',
'{
"lang": "es",
"mode": "strict",
"pii_filter": true,
"system_instruction": "Eres un procesador de documentos de THE FACTORY. Extrae información de forma estructurada. Mantén fidelidad al documento original. Señala incertidumbres con [INCIERTO: razón].",
"datasets": [],
"tags": {"hst": [], "hsu": [], "emp": [], "pjt": []},
"ambiente": {
"timezone": "Europe/Madrid",
"locale": "es-ES",
"session_type": "batch"
}
}'::jsonb,
'{"mode": "SEMI", "tier_preference": ["EXTERNAL", "SELF_HOSTED"]}'::jsonb
)
ON CONFLICT (codigo) DO UPDATE SET
context = EXCLUDED.context,
deployment = EXCLUDED.deployment,
updated_at = CURRENT_TIMESTAMP;
-- Datasets base
INSERT INTO s_contract_datasets (codigo, nombre, tipo, contenido) VALUES
(
'DS_TZZR_ECOSYSTEM',
'Ecosistema TZZR',
'knowledge',
'TZZR es un ecosistema personal de productividad:
- DECK: Servidor central, iniciador de conexiones
- GRACE: Capa de procesamiento IA
- PENNY: Asistente de voz real-time
- THE FACTORY: Procesamiento documental
Sistema de etiquetas HST con h_maestro (SHA-256) como ID único.'
),
(
'DS_HST_INTRO',
'Introducción HST',
'vocabulary',
'Grupos de etiquetas HST:
- hst: Sistema (sync tzrtech.org)
- emp: Empresa
- hsu: Usuario
- pjt: Proyecto
Cada etiqueta tiene: codigo, nombre, descripcion, color, jerarquía.'
)
ON CONFLICT (codigo) DO UPDATE SET
contenido = EXCLUDED.contenido,
updated_at = CURRENT_TIMESTAMP;

View File

@@ -0,0 +1,277 @@
-- ============================================
-- LOG DE REQUESTS A SERVICIOS IA
-- Sistema TZZR - contratos-comunes
-- ============================================
-- Requiere: 00_types.sql, 02_task_manager.sql, 03_work_log.sql, 04_ai_context.sql
-- ============================================
-- TABLA: task_ai_requests
-- Registro de todas las requests a servicios IA
-- ============================================
CREATE TABLE IF NOT EXISTS task_ai_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Trazabilidad S-CONTRACT
trace_id VARCHAR(64) NOT NULL, -- trace_id del S-CONTRACT
idempotency_key VARCHAR(64), -- Para deduplicación
step_id VARCHAR(64), -- ID del paso en cadena
step_index INTEGER, -- Posición en la cadena
-- Origen
task_id UUID REFERENCES task_manager(id) ON DELETE SET NULL,
work_log_id UUID REFERENCES task_work_log(id) ON DELETE SET NULL,
project_id UUID REFERENCES task_projects(id) ON DELETE SET NULL,
-- Destino
target_service ai_service NOT NULL,
target_module VARCHAR(50) NOT NULL, -- ASR_ENGINE, CLASSIFIER, OCR_CORE, etc.
-- Contexto usado
context_id UUID REFERENCES task_contexts(id) ON DELETE SET NULL,
context_snapshot JSONB, -- Snapshot del contexto en el momento
-- Deployment
deployment_mode deployment_mode,
tier_requested provider_tier,
tier_used provider_tier,
provider_used VARCHAR(50),
endpoint_used TEXT,
-- Request (sin contenido sensible)
request_summary JSONB, -- Resumen de la request
payload_type VARCHAR(50), -- text, audio, image, document
payload_size_bytes BIGINT,
payload_hash VARCHAR(64),
-- Response
status VARCHAR(20) NOT NULL, -- SUCCESS, PARTIAL, ERROR, TIMEOUT, FALLBACK
fallback_level INTEGER DEFAULT 0,
response_summary JSONB, -- Resumen de respuesta
output_schema VARCHAR(100),
output_hash VARCHAR(64),
-- Calidad
confidence DECIMAL(4,3),
coverage DECIMAL(4,3),
validation_passed BOOLEAN,
-- Métricas
latency_ms INTEGER,
queue_wait_ms INTEGER,
tokens_input INTEGER,
tokens_output INTEGER,
cost_usd DECIMAL(10,6),
-- Errores
error_code VARCHAR(50),
error_message TEXT,
retry_count INTEGER DEFAULT 0,
-- Timestamps
timestamp_init TIMESTAMP NOT NULL,
timestamp_end TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Índices principales
CREATE INDEX IF NOT EXISTS idx_ai_requests_trace ON task_ai_requests(trace_id);
CREATE INDEX IF NOT EXISTS idx_ai_requests_idempotency ON task_ai_requests(idempotency_key);
CREATE INDEX IF NOT EXISTS idx_ai_requests_task ON task_ai_requests(task_id);
CREATE INDEX IF NOT EXISTS idx_ai_requests_work_log ON task_ai_requests(work_log_id);
CREATE INDEX IF NOT EXISTS idx_ai_requests_project ON task_ai_requests(project_id);
CREATE INDEX IF NOT EXISTS idx_ai_requests_service ON task_ai_requests(target_service);
CREATE INDEX IF NOT EXISTS idx_ai_requests_module ON task_ai_requests(target_module);
CREATE INDEX IF NOT EXISTS idx_ai_requests_status ON task_ai_requests(status);
CREATE INDEX IF NOT EXISTS idx_ai_requests_fecha ON task_ai_requests(timestamp_init);
CREATE INDEX IF NOT EXISTS idx_ai_requests_tier ON task_ai_requests(tier_used);
CREATE INDEX IF NOT EXISTS idx_ai_requests_provider ON task_ai_requests(provider_used);
-- Índice para métricas temporales
CREATE INDEX IF NOT EXISTS idx_ai_requests_fecha_service ON task_ai_requests(timestamp_init, target_service);
-- ============================================
-- TABLA: task_ai_request_chain
-- Cadenas de requests relacionadas
-- ============================================
CREATE TABLE IF NOT EXISTS task_ai_request_chain (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Trace principal de la cadena
chain_trace_id VARCHAR(64) NOT NULL,
-- Request en la cadena
request_id UUID NOT NULL REFERENCES task_ai_requests(id) ON DELETE CASCADE,
-- Posición
chain_index INTEGER NOT NULL,
-- Dependencias (requests previas necesarias)
depends_on JSONB DEFAULT '[]', -- Array de request_ids
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_chain_position UNIQUE(chain_trace_id, chain_index)
);
CREATE INDEX IF NOT EXISTS idx_request_chain_trace ON task_ai_request_chain(chain_trace_id);
CREATE INDEX IF NOT EXISTS idx_request_chain_request ON task_ai_request_chain(request_id);
-- ============================================
-- VISTAS DE MÉTRICAS
-- ============================================
-- Vista: Métricas por servicio (últimos 30 días)
CREATE OR REPLACE VIEW v_ai_metrics_by_service AS
SELECT
target_service,
target_module,
COUNT(*) AS total_requests,
COUNT(*) FILTER (WHERE status = 'SUCCESS') AS successful,
COUNT(*) FILTER (WHERE status = 'ERROR') AS errors,
COUNT(*) FILTER (WHERE status = 'FALLBACK') AS fallbacks,
ROUND(AVG(latency_ms)::numeric, 2) AS avg_latency_ms,
ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms)::numeric, 2) AS p95_latency_ms,
SUM(cost_usd) AS total_cost_usd,
SUM(tokens_input) AS total_tokens_in,
SUM(tokens_output) AS total_tokens_out,
ROUND(AVG(confidence)::numeric, 3) AS avg_confidence
FROM task_ai_requests
WHERE timestamp_init > CURRENT_DATE - INTERVAL '30 days'
GROUP BY target_service, target_module
ORDER BY total_requests DESC;
-- Vista: Métricas por tier
CREATE OR REPLACE VIEW v_ai_metrics_by_tier AS
SELECT
tier_used,
provider_used,
COUNT(*) AS total_requests,
COUNT(*) FILTER (WHERE status = 'SUCCESS') AS successful,
ROUND(AVG(latency_ms)::numeric, 2) AS avg_latency_ms,
SUM(cost_usd) AS total_cost_usd
FROM task_ai_requests
WHERE timestamp_init > CURRENT_DATE - INTERVAL '30 days'
AND tier_used IS NOT NULL
GROUP BY tier_used, provider_used
ORDER BY total_requests DESC;
-- Vista: Costos diarios
CREATE OR REPLACE VIEW v_ai_daily_costs AS
SELECT
DATE(timestamp_init) AS fecha,
target_service,
COUNT(*) AS requests,
SUM(cost_usd) AS cost_usd,
SUM(tokens_input) AS tokens_in,
SUM(tokens_output) AS tokens_out
FROM task_ai_requests
WHERE timestamp_init > CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE(timestamp_init), target_service
ORDER BY fecha DESC, cost_usd DESC;
-- Vista: Errores recientes
CREATE OR REPLACE VIEW v_ai_recent_errors AS
SELECT
id,
trace_id,
target_service,
target_module,
status,
error_code,
error_message,
provider_used,
tier_used,
timestamp_init
FROM task_ai_requests
WHERE status IN ('ERROR', 'TIMEOUT')
AND timestamp_init > CURRENT_DATE - INTERVAL '7 days'
ORDER BY timestamp_init DESC
LIMIT 100;
-- ============================================
-- FUNCIÓN: Registrar request de S-CONTRACT
-- ============================================
CREATE OR REPLACE FUNCTION log_ai_request(
p_trace_id VARCHAR(64),
p_service ai_service,
p_module VARCHAR(50),
p_context_id UUID DEFAULT NULL,
p_task_id UUID DEFAULT NULL,
p_work_log_id UUID DEFAULT NULL
)
RETURNS UUID AS $$
DECLARE
new_id UUID;
ctx_snapshot JSONB;
BEGIN
-- Obtener snapshot del contexto si existe
IF p_context_id IS NOT NULL THEN
ctx_snapshot := build_ai_context(p_context_id);
END IF;
INSERT INTO task_ai_requests (
trace_id,
target_service,
target_module,
context_id,
context_snapshot,
task_id,
work_log_id,
status,
timestamp_init
) VALUES (
p_trace_id,
p_service,
p_module,
p_context_id,
ctx_snapshot,
p_task_id,
p_work_log_id,
'pending',
CURRENT_TIMESTAMP
)
RETURNING id INTO new_id;
RETURN new_id;
END;
$$ LANGUAGE plpgsql;
-- ============================================
-- FUNCIÓN: Actualizar request completada
-- ============================================
CREATE OR REPLACE FUNCTION complete_ai_request(
p_request_id UUID,
p_status VARCHAR(20),
p_tier_used provider_tier,
p_provider VARCHAR(50),
p_latency_ms INTEGER,
p_tokens_in INTEGER DEFAULT NULL,
p_tokens_out INTEGER DEFAULT NULL,
p_cost_usd DECIMAL(10,6) DEFAULT NULL,
p_confidence DECIMAL(4,3) DEFAULT NULL,
p_error_code VARCHAR(50) DEFAULT NULL,
p_error_message TEXT DEFAULT NULL
)
RETURNS VOID AS $$
BEGIN
UPDATE task_ai_requests
SET
status = p_status,
tier_used = p_tier_used,
provider_used = p_provider,
latency_ms = p_latency_ms,
tokens_input = p_tokens_in,
tokens_output = p_tokens_out,
cost_usd = p_cost_usd,
confidence = p_confidence,
error_code = p_error_code,
error_message = p_error_message,
timestamp_end = CURRENT_TIMESTAMP
WHERE id = p_request_id;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,50 @@
-- Inicialización de la base de datos para CLARA
-- Servicio inmutable de log de entrada
-- Tabla principal: clara_log
CREATE TABLE IF NOT EXISTS clara_log (
id BIGSERIAL PRIMARY KEY,
h_instancia VARCHAR(64) NOT NULL,
h_entrada VARCHAR(64) NOT NULL,
contenedor JSONB NOT NULL,
r2_paths JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Índices para búsqueda eficiente
CREATE INDEX IF NOT EXISTS idx_clara_instancia ON clara_log(h_instancia);
CREATE INDEX IF NOT EXISTS idx_clara_entrada ON clara_log(h_entrada);
CREATE INDEX IF NOT EXISTS idx_clara_created ON clara_log(created_at DESC);
-- Índice para buscar por estado del contenedor
CREATE INDEX IF NOT EXISTS idx_clara_estado ON clara_log((contenedor->'estado'->>'actual'));
-- Índice para buscar por timestamp de captura
CREATE INDEX IF NOT EXISTS idx_clara_timestamp ON clara_log((contenedor->'origen'->>'timestamp_captura'));
-- Índice compuesto para búsquedas frecuentes
CREATE INDEX IF NOT EXISTS idx_clara_inst_entrada ON clara_log(h_instancia, h_entrada);
-- Vista para consultas comunes
CREATE OR REPLACE VIEW clara_summary AS
SELECT
id,
h_instancia,
h_entrada,
contenedor->>'id' as contenedor_id,
contenedor->'origen'->>'timestamp_captura' as timestamp_captura,
contenedor->'archivo'->>'tipo' as tipo_archivo,
contenedor->'archivo'->>'categoria' as categoria,
contenedor->'estado'->>'actual' as estado_actual,
jsonb_array_length(COALESCE(contenedor->'tags', '[]'::jsonb)) as num_tags,
created_at
FROM clara_log
ORDER BY id DESC;
-- Comentarios para documentación
COMMENT ON TABLE clara_log IS 'Log inmutable de contenedores recibidos de PACKET';
COMMENT ON COLUMN clara_log.h_instancia IS 'Hash de identificación de la instancia DECK';
COMMENT ON COLUMN clara_log.h_entrada IS 'Hash del archivo (sha256)';
COMMENT ON COLUMN clara_log.contenedor IS 'Contenedor completo según esquema de contratos-comunes';
COMMENT ON COLUMN clara_log.r2_paths IS 'Rutas de los archivos en Cloudflare R2';
COMMENT ON COLUMN clara_log.created_at IS 'Timestamp de recepción (inmutable)';

View File

@@ -0,0 +1,123 @@
-- Los Libros Contables - FELDMAN v2.0
-- Tablas para milestones, bloques y validaciones
-- MILESTONES
CREATE TABLE IF NOT EXISTS milestones (
id BIGSERIAL PRIMARY KEY,
h_milestone VARCHAR(64) NOT NULL UNIQUE,
h_instancia VARCHAR(64) NOT NULL,
secuencia BIGINT NOT NULL,
hash_previo VARCHAR(64),
hash_contenido VARCHAR(64) NOT NULL,
alias VARCHAR(200) NOT NULL,
tipo_item VARCHAR(50) NOT NULL,
descripcion TEXT,
datos JSONB DEFAULT '{}',
etiqueta_principal VARCHAR(64),
proyecto_tag VARCHAR(64),
id_padre_milestone BIGINT REFERENCES milestones(id),
id_bloque_asociado BIGINT,
blockchain_pending BOOLEAN DEFAULT TRUE,
blockchain_tx_ref VARCHAR(128),
notario_batch_id VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(64),
CONSTRAINT milestone_secuencia_unica UNIQUE (h_instancia, secuencia)
);
-- BLOQUES
CREATE TABLE IF NOT EXISTS bloques (
id BIGSERIAL PRIMARY KEY,
h_bloque VARCHAR(64) NOT NULL UNIQUE,
h_instancia VARCHAR(64) NOT NULL,
secuencia BIGINT NOT NULL,
hash_previo VARCHAR(64),
hash_contenido VARCHAR(64) NOT NULL,
alias VARCHAR(200) NOT NULL,
tipo_accion VARCHAR(50) NOT NULL,
descripcion TEXT,
datos JSONB DEFAULT '{}',
evidencia_hash VARCHAR(64) NOT NULL,
evidencia_url VARCHAR(500) NOT NULL,
evidencia_tipo VARCHAR(50) NOT NULL,
etiqueta_principal VARCHAR(64),
proyecto_tag VARCHAR(64),
id_padre_bloque BIGINT REFERENCES bloques(id),
id_milestone_asociado BIGINT REFERENCES milestones(id),
blockchain_pending BOOLEAN DEFAULT TRUE,
blockchain_tx_ref VARCHAR(128),
notario_batch_id VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(64),
CONSTRAINT bloque_secuencia_unica UNIQUE (h_instancia, secuencia)
);
-- COLA DE VALIDACION
CREATE TABLE IF NOT EXISTS feldman_cola (
id BIGSERIAL PRIMARY KEY,
h_entrada VARCHAR(64) NOT NULL UNIQUE,
h_instancia VARCHAR(64) NOT NULL,
origen VARCHAR(50) NOT NULL,
h_origen VARCHAR(64),
tipo_destino VARCHAR(20) NOT NULL,
datos JSONB NOT NULL,
estado VARCHAR(20) DEFAULT 'pendiente',
error_mensaje TEXT,
intentos INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP
);
-- VALIDACIONES
CREATE TABLE IF NOT EXISTS feldman_validaciones (
id BIGSERIAL PRIMARY KEY,
h_entrada VARCHAR(64) NOT NULL,
validacion_ok BOOLEAN NOT NULL,
reglas_aplicadas JSONB NOT NULL,
tipo_registro VARCHAR(20),
h_registro VARCHAR(64),
validated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- INDICES
CREATE INDEX IF NOT EXISTS idx_milestones_pending ON milestones(blockchain_pending) WHERE blockchain_pending = TRUE;
CREATE INDEX IF NOT EXISTS idx_bloques_pending ON bloques(blockchain_pending) WHERE blockchain_pending = TRUE;
CREATE INDEX IF NOT EXISTS idx_milestones_proyecto ON milestones(proyecto_tag);
CREATE INDEX IF NOT EXISTS idx_bloques_proyecto ON bloques(proyecto_tag);
CREATE INDEX IF NOT EXISTS idx_feldman_estado ON feldman_cola(estado);
CREATE INDEX IF NOT EXISTS idx_feldman_instancia ON feldman_cola(h_instancia);
-- FUNCIONES
CREATE OR REPLACE FUNCTION get_ultimo_hash_milestone(p_h_instancia VARCHAR)
RETURNS VARCHAR AS $$
DECLARE v_hash VARCHAR;
BEGIN
SELECT hash_contenido INTO v_hash FROM milestones
WHERE h_instancia = p_h_instancia ORDER BY secuencia DESC LIMIT 1;
RETURN COALESCE(v_hash, 'GENESIS');
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION get_ultimo_hash_bloque(p_h_instancia VARCHAR)
RETURNS VARCHAR AS $$
DECLARE v_hash VARCHAR;
BEGIN
SELECT hash_contenido INTO v_hash FROM bloques
WHERE h_instancia = p_h_instancia ORDER BY secuencia DESC LIMIT 1;
RETURN COALESCE(v_hash, 'GENESIS');
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION get_siguiente_secuencia_milestone(p_h_instancia VARCHAR)
RETURNS BIGINT AS $$
BEGIN
RETURN (SELECT COALESCE(MAX(secuencia), 0) + 1 FROM milestones WHERE h_instancia = p_h_instancia);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION get_siguiente_secuencia_bloque(p_h_instancia VARCHAR)
RETURNS BIGINT AS $$
BEGIN
RETURN (SELECT COALESCE(MAX(secuencia), 0) + 1 FROM bloques WHERE h_instancia = p_h_instancia);
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,36 @@
-- ALFRED - Flujos Predefinidos
-- Deploy en DECK PostgreSQL
-- Flujos predefinidos
CREATE TABLE IF NOT EXISTS flujos_predefinidos (
id VARCHAR(64) PRIMARY KEY,
h_instancia VARCHAR(64) NOT NULL,
nombre VARCHAR(100) NOT NULL,
descripcion TEXT,
pasos JSONB NOT NULL,
campos_fijos JSONB DEFAULT '{}',
campos_variables JSONB DEFAULT '[]',
activo BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Ejecuciones de flujos
CREATE TABLE IF NOT EXISTS flujo_ejecuciones (
id BIGSERIAL PRIMARY KEY,
h_flujo VARCHAR(64) REFERENCES flujos_predefinidos(id),
h_instancia VARCHAR(64) NOT NULL,
h_ejecucion VARCHAR(64) NOT NULL UNIQUE,
datos JSONB NOT NULL,
estado VARCHAR(20) DEFAULT 'ok',
destino VARCHAR(20) DEFAULT 'feldman',
notas TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indices
CREATE INDEX IF NOT EXISTS idx_flujos_h_instancia ON flujos_predefinidos(h_instancia);
CREATE INDEX IF NOT EXISTS idx_flujos_activo ON flujos_predefinidos(activo);
CREATE INDEX IF NOT EXISTS idx_ejecuciones_h_flujo ON flujo_ejecuciones(h_flujo);
CREATE INDEX IF NOT EXISTS idx_ejecuciones_estado ON flujo_ejecuciones(estado);
CREATE INDEX IF NOT EXISTS idx_ejecuciones_created ON flujo_ejecuciones(created_at DESC);

View File

@@ -0,0 +1,82 @@
# Schemas SQL
**Versión:** 1.0
**Estado:** Implementado
---
## Archivos
| Archivo | Descripción | Tablas |
|---------|-------------|--------|
| 00_types.sql | Tipos y enums | task_status, task_priority, file_direction, ai_service, hst_grupo, deployment_mode |
| 01_hst_tags.sql | Tags semánticos | hst_tags, hst_biblioteca |
| 02_task_manager.sql | Gestión de tareas | tasks, task_dependencies, task_comments |
| 03_work_log.sql | Log de trabajo | work_log, work_log_files |
| 04_ai_context.sql | Contexto IA | ai_context_blocks, ai_context_datasets |
| 05_ai_requests.sql | Peticiones IA | ai_requests, ai_responses |
| 06_clara.sql | Clara (secretaría) | clara_log, clara_summary (vista) |
| 07_feldman.sql | Feldman (contable) | milestones, bloques, feldman_cola, feldman_validaciones |
| 08_alfred.sql | Alfred (producción) | flujos_predefinidos, flujo_ejecuciones |
---
## Orden de Aplicación
```bash
# Aplicar en este orden
psql -U tzzr -d tzzr -f 00_types.sql
psql -U tzzr -d tzzr -f 01_hst_tags.sql
psql -U tzzr -d tzzr -f 02_task_manager.sql
psql -U tzzr -d tzzr -f 03_work_log.sql
psql -U tzzr -d tzzr -f 04_ai_context.sql
psql -U tzzr -d tzzr -f 05_ai_requests.sql
psql -U tzzr -d tzzr -f 06_clara.sql
psql -U tzzr -d tzzr -f 07_feldman.sql
psql -U tzzr -d tzzr -f 08_alfred.sql
```
---
## Tipos Enumerados (00_types.sql)
```sql
-- Estados de tarea
CREATE TYPE task_status AS ENUM (
'draft', 'pending', 'in_progress',
'blocked', 'review', 'completed', 'cancelled'
);
-- Prioridad de tarea
CREATE TYPE task_priority AS ENUM (
'critical', 'high', 'medium', 'low', 'someday'
);
-- Dirección de archivo
CREATE TYPE file_direction AS ENUM (
'inbound', 'outbound', 'internal', 'reference'
);
-- Servicios IA
CREATE TYPE ai_service AS ENUM (
'grace', 'penny', 'factory'
);
-- Grupos HST
CREATE TYPE hst_grupo AS ENUM (
'hst', 'emp', 'hsu', 'pjt'
);
-- Modo despliegue
CREATE TYPE deployment_mode AS ENUM (
'EXTERNAL', 'SELF_HOSTED', 'SEMI'
);
```
---
## Notas
- Todos los schemas usan `DO $$ BEGIN ... EXCEPTION ... END $$` para ser idempotentes
- Función `update_updated_at_column()` actualiza `updated_at` automáticamente
- Los SQL están en `/schemas/` junto a este README