Saltar a contenido

SYS-ADR-009 — Forma canónica del aasPath cuando la colección está vacía

Estado Aceptada — camino A: la colección vacía se colapsa. Falta el paso 3
Decisión 2026-10-07, por Federico Degiovanni
Completa a SYS-ADR-004, que fija el aasPath y su fórmula
Relacionadas SYS-ADR-002 (el aasPath entra en el eventId), SYS-ADR-004

El aasPath canónico de las dos propiedades de Alarmas es Alarmas/paradaEmergencia, con una sola barra: la forma que el catálogo describe y que el frontend pide.

Contexto

SYS-ADR-004 fijó el aasPath como identificador semántico común y su fórmula de composición:

aasPath = aas_submodel + "/" + aas_collection + "/" + aas_property

El historian no emite el tag de colección para el submodel Alarmas: esas dos propiedades cuelgan directo del submodelo. La fórmula concatena igual, con el segmento del medio vacío.

Entry.Path() del Edge, sobre una entrada con colección vacía, producía:

Entry{Submodel:"Alarmas", Collection:"", Property:"paradaEmergencia"}.Path()
  → "Alarmas//paradaEmergencia"
Entry{Submodel:"Operativos", Collection:"Pesada", Property:"peso"}.Path()
  → "Operativos/Pesada/peso"

Del otro lado, src/lib/widget-registry.ts del frontend pide Alarmas/paradaEmergencia, con una sola barra, y el Cloud filtra por igualdad exacta sobre payload.aasPath. Nunca coinciden: serie vacía, sin error. Afecta a 3 de los 14 literales del registry.

Lo que hace que la forma importe fuera del Edge es que el aasPath viaja en el payload de cada medición: no es una particularidad interna, es parte del contrato de ingesta y de todo dato almacenado.

El problema a decidir

¿Cuál de las dos formas es la canónica? No hay una tercera: o la colección vacía se colapsa, o no.

Lo que hace que la decisión no sea trivial es que el aasPath entra en el cálculo del eventId (sha256(machineId | aas_path | ts_nano), internal/app/forwarder/eventid.go del Edge), y el eventId es toda la idempotencia del sistema. Cambiar la composición cambia el identificador de las mediciones de alarma: las ya almacenadas quedan bajo la forma vieja, y una reenviada después del cambio no se deduplica contra su propia copia anterior.

Alternativas

A · Colapsar el segmento vacío en el Edge — elegida

Entry.Path() omite la colección cuando está vacía. El aasPath canónico de las alarmas pasa a ser Alarmas/paradaEmergencia.

Cambia Una condición en internal/domain/aas/catalog.go del Edge, y el test que hoy fija el formato de tres segmentos
No cambia El frontend, que ya pide esta forma. Ni aas_mapping.yaml, ni el Cloud
A favor El catálogo —la única fuente de verdad— no tiene colección para esas entradas. La forma de tres segmentos inventa un segmento que la fuente no representa, y obliga a cualquier validador a sintetizarlo. La forma colapsada es la que el catálogo describe
En contra Las mediciones de alarma ya almacenadas quedan bajo Alarmas//… y una consulta con el nombre nuevo no las ve
Cuánto pesa ese contra Poco: son 2 variables, y sus series nunca se mostraron en ningún widget. No hay historia que nadie esté leyendo

B · Conservar los tres segmentos siempre y corregir el frontend — descartada

El registry pasa a pedir Alarmas//paradaEmergencia.

Cambia 3 literales de src/lib/widget-registry.ts, y la composición de check-aas-paths.mjs
No cambia El Edge, ni el eventId de nada ya almacenado
A favor Continuidad total de los datos: ninguna medición cambia de identidad. Es la opción que no toca producción
En contra Congela Alarmas//paradaEmergencia como identificador público del sistema: queda en el contrato de ingesta, en Mongo, en el dashboard y en la tesina. Y el catálogo canónico no lo contiene: un validador tiene que fabricar la doble barra a partir de una colección que el YAML no declara

C · Normalizar en el Cloud, al ingerir — descartada

Rechazada sin más análisis. El eventId lo calcula el Edge antes de enviar, así que normalizar del lado del Cloud arregla la consulta y no la identidad: dos formas del mismo dato seguirían produciendo dos eventId distintos. Además esconde la divergencia en lugar de resolverla, y agrega una regla de transformación en la frontera, que es justamente lo que SYS-ADR-004 quiso evitar al decidir que el aasPath se propaga sin transformarse.

Por qué A

El argumento decisivo no es el costo —los dos son chicos— sino cuál de las dos formas describe el catálogo. aas_mapping.yaml no declara colección para Alarmas; la forma de tres segmentos existe solo porque la función que las concatena no contempla el caso. Elegir B habría sido ascender un error de composición a contrato.

El contra de A —perder la continuidad de dos series— es real pero está acotado: esas series nunca se mostraron.

Cómo se aplica

Tres pasos, en este orden. El orden no es una preferencia: el frontend ya pide la forma elegida, así que corregir el Edge primero deja al sistema consistente desde el primer despliegue. Hacerlo al revés rompe los 11 literales que funcionan.

  1. Edge — Entry.Path() en internal/domain/aas/catalog.go omite la colección cuando está vacía, y el test de internal/app/forwarder/grouper_test.go fija esa forma. Lo registra EDGE-ADR-012.
  2. Frontend — scripts/check-aas-paths.mjs deriva la regla del productor en lugar de implementarla, desde src/historian/aas_mapping.yaml. Lo registra FE-ADR-015, que reemplaza a FE-ADR-010.
  3. Recién entonces, dejar el job bloqueante. Antes no: hasta que los dos extremos compongan igual, el job o falla sin que haya nada que arreglar en el commit que lo dispara, o pasa sin verificar nada.

Los literales del widget-registry.ts no se tocan: ya están en la forma canónica.

Estado de aplicación

Paso Estado
1 · Edge ✅ Entry.Path() colapsa el segmento vacío, y el test fija Alarmas/paradaEmergencia
2 · Frontend ✅ La regla vive en scripts/lib/aas-path.mjs, en un solo lugar y con test propio, y el validador lee el YAML canónico
3 · Job bloqueante ❌ El job del frontend sigue declarado como no bloqueante en PR. Nada lo espera: se puede tomar

Lo que queda abierto

Los 3 widgets de alarma están deshabilitados a mano, con unavailableReason: 'aaspath-composition' en los tres literales del registry y un texto de usuario que atribuye la causa a que el historian no emite el segmento de colección. Esa explicación no describe al sistema: los dos extremos componen igual y la cadena cierra. Sacar la bandera y corregir el texto es trabajo del frontend.

Las mediciones guardadas bajo Alarmas//… son invisibles. El Cloud filtra por igualdad exacta sobre payload.aasPath —baseMatch en internal/repo/mongo/metrics/repository.go—, así que al rehabilitar los widgets solo hay serie hacia adelante. Cuántas mediciones de alarma hay bajo esa forma no se sabe: se responde mirando el almacén.

Fijar el catálogo a un tag del historian sigue pendiente, y no depende del camino elegido: exige que ese tag exista, y hoy el repositorio tiene uno solo, de abril, de un autor externo.

El defecto que el validador tenía no era elegir mal la forma, sino implementar la regla por su cuenta en vez de derivarla del productor. Por eso el paso 2 cambia de dónde sale la regla, no la condición: si no, el próximo caso que el catálogo no contemple vuelve a divergir en silencio.

Evidencia

  • Composición del aasPath y su test: internal/domain/aas/catalog.go e internal/app/forwarder/grouper_test.go del Edge, con EDGE-ADR-012
  • El aasPath entra en el eventId: internal/app/forwarder/eventid.go del Edge
  • El aasPath viaja en el payload de cada medición: internal/app/forwarder/grouper.go del Edge
  • Igualdad exacta sobre payload.aasPath: internal/repo/mongo/metrics/repository.go del Cloud, función baseMatch
  • El catálogo no declara colección para Alarmas: src/historian/aas_mapping.yaml
  • Literales del dashboard y regla derivada del productor: src/lib/widget-registry.ts y scripts/lib/aas-path.mjs del frontend, con FE-ADR-015

Relacionado