diff --git a/ucepsa/edge-oee-demo/docs/runbooks/112_context_regression_hardening_shadow_v0.3.13.md b/ucepsa/edge-oee-demo/docs/runbooks/112_context_regression_hardening_shadow_v0.3.13.md index 04013f2..ceb5e0b 100644 --- a/ucepsa/edge-oee-demo/docs/runbooks/112_context_regression_hardening_shadow_v0.3.13.md +++ b/ucepsa/edge-oee-demo/docs/runbooks/112_context_regression_hardening_shadow_v0.3.13.md @@ -1,4 +1,4 @@ -# UCEPSA — Regresión y hardening SHADOW v0.3.13 +# UCEPSA — Regresión y hardening SHADOW v0.3.13.1 ## Objetivo @@ -172,3 +172,24 @@ Histórico de regresión ``` Paneles totales esperados: 31. + + +## Hotfix 0.3.13.1 + +La vista `li_context_arbitration_conflicts_v1` es derivada del estado canónico +actual. Al corregir Odoo, el conflicto desaparece correctamente de esa vista. + +Los contratos `CONFLICT-001` y `CONFLICT-002` pasan a verificar: + +```text +episodios persistentes 7/8/9 +caso raíz 6 +decisión consolidada del doble arranque +cero elegibilidad oficial +``` + +No dependen ya de una ventana derivada vacía y dejan de ejecutar la consulta de +aproximadamente 18 segundos. + +La migración 116 solo actualiza la versión de política y el mínimo esperado de +tests a 25. La migración 115 permanece inmutable. diff --git a/ucepsa/edge-oee-demo/sql/versions/116_ucepsa_context_regression_hardening_hotfix_v03131.sql b/ucepsa/edge-oee-demo/sql/versions/116_ucepsa_context_regression_hardening_hotfix_v03131.sql new file mode 100644 index 0000000..0710163 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/versions/116_ucepsa_context_regression_hardening_hotfix_v03131.sql @@ -0,0 +1,31 @@ +BEGIN; + +DO $$ +DECLARE + affected_rows integer; +BEGIN + UPDATE + mv_loss_intelligence.context_regression_policies + SET + suite_version = '0.3.13.1', + minimum_expected_test_count = 25, + notes = + 'Hotfix 0.3.13.1: los contratos del doble inicio 649/698 usan episodios, caso raíz y decisión persistentes. La vista de conflictos canónicos es derivada y puede quedar vacía tras corregir Odoo.', + updated_at = now() + WHERE tenant = 'ucepsa' + AND site = 'ucepsa_onpremise' + AND policy_code = + 'CONTEXT_REGRESSION_V1'; + + GET DIAGNOSTICS + affected_rows = ROW_COUNT; + + IF affected_rows <> 1 THEN + RAISE EXCEPTION + 'La actualización de CONTEXT_REGRESSION_V1 afectó % filas; se esperaba exactamente 1.', + affected_rows; + END IF; +END +$$; + +COMMIT; diff --git a/ucepsa/edge-oee-demo/tools/context_regression_suite.py b/ucepsa/edge-oee-demo/tools/context_regression_suite.py index 5a27204..b4e1ad5 100755 --- a/ucepsa/edge-oee-demo/tools/context_regression_suite.py +++ b/ucepsa/edge-oee-demo/tools/context_regression_suite.py @@ -18,7 +18,7 @@ import psycopg2 from psycopg2.extras import Json, RealDictCursor -VERSION = "0.3.13" +VERSION = "0.3.13.1" POLICY_CODE = "CONTEXT_REGRESSION_V1" @@ -472,79 +472,169 @@ def contract_tests() -> list[TestCase]: TestCase( code="CONFLICT-001", group="HISTORICAL_CONTRACT", - title="Doble inicio 649/698 conservado como conflicto", + title="Doble inicio 649/698 conservado en episodios persistentes", severity="CRITICAL", failure_status="FAIL", expected={"violations": 0}, - detail_pass="El solape histórico de WO 649 y 698 sigue documentado.", - detail_fail="No existe el conflicto histórico esperado entre WO 649 y 698.", + detail_pass="Los episodios 7/8/9 conservan el doble inicio ya corregido en Odoo.", + detail_fail="La evidencia persistente del doble inicio 649/698 ha cambiado o desaparecido.", sql=r""" + WITH contract AS ( + SELECT + COUNT(*) AS episode_count, + COUNT(*) FILTER ( + WHERE incident_scope = + 'MACHINE' + ) AS machine_scope_count, + COUNT(*) FILTER ( + WHERE incident_scope = + 'SESSION' + ) AS session_scope_count, + COUNT(*) FILTER ( + WHERE incident_code = + 'MULTIPLE_OPEN_SESSIONS' + ) AS code_count, + COUNT(*) FILTER ( + WHERE episode_status = + 'CLOSED' + ) AS closed_count, + COUNT(*) FILTER ( + WHERE max_severity = + 'CRITICAL' + ) AS critical_count, + COUNT(*) FILTER ( + WHERE NOT official_eligible + ) AS shadow_count, + ARRAY_AGG( + DISTINCT odoo_workorder_id + ORDER BY odoo_workorder_id + ) FILTER ( + WHERE odoo_workorder_id + IS NOT NULL + ) AS workorder_ids, + ARRAY_AGG( + DISTINCT production_order + ORDER BY production_order + ) FILTER ( + WHERE production_order + IS NOT NULL + ) AS production_orders, + MIN(first_seen_at) + AS first_seen_at, + MAX(ended_at) + AS ended_at + FROM + mv_reports_ucepsa_prod + .li_shopfloor_context_incident_history_v1 + WHERE episode_id IN ( + 7, + 8, + 9 + ) + ) SELECT CASE - WHEN COUNT(*) >= 1 + WHEN episode_count = 3 + AND machine_scope_count = 1 + AND session_scope_count = 2 + AND code_count = 3 + AND closed_count = 3 + AND critical_count = 3 + AND shadow_count = 3 + AND workorder_ids = + ARRAY[649, 698]::bigint[] + AND production_orders = + ARRAY[ + 'WH/MO/00660', + 'WH/MO/00706' + ]::text[] + AND first_seen_at = + '2026-07-20 15:43:50.563848+02' + ::timestamptz + AND ended_at IS NOT NULL THEN 0 ELSE 1 END AS violations, - COUNT(*) AS conflict_rows, - COALESCE( - SUM(segment_duration_s), - 0 - ) AS conflict_seconds - FROM - mv_reports_ucepsa_prod - .li_context_arbitration_conflicts_v1 - WHERE machine_id = 'CORT-00' - AND conflict_type = - 'MULTIPLE_ODOO_SHOPFLOOR_SESSIONS' - AND authoritative_context_keys @> - ARRAY[ - 'ODOO_SHOPFLOOR:WORKORDER:649', - 'ODOO_SHOPFLOOR:WORKORDER:698' - ]::text[] + * + FROM contract """, ), TestCase( code="CONFLICT-002", group="HISTORICAL_CONTRACT", - title="El conflicto 649/698 nunca recibe prioridad canónica", + title="Caso y decisión conservan la trazabilidad del doble inicio", severity="CRITICAL", failure_status="FAIL", expected={"violations": 0}, - detail_pass="Todos los tramos del doble inicio permanecen bloqueados.", - detail_fail="Algún tramo del conflicto recibió una clave canónica.", + detail_pass="El caso raíz y la decisión mantienen workorders, órdenes y evidencias sin elegibilidad oficial.", + detail_fail="La trazabilidad persistente del doble inicio no coincide con el contrato.", sql=r""" - WITH conflict_window AS ( + WITH root_case AS ( SELECT - MIN(segment_from) AS conflict_from, - MAX(segment_to) AS conflict_to + COUNT(*) AS row_count, + COUNT(*) FILTER ( + WHERE case_family = + 'MULTIPLE_OPEN_SESSIONS' + AND machine_id = + 'CORT-00' + AND episode_ids = + ARRAY[7, 8, 9]::bigint[] + AND production_orders = + ARRAY[ + 'WH/MO/00660', + 'WH/MO/00706' + ]::text[] + AND odoo_workorder_ids = + ARRAY[649, 698]::bigint[] + AND NOT official_eligible + ) AS contract_count FROM mv_reports_ucepsa_prod - .li_context_arbitration_conflicts_v1 - WHERE machine_id = 'CORT-00' - AND authoritative_context_keys @> - ARRAY[ - 'ODOO_SHOPFLOOR:WORKORDER:649', - 'ODOO_SHOPFLOOR:WORKORDER:698' - ]::text[] + .li_context_review_case_history_v1 + WHERE case_id = 6 + ), + decision AS ( + SELECT + COUNT(*) AS row_count, + COUNT(*) FILTER ( + WHERE decision_family = + 'ODOO_ORDER_START_CONFLICT' + AND machine_id = + 'CORT-00' + AND root_case_ids = + ARRAY[5, 6]::bigint[] + AND production_orders = + ARRAY[ + 'WH/MO/00660', + 'WH/MO/00706' + ]::text[] + AND odoo_workorder_ids = + ARRAY[649, 698]::bigint[] + AND NOT official_eligible + ) AS contract_count + FROM + mv_reports_ucepsa_prod + .li_context_decision_group_history_v1 + WHERE decision_group_key = + 'DECISION:ODOO_ORDER_START_CONFLICT:CORT-00:20260720:5' ) - SELECT COUNT(*) AS violations - FROM - mv_reports_ucepsa_prod - .li_canonical_context_segments_v1 s - CROSS JOIN conflict_window c - WHERE c.conflict_from IS NOT NULL - AND s.machine_id = 'CORT-00' - AND s.segment_to > - c.conflict_from - AND s.segment_from < - c.conflict_to - AND ( - s.canonical_context_status <> - 'CONTEXT_CONFLICT' - OR s.canonical_context_key - IS NOT NULL - OR s.authoritative_candidate_count <= 1 - ) + SELECT + CASE + WHEN r.row_count = 1 + AND r.contract_count = 1 + AND d.row_count = 1 + AND d.contract_count = 1 + THEN 0 + ELSE 1 + END AS violations, + r.row_count AS root_case_rows, + r.contract_count + AS root_case_contract_rows, + d.row_count AS decision_rows, + d.contract_count + AS decision_contract_rows + FROM root_case r + CROSS JOIN decision d """, ), TestCase( @@ -793,20 +883,22 @@ def contract_tests() -> list[TestCase]: detail_pass="Las cabeceras y los elementos de snapshot coinciden.", detail_fail="Un snapshot declara un número de elementos incorrecto.", sql=r""" - SELECT COUNT(*) AS violations, - COUNT(*) AS snapshot_count + SELECT + COUNT(*) FILTER ( + WHERE s.item_count <> + ( + SELECT COUNT(*) + FROM + mv_loss_intelligence + .context_governance_agenda_snapshot_items i + WHERE i.snapshot_id = + s.snapshot_id + ) + ) AS violations, + COUNT(*) AS total_snapshot_count FROM mv_loss_intelligence .context_governance_agenda_snapshots s - WHERE s.item_count <> - ( - SELECT COUNT(*) - FROM - mv_loss_intelligence - .context_governance_agenda_snapshot_items i - WHERE i.snapshot_id = - s.snapshot_id - ) """, ), TestCase(