fix(ucepsa): use persistent conflict evidence in regression suite

This commit is contained in:
Victor Fraile Garcia 2026-07-20 18:44:18 +02:00
parent 3d0aef0410
commit 27a653eb42
3 changed files with 208 additions and 64 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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(