diff --git a/ucepsa/edge-oee-demo/docs/runbooks/100_shopfloor_stop_summary_semantics_v0.3.4.1.md b/ucepsa/edge-oee-demo/docs/runbooks/100_shopfloor_stop_summary_semantics_v0.3.4.1.md new file mode 100644 index 0000000..87ee598 --- /dev/null +++ b/ucepsa/edge-oee-demo/docs/runbooks/100_shopfloor_stop_summary_semantics_v0.3.4.1.md @@ -0,0 +1,94 @@ +# UCEPSA — Semántica del resumen de paros Shop Floor v0.3.4.1 + +## Hallazgo + +La validación de v0.3.4 mostró para el workorder 340: + +```text +physical_stop_count = 31 +eligible_stop_count = 28 +eligible_stop_minutes = 55,25 +``` + +El diagnóstico descomponía esos 55 minutos en: + +```text +CANDIDATE_GROUP 32,88 min +CANDIDATE_INDIVIDUAL 17,51 min +EXCLUDED_NOISE 0,11 min +OPEN_MONITORING 4,81 min +``` + +La vista de resumen sumaba todos los minutos observados en la columna +`eligible_stop_minutes`, aunque el importe económico sí excluía correctamente +el ruido y el paro abierto. + +## Corrección + +`eligible_stop_minutes` pasa a incluir únicamente filas con: + +```text +technically_eligible = true +``` + +Se añaden columnas separadas: + +- `observed_stop_minutes` +- `telemetry_covered_stop_minutes` +- `candidate_group_minutes` +- `candidate_individual_minutes` +- `excluded_noise_minutes` +- `open_monitoring_minutes` +- `blocked_minutes` + +No se modifica la vista de candidatos, la cola SHADOW, la cola oficial ni los +datos raw. + +## Resultado esperado para la sesión 340 + +A la hora de la validación inicial, aproximadamente: + +```text +observed_stop_minutes 55,3 +eligible_stop_minutes 50,4 +candidate_group_minutes 32,9 +candidate_individual 17,5 +excluded_noise 0,1 +open_monitoring 4,8 +shadow_capacity_loss_eur 30,2 +``` + +El paro abierto puede seguir creciendo hasta que la máquina vuelva a arrancar, +por lo que los valores exactos pueden variar. + +## Despliegue + +No es necesario detener servicios. + +```bash +FILE=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/sql/versions/107_fix_shopfloor_stop_summary_semantics_v0341.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_shopfloor_stop_summary_semantics_v0341.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 +eligible_decomposition_errors = 0 +summary_candidate_mismatches = 0 +open_or_noise_inflation_errors = 0 +official_summary_rows = 0 +official_operator_queue_rows = 0 +``` diff --git a/ucepsa/edge-oee-demo/ops/validate_shopfloor_stop_summary_semantics_v0341.sql b/ucepsa/edge-oee-demo/ops/validate_shopfloor_stop_summary_semantics_v0341.sql new file mode 100644 index 0000000..7207ed3 --- /dev/null +++ b/ucepsa/edge-oee-demo/ops/validate_shopfloor_stop_summary_semantics_v0341.sql @@ -0,0 +1,118 @@ +\pset pager off + +\echo '=== 1. Columnas semánticas v0.3.4.1 ===' + +SELECT + column_name, + data_type, + ordinal_position +FROM information_schema.columns +WHERE table_schema = 'mv_reports_ucepsa_prod' + AND table_name = + 'li_shopfloor_session_stop_summary_v1' +ORDER BY ordinal_position; + +\echo '=== 2. Resumen corregido de la sesión 340 ===' + +SELECT + shopfloor_session_id, + machine_id, + production_order, + odoo_workorder_id, + session_status, + operator_name, + physical_stop_count, + eligible_stop_count, + observed_stop_minutes, + telemetry_covered_stop_minutes, + eligible_stop_minutes, + candidate_group_minutes, + candidate_individual_minutes, + excluded_noise_minutes, + open_monitoring_minutes, + blocked_minutes, + shadow_capacity_loss_eur, + official_ledger_eligible +FROM + mv_reports_ucepsa_prod + .li_shopfloor_session_stop_summary_v1 +WHERE odoo_workorder_id = 340; + +\echo '=== 3. Eligible = grupo + individual ===' + +SELECT count(*) AS eligible_decomposition_errors +FROM + mv_reports_ucepsa_prod + .li_shopfloor_session_stop_summary_v1 +WHERE abs( + eligible_stop_minutes + - ( + candidate_group_minutes + + candidate_individual_minutes + ) + ) > 0.02; + +\echo '=== 4. Resumen coincide con candidatos técnicamente elegibles ===' + +WITH expected AS ( + SELECT + shopfloor_session_id, + round( + sum(eligible_duration_s) FILTER ( + WHERE technically_eligible + ) / 60.0, + 2 + ) AS expected_eligible_minutes, + count( + DISTINCT source_stop_id + ) FILTER ( + WHERE technically_eligible + ) AS expected_eligible_count + FROM + mv_reports_ucepsa_prod + .li_stop_shopfloor_candidates_v1 + WHERE shopfloor_session_id IS NOT NULL + GROUP BY shopfloor_session_id +) +SELECT count(*) AS summary_candidate_mismatches +FROM expected e +JOIN + mv_reports_ucepsa_prod + .li_shopfloor_session_stop_summary_v1 s + ON s.shopfloor_session_id = + e.shopfloor_session_id +WHERE abs( + s.eligible_stop_minutes + - coalesce(e.expected_eligible_minutes, 0) + ) > 0.02 + OR s.eligible_stop_count + <> e.expected_eligible_count; + +\echo '=== 5. Abiertos y ruido no inflan eligible_stop_minutes ===' + +SELECT count(*) AS open_or_noise_inflation_errors +FROM + mv_reports_ucepsa_prod + .li_shopfloor_session_stop_summary_v1 +WHERE eligible_stop_minutes + > observed_stop_minutes + 0.02 + OR eligible_stop_minutes + + excluded_noise_minutes + + open_monitoring_minutes + + blocked_minutes + > observed_stop_minutes + 0.05; + +\echo '=== 6. Nada oficial ===' + +SELECT + count(*) FILTER ( + WHERE official_ledger_eligible + ) AS official_summary_rows +FROM + mv_reports_ucepsa_prod + .li_shopfloor_session_stop_summary_v1; + +\echo '=== 7. Cola oficial original sigue vacía ===' + +SELECT count(*) AS official_operator_queue_rows +FROM mv_reports_ucepsa_prod.li_operator_stop_queue_v2; diff --git a/ucepsa/edge-oee-demo/sql/versions/107_fix_shopfloor_stop_summary_semantics_v0341.sql b/ucepsa/edge-oee-demo/sql/versions/107_fix_shopfloor_stop_summary_semantics_v0341.sql new file mode 100644 index 0000000..ed097f6 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/versions/107_fix_shopfloor_stop_summary_semantics_v0341.sql @@ -0,0 +1,158 @@ +BEGIN; + +CREATE OR REPLACE VIEW +mv_reports_ucepsa_prod.li_shopfloor_session_stop_summary_v1 +AS +SELECT + c.shopfloor_session_id, + c.machine_id, + c.production_chain_key, + c.production_order, + c.odoo_production_id, + c.odoo_workorder_id, + c.product_id, + c.product_name, + c.session_from, + c.session_to, + c.session_status, + c.operator_employee_id, + c.operator_name, + c.operator_candidate_employee_id, + c.operator_candidate_name, + c.operator_attribution_status, + + COUNT( + DISTINCT c.source_stop_id + ) AS physical_stop_count, + + COUNT( + DISTINCT c.source_stop_id + ) FILTER ( + WHERE c.technically_eligible + ) AS eligible_stop_count, + + COALESCE( + ROUND( + SUM(c.eligible_duration_s) FILTER ( + WHERE c.technically_eligible + ) / 60.0, + 2 + ), + 0::numeric + ) AS eligible_stop_minutes, + + ROUND( + SUM( + COALESCE( + c.shadow_capacity_loss_eur, + 0 + ) + ), + 2 + ) AS shadow_capacity_loss_eur, + + BOOL_OR(c.session_clipped) + AS any_session_clipped, + + BOOL_OR(c.telemetry_clipped) + AS any_telemetry_clipped, + + BOOL_OR(c.crosses_shopfloor_boundary) + AS any_crosses_shopfloor_boundary, + + false AS official_ledger_eligible, + + ROUND( + SUM(c.context_segment_duration_s) / 60.0, + 2 + ) AS observed_stop_minutes, + + ROUND( + SUM(c.telemetry_covered_duration_s) / 60.0, + 2 + ) AS telemetry_covered_stop_minutes, + + COALESCE( + ROUND( + SUM(c.eligible_duration_s) FILTER ( + WHERE c.eligibility_status = + 'CANDIDATE_GROUP' + ) / 60.0, + 2 + ), + 0::numeric + ) AS candidate_group_minutes, + + COALESCE( + ROUND( + SUM(c.eligible_duration_s) FILTER ( + WHERE c.eligibility_status = + 'CANDIDATE_INDIVIDUAL' + ) / 60.0, + 2 + ), + 0::numeric + ) AS candidate_individual_minutes, + + COALESCE( + ROUND( + SUM(c.eligible_duration_s) FILTER ( + WHERE c.eligibility_status = + 'EXCLUDED_NOISE' + ) / 60.0, + 2 + ), + 0::numeric + ) AS excluded_noise_minutes, + + COALESCE( + ROUND( + SUM(c.eligible_duration_s) FILTER ( + WHERE c.eligibility_status = + 'OPEN_MONITORING' + ) / 60.0, + 2 + ), + 0::numeric + ) AS open_monitoring_minutes, + + COALESCE( + ROUND( + SUM(c.context_segment_duration_s) FILTER ( + WHERE c.eligibility_status LIKE + 'BLOCKED_%' + ) / 60.0, + 2 + ), + 0::numeric + ) AS blocked_minutes + +FROM + mv_reports_ucepsa_prod + .li_stop_shopfloor_candidates_v1 c + +WHERE c.shopfloor_session_id IS NOT NULL + +GROUP BY + c.shopfloor_session_id, + c.machine_id, + c.production_chain_key, + c.production_order, + c.odoo_production_id, + c.odoo_workorder_id, + c.product_id, + c.product_name, + c.session_from, + c.session_to, + c.session_status, + c.operator_employee_id, + c.operator_name, + c.operator_candidate_employee_id, + c.operator_candidate_name, + c.operator_attribution_status; + +COMMENT ON VIEW +mv_reports_ucepsa_prod.li_shopfloor_session_stop_summary_v1 IS +'Resumen SHADOW por sesión Shop Floor. eligible_stop_minutes incluye solo paros cerrados técnicamente elegibles; ruido, abiertos y bloqueados se muestran por separado.'; + +COMMIT;