{
  "schemaVersion": 1,
  "methodology": {
    "name": "Puntuación transparente de auditoría técnica externa inicial",
    "scope": "Evalúa únicamente señales observables en la respuesta HTTP y el HTML inicial. No ejecuta JavaScript ni equivale a Lighthouse, pentest, auditoría legal o revisión autenticada.",
    "categoryScore": "Para cada categoría: round(100 * suma de pesos aprobados / suma de pesos evaluables). Las reglas no verificables se excluyen de ambos sumandos.",
    "generalScore": "Promedio ponderado de las categorías evaluables usando generalWeight. Los pesos de categorías no verificables se excluyen y el denominador se normaliza.",
    "rounding": "Entero más cercano entre 0 y 100.",
    "availability": "Si una regla declara availabilityPath, solo se evalúa cuando ese valor es true. Una ruta ausente, un valor inválido o una comprobación inconclusa produce no-verificable, nunca un fallo.",
    "coverage": {
      "category": "round(100 * peso evaluable / peso total configurado de la categoría)",
      "general": "round(suma de generalWeight * coberturaCategoria / 100)",
      "minimumCategoryPercent": 40,
      "minimumGeneralPercent": 60,
      "insufficientCoverageStatus": "no-verificable",
      "display": "Toda puntuación debe mostrarse junto a su porcentaje de cobertura. Si no alcanza el mínimo, la puntuación pública es null y el estado es no-verificable."
    },
    "operators": {
      "equals": "Aprueba cuando el valor observado es idéntico a test.value.",
      "present": "Aprueba cuando la ruta existe y contiene un valor no nulo y no vacío.",
      "min": "Aprueba cuando el número observado es mayor o igual a test.value.",
      "max": "Aprueba cuando el número observado es menor o igual a test.value.",
      "range": "Aprueba cuando el número observado está dentro del intervalo inclusivo [test.value[0], test.value[1]].",
      "ratio": "Aprueba cuando la proporción observada, entre 0 y 1, es mayor o igual a test.value."
    },
    "ratioDenominators": {
      "openGraph": "Propiedades requeridas observables: og:title, og:description, og:image y og:url.",
      "cookieSecure": "Cookies recibidas en una respuesta HTTPS para las que el atributo Secure es aplicable.",
      "cookieHttpOnly": "Cookies observables clasificadas con evidencia como sesión o autenticación; las demás se excluyen.",
      "cookieSameSite": "Cookies observables para las que se pudo analizar Set-Cookie.",
      "imageDimensions": "Elementos img del HTML inicial, excluyendo data URI, SVG inline y elementos sin una URL analizable.",
      "responsiveImages": "Imágenes de contenido elegibles; se excluyen íconos, píxeles de seguimiento y recursos pequeños cuando esa exclusión es verificable.",
      "imageAlt": "Elementos img del HTML inicial. alt vacío cuenta como atributo presente porque puede representar una imagen decorativa; la calidad semántica no se infiere.",
      "headingText": "Encabezados h1-h6 encontrados en el HTML inicial.",
      "formLabels": "Controles etiquetables encontrados dentro de formularios; solo se evalúa con denominador mayor que cero.",
      "formActions": "Formularios con action observable; acciones relativas y HTTPS cuentan como seguras."
    },
    "ruleResult": {
      "pass": "correcto",
      "fail": "Usar la prioridad de la regla para ordenar el hallazgo; no convertir automáticamente una falla aislada en una afirmación de vulnerabilidad.",
      "unavailable": "no-verificable"
    },
    "limitations": [
      "TTFB es una medición puntual desde el servidor del auditor y no representa rendimiento global ni rendimiento de backend.",
      "Las señales responsive, srcset y dimensiones no demuestran compatibilidad móvil completa porque no existe renderizado real.",
      "Solo se observan cookies de la respuesta auditada; no se ejecuta JavaScript ni se inspeccionan sesiones posteriores.",
      "No se miden Core Web Vitals, firewall, bases de datos, vulnerabilidades internas, flujos autenticados, integraciones ni automatizaciones."
    ]
  },
  "statuses": {
    "correcto": {
      "label": "Correcto",
      "minScore": 90,
      "maxScore": 100
    },
    "atencion": {
      "label": "Atención",
      "minScore": 75,
      "maxScore": 89
    },
    "mejorable": {
      "label": "Mejorable",
      "minScore": 50,
      "maxScore": 74
    },
    "critico": {
      "label": "Crítico",
      "minScore": 0,
      "maxScore": 49
    },
    "no-verificable": {
      "label": "No verificable desde esta auditoría externa",
      "minScore": null,
      "maxScore": null
    }
  },
  "categories": {
    "seo": {
      "label": "SEO técnico",
      "generalWeight": 25
    },
    "security": {
      "label": "Seguridad básica",
      "generalWeight": 25
    },
    "performance": {
      "label": "Rendimiento inicial",
      "generalWeight": 20
    },
    "mobile": {
      "label": "Preparación móvil",
      "generalWeight": 15
    },
    "content": {
      "label": "Contenido esencial",
      "generalWeight": 15
    }
  },
  "rules": [
    {
      "id": "seo-title-present",
      "category": "seo",
      "weight": 12,
      "expected": "correcto",
      "description": "La página incluye un title observable.",
      "priority": "high",
      "recommendation": "Añadir un title único y descriptivo para la página.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.title.value",
        "operator": "present",
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-title-length",
      "category": "seo",
      "weight": 5,
      "expected": "correcto",
      "description": "El title tiene entre 10 y 60 caracteres como referencia inicial.",
      "priority": "medium",
      "recommendation": "Ajustar el title para que sea claro, específico y legible en resultados de búsqueda.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.title.length",
        "operator": "range",
        "value": [
          10,
          60
        ],
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-meta-description-present",
      "category": "seo",
      "weight": 10,
      "expected": "correcto",
      "description": "Existe una meta description observable.",
      "priority": "high",
      "recommendation": "Añadir una descripción específica que resuma la página sin promesas engañosas.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.metaDescription.value",
        "operator": "present",
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-meta-description-length",
      "category": "seo",
      "weight": 4,
      "expected": "correcto",
      "description": "La meta description tiene entre 70 y 160 caracteres como referencia inicial.",
      "priority": "low",
      "recommendation": "Resumir el contenido principal en una descripción concisa y diferenciada.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.metaDescription.length",
        "operator": "range",
        "value": [
          70,
          160
        ],
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-canonical-valid",
      "category": "seo",
      "weight": 10,
      "expected": "correcto",
      "description": "La página declara una URL canonical absoluta y válida.",
      "priority": "high",
      "recommendation": "Declarar una canonical absoluta que represente la versión preferida de la página.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.canonical.valid",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-robots-accessible",
      "category": "seo",
      "weight": 8,
      "expected": "correcto",
      "description": "robots.txt fue comprobado y responde de forma accesible.",
      "priority": "medium",
      "recommendation": "Publicar un robots.txt válido y revisar que no bloquee contenido importante por accidente.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.robots.accessible",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.seo.robots.checked"
      }
    },
    {
      "id": "seo-sitemap-accessible",
      "category": "seo",
      "weight": 8,
      "expected": "correcto",
      "description": "Se descubrió y comprobó un sitemap XML accesible.",
      "priority": "medium",
      "recommendation": "Publicar un sitemap XML y declararlo en robots.txt cuando corresponda.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.sitemap.accessible",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.seo.sitemap.checked"
      }
    },
    {
      "id": "seo-favicon-present",
      "category": "seo",
      "weight": 5,
      "expected": "correcto",
      "description": "El HTML inicial declara una señal de favicon.",
      "priority": "low",
      "recommendation": "Declarar un favicon válido para reforzar identificación y consistencia del sitio.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.favicon.present",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-open-graph",
      "category": "seo",
      "weight": 8,
      "expected": "correcto",
      "description": "Están presentes al menos tres de las cuatro propiedades Open Graph esenciales.",
      "priority": "medium",
      "recommendation": "Completar og:title, og:description, og:image y og:url con valores coherentes.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.openGraph.completenessRatio",
        "operator": "ratio",
        "value": 0.75,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-h1-single",
      "category": "seo",
      "weight": 10,
      "expected": "correcto",
      "description": "El HTML inicial contiene exactamente un H1.",
      "priority": "high",
      "recommendation": "Usar un H1 principal claro y evitar múltiples títulos principales ambiguos.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.headings.h1Count",
        "operator": "equals",
        "value": 1,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-html-lang",
      "category": "seo",
      "weight": 5,
      "expected": "correcto",
      "description": "El elemento html declara un idioma.",
      "priority": "medium",
      "recommendation": "Añadir un atributo lang válido que corresponda al idioma principal.",
      "difficulty": "low",
      "test": {
        "path": "observations.seo.htmlLang.value",
        "operator": "present",
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-jsonld-valid",
      "category": "seo",
      "weight": 7,
      "expected": "correcto",
      "description": "Existe al menos un bloque JSON-LD que pudo analizarse como JSON.",
      "priority": "medium",
      "recommendation": "Añadir datos estructurados pertinentes y validar su sintaxis y tipo.",
      "difficulty": "medium",
      "test": {
        "path": "observations.seo.jsonLd.validCount",
        "operator": "min",
        "value": 1,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "seo-heading-hierarchy",
      "category": "seo",
      "weight": 8,
      "expected": "correcto",
      "description": "La jerarquía aproximada de encabezados no presenta saltos observables.",
      "priority": "medium",
      "recommendation": "Ordenar los encabezados por niveles semánticos sin usarlos solo por su apariencia.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.headings.approximateHierarchyValid",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "security-https",
      "category": "security",
      "weight": 20,
      "expected": "correcto",
      "description": "La URL final utiliza HTTPS.",
      "priority": "critical",
      "recommendation": "Configurar HTTPS válido y redirigir el tráfico HTTP al origen seguro.",
      "difficulty": "medium",
      "test": {
        "path": "observations.connection.finalHttps",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.connection.completed"
      }
    },
    {
      "id": "security-hsts",
      "category": "security",
      "weight": 14,
      "expected": "correcto",
      "description": "La respuesta final incluye Strict-Transport-Security.",
      "priority": "high",
      "recommendation": "Configurar HSTS después de confirmar que todo el sitio y sus subdominios aplicables funcionan por HTTPS.",
      "difficulty": "medium",
      "test": {
        "path": "observations.security.headers.hsts.present",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "security-csp",
      "category": "security",
      "weight": 14,
      "expected": "correcto",
      "description": "La respuesta final incluye Content-Security-Policy.",
      "priority": "high",
      "recommendation": "Definir una CSP compatible con los recursos reales y desplegarla de forma gradual.",
      "difficulty": "high",
      "test": {
        "path": "observations.security.headers.csp.present",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "security-nosniff",
      "category": "security",
      "weight": 10,
      "expected": "correcto",
      "description": "X-Content-Type-Options contiene nosniff.",
      "priority": "medium",
      "recommendation": "Enviar X-Content-Type-Options: nosniff en las respuestas aplicables.",
      "difficulty": "low",
      "test": {
        "path": "observations.security.headers.nosniff.enabled",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "security-referrer-policy",
      "category": "security",
      "weight": 8,
      "expected": "correcto",
      "description": "La respuesta declara Referrer-Policy.",
      "priority": "medium",
      "recommendation": "Definir una política de referencia acorde con la privacidad y las integraciones necesarias.",
      "difficulty": "low",
      "test": {
        "path": "observations.security.headers.referrerPolicy.value",
        "operator": "present",
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "security-permissions-policy",
      "category": "security",
      "weight": 8,
      "expected": "correcto",
      "description": "La respuesta declara Permissions-Policy.",
      "priority": "low",
      "recommendation": "Restringir capacidades del navegador que el sitio no utiliza.",
      "difficulty": "medium",
      "test": {
        "path": "observations.security.headers.permissionsPolicy.value",
        "operator": "present",
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "security-frame-protection",
      "category": "security",
      "weight": 10,
      "expected": "correcto",
      "description": "Existe protección observable mediante X-Frame-Options o frame-ancestors en CSP.",
      "priority": "high",
      "recommendation": "Restringir el framing con CSP frame-ancestors y mantener X-Frame-Options cuando se requiera compatibilidad.",
      "difficulty": "medium",
      "test": {
        "path": "observations.security.headers.frameProtection.present",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "security-cookie-secure",
      "category": "security",
      "weight": 6,
      "expected": "correcto",
      "description": "Todas las cookies elegibles observadas usan Secure.",
      "priority": "high",
      "recommendation": "Marcar Secure las cookies que solo deben viajar por HTTPS.",
      "difficulty": "low",
      "test": {
        "path": "observations.security.cookies.secureRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.security.cookies.secureApplicable"
      }
    },
    {
      "id": "security-cookie-httponly",
      "category": "security",
      "weight": 5,
      "expected": "correcto",
      "description": "Todas las cookies sensibles elegibles observadas usan HttpOnly.",
      "priority": "high",
      "recommendation": "Marcar HttpOnly las cookies de sesión o autenticación que no necesita leer JavaScript.",
      "difficulty": "low",
      "test": {
        "path": "observations.security.cookies.httpOnlyRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.security.cookies.httpOnlyApplicable"
      }
    },
    {
      "id": "security-cookie-samesite",
      "category": "security",
      "weight": 5,
      "expected": "correcto",
      "description": "Todas las cookies observables declaran SameSite.",
      "priority": "medium",
      "recommendation": "Definir SameSite de acuerdo con el flujo real de navegación e integraciones.",
      "difficulty": "low",
      "test": {
        "path": "observations.security.cookies.sameSiteRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.security.cookies.sameSiteApplicable"
      }
    },
    {
      "id": "performance-ttfb",
      "category": "performance",
      "weight": 20,
      "expected": "correcto",
      "description": "El tiempo puntual hasta el primer byte no supera 800 ms.",
      "priority": "high",
      "recommendation": "Revisar hosting, caché y cadena de redirecciones; confirmar con varias mediciones antes de concluir.",
      "difficulty": "medium",
      "test": {
        "path": "observations.performance.ttfbMs",
        "operator": "max",
        "value": 800,
        "availabilityPath": "observations.performance.ttfbAvailable"
      }
    },
    {
      "id": "performance-compression",
      "category": "performance",
      "weight": 15,
      "expected": "correcto",
      "description": "La respuesta de texto elegible usa compresión HTTP observable.",
      "priority": "high",
      "recommendation": "Activar Brotli o gzip para contenido de texto cuando sea aplicable.",
      "difficulty": "low",
      "test": {
        "path": "observations.performance.compression.enabled",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.performance.compression.applicable"
      }
    },
    {
      "id": "performance-cache-control",
      "category": "performance",
      "weight": 12,
      "expected": "correcto",
      "description": "La respuesta final declara Cache-Control.",
      "priority": "medium",
      "recommendation": "Definir una política de caché coherente con la naturaleza pública o dinámica del contenido.",
      "difficulty": "medium",
      "test": {
        "path": "observations.performance.cacheControl.value",
        "operator": "present",
        "availabilityPath": "observations.response.headersAvailable"
      }
    },
    {
      "id": "performance-html-size",
      "category": "performance",
      "weight": 15,
      "expected": "correcto",
      "description": "El HTML inicial descargado no supera 500000 bytes.",
      "priority": "medium",
      "recommendation": "Reducir marcado repetido, datos embebidos y contenido innecesario en la respuesta inicial.",
      "difficulty": "medium",
      "test": {
        "path": "observations.performance.htmlBytes",
        "operator": "max",
        "value": 500000,
        "availabilityPath": "observations.performance.htmlSizeAvailable"
      }
    },
    {
      "id": "performance-script-count",
      "category": "performance",
      "weight": 12,
      "expected": "correcto",
      "description": "El HTML inicial referencia como máximo 20 scripts.",
      "priority": "medium",
      "recommendation": "Eliminar dependencias innecesarias y cargar scripts según necesidad.",
      "difficulty": "medium",
      "test": {
        "path": "observations.performance.resources.scriptCount",
        "operator": "max",
        "value": 20,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "performance-style-count",
      "category": "performance",
      "weight": 10,
      "expected": "correcto",
      "description": "El HTML inicial referencia como máximo 10 hojas de estilo externas.",
      "priority": "low",
      "recommendation": "Consolidar o cargar por contexto las hojas de estilo sin bloquear contenido necesario.",
      "difficulty": "medium",
      "test": {
        "path": "observations.performance.resources.stylesheetCount",
        "operator": "max",
        "value": 10,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "performance-image-count",
      "category": "performance",
      "weight": 6,
      "expected": "correcto",
      "description": "El HTML inicial contiene como máximo 30 imágenes.",
      "priority": "low",
      "recommendation": "Revisar si todas las imágenes iniciales son necesarias y aplicar carga diferida cuando corresponda.",
      "difficulty": "low",
      "test": {
        "path": "observations.performance.resources.imageCount",
        "operator": "max",
        "value": 30,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "performance-render-blocking",
      "category": "performance",
      "weight": 10,
      "expected": "correcto",
      "description": "Se observan como máximo 6 recursos potencialmente bloqueantes en el HTML inicial.",
      "priority": "medium",
      "recommendation": "Diferir recursos no críticos y preservar solo los necesarios para el primer render.",
      "difficulty": "medium",
      "test": {
        "path": "observations.performance.resources.approximateBlockingCount",
        "operator": "max",
        "value": 6,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "mobile-viewport",
      "category": "mobile",
      "weight": 40,
      "expected": "correcto",
      "description": "Existe un meta viewport válido con una señal de ancho adaptable.",
      "priority": "critical",
      "recommendation": "Configurar un viewport adaptable sin impedir innecesariamente el zoom.",
      "difficulty": "low",
      "test": {
        "path": "observations.mobile.viewport.valid",
        "operator": "equals",
        "value": true,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "mobile-image-dimensions",
      "category": "mobile",
      "weight": 20,
      "expected": "correcto",
      "description": "Al menos 80% de las imágenes elegibles define dimensiones observables.",
      "priority": "medium",
      "recommendation": "Definir width y height o una relación de aspecto equivalente para reducir saltos de diseño.",
      "difficulty": "low",
      "test": {
        "path": "observations.mobile.images.dimensionsRatio",
        "operator": "ratio",
        "value": 0.8,
        "availabilityPath": "observations.mobile.images.dimensionsApplicable"
      }
    },
    {
      "id": "mobile-responsive-images",
      "category": "mobile",
      "weight": 20,
      "expected": "correcto",
      "description": "Al menos 50% de las imágenes de contenido elegibles usa srcset, sizes o picture.",
      "priority": "medium",
      "recommendation": "Servir variantes de imagen adecuadas al ancho y densidad de pantalla.",
      "difficulty": "medium",
      "test": {
        "path": "observations.mobile.images.responsiveRatio",
        "operator": "ratio",
        "value": 0.5,
        "availabilityPath": "observations.mobile.images.responsiveApplicable"
      }
    },
    {
      "id": "mobile-responsive-signals",
      "category": "mobile",
      "weight": 20,
      "expected": "correcto",
      "description": "El HTML inicial contiene al menos una señal responsive verificable.",
      "priority": "medium",
      "recommendation": "Incorporar señales adaptables observables y validar después el diseño con renderizado en dispositivos reales.",
      "difficulty": "medium",
      "test": {
        "path": "observations.mobile.responsiveSignals.count",
        "operator": "min",
        "value": 1,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "content-h1-single",
      "category": "content",
      "weight": 18,
      "expected": "correcto",
      "description": "El contenido inicial presenta exactamente un encabezado principal H1.",
      "priority": "high",
      "recommendation": "Definir un encabezado principal que comunique con claridad el propósito de la página.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.headings.h1Count",
        "operator": "equals",
        "value": 1,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "content-headings-present",
      "category": "content",
      "weight": 12,
      "expected": "correcto",
      "description": "Existe al menos un encabezado h1-h6 en el HTML inicial.",
      "priority": "medium",
      "recommendation": "Estructurar el contenido con encabezados descriptivos.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.headings.totalCount",
        "operator": "min",
        "value": 1,
        "availabilityPath": "observations.document.htmlParsed"
      }
    },
    {
      "id": "content-heading-text",
      "category": "content",
      "weight": 12,
      "expected": "correcto",
      "description": "Todos los encabezados observados contienen texto accesible no vacío.",
      "priority": "medium",
      "recommendation": "Evitar encabezados vacíos o compuestos solo por elementos sin nombre accesible.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.headings.textRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.content.headings.textApplicable"
      }
    },
    {
      "id": "content-image-alt",
      "category": "content",
      "weight": 22,
      "expected": "correcto",
      "description": "Todas las imágenes del HTML inicial incluyen el atributo alt.",
      "priority": "high",
      "recommendation": "Añadir texto alternativo útil o alt vacío cuando la imagen sea realmente decorativa.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.images.altAttributeRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.content.images.altApplicable"
      }
    },
    {
      "id": "content-insecure-links",
      "category": "content",
      "weight": 22,
      "expected": "correcto",
      "description": "No hay enlaces HTTP inseguros observables dentro de una página HTTPS.",
      "priority": "high",
      "recommendation": "Actualizar enlaces HTTP a destinos HTTPS válidos cuando estén disponibles.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.links.insecureCount",
        "operator": "max",
        "value": 0,
        "availabilityPath": "observations.content.links.insecureCheckApplicable"
      }
    },
    {
      "id": "content-form-labels",
      "category": "content",
      "weight": 8,
      "expected": "correcto",
      "description": "Todos los controles de formulario elegibles tienen una etiqueta observable.",
      "priority": "high",
      "recommendation": "Asociar labels o nombres accesibles explícitos a cada control.",
      "difficulty": "low",
      "test": {
        "path": "observations.content.forms.labelRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.content.forms.labelsApplicable"
      }
    },
    {
      "id": "content-form-actions",
      "category": "content",
      "weight": 6,
      "expected": "correcto",
      "description": "Todas las acciones de formulario observables son relativas o HTTPS.",
      "priority": "critical",
      "recommendation": "Enviar formularios únicamente a destinos HTTPS controlados y validar también en el servidor.",
      "difficulty": "medium",
      "test": {
        "path": "observations.content.forms.safeActionRatio",
        "operator": "ratio",
        "value": 1,
        "availabilityPath": "observations.content.forms.actionsApplicable"
      }
    }
  ],
  "recommendationMatrix": {
    "pricingSource": "data/pricing.json",
    "policy": [
      "Una recomendación de corrección requiere al menos un hallazgo verificable; no se deriva de reglas no verificables.",
      "La mantención preventiva solo puede sugerirse sin hallazgos cuando la puntuación general es al menos 90 y la cobertura general alcanza 60%.",
      "La auditoría no recomienda automáticamente el plan más caro.",
      "Los planes publicados se referencian solo por su id y sus precios se leen desde pricingSource.",
      "La corrección de una web existente se ofrece primero como evaluación técnica personalizada, no como un plan o precio inventado."
    ],
    "rules": [
      {
        "id": "verified-clean-site-maintenance",
        "trigger": {
          "type": "score-and-coverage",
          "maximumVerifiableFailures": 0,
          "minimumGeneralScore": 90,
          "minimumGeneralCoverage": 60
        },
        "recommendations": [
          {
            "kind": "plan",
            "planId": "mantencion-esencial"
          }
        ]
      },
      {
        "id": "existing-site-technical-rescue",
        "trigger": {
          "type": "failed-rules",
          "minimumVerifiableFailures": 1
        },
        "recommendations": [
          {
            "kind": "custom",
            "label": "Auditoría y rescate",
            "price": "Evaluación personalizada",
            "publishedPlan": false
          }
        ]
      },
      {
        "id": "seo-content-or-declared-design",
        "trigger": {
          "type": "any",
          "failedCategories": [
            "seo",
            "content"
          ],
          "optionalVerifiedContextPath": "context.designIssueVerified",
          "requiresVerifiable": true
        },
        "recommendations": [
          {
            "kind": "plan",
            "planId": "web-informativa-esencial"
          },
          {
            "kind": "plan",
            "planId": "web-profesional"
          },
          {
            "kind": "plan",
            "planId": "mantencion-esencial"
          }
        ]
      },
      {
        "id": "verified-booking-or-agenda",
        "trigger": {
          "type": "all",
          "tests": [
            {
              "path": "observations.content.forms.detected",
              "operator": "equals",
              "value": true
            },
            {
              "path": "observations.capabilities.bookingOrAgendaVerified",
              "operator": "equals",
              "value": true
            }
          ],
          "requiresVerifiable": true
        },
        "recommendations": [
          {
            "kind": "plan",
            "planId": "reservas"
          }
        ]
      },
      {
        "id": "verified-persistence-or-admin",
        "trigger": {
          "type": "any",
          "tests": [
            {
              "path": "observations.capabilities.persistenceVerified",
              "operator": "equals",
              "value": true
            },
            {
              "path": "observations.capabilities.adminPanelVerified",
              "operator": "equals",
              "value": true
            }
          ],
          "requiresEvidenceBeyondInitialHtml": true
        },
        "recommendations": [
          {
            "kind": "plan",
            "planId": "sistema-completo"
          }
        ]
      }
    ],
    "neverInferFromInitialHtml": [
      "integrations",
      "automation",
      "artificial-intelligence",
      "database",
      "backend-security",
      "authenticated-workflows"
    ],
    "restrictedRecommendations": [
      {
        "planId": "ecosistema-digital",
        "reason": "Integraciones, automatización e IA requieren levantamiento verificable fuera de esta auditoría."
      },
      {
        "planId": "mantencion-sistemas",
        "reason": "Solo corresponde cuando se confirma que existe una web dinámica, panel o sistema; no se infiere desde el HTML inicial."
      }
    ]
  }
}
