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:
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.
- Edge —
Entry.Path()eninternal/domain/aas/catalog.goomite la colección cuando está vacía, y el test deinternal/app/forwarder/grouper_test.gofija esa forma. Lo registraEDGE-ADR-012. - Frontend —
scripts/check-aas-paths.mjsderiva la regla del productor en lugar de implementarla, desdesrc/historian/aas_mapping.yaml. Lo registraFE-ADR-015, que reemplaza aFE-ADR-010. - 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
aasPathy su test:internal/domain/aas/catalog.goeinternal/app/forwarder/grouper_test.godel Edge, conEDGE-ADR-012 - El
aasPathentra en eleventId:internal/app/forwarder/eventid.godel Edge - El
aasPathviaja en el payload de cada medición:internal/app/forwarder/grouper.godel Edge - Igualdad exacta sobre
payload.aasPath:internal/repo/mongo/metrics/repository.godel Cloud, funciónbaseMatch - 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.tsyscripts/lib/aas-path.mjsdel frontend, conFE-ADR-015
Relacionado¶
SYS-ADR-004— elaasPathcomo identificador semántico común- Catálogo de variables — el contrato completo y cómo se valida
- Flujo de datos — las cuatro identidades que tiene una variable en el camino