paperclip-plugin-autopilot-ops 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manifest.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/manifest.ts
2
2
  var PLUGIN_ID = "alephoo.autopilot-ops";
3
- var PLUGIN_VERSION = "0.1.8";
3
+ var PLUGIN_VERSION = "0.1.9";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -149,7 +149,7 @@ var manifest = {
149
149
  {
150
150
  name: TOOL_NAMES.publicarTablero,
151
151
  displayName: "Publicar tablero",
152
- description: "Publica o actualiza un tablero de gesti\xF3n para la direcci\xF3n del hospital, armado sobre datos reales de Alephoo (turnos, facturaci\xF3n, internaci\xF3n, producci\xF3n). `clave` identifica el tablero de forma estable (min\xFAsculas y guiones; volver a publicar con la misma clave lo actualiza en el lugar). `widgets` es la lista de piezas que se muestran, en orden: {tipo:'indicador', etiqueta, valor, unidad?, contexto?, tono?: neutro|ambar|senal|corte} \xB7 {tipo:'barras', titulo, unidad?, series:[{etiqueta, valor}]} \xB7 {tipo:'linea', titulo, unidad?, puntos:[{x, y}]} \xB7 {tipo:'tabla', titulo, columnas:[\u2026], filas:[[\u2026]]} \xB7 {tipo:'texto', titulo?, texto}. Cada widget admite `ancho` 1, 2 o 3 (columnas de 3). `fuentes` lista qu\xE9 se consult\xF3 (p. ej. 'Alephoo \xB7 api-consultas \xB7 turnos 01/09\u201330/09'). Si el pedido vino de una tarea, pasar `issueId`.",
152
+ description: "Publica o actualiza un tablero de gesti\xF3n para la direcci\xF3n del hospital, armado sobre datos reales de Alephoo (turnos, facturaci\xF3n, internaci\xF3n, producci\xF3n). `clave` identifica el tablero de forma estable (min\xFAsculas y guiones; volver a publicar con la misma clave lo actualiza en el lugar). `widgets` es la lista de piezas que se muestran, en orden: {tipo:'indicador', etiqueta, valor, unidad?, contexto?, tono?: neutro|ambar|senal|corte} \xB7 {tipo:'barras', titulo, unidad?, series:[{etiqueta, valor}]} \xB7 {tipo:'linea', titulo, unidad?, puntos:[{x, y}]} \xB7 {tipo:'tabla', titulo, columnas:[\u2026], filas:[[\u2026]]} \xB7 {tipo:'texto', titulo?, texto}. Cada widget admite `ancho` 1, 2 o 3 (columnas de 3). **N\xFAmeros oficiales:** cada indicador, barras, l\xEDnea o tabla calculado con calcular.mjs lleva `calculo` = el bloque `calculo` que devuelve calcular.mjs, tal cual (cat\xE1logo, m\xE9tricas con versi\xF3n, par\xE1metros, huella, consulta); el plugin lo valida contra el cat\xE1logo de m\xE9tricas y marca el widget como certificado, propuesta (m\xE9trica sin aprobar) o exploratorio (sin `calculo`). Controles que frenan la publicaci\xF3n: una tabla con fila \xABTOTAL\xBB tiene que sumar ese total en cada columna num\xE9rica (las filas \xABSubtotal \u2026\xBB no suman); un desglose con `calculo` y `por` lleva la fila TOTAL; un porcentaje (`unidad` '%' o columna con '%') va de 0 a 100 salvo que el t\xEDtulo diga variaci\xF3n, diferencia o cumplimiento de meta. `fuentes` lista qu\xE9 se consult\xF3 (p. ej. 'Alephoo \xB7 base \xB7 semantica_turnos \xB7 turnos 01/09\u201330/09'). Si el pedido vino de una tarea, pasar `issueId`.",
153
153
  parametersSchema: {
154
154
  type: "object",
155
155
  properties: {
@@ -277,6 +277,14 @@ var manifest = {
277
277
  capability: "api.routes.register",
278
278
  companyResolution: { from: "body", key: "companyId" }
279
279
  },
280
+ {
281
+ routeKey: "metricas",
282
+ method: "GET",
283
+ path: "/metricas",
284
+ auth: "board",
285
+ capability: "api.routes.register",
286
+ companyResolution: { from: "query", key: "companyId" }
287
+ },
280
288
  {
281
289
  routeKey: "tableros",
282
290
  method: "GET",
package/dist/ui/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/manifest.ts
10
10
  var PLUGIN_ID = "alephoo.autopilot-ops";
11
- var PLUGIN_VERSION = "0.1.8";
11
+ var PLUGIN_VERSION = "0.1.9";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -157,7 +157,7 @@ var manifest = {
157
157
  {
158
158
  name: TOOL_NAMES.publicarTablero,
159
159
  displayName: "Publicar tablero",
160
- description: "Publica o actualiza un tablero de gesti\xF3n para la direcci\xF3n del hospital, armado sobre datos reales de Alephoo (turnos, facturaci\xF3n, internaci\xF3n, producci\xF3n). `clave` identifica el tablero de forma estable (min\xFAsculas y guiones; volver a publicar con la misma clave lo actualiza en el lugar). `widgets` es la lista de piezas que se muestran, en orden: {tipo:'indicador', etiqueta, valor, unidad?, contexto?, tono?: neutro|ambar|senal|corte} \xB7 {tipo:'barras', titulo, unidad?, series:[{etiqueta, valor}]} \xB7 {tipo:'linea', titulo, unidad?, puntos:[{x, y}]} \xB7 {tipo:'tabla', titulo, columnas:[\u2026], filas:[[\u2026]]} \xB7 {tipo:'texto', titulo?, texto}. Cada widget admite `ancho` 1, 2 o 3 (columnas de 3). `fuentes` lista qu\xE9 se consult\xF3 (p. ej. 'Alephoo \xB7 api-consultas \xB7 turnos 01/09\u201330/09'). Si el pedido vino de una tarea, pasar `issueId`.",
160
+ description: "Publica o actualiza un tablero de gesti\xF3n para la direcci\xF3n del hospital, armado sobre datos reales de Alephoo (turnos, facturaci\xF3n, internaci\xF3n, producci\xF3n). `clave` identifica el tablero de forma estable (min\xFAsculas y guiones; volver a publicar con la misma clave lo actualiza en el lugar). `widgets` es la lista de piezas que se muestran, en orden: {tipo:'indicador', etiqueta, valor, unidad?, contexto?, tono?: neutro|ambar|senal|corte} \xB7 {tipo:'barras', titulo, unidad?, series:[{etiqueta, valor}]} \xB7 {tipo:'linea', titulo, unidad?, puntos:[{x, y}]} \xB7 {tipo:'tabla', titulo, columnas:[\u2026], filas:[[\u2026]]} \xB7 {tipo:'texto', titulo?, texto}. Cada widget admite `ancho` 1, 2 o 3 (columnas de 3). **N\xFAmeros oficiales:** cada indicador, barras, l\xEDnea o tabla calculado con calcular.mjs lleva `calculo` = el bloque `calculo` que devuelve calcular.mjs, tal cual (cat\xE1logo, m\xE9tricas con versi\xF3n, par\xE1metros, huella, consulta); el plugin lo valida contra el cat\xE1logo de m\xE9tricas y marca el widget como certificado, propuesta (m\xE9trica sin aprobar) o exploratorio (sin `calculo`). Controles que frenan la publicaci\xF3n: una tabla con fila \xABTOTAL\xBB tiene que sumar ese total en cada columna num\xE9rica (las filas \xABSubtotal \u2026\xBB no suman); un desglose con `calculo` y `por` lleva la fila TOTAL; un porcentaje (`unidad` '%' o columna con '%') va de 0 a 100 salvo que el t\xEDtulo diga variaci\xF3n, diferencia o cumplimiento de meta. `fuentes` lista qu\xE9 se consult\xF3 (p. ej. 'Alephoo \xB7 base \xB7 semantica_turnos \xB7 turnos 01/09\u201330/09'). Si el pedido vino de una tarea, pasar `issueId`.",
161
161
  parametersSchema: {
162
162
  type: "object",
163
163
  properties: {
@@ -285,6 +285,14 @@ var manifest = {
285
285
  capability: "api.routes.register",
286
286
  companyResolution: { from: "body", key: "companyId" }
287
287
  },
288
+ {
289
+ routeKey: "metricas",
290
+ method: "GET",
291
+ path: "/metricas",
292
+ auth: "board",
293
+ capability: "api.routes.register",
294
+ companyResolution: { from: "query", key: "companyId" }
295
+ },
288
296
  {
289
297
  routeKey: "tableros",
290
298
  method: "GET",
package/dist/worker.js CHANGED
@@ -39932,7 +39932,7 @@ function startWorkerRpcHost(options) {
39932
39932
 
39933
39933
  // src/manifest.ts
39934
39934
  var PLUGIN_ID = "alephoo.autopilot-ops";
39935
- var PLUGIN_VERSION = "0.1.8";
39935
+ var PLUGIN_VERSION = "0.1.9";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -40081,7 +40081,7 @@ var manifest = {
40081
40081
  {
40082
40082
  name: TOOL_NAMES.publicarTablero,
40083
40083
  displayName: "Publicar tablero",
40084
- description: "Publica o actualiza un tablero de gesti\xF3n para la direcci\xF3n del hospital, armado sobre datos reales de Alephoo (turnos, facturaci\xF3n, internaci\xF3n, producci\xF3n). `clave` identifica el tablero de forma estable (min\xFAsculas y guiones; volver a publicar con la misma clave lo actualiza en el lugar). `widgets` es la lista de piezas que se muestran, en orden: {tipo:'indicador', etiqueta, valor, unidad?, contexto?, tono?: neutro|ambar|senal|corte} \xB7 {tipo:'barras', titulo, unidad?, series:[{etiqueta, valor}]} \xB7 {tipo:'linea', titulo, unidad?, puntos:[{x, y}]} \xB7 {tipo:'tabla', titulo, columnas:[\u2026], filas:[[\u2026]]} \xB7 {tipo:'texto', titulo?, texto}. Cada widget admite `ancho` 1, 2 o 3 (columnas de 3). `fuentes` lista qu\xE9 se consult\xF3 (p. ej. 'Alephoo \xB7 api-consultas \xB7 turnos 01/09\u201330/09'). Si el pedido vino de una tarea, pasar `issueId`.",
40084
+ description: "Publica o actualiza un tablero de gesti\xF3n para la direcci\xF3n del hospital, armado sobre datos reales de Alephoo (turnos, facturaci\xF3n, internaci\xF3n, producci\xF3n). `clave` identifica el tablero de forma estable (min\xFAsculas y guiones; volver a publicar con la misma clave lo actualiza en el lugar). `widgets` es la lista de piezas que se muestran, en orden: {tipo:'indicador', etiqueta, valor, unidad?, contexto?, tono?: neutro|ambar|senal|corte} \xB7 {tipo:'barras', titulo, unidad?, series:[{etiqueta, valor}]} \xB7 {tipo:'linea', titulo, unidad?, puntos:[{x, y}]} \xB7 {tipo:'tabla', titulo, columnas:[\u2026], filas:[[\u2026]]} \xB7 {tipo:'texto', titulo?, texto}. Cada widget admite `ancho` 1, 2 o 3 (columnas de 3). **N\xFAmeros oficiales:** cada indicador, barras, l\xEDnea o tabla calculado con calcular.mjs lleva `calculo` = el bloque `calculo` que devuelve calcular.mjs, tal cual (cat\xE1logo, m\xE9tricas con versi\xF3n, par\xE1metros, huella, consulta); el plugin lo valida contra el cat\xE1logo de m\xE9tricas y marca el widget como certificado, propuesta (m\xE9trica sin aprobar) o exploratorio (sin `calculo`). Controles que frenan la publicaci\xF3n: una tabla con fila \xABTOTAL\xBB tiene que sumar ese total en cada columna num\xE9rica (las filas \xABSubtotal \u2026\xBB no suman); un desglose con `calculo` y `por` lleva la fila TOTAL; un porcentaje (`unidad` '%' o columna con '%') va de 0 a 100 salvo que el t\xEDtulo diga variaci\xF3n, diferencia o cumplimiento de meta. `fuentes` lista qu\xE9 se consult\xF3 (p. ej. 'Alephoo \xB7 base \xB7 semantica_turnos \xB7 turnos 01/09\u201330/09'). Si el pedido vino de una tarea, pasar `issueId`.",
40085
40085
  parametersSchema: {
40086
40086
  type: "object",
40087
40087
  properties: {
@@ -40209,6 +40209,14 @@ var manifest = {
40209
40209
  capability: "api.routes.register",
40210
40210
  companyResolution: { from: "body", key: "companyId" }
40211
40211
  },
40212
+ {
40213
+ routeKey: "metricas",
40214
+ method: "GET",
40215
+ path: "/metricas",
40216
+ auth: "board",
40217
+ capability: "api.routes.register",
40218
+ companyResolution: { from: "query", key: "companyId" }
40219
+ },
40212
40220
  {
40213
40221
  routeKey: "tableros",
40214
40222
  method: "GET",
@@ -41669,6 +41677,32 @@ function errorParaAgente(err) {
41669
41677
  return { error: `error_interno: ${message2}`, data: { codigo: "error_interno" } };
41670
41678
  }
41671
41679
 
41680
+ // src/metricas.ts
41681
+ var CATALOGO_VERSION = "2026-10-15.1";
41682
+ var METRICAS = [
41683
+ { id: "turnos_dados", version: 1, estado: "validada", nombre: "Turnos dados", unidad: "turnos", definicion: "Turnos agendados en el per\xEDodo que no fueron cancelados ni bloqueados (incluye atendidos, ausentes y los que siguen pendientes)." },
41684
+ { id: "turnos_atendidos", version: 1, estado: "validada", nombre: "Turnos atendidos", unidad: "turnos", definicion: "Turnos en los que hubo atenci\xF3n: en atenci\xF3n, atendido o revisado por el profesional." },
41685
+ { id: "ausencias", version: 1, estado: "validada", nombre: "Ausencias", unidad: "turnos", definicion: "Turnos marcados como \xABno asisti\xF3\xBB." },
41686
+ { id: "ausentismo", version: 1, estado: "validada", nombre: "Ausentismo", unidad: "%", definicion: "Ausencias sobre turnos dados. Las cancelaciones no cuentan como ausencia. Los turnos que nadie cerr\xF3 no figuran como ausencia, as\xED que con muchos turnos sin cerrar el ausentismo real es mayor." },
41687
+ { id: "turnos_sin_cerrar", version: 1, estado: "propuesta", nombre: "Turnos sin cerrar", unidad: "turnos", definicion: "Turnos con fecha ya pasada que siguen en \xABpendiente\xBB o \xABanunciado\xBB: nadie los cerr\xF3 como atendidos ni como ausentes." },
41688
+ { id: "cancelaciones", version: 1, estado: "validada", nombre: "Cancelaciones", unidad: "turnos", definicion: "Turnos cancelados del per\xEDodo." },
41689
+ { id: "cancelaciones_mismo_dia", version: 1, estado: "validada", nombre: "Cancelaciones del mismo d\xEDa", unidad: "turnos", definicion: "Turnos cancelados el mismo d\xEDa del turno (el cupo ya casi no se puede reutilizar). Es una aproximaci\xF3n: Alephoo no guarda la fecha de cancelaci\xF3n y se usa la \xFAltima modificaci\xF3n del turno." },
41690
+ { id: "tasa_cancelacion", version: 1, estado: "validada", nombre: "Tasa de cancelaci\xF3n", unidad: "%", definicion: "Cancelaciones sobre lo agendado (turnos dados m\xE1s cancelados)." },
41691
+ { id: "turnos_agendados", version: 1, estado: "validada", nombre: "Turnos agendados", unidad: "turnos", definicion: "Turnos dados m\xE1s cancelados: todo lo que se agend\xF3 en el per\xEDodo, sin los cupos bloqueados." },
41692
+ { id: "facturado", version: 1, estado: "validada", nombre: "Facturado", unidad: "$", definicion: "Importe de las facturas emitidas en el per\xEDodo, sin las anuladas, con IVA, por fecha de emisi\xF3n (la de AFIP en las electr\xF3nicas)." },
41693
+ { id: "facturas_emitidas", version: 1, estado: "validada", nombre: "Facturas emitidas", unidad: "facturas", definicion: "Cantidad de facturas emitidas en el per\xEDodo, sin las anuladas." },
41694
+ { id: "notas_credito", version: 1, estado: "validada", nombre: "Notas de cr\xE9dito", unidad: "$", definicion: "Notas de cr\xE9dito no anuladas sobre las facturas emitidas en el per\xEDodo (se asignan al mes de la factura). Incluyen d\xE9bitos de financiadores, anulaciones parciales y ajustes propios: no son lo mismo que \xABd\xE9bitos\xBB." },
41695
+ { id: "notas_debito", version: 1, estado: "validada", nombre: "Notas de d\xE9bito", unidad: "$", definicion: "Notas de d\xE9bito no anuladas sobre las facturas emitidas en el per\xEDodo." },
41696
+ { id: "cobrado_de_lo_facturado", version: 1, estado: "validada", nombre: "Cobrado de lo facturado", unidad: "$", definicion: "Lo cobrado (recibos no anulados aplicados) sobre las facturas emitidas en el per\xEDodo, cualquiera sea la fecha del cobro." },
41697
+ { id: "deuda_pendiente", version: 1, estado: "validada", nombre: "Deuda pendiente", unidad: "$", definicion: "Saldo sin cobrar de las facturas emitidas en el per\xEDodo que siguen pendientes (facturado menos notas de cr\xE9dito, m\xE1s notas de d\xE9bito, menos lo cobrado)." },
41698
+ { id: "bonos_pendientes_de_facturar", version: 1, estado: "validada", nombre: "Bonos pendientes de facturar", unidad: "bonos", definicion: "Prestaciones (bonos) del per\xEDodo todav\xEDa no confirmadas o confirmadas y sin prefacturar ni facturar." },
41699
+ { id: "monto_pendiente_de_facturar", version: 1, estado: "validada", nombre: "Monto pendiente de facturar", unidad: "$", definicion: "Importe de los bonos pendientes de facturar del per\xEDodo, por fecha de la prestaci\xF3n." },
41700
+ { id: "debitos_financiador", version: 1, estado: "a_definir", nombre: "D\xE9bitos de financiadores", unidad: "$", definicion: "Sin definir: el hospital tiene que decidir si cuenta todas las notas de cr\xE9dito o solo las que responden a un d\xE9bito del financiador. Hasta entonces no se publica como n\xFAmero oficial." }
41701
+ ];
41702
+ function metricaPorId(id) {
41703
+ return METRICAS.find((m) => m.id === id) ?? null;
41704
+ }
41705
+
41672
41706
  // src/tableros.ts
41673
41707
  import { randomUUID as randomUUID2 } from "node:crypto";
41674
41708
  var TIPOS = ["indicador", "barras", "linea", "tabla", "texto"];
@@ -41715,6 +41749,79 @@ function numero2(v, campo, problemas) {
41715
41749
  function ancho(v) {
41716
41750
  return v === 1 || v === 2 || v === 3 ? v : void 0;
41717
41751
  }
41752
+ function aNumero(v) {
41753
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
41754
+ if (typeof v !== "string") return null;
41755
+ let s = v.trim().replace(/[$\s%]/g, "");
41756
+ if (s === "") return null;
41757
+ if (s.includes(",")) s = s.replace(/\./g, "").replace(",", ".");
41758
+ else if (/^-?\d{1,3}(\.\d{3})+$/.test(s)) s = s.replace(/\./g, "");
41759
+ const n = Number(s);
41760
+ return Number.isFinite(n) ? n : null;
41761
+ }
41762
+ var ES_VARIACION = /varia|cambio|crec|difer|dif\.|Δ|meta|cumpl|\bvs\b|versus/i;
41763
+ function controlarPorcentaje(valor, donde, problemas) {
41764
+ const n = aNumero(valor);
41765
+ if (n !== null && (n < 0 || n > 100)) problemas.push(`${donde} es un porcentaje y vale ${n}: tiene que estar entre 0 y 100 (si es una variaci\xF3n o un cumplimiento sobre meta, decirlo en el t\xEDtulo)`);
41766
+ }
41767
+ var MAX_CONSULTA = 8e3;
41768
+ function leerCalculo(v, ref, problemas) {
41769
+ if (v === void 0 || v === null) return { origen: "exploratorio" };
41770
+ if (typeof v !== "object" || Array.isArray(v)) {
41771
+ problemas.push(`${ref}.calculo tiene que ser el bloque \xABcalculo\xBB que devuelve calcular.mjs`);
41772
+ return { origen: "exploratorio" };
41773
+ }
41774
+ const o = v;
41775
+ const lista = arr(o.metricas);
41776
+ if (lista.length === 0) problemas.push(`${ref}.calculo.metricas est\xE1 vac\xEDo`);
41777
+ let propuesta = false;
41778
+ const metricas = [];
41779
+ lista.slice(0, 12).forEach((m, j) => {
41780
+ const mo = m && typeof m === "object" ? m : {};
41781
+ const id = typeof mo.id === "string" ? mo.id : "";
41782
+ const version2 = Number(mo.version);
41783
+ const def2 = metricaPorId(id);
41784
+ if (!def2) {
41785
+ problemas.push(`${ref}.calculo.metricas[${j}]: \xAB${id}\xBB no est\xE1 en el cat\xE1logo de m\xE9tricas; un n\xFAmero fuera del cat\xE1logo se publica sin calculo (queda como exploratorio)`);
41786
+ return;
41787
+ }
41788
+ if (def2.version !== version2) problemas.push(`${ref}.calculo.metricas[${j}]: \xAB${id}\xBB versi\xF3n ${Number.isFinite(version2) ? version2 : "?"} no es la vigente (${def2.version}): recalcular con calcular.mjs`);
41789
+ if (def2.estado === "a_definir") problemas.push(`${ref}.calculo.metricas[${j}]: \xAB${id}\xBB todav\xEDa no tiene definici\xF3n aprobada y no se publica como n\xFAmero oficial`);
41790
+ if (def2.estado === "propuesta") propuesta = true;
41791
+ metricas.push({ id, version: def2.version });
41792
+ });
41793
+ const huella = typeof o.huella === "string" && /^[0-9a-f]{16}$/.test(o.huella) ? o.huella : "";
41794
+ if (!huella) problemas.push(`${ref}.calculo.huella falta o no tiene la forma de calcular.mjs (16 caracteres hexadecimales)`);
41795
+ const parametros = o.parametros && typeof o.parametros === "object" && !Array.isArray(o.parametros) ? o.parametros : {};
41796
+ if (JSON.stringify(parametros).length > 2e3) problemas.push(`${ref}.calculo.parametros es demasiado largo`);
41797
+ const consulta = typeof o.consulta === "string" && o.consulta.trim() ? o.consulta.slice(0, MAX_CONSULTA) : void 0;
41798
+ const catalogo = typeof o.catalogo === "string" ? o.catalogo.slice(0, 40) : "";
41799
+ return { calculo: { catalogo, metricas, parametros, huella, ...consulta ? { consulta } : {} }, origen: propuesta ? "propuesta" : "certificado" };
41800
+ }
41801
+ var esTotal = (c) => typeof c === "string" && /^total\b/i.test(c.trim());
41802
+ var esSubtotal = (c) => typeof c === "string" && /^subtotal\b/i.test(c.trim());
41803
+ function controlarTabla(ref, columnas, filas, problemas) {
41804
+ columnas.forEach((col, j) => {
41805
+ if (!col.includes("%") || ES_VARIACION.test(col)) return;
41806
+ filas.forEach((f, i) => controlarPorcentaje(f[j], `${ref}.filas[${i}] \xAB${col}\xBB`, problemas));
41807
+ });
41808
+ const totales = filas.filter((f) => esTotal(f[0]));
41809
+ if (totales.length !== 1) return;
41810
+ const total = totales[0];
41811
+ const detalle = filas.filter((f) => !esTotal(f[0]) && !esSubtotal(f[0]));
41812
+ if (detalle.length === 0) return;
41813
+ for (let j = 1; j < columnas.length; j += 1) {
41814
+ if (columnas[j].includes("%")) continue;
41815
+ const esperado = aNumero(total[j]);
41816
+ if (esperado === null) continue;
41817
+ const valores = detalle.map((f) => f[j]);
41818
+ if (valores.some((x) => x !== null && x !== "" && aNumero(x) === null)) continue;
41819
+ const suma = valores.reduce((acc, x) => acc + (aNumero(x) ?? 0), 0);
41820
+ if (Math.abs(suma - esperado) > Math.max(0.011, Math.abs(esperado) * 1e-6)) {
41821
+ problemas.push(`${ref}: la columna \xAB${columnas[j]}\xBB suma ${Math.round(suma * 100) / 100} y la fila TOTAL dice ${esperado}`);
41822
+ }
41823
+ }
41824
+ }
41718
41825
  function validarWidgets(entrada) {
41719
41826
  const problemas = [];
41720
41827
  const lista = Array.isArray(entrada) ? entrada : [];
@@ -41736,39 +41843,38 @@ function validarWidgets(entrada) {
41736
41843
  const valor = typeof o.valor === "number" && Number.isFinite(o.valor) ? o.valor : texto2(o.valor, 40, `${ref}.valor`, problemas, true) ?? "";
41737
41844
  const tono = typeof o.tono === "string" && TONOS.includes(o.tono) ? o.tono : void 0;
41738
41845
  if (o.tono !== void 0 && !tono) problemas.push(`${ref}.tono debe ser uno de ${TONOS.join(", ")}`);
41739
- salida.push({ tipo, etiqueta, valor, unidad: texto2(o.unidad, 16, `${ref}.unidad`, problemas), contexto: texto2(o.contexto, 160, `${ref}.contexto`, problemas), tono, ancho: a });
41846
+ const unidad = texto2(o.unidad, 16, `${ref}.unidad`, problemas);
41847
+ if (unidad === "%" && !ES_VARIACION.test(etiqueta)) controlarPorcentaje(valor, `${ref}.valor`, problemas);
41848
+ const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
41849
+ salida.push({ tipo, etiqueta, valor, unidad, contexto: texto2(o.contexto, 160, `${ref}.contexto`, problemas), tono, ancho: a, ...calculo ? { calculo } : {}, origen });
41740
41850
  break;
41741
41851
  }
41742
41852
  case "barras": {
41743
41853
  const titulo = texto2(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
41744
41854
  const series = arr(o.series);
41745
41855
  if (series.length === 0 || series.length > 40) problemas.push(`${ref}.series debe tener entre 1 y 40 barras`);
41746
- salida.push({
41747
- tipo,
41748
- titulo,
41749
- unidad: texto2(o.unidad, 16, `${ref}.unidad`, problemas),
41750
- series: series.slice(0, 40).map((s, j) => {
41751
- const so = s && typeof s === "object" ? s : {};
41752
- return { etiqueta: texto2(so.etiqueta, 60, `${ref}.series[${j}].etiqueta`, problemas, true) ?? "", valor: numero2(so.valor, `${ref}.series[${j}].valor`, problemas) };
41753
- }),
41754
- ancho: a
41856
+ const unidad = texto2(o.unidad, 16, `${ref}.unidad`, problemas);
41857
+ const barras = series.slice(0, 40).map((s, j) => {
41858
+ const so = s && typeof s === "object" ? s : {};
41859
+ return { etiqueta: texto2(so.etiqueta, 60, `${ref}.series[${j}].etiqueta`, problemas, true) ?? "", valor: numero2(so.valor, `${ref}.series[${j}].valor`, problemas) };
41755
41860
  });
41861
+ if (unidad === "%" && !ES_VARIACION.test(titulo)) barras.forEach((b, j) => controlarPorcentaje(b.valor, `${ref}.series[${j}].valor`, problemas));
41862
+ const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
41863
+ salida.push({ tipo, titulo, unidad, series: barras, ancho: a, ...calculo ? { calculo } : {}, origen });
41756
41864
  break;
41757
41865
  }
41758
41866
  case "linea": {
41759
41867
  const titulo = texto2(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
41760
41868
  const puntos = arr(o.puntos);
41761
41869
  if (puntos.length === 0 || puntos.length > 400) problemas.push(`${ref}.puntos debe tener entre 1 y 400 puntos`);
41762
- salida.push({
41763
- tipo,
41764
- titulo,
41765
- unidad: texto2(o.unidad, 16, `${ref}.unidad`, problemas),
41766
- puntos: puntos.slice(0, 400).map((p, j) => {
41767
- const po = p && typeof p === "object" ? p : {};
41768
- return { x: texto2(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
41769
- }),
41770
- ancho: a
41870
+ const unidad = texto2(o.unidad, 16, `${ref}.unidad`, problemas);
41871
+ const serie2 = puntos.slice(0, 400).map((p, j) => {
41872
+ const po = p && typeof p === "object" ? p : {};
41873
+ return { x: texto2(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
41771
41874
  });
41875
+ if (unidad === "%" && !ES_VARIACION.test(titulo)) serie2.forEach((p, j) => controlarPorcentaje(p.y, `${ref}.puntos[${j}].y`, problemas));
41876
+ const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
41877
+ salida.push({ tipo, titulo, unidad, puntos: serie2, ancho: a, ...calculo ? { calculo } : {}, origen });
41772
41878
  break;
41773
41879
  }
41774
41880
  case "tabla": {
@@ -41778,17 +41884,18 @@ function validarWidgets(entrada) {
41778
41884
  const filas = arr(o.filas);
41779
41885
  if (filas.length > 200) problemas.push(`${ref}.filas admite hasta 200 filas`);
41780
41886
  const cols = columnas.slice(0, 12).map((c, j) => texto2(c, 60, `${ref}.columnas[${j}]`, problemas, true) ?? "");
41781
- salida.push({
41782
- tipo,
41783
- titulo,
41784
- columnas: cols,
41785
- filas: filas.slice(0, 200).map((f, j) => {
41786
- const celdas = arr(f);
41787
- if (celdas.length !== cols.length) problemas.push(`${ref}.filas[${j}] tiene ${celdas.length} celdas y la tabla ${cols.length} columnas`);
41788
- return celdas.slice(0, cols.length).map((c) => typeof c === "number" && Number.isFinite(c) ? c : c === null || c === void 0 ? null : String(c).slice(0, 200));
41789
- }),
41790
- ancho: a
41887
+ const celdasTabla = filas.slice(0, 200).map((f, j) => {
41888
+ const celdas = arr(f);
41889
+ if (celdas.length !== cols.length) problemas.push(`${ref}.filas[${j}] tiene ${celdas.length} celdas y la tabla ${cols.length} columnas`);
41890
+ return celdas.slice(0, cols.length).map((c) => typeof c === "number" && Number.isFinite(c) ? c : c === null || c === void 0 ? null : String(c).slice(0, 200));
41791
41891
  });
41892
+ controlarTabla(ref, cols, celdasTabla, problemas);
41893
+ const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
41894
+ const por = calculo && Array.isArray(calculo.parametros.por) ? calculo.parametros.por : [];
41895
+ if (calculo && por.length > 0 && !celdasTabla.some((f) => esTotal(f[0]))) {
41896
+ problemas.push(`${ref}: una tabla con desglose de m\xE9tricas del cat\xE1logo lleva la fila TOTAL que devuelve calcular.mjs (primera celda \xABTOTAL\xBB)`);
41897
+ }
41898
+ salida.push({ tipo, titulo, columnas: cols, filas: celdasTabla, ancho: a, ...calculo ? { calculo } : {}, origen });
41792
41899
  break;
41793
41900
  }
41794
41901
  case "texto": {
@@ -42120,13 +42227,15 @@ async function manejarApi(ctx, input2) {
42120
42227
  return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "no_corresponde", motivo: motivoConPersona(str2(body.motivo)), datos: conPersona(obj(body.datos)), idempotencyKey: str2(body.idempotencyKey), actor }));
42121
42228
  }
42122
42229
  case "version":
42123
- return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa"], workflows: listarWorkflows().map((w) => w.clave) });
42230
+ return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas"], workflows: listarWorkflows().map((w) => w.clave) });
42124
42231
  case "politicas":
42125
42232
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
42126
42233
  case "serie":
42127
42234
  return respuesta(200, await serie(ctx, companyId, numOrNull(qs("dias"))));
42128
42235
  case "historial-politicas":
42129
42236
  return respuesta(200, { cambios: await historialPoliticas(ctx, companyId, numOrNull(qs("limit"))) });
42237
+ case "metricas":
42238
+ return respuesta(200, { version: CATALOGO_VERSION, metricas: METRICAS });
42130
42239
  case "tableros":
42131
42240
  return respuesta(200, { tableros: await listarTableros(ctx, companyId) });
42132
42241
  case "tablero-publicado": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Autopilot Care · operación autónoma gobernada para instituciones de salud: workflows determinísticos, validaciones, registro de negocio, políticas (observa/propone/ejecuta/corte) y tablero del director, como plugin de Paperclip.",
6
6
  "files": [