codigo0/docs/CONTENT_PACK_SPEC.md

19 KiB

📦 ESPECIFICACIÓN CONTENT PACK

Versión: 1.0.0
Fecha: 2025-01-06
Estado: Especificación Final


🎯 PROPÓSITO

El Content Pack es un archivo JSON que contiene todo el contenido del sistema (protocolos, guías, manuales, fármacos, recursos) en un formato optimizado para distribución offline-first.

Características:

  • Distribución como archivo JSON único
  • Cacheable localmente (IndexedDB)
  • Offline-first garantizado
  • Versionado semántico
  • Verificación de integridad (hash)

📋 ESTRUCTURA DEL CONTENT PACK

{
  "metadata": {
    "version": "1.0.0",
    "generated_at": "2025-01-06T12:00:00Z",
    "source": "supabase",
    "hash": "sha256:abc123...",
    "min_app_version": "1.0.0",
    "max_app_version": "2.0.0"
  },
  "content": {
    "protocols": [...],
    "guides": [...],
    "manuals": [...],
    "drugs": [...],
    "checklists": [...]
  },
  "media": {
    "resources": [...],
    "associations": [...]
  },
  "links": {
    "protocol_to_guide": [...],
    "guide_to_protocol": [...],
    "manual_to_protocols": [...]
  }
}

📐 DETALLE DE SECCIONES

1. Metadata

interface ContentPackMetadata {
  version: string;              // Versión semántica del pack (ej: "1.2.3")
  generated_at: string;         // ISO timestamp de generación
  source: string;               // Origen: "supabase" | "local" | "manual"
  hash: string;                 // Hash SHA-256 del contenido (sin metadata)
  min_app_version?: string;     // Versión mínima de app requerida
  max_app_version?: string;     // Versión máxima de app compatible
  total_items: number;          // Total de items de contenido
  total_resources: number;      // Total de recursos multimedia
  generated_by?: string;        // ID del generador
}

Ejemplo:

{
  "metadata": {
    "version": "1.0.0",
    "generated_at": "2025-01-06T12:00:00Z",
    "source": "supabase",
    "hash": "sha256:7f8e9d2c1b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d",
    "min_app_version": "1.0.0",
    "total_items": 45,
    "total_resources": 120
  }
}

2. Content

Contiene arrays de ContentItems según tipo:

interface ContentPackContent {
  protocols: ContentItem[];     // Solo type: 'protocol'
  guides: ContentItem[];        // Solo type: 'guide'
  manuals: ContentItem[];       // Solo type: 'manual'
  drugs: ContentItem[];         // Solo type: 'drug'
  checklists: ContentItem[];    // Solo type: 'checklist'
}

Filtrado:

  • Solo incluye items con status: 'published'
  • Solo incluye la última versión (version === latest_version)
  • Ordenados por priority (critica → baja) y luego por title

3. Media

Contiene recursos multimedia y sus asociaciones:

interface ContentPackMedia {
  resources: MediaResource[];                    // Recursos multimedia
  associations: ContentResourceAssociation[];    // Asociaciones contenido ⇄ recursos
}

Filtrado:

  • Solo recursos con status: 'published'
  • Solo asociaciones de contenido publicado
  • Recursos ordenados por priority y uploaded_at

Enlaces bidireccionales entre contenido:

interface ContentPackLinks {
  protocol_to_guide: Array<{
    protocol_id: string;
    guide_id: string;
  }>;
  guide_to_protocol: Array<{
    guide_id: string;
    protocol_id: string;
  }>;
  manual_to_protocols: Array<{
    manual_id: string;
    protocol_ids: string[];
  }>;
  manual_to_guides: Array<{
    manual_id: string;
    guide_ids: string[];
  }>;
}

Propósito:

  • Navegación rápida sin consultar BD
  • Enlaces bidireccionales pre-calculados
  • Optimización de búsqueda

📝 EJEMPLO COMPLETO

Content Pack: RCP Adulto SVB

{
  "metadata": {
    "version": "1.0.0",
    "generated_at": "2025-01-06T12:00:00Z",
    "source": "supabase",
    "hash": "sha256:abc123...",
    "min_app_version": "1.0.0",
    "total_items": 1,
    "total_resources": 3
  },
  "content": {
    "protocols": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "type": "protocol",
        "slug": "rcp-adulto-svb",
        "title": "RCP Adulto - Soporte Vital Básico",
        "short_title": "RCP Adulto SVB",
        "description": "Protocolo de reanimación cardiopulmonar básica en adultos",
        "clinical_context": "RCP",
        "usage_type": "operativo",
        "priority": "critica",
        "status": "published",
        "source_guideline": "ERC",
        "source_year": 2021,
        "source_url": "https://www.erc.edu/...",
        "version": "1.0.0",
        "latest_version": "1.0.0",
        "content": {
          "steps": [
            {
              "order": 1,
              "text": "Garantizar seguridad de la escena",
              "critical": true,
              "time_estimate": "5-10s"
            },
            {
              "order": 2,
              "text": "Comprobar consciencia: estimular y preguntar '¿Se encuentra bien?'",
              "critical": true,
              "time_estimate": "5-10s"
            },
            {
              "order": 3,
              "text": "Si no responde, llamar inmediatamente al 112",
              "critical": true,
              "time_estimate": "10-15s"
            },
            {
              "order": 4,
              "text": "Abrir vía aérea: maniobra frente-mentón",
              "critical": true,
              "time_estimate": "5-10s"
            },
            {
              "order": 5,
              "text": "Comprobar respiración: VER-OÍR-SENTIR (máx. 10 segundos)",
              "critical": true,
              "time_estimate": "10s"
            },
            {
              "order": 6,
              "text": "Si no respira normal: iniciar RCP",
              "critical": true
            },
            {
              "order": 7,
              "text": "Iniciar compresiones torácicas: 30 compresiones",
              "critical": true,
              "equipment": ["DEA"],
              "time_estimate": "20-30s"
            },
            {
              "order": 8,
              "text": "Dar 2 ventilaciones de rescate",
              "critical": true,
              "equipment": ["Bolsa-mascarilla"],
              "time_estimate": "5-10s"
            },
            {
              "order": 9,
              "text": "Continuar ciclos 30:2 sin interrupción",
              "critical": true
            },
            {
              "order": 10,
              "text": "Solicitar DEA cuando esté disponible",
              "critical": false
            }
          ],
          "checklist": {
            "enabled": true,
            "title": "Checklist RCP Adulto SVB",
            "items": [
              {
                "id": "check-1",
                "text": "Seguridad de la escena",
                "order": 1,
                "critical": true
              },
              {
                "id": "check-2",
                "text": "Comprobación de respuesta",
                "order": 2,
                "critical": true
              },
              {
                "id": "check-3",
                "text": "Apertura de vía aérea",
                "order": 3,
                "critical": true
              },
              {
                "id": "check-4",
                "text": "Comprobación de respiración (<10s)",
                "order": 4,
                "critical": true
              },
              {
                "id": "check-5",
                "text": "Activación de emergencias",
                "order": 5,
                "critical": true
              },
              {
                "id": "check-6",
                "text": "Inicio de compresiones",
                "order": 6,
                "critical": true
              },
              {
                "id": "check-7",
                "text": "Colocación del DEA",
                "order": 7,
                "critical": false
              },
              {
                "id": "check-8",
                "text": "Análisis y descarga si indicada",
                "order": 8,
                "critical": false
              },
              {
                "id": "check-9",
                "text": "RCP de alta calidad continua",
                "order": 9,
                "critical": true
              },
              {
                "id": "check-10",
                "text": "Reevaluación cada 2 minutos",
                "order": 10,
                "critical": true
              }
            ]
          },
          "warnings": [
            "Profundidad compresiones: 5-6 cm",
            "Frecuencia: 100-120 compresiones/min",
            "Permitir descompresión completa",
            "Minimizar interrupciones (<10 seg)",
            "Cambiar reanimador cada 2 min"
          ],
          "key_points": [
            "Compresiones de calidad salvan vidas",
            "No interrumpir para pulso hasta que haya signos de vida",
            "La desfibrilación precoz aumenta supervivencia"
          ],
          "equipment": ["DEA", "Bolsa-mascarilla", "Cánula orofaríngea"],
          "age_group": "adulto",
          "estimated_duration": "5-10 min"
        },
        "related_content_ids": [
          "550e8400-e29b-41d4-a716-446655440001"  // Guía formativa RCP
        ],
        "related_guide_ids": [
          "550e8400-e29b-41d4-a716-446655440001"
        ],
        "tags": ["rcp", "svb", "adulto", "emergencia", "critica"],
        "category": "soporte_vital",
        "created_by": "system",
        "created_at": "2025-01-01T00:00:00Z",
        "updated_at": "2025-01-06T12:00:00Z"
      }
    ],
    "guides": [],
    "manuals": [],
    "drugs": [],
    "checklists": []
  },
  "media": {
    "resources": [
      {
        "id": "660e8400-e29b-41d4-a716-446655440000",
        "type": "image",
        "file_url": "https://[project].supabase.co/storage/v1/object/public/infografias/rcp/rcp_posicion_manos_adulto.png",
        "filename": "rcp_posicion_manos_adulto.png",
        "path": "/assets/infografias/rcp/rcp_posicion_manos_adulto.png",
        "title": "Posición de Manos - RCP Adulto",
        "description": "Infografía mostrando la posición correcta de las manos para compresiones torácicas en RCP adulto",
        "alt_text": "Posición de manos para compresiones torácicas RCP adulto: talón de una mano sobre el esternón, otra mano encima, brazos extendidos",
        "caption": "Posición correcta de manos para compresiones torácicas en RCP adulto",
        "tags": ["rcp", "adulto", "compresiones", "posicion", "operativo"],
        "block": "bloque-4-soporte-vital",
        "priority": "critica",
        "usage_type": ["operativo"],
        "width": 1200,
        "height": 800,
        "format": "png",
        "file_size": 245678,
        "status": "published",
        "uploaded_by": "system",
        "uploaded_at": "2025-01-05T10:00:00Z",
        "updated_at": "2025-01-05T10:00:00Z"
      },
      {
        "id": "660e8400-e29b-41d4-a716-446655440001",
        "type": "image",
        "file_url": "https://[project].supabase.co/storage/v1/object/public/infografias/rcp/rcp_profundidad_compresiones.png",
        "filename": "rcp_profundidad_compresiones.png",
        "path": "/assets/infografias/rcp/rcp_profundidad_compresiones.png",
        "title": "Profundidad de Compresiones - RCP",
        "description": "Diagrama mostrando la profundidad correcta de compresiones (5-6 cm) y frecuencia (100-120/min)",
        "alt_text": "Profundidad de compresiones RCP: 5-6 cm, frecuencia 100-120 por minuto",
        "caption": "Profundidad y frecuencia de compresiones torácicas",
        "tags": ["rcp", "compresiones", "profundidad", "frecuencia", "operativo"],
        "block": "bloque-4-soporte-vital",
        "priority": "critica",
        "usage_type": ["operativo"],
        "width": 1000,
        "height": 600,
        "format": "png",
        "file_size": 189234,
        "status": "published",
        "uploaded_by": "system",
        "uploaded_at": "2025-01-05T10:00:00Z",
        "updated_at": "2025-01-05T10:00:00Z"
      },
      {
        "id": "660e8400-e29b-41d4-a716-446655440002",
        "type": "video",
        "file_url": "https://[project].supabase.co/storage/v1/object/public/videos/rcp/rcp_adulto_svb.mp4",
        "thumbnail_url": "https://[project].supabase.co/storage/v1/object/public/videos/rcp/rcp_adulto_svb_thumb.jpg",
        "filename": "rcp_adulto_svb.mp4",
        "path": "/assets/videos/rcp/rcp_adulto_svb.mp4",
        "title": "RCP Adulto SVB - Técnica Completa",
        "description": "Vídeo demostrativo de la técnica completa de RCP Adulto SVB (45 segundos)",
        "alt_text": "Vídeo demostrativo RCP Adulto SVB",
        "caption": "Técnica completa de RCP Adulto SVB",
        "tags": ["rcp", "adulto", "svb", "video", "operativo"],
        "block": "bloque-4-soporte-vital",
        "priority": "critica",
        "usage_type": ["operativo"],
        "duration_seconds": 45,
        "video_format": "mp4",
        "file_size": 5242880,
        "status": "published",
        "uploaded_by": "system",
        "uploaded_at": "2025-01-05T11:00:00Z",
        "updated_at": "2025-01-05T11:00:00Z"
      }
    ],
    "associations": [
      {
        "id": "770e8400-e29b-41d4-a716-446655440000",
        "content_item_id": "550e8400-e29b-41d4-a716-446655440000",
        "media_resource_id": "660e8400-e29b-41d4-a716-446655440000",
        "section": "pasos",
        "position": 7,
        "placement": "inline",
        "caption": "Posición correcta de manos para compresiones",
        "is_critical": true,
        "priority": "critica",
        "created_at": "2025-01-06T10:00:00Z"
      },
      {
        "id": "770e8400-e29b-41d4-a716-446655440001",
        "content_item_id": "550e8400-e29b-41d4-a716-446655440000",
        "media_resource_id": "660e8400-e29b-41d4-a716-446655440001",
        "section": "pasos",
        "position": 7,
        "placement": "after",
        "caption": "Profundidad y frecuencia de compresiones",
        "is_critical": true,
        "priority": "critica",
        "created_at": "2025-01-06T10:00:00Z"
      },
      {
        "id": "770e8400-e29b-41d4-a716-446655440002",
        "content_item_id": "550e8400-e29b-41d4-a716-446655440000",
        "media_resource_id": "660e8400-e29b-41d4-a716-446655440002",
        "section": "pasos",
        "position": 0,
        "placement": "before",
        "caption": "Vídeo demostrativo completo",
        "is_critical": true,
        "priority": "critica",
        "created_at": "2025-01-06T10:00:00Z"
      }
    ]
  },
  "links": {
    "protocol_to_guide": [
      {
        "protocol_id": "550e8400-e29b-41d4-a716-446655440000",
        "guide_id": "550e8400-e29b-41d4-a716-446655440001"
      }
    ],
    "guide_to_protocol": [
      {
        "guide_id": "550e8400-e29b-41d4-a716-446655440001",
        "protocol_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ],
    "manual_to_protocols": [],
    "manual_to_guides": []
  }
}

🔐 VERIFICACIÓN DE INTEGRIDAD

Hash SHA-256

El hash se calcula sobre el contenido (sin metadata):

function calculatePackHash(pack: ContentPack): string {
  const contentToHash = {
    content: pack.content,
    media: pack.media,
    links: pack.links
  };
  
  const contentString = JSON.stringify(contentToHash, null, 0);
  const hash = sha256(contentString);
  
  return `sha256:${hash}`;
}

Verificación en app:

function verifyPackIntegrity(pack: ContentPack): boolean {
  const calculatedHash = calculatePackHash(pack);
  return calculatedHash === pack.metadata.hash;
}

📦 GENERACIÓN DEL PACK

Desde Supabase

-- Query para generar Content Pack
SELECT json_build_object(
  'metadata', json_build_object(
    'version', '1.0.0',
    'generated_at', NOW(),
    'source', 'supabase',
    'hash', '...', -- Calculado después
    'total_items', (SELECT COUNT(*) FROM content_items WHERE status = 'published'),
    'total_resources', (SELECT COUNT(*) FROM media_resources WHERE status = 'published')
  ),
  'content', json_build_object(
    'protocols', (SELECT json_agg(row_to_json(p)) FROM content_items p WHERE p.type = 'protocol' AND p.status = 'published'),
    'guides', (SELECT json_agg(row_to_json(g)) FROM content_items g WHERE g.type = 'guide' AND g.status = 'published'),
    'manuals', (SELECT json_agg(row_to_json(m)) FROM content_items m WHERE m.type = 'manual' AND m.status = 'published'),
    'drugs', (SELECT json_agg(row_to_json(d)) FROM content_items d WHERE d.type = 'drug' AND d.status = 'published'),
    'checklists', (SELECT json_agg(row_to_json(c)) FROM content_items c WHERE c.type = 'checklist' AND c.status = 'published')
  ),
  'media', json_build_object(
    'resources', (SELECT json_agg(row_to_json(r)) FROM media_resources r WHERE r.status = 'published'),
    'associations', (SELECT json_agg(row_to_json(a)) FROM content_resource_associations a)
  ),
  'links', json_build_object(
    'protocol_to_guide', (SELECT json_agg(json_build_object('protocol_id', p.id, 'guide_id', g.id)) FROM content_items p, content_items g WHERE p.type = 'protocol' AND g.type = 'guide' AND g.id = ANY(p.related_guide_ids)),
    'guide_to_protocol', (SELECT json_agg(json_build_object('guide_id', g.id, 'protocol_id', p.id)) FROM content_items g, content_items p WHERE g.type = 'guide' AND p.type = 'protocol' AND p.id = ANY(g.related_protocol_ids)),
    'manual_to_protocols', (SELECT json_agg(json_build_object('manual_id', m.id, 'protocol_ids', m.related_protocol_ids)) FROM content_items m WHERE m.type = 'manual' AND m.related_protocol_ids IS NOT NULL)
  )
) as content_pack;

📥 CONSUMO EN APP

Cache Local (IndexedDB)

interface CachedContentPack {
  pack: ContentPack;
  cached_at: string;
  expires_at: string;
}

// Almacenar pack
async function cacheContentPack(pack: ContentPack): Promise<void> {
  const cached: CachedContentPack = {
    pack,
    cached_at: new Date().toISOString(),
    expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 días
  };
  
  await db.put('content_packs', cached);
}

// Obtener pack cacheado
async function getCachedContentPack(): Promise<ContentPack | null> {
  const cached = await db.get('content_packs', 'latest');
  
  if (!cached) return null;
  
  // Verificar expiración
  if (new Date(cached.expires_at) < new Date()) {
    return null;
  }
  
  // Verificar integridad
  if (!verifyPackIntegrity(cached.pack)) {
    return null;
  }
  
  return cached.pack;
}

VALIDACIÓN

Checklist de Validación

  • Metadata completa y válida
  • Hash calculado correctamente
  • Solo contenido published
  • Solo última versión de cada item
  • Recursos asociados existen
  • Enlaces bidireccionales correctos
  • JSON válido y parseable
  • Tamaño razonable (< 10MB recomendado)

Fin de la Especificación