diff --git a/ucepsa/edge-oee-demo/docs/runbooks/107_context_rollout_readiness_shadow_v0.3.9.md b/ucepsa/edge-oee-demo/docs/runbooks/107_context_rollout_readiness_shadow_v0.3.9.md new file mode 100644 index 0000000..6706dbe --- /dev/null +++ b/ucepsa/edge-oee-demo/docs/runbooks/107_context_rollout_readiness_shadow_v0.3.9.md @@ -0,0 +1,161 @@ +# UCEPSA — Readiness de rollout del contexto canónico v0.3.9 + +## Objetivo + +Responder con números a esta pregunta: + +> ¿Qué le falta a cada cortadora antes de poder usar el contexto canónico en un +> piloto controlado? + +La versión no activa el Ledger oficial. Incluso una máquina en +`READY_FOR_CONTROLLED_SHADOW` conserva: + +```text +ready_for_official_ledger = false +official_ledger_eligible = false +``` + +## Criterios iniciales + +Ventana: + +```text +7 días +``` + +Umbrales configurables: + +```text +mínimo observado 8 h +contexto confirmado mínimo 90 % +contexto sin confirmar máximo 10 % +conflicto máximo 0 min +revisiones pendientes máximas 0 +antigüedad máxima revisión 24 h +DQ crítica máxima 0 +sync requerido HEALTHY +colector requerido HEALTHY +``` + +Estos valores son iniciales y pueden revisarse antes de octubre. + +## Estados + +```text +TECHNICAL_BLOCKED +INSUFFICIENT_OBSERVATION +REVIEW_BACKLOG +COVERAGE_NOT_READY +READY_FOR_CONTROLLED_SHADOW +``` + +La evaluación se detiene en el primer bloqueo operativo relevante. + +## Beneficios + +### Evita activar por intuición + +No basta con que un dashboard se vea bien. La vista exige salud técnica, +observación suficiente, ausencia de conflictos, backlog resuelto y cobertura +mínima. + +### Convierte el rollout en una lista de acciones + +Ejemplos: + +```text +CORT-00 +REVIEW_BACKLOG +→ preguntar a Producción por el episodio pendiente + +CORT-01 +INSUFFICIENT_OBSERVATION +→ acumular horas reales + +CORT-02 +COVERAGE_NOT_READY +→ aumentar uso correcto de Shop Floor +``` + +### Separa readiness SHADOW de activación oficial + +El sistema puede indicar que una máquina está preparada para una prueba +controlada sin permitir todavía que genere pérdidas oficiales. + +### Da una evolución medible + +Cada semana puede observarse: + +```text +sube % confirmado +baja % sin confirmar +desaparece backlog +conflictos siguen en cero +``` + +## Objetos + +```text +context_rollout_readiness_policies + +li_canonical_context_readiness_metrics_v1 +li_canonical_context_readiness_gate_v1 +li_context_rollout_blockers_v1 +li_context_rollout_readiness_summary_v1 +``` + +## Despliegue + +```bash +FILE=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/sql/versions/111_ucepsa_context_rollout_readiness_shadow_v039.sql + +docker exec -i mv_ucepsa_postgres_hot sh -lc \ + 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ + < "$FILE" +``` + +## Validación + +```bash +VALIDATION=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/ops/validate_context_rollout_readiness_v039.sql + +docker exec -i mv_ucepsa_postgres_hot sh -lc \ + 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ + < "$VALIDATION" +``` + +Valores obligatorios: + +```text +machine_rows = 3 +distinct_machines = 3 +official_metric_rows = 0 + +negative_observed_hours = 0 +negative_confirmed_hours = 0 +negative_unconfirmed_hours = 0 +negative_conflict_minutes = 0 + +invalid_confirmed_pct = 0 +invalid_unconfirmed_pct = 0 +invalid_operator_pct = 0 + +activity_decomposition_errors = 0 +ready_gate_mismatches = 0 + +official_gate_rows = 0 +official_blocker_rows = 0 +official_operator_queue_rows = 0 +``` + +## Dashboard + +Se añaden al dashboard existente: + +```text +Readiness de contexto por máquina +Bloqueos de rollout +Resumen de readiness +``` + +El UID no cambia y el refresco permanece en 15 segundos. diff --git a/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json b/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json index 3abe94a..33fd514 100644 --- a/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json +++ b/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json @@ -15,7 +15,7 @@ } ] }, - "description": "Supervisión SHADOW de máquina, Odoo Shop Floor, contextos explícitos, histórico de incidencias y arbitraje canónico. Cero fuentes autoritativas queda sin confirmar y más de una queda bloqueada por conflicto. No alimenta el Ledger oficial.", + "description": "Supervisión SHADOW de sesiones, incidencias, arbitraje canónico y readiness de rollout. Los criterios muestran qué falta antes de un piloto controlado; nunca habilitan el Ledger oficial.", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, @@ -1920,6 +1920,323 @@ ], "title": "Cobertura de contexto canónico · últimos 30 días", "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Readiness" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "READY_FOR_CONTROLLED_SHADOW": { + "text": "READY SHADOW", + "color": "green", + "index": 0 + }, + "TECHNICAL_BLOCKED": { + "text": "BLOQUEO TÉCNICO", + "color": "red", + "index": 1 + }, + "INSUFFICIENT_OBSERVATION": { + "text": "FALTA OBSERVACIÓN", + "color": "blue", + "index": 2 + }, + "REVIEW_BACKLOG": { + "text": "REVISIÓN PENDIENTE", + "color": "orange", + "index": 3 + }, + "COVERAGE_NOT_READY": { + "text": "COBERTURA INSUFICIENTE", + "color": "orange", + "index": 4 + } + } + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + }, + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Siguiente acción" + }, + "properties": [ + { + "id": "custom.width", + "value": 540 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 99 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n machine_id AS \"Máquina\",\n observed_activity_hours AS \"Horas observadas\",\n confirmed_context_pct AS \"% confirmado\",\n unconfirmed_context_pct AS \"% sin confirmar\",\n pending_review_count AS \"Revisiones pendientes\",\n oldest_pending_review_hours AS \"Antigüedad revisión h\",\n recent_conflict_minutes AS \"Conflicto min\",\n critical_session_dq_count AS \"DQ crítica\",\n readiness_status AS \"Readiness\",\n next_action AS \"Siguiente acción\"\nFROM\n mv_reports_ucepsa_prod\n .li_canonical_context_readiness_gate_v1\nORDER BY machine_id", + "refId": "A" + } + ], + "title": "Readiness de contexto por máquina · ventana de 7 días", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Severidad" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "INFO": { + "text": "INFO", + "color": "blue", + "index": 0 + }, + "WARNING": { + "text": "WARNING", + "color": "orange", + "index": 1 + }, + "CRITICAL": { + "text": "CRITICAL", + "color": "red", + "index": 2 + } + } + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "custom.width", + "value": 110 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción" + }, + "properties": [ + { + "id": "custom.width", + "value": 540 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 109 + }, + "id": 18, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n machine_id AS \"Máquina\",\n blocker_severity AS \"Severidad\",\n blocker_code AS \"Criterio\",\n current_value AS \"Valor actual\",\n target_value AS \"Objetivo\",\n recommended_action AS \"Acción\"\nFROM\n mv_reports_ucepsa_prod\n .li_context_rollout_blockers_v1\nORDER BY\n CASE blocker_severity\n WHEN 'CRITICAL' THEN 1\n WHEN 'WARNING' THEN 2\n ELSE 3\n END,\n machine_id,\n blocker_code", + "refId": "A" + } + ], + "title": "Bloqueos de rollout", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 119 + }, + "id": 19, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n overall_readiness_status AS \"Estado global\",\n machine_count AS \"Máquinas\",\n ready_machine_count AS \"Ready SHADOW\",\n technical_blocked_machine_count AS \"Bloqueo técnico\",\n insufficient_observation_machine_count AS \"Falta observación\",\n review_backlog_machine_count AS \"Revisión pendiente\",\n coverage_not_ready_machine_count AS \"Cobertura insuficiente\",\n blocker_count AS \"Bloqueos\",\n critical_blocker_count AS \"Bloqueos críticos\"\nFROM\n mv_reports_ucepsa_prod\n .li_context_rollout_readiness_summary_v1", + "refId": "A" + } + ], + "title": "Resumen de readiness del contexto canónico", + "type": "table" } ], "refresh": "15s", @@ -1935,7 +2252,10 @@ "v037", "canonical-context", "arbitration", - "v038" + "v038", + "readiness", + "rollout-gate", + "v039" ], "templating": { "list": [] diff --git a/ucepsa/edge-oee-demo/ops/deploy_context_rollout_readiness_dashboard_v039.sh b/ucepsa/edge-oee-demo/ops/deploy_context_rollout_readiness_dashboard_v039.sh new file mode 100755 index 0000000..0f781e3 --- /dev/null +++ b/ucepsa/edge-oee-demo/ops/deploy_context_rollout_readiness_dashboard_v039.sh @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +set -euo pipefail + +ACTION="${1:-deploy}" +GRAFANA_CONTAINER="${GRAFANA_CONTAINER:-mv_ucepsa_grafana}" +REPO_ROOT="${READINESS_DASHBOARD_REPO_ROOT:-/srv/mesavault/40-clients/ucepsa/edge-oee-demo}" +DASHBOARD_FILE="${READINESS_DASHBOARD_FILE:-$REPO_ROOT/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json}" +FOLDER_UID="${READINESS_DASHBOARD_FOLDER_UID:-ucepsa-mesavault}" +DASHBOARD_UID="ucepsa-shopfloor-context-health" +DATASOURCE_UID="bfnbcasbm6hhca" + +resolve_url() { + if [[ -n "${GRAFANA_URL:-}" ]]; then + printf '%s' "$GRAFANA_URL" + return + fi + + local port_line host_port container_ip + + port_line="$( + docker port "$GRAFANA_CONTAINER" 3000/tcp 2>/dev/null \ + | head -n 1 || true + )" + + if [[ -n "$port_line" ]]; then + host_port="${port_line##*:}" + printf 'http://127.0.0.1:%s' "$host_port" + return + fi + + container_ip="$( + docker inspect "$GRAFANA_CONTAINER" \ + --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' + )" + + if [[ -z "$container_ip" ]]; then + echo "ERROR: no se pudo resolver Grafana." >&2 + exit 1 + fi + + printf 'http://%s:3000' "$container_ip" +} + +if [[ -z "${GRAFANA_API_TOKEN:-}" ]]; then + cat >&2 <<'MSG' +ERROR: falta GRAFANA_API_TOKEN. + + read -rsp "Token temporal de Grafana: " GRAFANA_API_TOKEN + echo + export GRAFANA_API_TOKEN +MSG + exit 1 +fi + +export GRAFANA_URL_RESOLVED +GRAFANA_URL_RESOLVED="$(resolve_url)" + +python3 - \ + "$ACTION" \ + "$DASHBOARD_FILE" \ + "$FOLDER_UID" \ + "$DASHBOARD_UID" \ + "$DATASOURCE_UID" <<'PY' +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request + +( + action, + dashboard_file, + folder_uid, + dashboard_uid, + datasource_uid, +) = sys.argv[1:] + +base_url = os.environ[ + "GRAFANA_URL_RESOLVED" +].rstrip("/") +token = os.environ["GRAFANA_API_TOKEN"] + + +def request(method, path, payload=None): + body = None + headers = { + "Accept": "application/json", + "Authorization": f"Bearer {token}", + } + + if payload is not None: + body = json.dumps( + payload, + ensure_ascii=False, + ).encode("utf-8") + headers["Content-Type"] = "application/json" + + req = urllib.request.Request( + base_url + path, + data=body, + headers=headers, + method=method, + ) + + try: + with urllib.request.urlopen( + req, + timeout=20, + ) as response: + raw = response.read() + return ( + json.loads( + raw.decode("utf-8") + ) + if raw + else None + ) + except urllib.error.HTTPError as exc: + detail = exc.read().decode( + "utf-8", + errors="replace", + ) + raise RuntimeError( + f"Grafana API {method} {path}: " + f"HTTP {exc.code}: {detail}" + ) from exc + + +health = request("GET", "/api/health") +if not health or health.get("database") != "ok": + raise RuntimeError( + f"Grafana no está saludable: {health!r}" + ) + +if action == "deploy": + with open( + dashboard_file, + encoding="utf-8", + ) as handle: + dashboard = json.load(handle) + + result = request( + "POST", + "/api/dashboards/db", + { + "dashboard": dashboard, + "folderUid": folder_uid, + "overwrite": True, + "message": ( + "MESAVAULT context rollout " + "readiness SHADOW v0.3.9" + ), + }, + ) + + print(json.dumps( + { + "status": "deployed", + "dashboard_uid": dashboard_uid, + "folder_uid": folder_uid, + "response": result, + }, + ensure_ascii=False, + indent=2, + )) + +elif action == "validate": + result = request( + "GET", + ( + "/api/dashboards/uid/" + + urllib.parse.quote( + dashboard_uid + ) + ), + ) + + dashboard = result["dashboard"] + panels = { + panel.get("id"): panel + for panel in dashboard.get( + "panels", + [], + ) + } + + datasource_uids = sorted({ + panel.get( + "datasource", + {}, + ).get("uid") + for panel in dashboard.get( + "panels", + [], + ) + if isinstance( + panel.get("datasource"), + dict, + ) + and panel.get( + "datasource", + {}, + ).get("uid") + }) + + errors = [] + + if len( + dashboard.get( + "panels", + [], + ) + ) != 19: + errors.append( + "El dashboard no tiene 19 paneles" + ) + + if dashboard.get("refresh") != "15s": + errors.append( + "El refresco no está en 15s" + ) + + if datasource_uids != [datasource_uid]: + errors.append( + f"Datasource inesperado: " + f"{datasource_uids!r}" + ) + + expected = { + 17: + "li_canonical_context_readiness_gate_v1", + 18: + "li_context_rollout_blockers_v1", + 19: + "li_context_rollout_readiness_summary_v1", + } + + for panel_id, fragment in expected.items(): + sql = ( + panels.get( + panel_id, + {}, + ) + .get( + "targets", + [{}], + )[0] + .get( + "rawSql", + "", + ) + ) + if fragment not in sql: + errors.append( + f"Panel {panel_id} " + f"no consulta {fragment}" + ) + + output = { + "status": + "ok" if not errors else "error", + "dashboard_uid": + dashboard.get("uid"), + "folder_uid": + result.get( + "meta", + {}, + ).get("folderUid"), + "panel_count": + len( + dashboard.get( + "panels", + [], + ) + ), + "refresh": + dashboard.get("refresh"), + "datasource_uids": + datasource_uids, + "version": + dashboard.get("version"), + "url": + result.get( + "meta", + {}, + ).get("url"), + "readiness_panels": { + "machine_gate": + 17 in panels, + "blockers": + 18 in panels, + "summary": + 19 in panels, + }, + "errors": + errors, + } + + print(json.dumps( + output, + ensure_ascii=False, + indent=2, + )) + + if errors: + raise SystemExit(1) + +else: + raise SystemExit( + "Uso: " + "deploy_context_rollout_readiness_dashboard_v039.sh " + "{deploy|validate}" + ) +PY diff --git a/ucepsa/edge-oee-demo/ops/validate_context_rollout_readiness_v039.sql b/ucepsa/edge-oee-demo/ops/validate_context_rollout_readiness_v039.sql new file mode 100644 index 0000000..811f88d --- /dev/null +++ b/ucepsa/edge-oee-demo/ops/validate_context_rollout_readiness_v039.sql @@ -0,0 +1,179 @@ +\pset pager off + +\echo '=== 1. Objetos v0.3.9 ===' + +SELECT + to_regclass( + 'mv_loss_intelligence.context_rollout_readiness_policies' + ) AS policy_table, + to_regclass( + 'mv_reports_ucepsa_prod.li_canonical_context_readiness_metrics_v1' + ) AS metrics_view, + to_regclass( + 'mv_reports_ucepsa_prod.li_canonical_context_readiness_gate_v1' + ) AS gate_view, + to_regclass( + 'mv_reports_ucepsa_prod.li_context_rollout_blockers_v1' + ) AS blockers_view, + to_regclass( + 'mv_reports_ucepsa_prod.li_context_rollout_readiness_summary_v1' + ) AS summary_view; + +\echo '=== 2. Política activa ===' + +SELECT * +FROM + mv_loss_intelligence + .context_rollout_readiness_policies +WHERE tenant = 'ucepsa' + AND site = 'ucepsa_onpremise' + AND policy_code = + 'CUTTER_CONTEXT_ROLLOUT_READINESS_V1'; + +\echo '=== 3. Una fila por máquina ===' + +SELECT + COUNT(*) AS machine_rows, + COUNT( + DISTINCT machine_id + ) AS distinct_machines, + COUNT(*) FILTER ( + WHERE official_ledger_eligible + ) AS official_metric_rows +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_metrics_v1; + +\echo '=== 4. Rangos y duraciones ===' + +SELECT + COUNT(*) FILTER ( + WHERE observed_activity_hours < 0 + ) AS negative_observed_hours, + COUNT(*) FILTER ( + WHERE confirmed_context_hours < 0 + ) AS negative_confirmed_hours, + COUNT(*) FILTER ( + WHERE unconfirmed_context_hours < 0 + ) AS negative_unconfirmed_hours, + COUNT(*) FILTER ( + WHERE conflict_context_minutes < 0 + ) AS negative_conflict_minutes, + COUNT(*) FILTER ( + WHERE confirmed_context_pct < 0 + OR confirmed_context_pct > 100 + ) AS invalid_confirmed_pct, + COUNT(*) FILTER ( + WHERE unconfirmed_context_pct < 0 + OR unconfirmed_context_pct > 100 + ) AS invalid_unconfirmed_pct, + COUNT(*) FILTER ( + WHERE productive_operator_confirmed_pct < 0 + OR productive_operator_confirmed_pct > 100 + ) AS invalid_operator_pct +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_metrics_v1; + +\echo '=== 5. Descomposición de actividad ===' + +SELECT COUNT(*) AS activity_decomposition_errors +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_metrics_v1 +WHERE confirmed_context_hours + + unconfirmed_context_hours + + ( + conflict_context_minutes + / 60.0 + ) + > observed_activity_hours + 0.05; + +\echo '=== 6. Gate por máquina ===' + +SELECT + machine_id, + observed_activity_hours, + confirmed_context_pct, + unconfirmed_context_pct, + recent_conflict_minutes, + pending_review_count, + oldest_pending_review_hours, + critical_session_dq_count, + sync_health_status, + collector_health_status, + technical_gate_pass, + observation_gate_pass, + review_gate_pass, + coverage_gate_pass, + readiness_status, + readiness_stage, + next_action, + ready_for_official_ledger, + official_ledger_eligible +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 +ORDER BY machine_id; + +\echo '=== 7. Estados READY coherentes ===' + +SELECT COUNT(*) AS ready_gate_mismatches +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 +WHERE readiness_status = + 'READY_FOR_CONTROLLED_SHADOW' + AND NOT ( + technical_gate_pass + AND observation_gate_pass + AND review_gate_pass + AND coverage_gate_pass + ); + +\echo '=== 8. Bloqueos explicables ===' + +SELECT * +FROM + mv_reports_ucepsa_prod + .li_context_rollout_blockers_v1 +ORDER BY + CASE blocker_severity + WHEN 'CRITICAL' THEN 1 + WHEN 'WARNING' THEN 2 + ELSE 3 + END, + machine_id, + blocker_code; + +\echo '=== 9. Resumen ===' + +SELECT * +FROM + mv_reports_ucepsa_prod + .li_context_rollout_readiness_summary_v1; + +\echo '=== 10. Nada oficial ===' + +SELECT + ( + SELECT COUNT(*) + FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 + WHERE ready_for_official_ledger + OR official_ledger_eligible + ) AS official_gate_rows, + ( + SELECT COUNT(*) + FROM + mv_reports_ucepsa_prod + .li_context_rollout_blockers_v1 + WHERE official_ledger_eligible + ) AS official_blocker_rows, + ( + SELECT COUNT(*) + FROM + mv_reports_ucepsa_prod + .li_operator_stop_queue_v2 + ) AS official_operator_queue_rows; diff --git a/ucepsa/edge-oee-demo/sql/versions/111_ucepsa_context_rollout_readiness_shadow_v039.sql b/ucepsa/edge-oee-demo/sql/versions/111_ucepsa_context_rollout_readiness_shadow_v039.sql new file mode 100644 index 0000000..80ef274 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/versions/111_ucepsa_context_rollout_readiness_shadow_v039.sql @@ -0,0 +1,962 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS +mv_loss_intelligence.context_rollout_readiness_policies ( + tenant text NOT NULL, + site text NOT NULL, + policy_code text NOT NULL, + lookback_days integer NOT NULL + CHECK (lookback_days > 0), + minimum_observed_activity_hours numeric NOT NULL + CHECK (minimum_observed_activity_hours >= 0), + minimum_confirmed_context_pct numeric NOT NULL + CHECK ( + minimum_confirmed_context_pct >= 0 + AND minimum_confirmed_context_pct <= 100 + ), + maximum_unconfirmed_context_pct numeric NOT NULL + CHECK ( + maximum_unconfirmed_context_pct >= 0 + AND maximum_unconfirmed_context_pct <= 100 + ), + maximum_conflict_minutes numeric NOT NULL + CHECK (maximum_conflict_minutes >= 0), + maximum_pending_review_count integer NOT NULL + CHECK (maximum_pending_review_count >= 0), + maximum_oldest_pending_review_hours numeric NOT NULL + CHECK (maximum_oldest_pending_review_hours >= 0), + maximum_critical_session_dq_count integer NOT NULL + CHECK (maximum_critical_session_dq_count >= 0), + require_sync_healthy boolean NOT NULL DEFAULT true, + require_incident_collector_healthy boolean NOT NULL DEFAULT true, + active boolean NOT NULL DEFAULT true, + notes text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY ( + tenant, + site, + policy_code + ) +); + +COMMENT ON TABLE +mv_loss_intelligence.context_rollout_readiness_policies IS +'Umbrales SHADOW para medir preparación del contexto canónico antes de cualquier activación controlada. No habilita el Ledger oficial.'; + +INSERT INTO +mv_loss_intelligence.context_rollout_readiness_policies ( + tenant, + site, + policy_code, + lookback_days, + minimum_observed_activity_hours, + minimum_confirmed_context_pct, + maximum_unconfirmed_context_pct, + maximum_conflict_minutes, + maximum_pending_review_count, + maximum_oldest_pending_review_hours, + maximum_critical_session_dq_count, + require_sync_healthy, + require_incident_collector_healthy, + active, + notes +) +VALUES ( + 'ucepsa', + 'ucepsa_onpremise', + 'CUTTER_CONTEXT_ROLLOUT_READINESS_V1', + 7, + 8, + 90, + 10, + 0, + 0, + 24, + 0, + true, + true, + true, + 'Criterios iniciales para readiness SHADOW. Son configurables y no constituyen autorización de puesta en producción oficial.' +) +ON CONFLICT ( + tenant, + site, + policy_code +) +DO UPDATE SET + lookback_days = + EXCLUDED.lookback_days, + minimum_observed_activity_hours = + EXCLUDED.minimum_observed_activity_hours, + minimum_confirmed_context_pct = + EXCLUDED.minimum_confirmed_context_pct, + maximum_unconfirmed_context_pct = + EXCLUDED.maximum_unconfirmed_context_pct, + maximum_conflict_minutes = + EXCLUDED.maximum_conflict_minutes, + maximum_pending_review_count = + EXCLUDED.maximum_pending_review_count, + maximum_oldest_pending_review_hours = + EXCLUDED.maximum_oldest_pending_review_hours, + maximum_critical_session_dq_count = + EXCLUDED.maximum_critical_session_dq_count, + require_sync_healthy = + EXCLUDED.require_sync_healthy, + require_incident_collector_healthy = + EXCLUDED.require_incident_collector_healthy, + active = true, + notes = + EXCLUDED.notes, + updated_at = now(); + +CREATE OR REPLACE VIEW +mv_reports_ucepsa_prod.li_canonical_context_readiness_metrics_v1 +AS +WITH policy AS ( + SELECT * + FROM + mv_loss_intelligence + .context_rollout_readiness_policies + WHERE tenant = 'ucepsa' + AND site = 'ucepsa_onpremise' + AND policy_code = + 'CUTTER_CONTEXT_ROLLOUT_READINESS_V1' + AND active + ORDER BY updated_at DESC + LIMIT 1 +), +machines AS ( + SELECT + l.tenant, + l.site, + l.machine_id, + l.telemetry_fresh, + l.machine_running, + l.canonical_context_status + AS live_canonical_context_status, + l.canonical_context_key + AS live_canonical_context_key, + l.canonical_health_status + AS live_canonical_health_status, + l.order_alignment_status + AS live_order_alignment_status, + l.recommended_action + AS live_recommended_action + FROM + mv_reports_ucepsa_prod + .li_machine_canonical_context_live_v1 l +), +window_config AS ( + SELECT + p.*, + now() - ( + p.lookback_days + * interval '1 day' + ) AS window_from, + now() AS window_to + FROM policy p +), +clipped_segments AS ( + SELECT + s.machine_id, + s.canonical_context_status, + s.canonical_context_source, + s.canonical_productive_context, + s.individual_attribution_eligible, + s.review_required, + GREATEST( + s.segment_from, + w.window_from + ) AS clipped_from, + LEAST( + s.segment_to, + w.window_to + ) AS clipped_to, + EXTRACT( + EPOCH FROM ( + LEAST( + s.segment_to, + w.window_to + ) + - GREATEST( + s.segment_from, + w.window_from + ) + ) + )::numeric AS clipped_duration_s + FROM + mv_reports_ucepsa_prod + .li_canonical_context_segments_v1 s + CROSS JOIN window_config w + WHERE s.segment_to > + w.window_from + AND s.segment_from < + w.window_to + AND LEAST( + s.segment_to, + w.window_to + ) > + GREATEST( + s.segment_from, + w.window_from + ) +), +activity_rollup AS ( + SELECT + machine_id, + COALESCE( + SUM(clipped_duration_s), + 0::numeric + ) AS observed_activity_s, + COALESCE( + SUM(clipped_duration_s) FILTER ( + WHERE canonical_context_status NOT IN ( + 'UNCONFIRMED_CONTEXT', + 'CONTEXT_CONFLICT', + 'UNSUPPORTED_CONTEXT' + ) + ), + 0::numeric + ) AS confirmed_context_s, + COALESCE( + SUM(clipped_duration_s) FILTER ( + WHERE canonical_productive_context + ), + 0::numeric + ) AS productive_context_s, + COALESCE( + SUM(clipped_duration_s) FILTER ( + WHERE canonical_context_status = + 'UNCONFIRMED_CONTEXT' + ), + 0::numeric + ) AS unconfirmed_context_s, + COALESCE( + SUM(clipped_duration_s) FILTER ( + WHERE canonical_context_status = + 'CONTEXT_CONFLICT' + ), + 0::numeric + ) AS conflict_context_s, + COALESCE( + SUM(clipped_duration_s) FILTER ( + WHERE canonical_productive_context + AND individual_attribution_eligible + ), + 0::numeric + ) AS operator_confirmed_productive_s, + COUNT(*) AS segment_count, + COUNT(*) FILTER ( + WHERE review_required + ) AS review_segment_count + FROM clipped_segments + GROUP BY machine_id +), +pending_reviews AS ( + SELECT + b.machine_id, + COUNT(*) AS pending_review_count, + MIN(b.first_seen_at) + AS oldest_pending_review_at, + COUNT(*) FILTER ( + WHERE b.max_severity = + 'CRITICAL' + ) AS pending_critical_review_count + FROM + mv_reports_ucepsa_prod + .li_shopfloor_context_incident_backlog_v1 b + WHERE b.machine_id IS NOT NULL + GROUP BY b.machine_id +), +session_dq AS ( + SELECT + d.machine_id, + COUNT(*) FILTER ( + WHERE d.dq_status = + 'CRITICAL' + ) AS critical_session_dq_count, + COUNT(*) FILTER ( + WHERE d.dq_status = + 'WARNING' + ) AS warning_session_dq_count + FROM + mv_reports_ucepsa_prod + .li_shopfloor_session_data_quality_v1 d + GROUP BY d.machine_id +), +recent_conflicts AS ( + SELECT + c.machine_id, + COUNT(*) AS conflict_segment_count, + COALESCE( + SUM(c.segment_duration_s) + / 60.0, + 0::numeric + ) AS conflict_minutes + FROM + mv_reports_ucepsa_prod + .li_context_arbitration_conflicts_v1 c + CROSS JOIN window_config w + WHERE c.segment_to > + w.window_from + AND c.segment_from < + w.window_to + GROUP BY c.machine_id +), +sync_health AS ( + SELECT + sync_health_status, + severity AS sync_severity, + seconds_since_success + FROM + mv_reports_ucepsa_prod + .li_shopfloor_sync_health_v1 +), +collector_health AS ( + SELECT + collector_health_status, + severity AS collector_severity, + seconds_since_success + AS collector_seconds_since_success + FROM + mv_reports_ucepsa_prod + .li_shopfloor_context_incident_collector_health_v1 +) +SELECT + m.tenant, + m.site, + m.machine_id, + w.policy_code, + w.lookback_days, + w.window_from, + w.window_to, + w.minimum_observed_activity_hours, + w.minimum_confirmed_context_pct, + w.maximum_unconfirmed_context_pct, + w.maximum_conflict_minutes, + w.maximum_pending_review_count, + w.maximum_oldest_pending_review_hours, + w.maximum_critical_session_dq_count, + w.require_sync_healthy, + w.require_incident_collector_healthy, + m.telemetry_fresh, + m.machine_running, + m.live_canonical_context_status, + m.live_canonical_context_key, + m.live_canonical_health_status, + m.live_order_alignment_status, + m.live_recommended_action, + COALESCE( + a.segment_count, + 0 + ) AS segment_count, + COALESCE( + a.review_segment_count, + 0 + ) AS review_segment_count, + ROUND( + COALESCE( + a.observed_activity_s, + 0 + ) / 3600.0, + 2 + ) AS observed_activity_hours, + ROUND( + COALESCE( + a.confirmed_context_s, + 0 + ) / 3600.0, + 2 + ) AS confirmed_context_hours, + ROUND( + COALESCE( + a.productive_context_s, + 0 + ) / 3600.0, + 2 + ) AS productive_context_hours, + ROUND( + COALESCE( + a.unconfirmed_context_s, + 0 + ) / 3600.0, + 2 + ) AS unconfirmed_context_hours, + ROUND( + COALESCE( + a.conflict_context_s, + 0 + ) / 60.0, + 2 + ) AS conflict_context_minutes, + ROUND( + CASE + WHEN COALESCE( + a.observed_activity_s, + 0 + ) > 0 + THEN ( + 100.0 + * COALESCE( + a.confirmed_context_s, + 0 + ) + / a.observed_activity_s + ) + ELSE 0 + END::numeric, + 2 + ) AS confirmed_context_pct, + ROUND( + CASE + WHEN COALESCE( + a.observed_activity_s, + 0 + ) > 0 + THEN ( + 100.0 + * COALESCE( + a.unconfirmed_context_s, + 0 + ) + / a.observed_activity_s + ) + ELSE 0 + END::numeric, + 2 + ) AS unconfirmed_context_pct, + ROUND( + CASE + WHEN COALESCE( + a.productive_context_s, + 0 + ) > 0 + THEN ( + 100.0 + * COALESCE( + a.operator_confirmed_productive_s, + 0 + ) + / a.productive_context_s + ) + ELSE 0 + END::numeric, + 2 + ) AS productive_operator_confirmed_pct, + COALESCE( + p.pending_review_count, + 0 + ) AS pending_review_count, + COALESCE( + p.pending_critical_review_count, + 0 + ) AS pending_critical_review_count, + p.oldest_pending_review_at, + ROUND( + CASE + WHEN p.oldest_pending_review_at + IS NULL + THEN 0 + ELSE EXTRACT( + EPOCH FROM ( + now() + - p.oldest_pending_review_at + ) + )::numeric / 3600.0 + END, + 2 + ) AS oldest_pending_review_hours, + COALESCE( + d.critical_session_dq_count, + 0 + ) AS critical_session_dq_count, + COALESCE( + d.warning_session_dq_count, + 0 + ) AS warning_session_dq_count, + COALESCE( + c.conflict_segment_count, + 0 + ) AS conflict_segment_count, + ROUND( + COALESCE( + c.conflict_minutes, + 0 + ), + 2 + ) AS recent_conflict_minutes, + s.sync_health_status, + s.sync_severity, + s.seconds_since_success + AS sync_seconds_since_success, + ch.collector_health_status, + ch.collector_severity, + ch.collector_seconds_since_success, + false AS official_ledger_eligible, + now() AS observed_at +FROM machines m +CROSS JOIN window_config w +CROSS JOIN sync_health s +CROSS JOIN collector_health ch +LEFT JOIN activity_rollup a + ON a.machine_id = + m.machine_id +LEFT JOIN pending_reviews p + ON p.machine_id = + m.machine_id +LEFT JOIN session_dq d + ON d.machine_id = + m.machine_id +LEFT JOIN recent_conflicts c + ON c.machine_id = + m.machine_id; + +COMMENT ON VIEW +mv_reports_ucepsa_prod.li_canonical_context_readiness_metrics_v1 IS +'Métricas SHADOW por máquina para decidir qué falta antes de un piloto controlado de contexto canónico. No concede elegibilidad oficial.'; + +CREATE OR REPLACE VIEW +mv_reports_ucepsa_prod.li_canonical_context_readiness_gate_v1 +AS +SELECT + m.*, + ( + ( + NOT m.require_sync_healthy + OR m.sync_health_status = + 'HEALTHY' + ) + AND ( + NOT m.require_incident_collector_healthy + OR m.collector_health_status = + 'HEALTHY' + ) + AND m.critical_session_dq_count <= + m.maximum_critical_session_dq_count + AND m.recent_conflict_minutes <= + m.maximum_conflict_minutes + AND m.live_canonical_health_status <> + 'CRITICAL' + ) AS technical_gate_pass, + ( + m.observed_activity_hours >= + m.minimum_observed_activity_hours + ) AS observation_gate_pass, + ( + m.pending_review_count <= + m.maximum_pending_review_count + AND m.oldest_pending_review_hours <= + m.maximum_oldest_pending_review_hours + ) AS review_gate_pass, + ( + m.confirmed_context_pct >= + m.minimum_confirmed_context_pct + AND m.unconfirmed_context_pct <= + m.maximum_unconfirmed_context_pct + ) AS coverage_gate_pass, + CASE + WHEN ( + ( + m.require_sync_healthy + AND m.sync_health_status <> + 'HEALTHY' + ) + OR ( + m.require_incident_collector_healthy + AND m.collector_health_status <> + 'HEALTHY' + ) + OR m.critical_session_dq_count > + m.maximum_critical_session_dq_count + OR m.recent_conflict_minutes > + m.maximum_conflict_minutes + OR m.live_canonical_health_status = + 'CRITICAL' + ) + THEN 'TECHNICAL_BLOCKED' + WHEN m.observed_activity_hours < + m.minimum_observed_activity_hours + THEN 'INSUFFICIENT_OBSERVATION' + WHEN ( + m.pending_review_count > + m.maximum_pending_review_count + OR m.oldest_pending_review_hours > + m.maximum_oldest_pending_review_hours + ) + THEN 'REVIEW_BACKLOG' + WHEN ( + m.confirmed_context_pct < + m.minimum_confirmed_context_pct + OR m.unconfirmed_context_pct > + m.maximum_unconfirmed_context_pct + ) + THEN 'COVERAGE_NOT_READY' + ELSE 'READY_FOR_CONTROLLED_SHADOW' + END AS readiness_status, + CASE + WHEN ( + ( + m.require_sync_healthy + AND m.sync_health_status <> + 'HEALTHY' + ) + OR ( + m.require_incident_collector_healthy + AND m.collector_health_status <> + 'HEALTHY' + ) + ) + THEN + 'Restablecer la salud de sincronización y del colector.' + WHEN m.critical_session_dq_count > + m.maximum_critical_session_dq_count + THEN + 'Resolver errores críticos de calidad en sesiones Shop Floor.' + WHEN m.recent_conflict_minutes > + m.maximum_conflict_minutes + OR m.live_canonical_health_status = + 'CRITICAL' + THEN + 'Resolver los conflictos de contexto antes de continuar.' + WHEN m.observed_activity_hours < + m.minimum_observed_activity_hours + THEN + 'Acumular más horas reales de observación.' + WHEN m.pending_review_count > + m.maximum_pending_review_count + THEN + 'Revisar los episodios pendientes con Producción o el responsable técnico.' + WHEN m.oldest_pending_review_hours > + m.maximum_oldest_pending_review_hours + THEN + 'Evitar que la revisión más antigua supere el plazo configurado.' + WHEN m.confirmed_context_pct < + m.minimum_confirmed_context_pct + THEN + 'Aumentar el uso correcto de Shop Floor o autorizar explícitamente los contextos legacy/manuales reales.' + WHEN m.unconfirmed_context_pct > + m.maximum_unconfirmed_context_pct + THEN + 'Reducir actividad física sin contexto confirmado.' + ELSE + 'La máquina cumple los criterios SHADOW configurados.' + END AS next_action, + ( + CASE + WHEN ( + ( + m.require_sync_healthy + AND m.sync_health_status <> + 'HEALTHY' + ) + OR ( + m.require_incident_collector_healthy + AND m.collector_health_status <> + 'HEALTHY' + ) + OR m.critical_session_dq_count > + m.maximum_critical_session_dq_count + OR m.recent_conflict_minutes > + m.maximum_conflict_minutes + OR m.live_canonical_health_status = + 'CRITICAL' + ) + THEN 1 + WHEN m.observed_activity_hours < + m.minimum_observed_activity_hours + THEN 2 + WHEN ( + m.pending_review_count > + m.maximum_pending_review_count + OR m.oldest_pending_review_hours > + m.maximum_oldest_pending_review_hours + ) + THEN 3 + WHEN ( + m.confirmed_context_pct < + m.minimum_confirmed_context_pct + OR m.unconfirmed_context_pct > + m.maximum_unconfirmed_context_pct + ) + THEN 4 + ELSE 5 + END + ) AS readiness_stage, + false AS ready_for_official_ledger, + false AS official_ledger_eligible +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_metrics_v1 m; + +COMMENT ON VIEW +mv_reports_ucepsa_prod.li_canonical_context_readiness_gate_v1 IS +'Puerta de readiness SHADOW por máquina. READY_FOR_CONTROLLED_SHADOW no significa activación oficial y ready_for_official_ledger permanece false.'; + +CREATE OR REPLACE VIEW +mv_reports_ucepsa_prod.li_context_rollout_blockers_v1 +AS +SELECT + g.machine_id, + 'SYNC_HEALTH'::text + AS blocker_code, + 'CRITICAL'::text + AS blocker_severity, + g.sync_health_status + AS current_value, + 'HEALTHY'::text + AS target_value, + 'Restablecer la sincronización Shop Floor.'::text + AS recommended_action, + false AS official_ledger_eligible +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.require_sync_healthy + AND g.sync_health_status <> + 'HEALTHY' + +UNION ALL + +SELECT + g.machine_id, + 'INCIDENT_COLLECTOR_HEALTH', + 'CRITICAL', + g.collector_health_status, + 'HEALTHY', + 'Restablecer el colector de episodios de incidencia.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.require_incident_collector_healthy + AND g.collector_health_status <> + 'HEALTHY' + +UNION ALL + +SELECT + g.machine_id, + 'CRITICAL_SESSION_DQ', + 'CRITICAL', + g.critical_session_dq_count::text, + g.maximum_critical_session_dq_count::text, + 'Resolver las sesiones Shop Floor con errores críticos.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.critical_session_dq_count > + g.maximum_critical_session_dq_count + +UNION ALL + +SELECT + g.machine_id, + 'CONTEXT_CONFLICT_MINUTES', + 'CRITICAL', + g.recent_conflict_minutes::text, + g.maximum_conflict_minutes::text, + 'Resolver los solapamientos entre fuentes autoritativas.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.recent_conflict_minutes > + g.maximum_conflict_minutes + +UNION ALL + +SELECT + g.machine_id, + 'OBSERVED_ACTIVITY_HOURS', + 'INFO', + g.observed_activity_hours::text, + g.minimum_observed_activity_hours::text, + 'Acumular más horas reales de observación.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.observed_activity_hours < + g.minimum_observed_activity_hours + +UNION ALL + +SELECT + g.machine_id, + 'PENDING_REVIEW_COUNT', + 'WARNING', + g.pending_review_count::text, + g.maximum_pending_review_count::text, + 'Resolver el backlog de revisión de incidencias.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.pending_review_count > + g.maximum_pending_review_count + +UNION ALL + +SELECT + g.machine_id, + 'OLDEST_PENDING_REVIEW_HOURS', + 'WARNING', + g.oldest_pending_review_hours::text, + g.maximum_oldest_pending_review_hours::text, + 'Revisar el episodio pendiente más antiguo.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.oldest_pending_review_hours > + g.maximum_oldest_pending_review_hours + +UNION ALL + +SELECT + g.machine_id, + 'CONFIRMED_CONTEXT_PCT', + 'WARNING', + g.confirmed_context_pct::text, + g.minimum_confirmed_context_pct::text, + 'Aumentar la actividad con contexto canónico confirmado.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.confirmed_context_pct < + g.minimum_confirmed_context_pct + +UNION ALL + +SELECT + g.machine_id, + 'UNCONFIRMED_CONTEXT_PCT', + 'WARNING', + g.unconfirmed_context_pct::text, + g.maximum_unconfirmed_context_pct::text, + 'Reducir actividad física sin contexto confirmado.', + false +FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 g +WHERE g.unconfirmed_context_pct > + g.maximum_unconfirmed_context_pct; + +COMMENT ON VIEW +mv_reports_ucepsa_prod.li_context_rollout_blockers_v1 IS +'Lista explicable de criterios que impiden que una máquina alcance READY_FOR_CONTROLLED_SHADOW.'; + +CREATE OR REPLACE VIEW +mv_reports_ucepsa_prod.li_context_rollout_readiness_summary_v1 +AS +WITH gate AS ( + SELECT * + FROM + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 +), +blockers AS ( + SELECT * + FROM + mv_reports_ucepsa_prod + .li_context_rollout_blockers_v1 +) +SELECT + now() AS observed_at, + COUNT(*) AS machine_count, + COUNT(*) FILTER ( + WHERE readiness_status = + 'READY_FOR_CONTROLLED_SHADOW' + ) AS ready_machine_count, + COUNT(*) FILTER ( + WHERE readiness_status = + 'TECHNICAL_BLOCKED' + ) AS technical_blocked_machine_count, + COUNT(*) FILTER ( + WHERE readiness_status = + 'INSUFFICIENT_OBSERVATION' + ) AS insufficient_observation_machine_count, + COUNT(*) FILTER ( + WHERE readiness_status = + 'REVIEW_BACKLOG' + ) AS review_backlog_machine_count, + COUNT(*) FILTER ( + WHERE readiness_status = + 'COVERAGE_NOT_READY' + ) AS coverage_not_ready_machine_count, + ( + SELECT COUNT(*) + FROM blockers + ) AS blocker_count, + ( + SELECT COUNT(*) + FROM blockers + WHERE blocker_severity = + 'CRITICAL' + ) AS critical_blocker_count, + CASE + WHEN COUNT(*) FILTER ( + WHERE readiness_status = + 'TECHNICAL_BLOCKED' + ) > 0 + THEN 'TECHNICAL_BLOCKED' + WHEN COUNT(*) FILTER ( + WHERE readiness_status = + 'READY_FOR_CONTROLLED_SHADOW' + ) = COUNT(*) + THEN 'READY_FOR_CONTROLLED_SHADOW' + ELSE 'NOT_READY' + END AS overall_readiness_status, + false AS ready_for_official_ledger, + false AS official_ledger_eligible +FROM gate; + +COMMENT ON VIEW +mv_reports_ucepsa_prod.li_context_rollout_readiness_summary_v1 IS +'Resumen global de readiness del contexto canónico. No autoriza activación oficial.'; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = + 'grafana_ucepsa_ro' + ) THEN + GRANT SELECT + ON + mv_loss_intelligence + .context_rollout_readiness_policies + TO grafana_ucepsa_ro; + + GRANT SELECT + ON + mv_reports_ucepsa_prod + .li_canonical_context_readiness_metrics_v1 + TO grafana_ucepsa_ro; + + GRANT SELECT + ON + mv_reports_ucepsa_prod + .li_canonical_context_readiness_gate_v1 + TO grafana_ucepsa_ro; + + GRANT SELECT + ON + mv_reports_ucepsa_prod + .li_context_rollout_blockers_v1 + TO grafana_ucepsa_ro; + + GRANT SELECT + ON + mv_reports_ucepsa_prod + .li_context_rollout_readiness_summary_v1 + TO grafana_ucepsa_ro; + END IF; +END +$$; + +COMMIT;