@dozimple/abap-adt 1.0.1 → 1.2.0

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.
@@ -45,6 +45,13 @@ export async function orderHeaders(sap, trkorrs) {
45
45
  }
46
46
  return out;
47
47
  }
48
+ /** Entradas E071 de una orden y de todas sus tareas. */
49
+ export async function orderEntries(sap, root) {
50
+ const tasks = await sap.query(`SELECT trkorr FROM e070 WHERE strkorr = ${sqlLiteral(root)}`, 500);
51
+ const ids = [root, ...tasks.values.map((v) => v.TRKORR)];
52
+ const rows = await sap.query(`SELECT pgmid, object, obj_name FROM e071 WHERE trkorr IN ( ${ids.map(sqlLiteral).join(", ")} )`, 5000);
53
+ return rows.values;
54
+ }
48
55
  export function describeOrder(h) {
49
56
  return (`${h.trkorr} «${h.text}» · ${TRFUNCTION[h.trfunction] ?? h.trfunction} · ${TRSTATUS[h.trstatus] ?? h.trstatus} · ` +
50
57
  `${h.owner}` + (h.tarsystem ? ` · destino ${h.tarsystem}` : ""));
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { dataClassOf } from "../../core/datapolicy.js";
2
3
  import { ToolError } from "../../core/errors.js";
3
4
  import { decodeEntities, htmlToText } from "../../core/feeds.js";
4
5
  import { budget } from "../../core/output.js";
@@ -11,16 +12,87 @@ function headerField(d, label) {
11
12
  const m = d.text.match(new RegExp(`<b>${label}&nbsp;</b></td><td[^>]*>([\\s\\S]*?)</td>`));
12
13
  return m ? decode(m[1]).trim() : undefined;
13
14
  }
15
+ const cat = (d, label) => d.categories.find((c) => c.label === label)?.term;
14
16
  function summary(d, i) {
15
- const cat = (label) => d.categories.find((c) => c.label === label)?.term;
16
- return (`${i}. ${headerField(d, "Date/Time")?.replace(" (System)", "") ?? "?"} · ${cat("ABAP runtime error") ?? "?"} · ` +
17
- `${cat("Terminated ABAP program") ?? "?"} · ${d.author ?? "?"}\n ${headerField(d, "Short Text") ?? ""}`);
17
+ return (`${i}. ${headerField(d, "Date/Time")?.replace(" (System)", "") ?? "?"} · ${cat(d, "ABAP runtime error") ?? "?"} · ` +
18
+ `${cat(d, "Terminated ABAP program") ?? "?"} · ${d.author ?? "?"}\n ${headerField(d, "Short Text") ?? ""}`);
19
+ }
20
+ /**
21
+ * Cabecera de un dump tal como la guarda ST22 en SNAP_BEG.FLIST: secuencia de «id (2) + longitud (3) + valor».
22
+ * Solo trae metadatos (error, programa, include, línea, clase de excepción, ids), nunca valores de variables: esos
23
+ * están en SNAP, que sigue vetada. Ids verificados en un NW 7.50 real.
24
+ */
25
+ export function parseFlist(flist) {
26
+ const out = {};
27
+ for (let i = 0; i + 5 <= flist.length;) {
28
+ const id = flist.slice(i, i + 2);
29
+ const len = Number(flist.slice(i + 2, i + 5));
30
+ if (!/^[A-Z0-9]{2}$/.test(id) || !Number.isInteger(len) || len < 0)
31
+ break;
32
+ out[id] = flist.slice(i + 5, i + 5 + len).trim();
33
+ i += 5 + len;
34
+ }
35
+ return out;
36
+ }
37
+ const GROUP_BY = ["error", "program", "error_program", "user", "day"];
38
+ /** El tope de filas que se piden a SNAP_BEG: por encima, la respuesta dice que puede haber más. */
39
+ export const GROUP_ROWS = 5000;
40
+ function groupKey(by, r, h) {
41
+ const error = h.FC || "?";
42
+ const program = h.AP || "?";
43
+ switch (by) {
44
+ case "error": return error;
45
+ case "program": return program;
46
+ case "error_program": return `${error} · ${program}`;
47
+ case "user": return String(r.UNAME ?? "?");
48
+ case "day": return String(r.DATUM ?? "?");
49
+ }
50
+ }
51
+ export function groupDumps(rows, by) {
52
+ const groups = new Map();
53
+ for (const r of rows) {
54
+ const h = parseFlist(String(r.FLIST ?? ""));
55
+ const key = groupKey(by, r, h);
56
+ const at = `${r.DATUM} ${String(r.UZEIT ?? "").slice(0, 6)}`;
57
+ let g = groups.get(key);
58
+ if (!g) {
59
+ g = { key, count: 0, first: at, last: at, users: 0, userSet: new Set() };
60
+ groups.set(key, g);
61
+ }
62
+ g.count++;
63
+ if (at < g.first)
64
+ g.first = at;
65
+ if (at >= g.last) {
66
+ g.last = at;
67
+ if (h.AI)
68
+ g.where = `${h.AI}${h.AL ? ` L${h.AL}` : ""}`;
69
+ }
70
+ g.userSet.add(String(r.UNAME ?? ""));
71
+ }
72
+ return [...groups.values()]
73
+ .map(({ userSet, ...g }) => ({ ...g, users: userSet.size }))
74
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
75
+ }
76
+ const GROUP = z.object({
77
+ key: z.string(),
78
+ count: z.number().int(),
79
+ first: z.string().describe("AAAAMMDD hhmmss del primer dump del grupo en el periodo"),
80
+ last: z.string(),
81
+ users: z.number().int().describe("Usuarios distintos afectados"),
82
+ where: z.string().optional().describe("Include y línea del dump más reciente del grupo"),
83
+ });
84
+ function dayStamp(daysAgo, now = new Date()) {
85
+ const d = new Date(now.getTime() - daysAgo * 86_400_000);
86
+ return d.toISOString().slice(0, 10).replace(/-/g, "");
18
87
  }
19
88
  export default defineTool({
20
89
  name: "dumps",
21
90
  title: "Dumps (ST22)",
22
91
  description: "Lista los dumps de ejecución (ST22): fecha, error, programa, usuario y texto corto. Filtra por usuario y por " +
23
- "texto (error o programa). Con detail=N devuelve el dump N completo (qué pasó, análisis, dónde terminó, fuente, pila).",
92
+ "texto (error o programa). Con detail=N devuelve el dump N completo (qué pasó, análisis, dónde terminó, fuente, pila). " +
93
+ "Con group_by (error, program, error_program, user, day) y days=N responde «los errores más frecuentes del periodo»: " +
94
+ "cuenta los dumps de los últimos N días agrupados, con primer y último caso, usuarios afectados y dónde terminó. " +
95
+ "La lista y el detalle salen del feed ADT (solo los más recientes); los grupos, de la cabecera de ST22 (SNAP_BEG).",
24
96
  access: "read",
25
97
  requires: { adt: ["/sap/bc/adt/runtime/dumps"] },
26
98
  input: {
@@ -28,29 +100,80 @@ export default defineTool({
28
100
  contains: z.string().optional().describe("Filtra por error o programa, p. ej. CALL_FUNCTION_NOT_FOUND o ZFI_REPORTE"),
29
101
  max: z.number().int().min(1).max(200).default(20),
30
102
  detail: z.number().int().min(1).optional().describe("Número de la lista para ver el dump completo"),
103
+ group_by: z.enum(GROUP_BY).optional().describe("Agrupar los dumps del periodo: por error, programa, error+programa, usuario o día"),
104
+ days: z.number().int().min(1).max(90).default(7).describe("Periodo hacia atrás para group_by (días)"),
31
105
  },
32
- async run({ user, contains, max, detail }, { sap }) {
106
+ output: {
107
+ mode: z.enum(["list", "detail", "groups"]),
108
+ total: z.number().int().describe("Dumps considerados (feed leído o filas del periodo)"),
109
+ period_days: z.number().int().optional(),
110
+ since: z.string().optional().describe("AAAAMMDD desde el que se contó"),
111
+ truncated: z.boolean().optional().describe("true si el periodo tiene más dumps de los que se pudieron leer"),
112
+ group_by: z.enum(GROUP_BY).optional(),
113
+ groups: z.array(GROUP).optional(),
114
+ dumps: z.array(z.object({ n: z.number().int(), error: z.string(), program: z.string(), user: z.string() })).optional(),
115
+ },
116
+ async run({ user, contains, max, detail, group_by, days }, { sap, system }) {
117
+ if (group_by) {
118
+ if (group_by === "user" && dataClassOf(system) !== "test") {
119
+ throw new ToolError("POLICY", `En ${system.id} (datos ${dataClassOf(system)}) no se agrupa por usuario: es una columna personal. Agrupa por error o programa; el número de usuarios afectados sale igualmente.`);
120
+ }
121
+ const since = dayStamp(days);
122
+ const where = [`seqno = '000'`, `datum >= '${since}'`, ...(user ? [`uname = '${user.toUpperCase()}'`] : [])].join(" AND ");
123
+ const r = await sap.query(`SELECT datum, uzeit, uname, flist FROM snap_beg WHERE ${where} ORDER BY datum DESCENDING, uzeit DESCENDING`, GROUP_ROWS);
124
+ const needle = contains?.toUpperCase();
125
+ const rows = r.values.filter((row) => {
126
+ if (!needle)
127
+ return true;
128
+ const h = parseFlist(String(row.FLIST ?? ""));
129
+ return (h.FC ?? "").toUpperCase().includes(needle) || (h.AP ?? "").toUpperCase().includes(needle);
130
+ });
131
+ const truncated = r.values.length >= GROUP_ROWS;
132
+ const groups = groupDumps(rows, group_by);
133
+ const shown = groups.slice(0, max);
134
+ const structured = { mode: "groups", total: rows.length, period_days: days, since, truncated, group_by, groups: shown };
135
+ if (!rows.length) {
136
+ return { text: `Sin dumps desde ${since} (${days} días)${user ? ` del usuario ${user}` : ""}${contains ? ` que casen con «${contains}»` : ""}: se consultó ST22 (SNAP_BEG) y no hay filas.`, structured };
137
+ }
138
+ const label = { error: "error", program: "programa", error_program: "error · programa", user: "usuario", day: "día" };
139
+ const lines = shown.map((g, i) => `${String(i + 1).padStart(3)}. ${String(g.count).padStart(5)} ${g.key} · ${g.users} usuario${g.users === 1 ? "" : "s"} · ${g.first.slice(0, 8)} → ${g.last.slice(0, 8)}${g.where ? ` · ${g.where}` : ""}`);
140
+ return {
141
+ text: `${rows.length} dumps desde ${since} (${days} días)${truncated ? ` — TOPE ALCANZADO (${GROUP_ROWS} filas): puede haber más; acota con days o contains` : ""}` +
142
+ `, ${groups.length} grupos por ${label[group_by]} (se muestran ${shown.length})\n\n` +
143
+ ` # dumps ${label[group_by]}\n${lines.join("\n")}\n\n` +
144
+ `Siguiente: dumps(contains="<error o programa>") para ver los casos recientes y dumps(detail=N) para uno completo.`,
145
+ structured,
146
+ };
147
+ }
33
148
  const c = await sap.adt();
34
149
  const feed = await c.dumps(user ? `and( equals( user, ${user.toUpperCase()} ) )` : "");
35
150
  const needle = contains?.toUpperCase();
36
151
  const list = feed.dumps.filter((d) => !needle || d.categories.some((x) => x.term.toUpperCase().includes(needle)));
152
+ const asRow = (d, i) => ({ n: i + 1, error: cat(d, "ABAP runtime error") ?? "?", program: cat(d, "Terminated ABAP program") ?? "?", user: d.author ?? "?" });
37
153
  if (!list.length) {
38
- return `El feed de dumps se leyó (${feed.dumps.length} en total) y ninguno casa${user ? ` con usuario ${user}` : ""}${contains ? ` y «${contains}»` : ""}.`;
154
+ return {
155
+ text: `El feed de dumps se leyó (${feed.dumps.length} en total) y ninguno casa${user ? ` con usuario ${user}` : ""}${contains ? ` y «${contains}»` : ""}. Para un periodo largo usa group_by.`,
156
+ structured: { mode: "list", total: feed.dumps.length, dumps: [] },
157
+ };
39
158
  }
40
159
  if (detail) {
41
160
  const d = list[detail - 1];
42
161
  if (!d)
43
162
  throw new ToolError("INPUT", `Solo hay ${list.length} dumps en la lista.`);
163
+ const structured = { mode: "detail", total: list.length, dumps: [asRow(d, detail - 1)] };
44
164
  const self = d.links.find((l) => l.rel === "self")?.href.replace(/^adt:\/\/[^/]+/, "");
45
165
  if (!self)
46
- return budget(stripHtml(d.text));
166
+ return { text: budget(stripHtml(d.text)), structured };
47
167
  // El recurso ofrece HTML (dump completo) o un XML índice; text/plain da 406.
48
168
  const r = await c.httpClient.request(self, { headers: { Accept: "text/html" } });
49
- return budget(`${summary(d, detail)}\n\n${stripHtml(String(r.body))}`, undefined, "El dump completo está en ST22.");
169
+ return { text: budget(`${summary(d, detail)}\n\n${stripHtml(String(r.body))}`, undefined, "El dump completo está en ST22."), structured };
50
170
  }
51
171
  const shown = list.slice(0, max);
52
- return (`${list.length} dumps${needle || user ? " que casan" : ""} (se muestran ${shown.length}; el feed trae los más recientes)\n\n` +
53
- shown.map((d, i) => summary(d, i + 1)).join("\n"));
172
+ return {
173
+ text: `${list.length} dumps${needle || user ? " que casan" : ""} (se muestran ${shown.length}; el feed trae solo los más recientes: para un periodo usa group_by)\n\n` +
174
+ shown.map((d, i) => summary(d, i + 1)).join("\n"),
175
+ structured: { mode: "list", total: list.length, dumps: shown.map(asRow) },
176
+ };
54
177
  },
55
178
  });
56
179
  //# sourceMappingURL=dumps.js.map
@@ -0,0 +1,135 @@
1
+ import { z } from "zod";
2
+ import { rowCap } from "../../core/datapolicy.js";
3
+ import { ToolError } from "../../core/errors.js";
4
+ import { sqlLiteral } from "../../core/objects.js";
5
+ import { tsv } from "../../core/output.js";
6
+ import { defineTool } from "../../core/tool.js";
7
+ /**
8
+ * Ampliaciones del sistema por SQL de diccionario (valen en 7.50 y S/4):
9
+ * - BAdI nuevas (framework de ampliaciones): BADIIMPL_ENH; clásicas: SXC_EXIT + SXC_ATTR.
10
+ * - Ampliaciones en código (puntos y secciones implícitos o explícitos): ENHINCINX, por programa principal.
11
+ * - Implementaciones de ampliación: ENHHEADER.
12
+ * Los significados vienen del diccionario (ENHMODE, ENHBOOLEAN, ACTIVEFLAG), verificados en un 7.50.
13
+ */
14
+ const ENHMODE = { S: "estática", D: "dinámica" };
15
+ /** ENHTOOLTYPE no tiene valores fijos en el diccionario: se muestra el código y, si se conoce, qué es. */
16
+ const TOOL = {
17
+ HOOK_IMPL: "ampliación de código (punto o sección)",
18
+ BADI_IMPL: "implementación de BAdI",
19
+ CLASENH: "ampliación de clase",
20
+ INTFENH: "ampliación de interfaz",
21
+ FUGRENH: "ampliación de grupo de funciones",
22
+ WDYENH: "ampliación Web Dynpro",
23
+ };
24
+ const tool = (t) => (TOOL[t] ? `${t} (${TOOL[t]})` : t);
25
+ /** Nombre del programa principal que usa ENHINCINX: clase → NOMBRE====…CP (32), grupo → SAPL<grupo>. */
26
+ export function mainProgram(name, type) {
27
+ const n = name.trim().toUpperCase();
28
+ if (type === "PROG")
29
+ return n;
30
+ if (type === "CLAS")
31
+ return `${n.padEnd(30, "=")}CP`;
32
+ const m = /^(\/[^/]+\/)(.+)$/.exec(n);
33
+ return m ? `${m[1]}SAPL${m[2]}` : `SAPL${n}`;
34
+ }
35
+ const NAME = /^[A-Z0-9_/]{1,40}$/i;
36
+ /** Propias del cliente primero: Z, Y y espacios de nombres que no son de SAP no se distinguen aquí; Z/Y sí. */
37
+ const isCustom = (n) => /^[ZY]/i.test(n);
38
+ export default defineTool({
39
+ name: "enhancements",
40
+ title: "Ampliaciones y BAdI",
41
+ description: "Qué ampliaciones hay y dónde. badi=NOMBRE: implementaciones de esa BAdI, nuevas y clásicas, activas o no. " +
42
+ "program + program_type (PROG, CLAS, FUGR): ampliaciones de código (puntos y secciones implícitos o explícitos) " +
43
+ "implementadas dentro de ese programa, clase o grupo — lo que cambia el comportamiento de un estándar sin tocar " +
44
+ "su código. prefix (p. ej. Z): implementaciones de ampliación de ese espacio de nombres, por tipo. Solo lectura.",
45
+ access: "read",
46
+ input: {
47
+ badi: z.string().regex(NAME).optional().describe("Definición de BAdI, p. ej. MB_MIGO_BADI"),
48
+ program: z.string().regex(NAME).optional().describe("Programa, clase o grupo de funciones ampliado, p. ej. SAPMV45A"),
49
+ program_type: z.enum(["PROG", "CLAS", "FUGR"]).default("PROG"),
50
+ prefix: z.string().regex(/^[A-Z0-9_/]{1,20}$/i).optional().describe("Implementaciones cuyo nombre empieza así, p. ej. Z o ZSD"),
51
+ tool_type: z.string().regex(/^[A-Z_]{3,20}$/i).optional().describe("Con prefix: filtrar por tipo, p. ej. BADI_IMPL o HOOK_IMPL"),
52
+ max: z.number().int().min(1).max(2000).default(200),
53
+ },
54
+ output: {
55
+ mode: z.enum(["badi", "program", "prefix"]),
56
+ target: z.string(),
57
+ rows: z.array(z.record(z.union([z.string(), z.number(), z.boolean()]))),
58
+ total: z.number().int(),
59
+ truncated: z.boolean(),
60
+ },
61
+ async run({ badi, program, program_type, prefix, tool_type, max }, { sap, system }) {
62
+ if ([badi, program, prefix].filter(Boolean).length !== 1)
63
+ throw new ToolError("INPUT", "Indica exactamente uno: badi, program o prefix.");
64
+ const cap = Math.min(max, rowCap(system));
65
+ if (badi) {
66
+ const b = badi.toUpperCase();
67
+ const kernel = await sap.query(`SELECT enhname, badi_impl, active, spotname FROM badiimpl_enh WHERE badi_name = ${sqlLiteral(b)}`, 2000);
68
+ const classic = await sap.query(`SELECT imp_name FROM sxc_exit WHERE exit_name = ${sqlLiteral(b)}`, 2000);
69
+ const names = classic.values.map((v) => String(v.IMP_NAME).trim());
70
+ const attr = names.length ? await sap.query(`SELECT imp_name, active FROM sxc_attr WHERE imp_name IN ( ${names.map(sqlLiteral).join(", ")} )`, 2000) : { values: [] };
71
+ const activeClassic = new Map(attr.values.map((v) => [String(v.IMP_NAME).trim(), v.ACTIVE === "X"]));
72
+ // Una BAdI clásica migrada aparece en las dos tablas: se muestra una vez, como nueva.
73
+ const kernelImpls = new Set(kernel.values.map((v) => String(v.BADI_IMPL).trim()));
74
+ const rows = [
75
+ ...kernel.values.map((v) => ({ implementation: String(v.BADI_IMPL).trim(), kind: "nueva", enhancement: String(v.ENHNAME).trim(), spot: String(v.SPOTNAME ?? "").trim(), active: v.ACTIVE === "X" })),
76
+ ...names.filter((n) => !kernelImpls.has(n)).map((n) => ({ implementation: n, kind: "clásica", enhancement: "", spot: "", active: activeClassic.get(n) ?? false })),
77
+ ].sort((a, b2) => Number(isCustom(b2.implementation)) - Number(isCustom(a.implementation)) || Number(b2.active) - Number(a.active) || a.implementation.localeCompare(b2.implementation));
78
+ const shown = rows.slice(0, cap);
79
+ const structured = { mode: "badi", target: b, rows: shown, total: rows.length, truncated: rows.length > cap };
80
+ if (!rows.length)
81
+ return { text: `La BAdI ${b} no tiene implementaciones en este sistema (se consultaron BADIIMPL_ENH y SXC_EXIT).`, structured };
82
+ const act = rows.filter((r) => r.active).length;
83
+ return {
84
+ text: `BAdI ${b}: ${rows.length} implementaciones, ${act} activas, ${rows.filter((r) => isCustom(r.implementation)).length} propias (Z/Y, primero)` +
85
+ `${rows.length > cap ? ` (se muestran ${cap})` : ""}\n\n` +
86
+ tsv(["implementación", "tipo", "activa", "ampliación", "spot"], shown.map((r) => [r.implementation, r.kind, r.active ? "sí" : "NO", r.enhancement, r.spot])),
87
+ structured,
88
+ };
89
+ }
90
+ if (program) {
91
+ const p = mainProgram(program, program_type);
92
+ const r = await sap.query(`SELECT enhname, version, enhmode, overwrite FROM enhincinx WHERE programname = ${sqlLiteral(p)} ORDER BY enhname`, 5000);
93
+ const by = new Map();
94
+ for (const v of r.values) {
95
+ const k = String(v.ENHNAME).trim();
96
+ const cur = by.get(k) ?? { enhancement: k, mode: ENHMODE[String(v.ENHMODE)] ?? String(v.ENHMODE), active: false, overwrite: false, places: 0 };
97
+ cur.places++;
98
+ if (v.VERSION === "A")
99
+ cur.active = true;
100
+ if (v.OVERWRITE === "X")
101
+ cur.overwrite = true;
102
+ by.set(k, cur);
103
+ }
104
+ const rows = [...by.values()].sort((a, b2) => a.enhancement.localeCompare(b2.enhancement));
105
+ const shown = rows.slice(0, cap);
106
+ const structured = { mode: "program", target: p, rows: shown, total: rows.length, truncated: rows.length > cap };
107
+ if (!rows.length)
108
+ return { text: `${program.toUpperCase()} (${p}) no tiene ampliaciones de código implementadas (se consultó ENHINCINX).`, structured };
109
+ const over = rows.filter((x) => x.overwrite).length;
110
+ return {
111
+ text: `${program.toUpperCase()} (${p}): ${rows.length} implementaciones de ampliación en su código` +
112
+ `${over ? ` — ${over} SUSTITUYEN código estándar (overwrite)` : ""}${rows.length > cap ? ` (se muestran ${cap})` : ""}\n\n` +
113
+ tsv(["ampliación", "modo", "activa", "sustituye", "lugares"], shown.map((x) => [x.enhancement, x.mode, x.active ? "sí" : "solo inactiva", x.overwrite ? "SÍ" : "", x.places])),
114
+ structured,
115
+ };
116
+ }
117
+ const pre = prefix.toUpperCase();
118
+ const typeFilter = tool_type ? ` AND enhtooltype = ${sqlLiteral(tool_type.toUpperCase())}` : "";
119
+ const where = `WHERE enhname LIKE ${sqlLiteral(`${pre}%`)} AND version = 'A'${typeFilter}`;
120
+ const counts = await sap.query(`SELECT enhtooltype, COUNT(*) AS n FROM enhheader ${where} GROUP BY enhtooltype`, 50);
121
+ const total = counts.values.reduce((s, v) => s + Number(v.N), 0);
122
+ const r = await sap.query(`SELECT enhname, enhtooltype FROM enhheader ${where} ORDER BY enhname`, cap);
123
+ const rows = r.values.map((v) => ({ enhancement: String(v.ENHNAME).trim(), type: String(v.ENHTOOLTYPE).trim() }));
124
+ const structured = { mode: "prefix", target: pre, rows, total, truncated: total > rows.length };
125
+ if (!total)
126
+ return { text: `No hay implementaciones de ampliación activas que empiecen por ${pre}${tool_type ? ` de tipo ${tool_type}` : ""}.`, structured };
127
+ const summary = counts.values.map((v) => `${tool(String(v.ENHTOOLTYPE).trim())}: ${Number(v.N)}`).join(" · ");
128
+ return {
129
+ text: `${total} implementaciones de ampliación activas que empiezan por ${pre}${total > rows.length ? ` (se muestran ${rows.length})` : ""}\n${summary}\n\n` +
130
+ tsv(["ampliación", "tipo"], rows.map((x) => [x.enhancement, x.type])),
131
+ structured,
132
+ };
133
+ },
134
+ });
135
+ //# sourceMappingURL=enhancements.js.map
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ToolError } from "../../core/errors.js";
3
3
  import { sqlLiteral } from "../../core/objects.js";
4
+ import { packageObjects, packageTree } from "../../core/packages.js";
4
5
  import { defineTool } from "../../core/tool.js";
5
6
  export default defineTool({
6
7
  name: "package_contents",
@@ -21,20 +22,8 @@ export default defineTool({
21
22
  if (!h)
22
23
  throw new ToolError("NOT_FOUND", `El paquete ${root} no existe en este sistema.`);
23
24
  const txt = await sap.query(`SELECT ctext FROM tdevct WHERE devclass = ${sqlLiteral(root)}`, 1);
24
- // Árbol de subpaquetes (hasta 5 niveles si es recursivo).
25
- const packages = [root];
26
- const subs = [];
27
- let frontier = [root];
28
- for (let level = 0; level < (recursive ? 5 : 1) && frontier.length; level++) {
29
- const r = await sap.query(`SELECT devclass FROM tdevc WHERE parentcl IN ( ${frontier.map(sqlLiteral).join(", ")} )`, 2000);
30
- frontier = r.values.map((v) => v.DEVCLASS);
31
- subs.push(...frontier);
32
- if (recursive)
33
- packages.push(...frontier);
34
- }
35
- const typeFilter = object_type ? ` AND object = ${sqlLiteral(object_type.toUpperCase())}` : "";
36
- const objs = await sap.query(`SELECT devclass, object, obj_name FROM tadir WHERE pgmid = 'R3TR' AND devclass IN ( ${packages.map(sqlLiteral).join(", ")} )` +
37
- `${typeFilter} AND object <> 'DEVC' ORDER BY object, obj_name`, max_objects);
25
+ const { packages, subs } = await packageTree(sap, root, recursive);
26
+ const objs = { values: await packageObjects(sap, packages, max_objects, object_type) };
38
27
  const byType = new Map();
39
28
  for (const o of objs.values) {
40
29
  const label = recursive && o.DEVCLASS !== root ? `${o.OBJ_NAME} [${o.DEVCLASS}]` : o.OBJ_NAME;
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ import { isError, renderSyntax, syntaxCheck } from "../../core/checks.js";
3
+ import { stateOf } from "../../core/confirm.js";
4
+ import { diffLines, unified } from "../../core/diff.js";
5
+ import { ToolError } from "../../core/errors.js";
6
+ import { CLASS_INCLUDES, resolveObject, sourceUrl, TYPE_HELP } from "../../core/objects.js";
7
+ import { assertTrkorr } from "../../core/policy.js";
8
+ import { versionNumber } from "../../core/revisions.js";
9
+ import { defineTool } from "../../core/tool.js";
10
+ import { transportWarnings } from "../../core/transport.js";
11
+ import writeSource from "./write_source.js";
12
+ /**
13
+ * Deshacer con confirmación, nunca automático. Tras un write_source cuya activación falló, el objeto queda con un
14
+ * borrador inactivo (versión 99999 de ADT) encima de la versión activa (00000). Esta tool escribe de nuevo la
15
+ * versión elegida —la activa, la anterior a la activa, o una concreta del historial— pasando por la misma vista
16
+ * previa, confirmación, bloqueo y orden que cualquier escritura. Un rollback que pisara una versión sin preguntar
17
+ * sería peor que dejar el objeto inactivo.
18
+ */
19
+ const TARGET_HELP = "«active»: la última versión activa (deshace un borrador inactivo, p. ej. un write_source cuya activación falló). " +
20
+ "«previous»: la versión anterior a la activa (deshace la última activación). " +
21
+ "Un número N: la versión N tal como la lista object_versions.";
22
+ function pickRevision(revs, target) {
23
+ if (typeof target === "number") {
24
+ const rev = revs[target - 1];
25
+ if (!rev)
26
+ throw new ToolError("INPUT", `Solo hay ${revs.length} versiones (object_versions las lista).`);
27
+ return { rev, label: `versión ${target} del historial` };
28
+ }
29
+ const active = revs.find((r) => versionNumber(r) === "00000");
30
+ if (target === "active") {
31
+ if (!active)
32
+ throw new ToolError("NOT_FOUND", "SAP no devuelve una versión activa de este objeto.");
33
+ return { rev: active, label: "última versión activa" };
34
+ }
35
+ const numbered = revs
36
+ .filter((r) => /^\d{5}$/.test(versionNumber(r)) && !["00000", "99999"].includes(versionNumber(r)))
37
+ .sort((a, b) => (Date.parse(b.date) || 0) - (Date.parse(a.date) || 0) || Number(versionNumber(b)) - Number(versionNumber(a)));
38
+ if (!numbered.length)
39
+ throw new ToolError("NOT_FOUND", "No hay ninguna versión grabada anterior a la activa.");
40
+ return { rev: numbered[0], label: "versión anterior a la activa" };
41
+ }
42
+ async function chosenSource(c, obj, include, target) {
43
+ const revs = await c.revisions(obj.uri, obj.type.startsWith("CLAS") ? include : undefined);
44
+ if (!revs.length)
45
+ throw new ToolError("NOT_FOUND", `${obj.name}: SAP no devuelve versiones para este objeto.`);
46
+ const { rev, label } = pickRevision(revs, target);
47
+ const hasDraft = revs.some((r) => versionNumber(r) === "99999");
48
+ return { rev, label, hasDraft, source: await c.getObjectSource(rev.uri) };
49
+ }
50
+ const input = {
51
+ object_name: z.string().min(1),
52
+ object_type: z.string().optional().describe(TYPE_HELP),
53
+ include: z.enum(CLASS_INCLUDES).default("main"),
54
+ target: z.union([z.enum(["active", "previous"]), z.number().int().min(1)]).default("active").describe(TARGET_HELP),
55
+ transport: z.string().optional().describe("Orden (o tarea) donde debe ir la reversión. Obligatoria salvo objetos locales"),
56
+ activate: z.boolean().default(true),
57
+ };
58
+ export default defineTool({
59
+ name: "revert_source",
60
+ title: "Volver a una versión anterior",
61
+ description: "Deshace un cambio escribiendo de nuevo una versión anterior del objeto: la última activa (para limpiar un " +
62
+ "borrador inactivo tras un write_source cuya activación falló), la anterior a la activa, o una concreta del " +
63
+ "historial de object_versions. Pasa por la misma vista previa, confirmación, bloqueo y orden que write_source: " +
64
+ "nunca revierte por su cuenta. Solo objetos de código fuente.",
65
+ access: "write",
66
+ input,
67
+ async preview({ object_name, object_type, include, target, transport, activate }, { sap, system }) {
68
+ const requested = transport ? assertTrkorr(transport) : undefined;
69
+ const c = await sap.adt();
70
+ const obj = await resolveObject(c, object_name, object_type);
71
+ const url = await sourceUrl(c, obj, include);
72
+ const current = await c.getObjectSource(url);
73
+ const { rev, label, hasDraft, source } = await chosenSource(c, obj, include, target);
74
+ const out = [
75
+ `${obj.name} (${obj.type})${include !== "main" ? ` · include ${include}` : ""} · paquete ${obj.packageName ?? "?"}`,
76
+ `Volver a: ${label} · ${rev.date} · ${rev.author}${rev.versionTitle ? ` · ${rev.versionTitle}` : ""}`,
77
+ `Orden: ${requested ?? "ninguna indicada (solo vale para objetos locales)"} · activar después: ${activate ? "sí" : "no"}`,
78
+ hasDraft ? "Estado: el objeto tiene un borrador inactivo (cambios guardados sin activar)." : "Estado: sin borrador inactivo.",
79
+ ];
80
+ if (target === "active" && !hasDraft)
81
+ out.push("Aviso: no hay borrador que deshacer; la versión activa ya es la que se ve.");
82
+ if (requested) {
83
+ const warn = await transportWarnings(sap, requested, system.user);
84
+ out.push(warn.length ? `⚠ AVISOS DE LA ORDEN ${requested}:\n${warn.map((w) => " ⚠ " + w).join("\n")}` : `Orden ${requested}: modificable, con destino y del usuario de esta conexión.`);
85
+ }
86
+ const msgs = await syntaxCheck(c, obj, url, source);
87
+ out.push(msgs.some(isError) ? `Sintaxis de la versión elegida: CON ERRORES, la escritura se detendrá.\n${renderSyntax(msgs)}` : `Sintaxis de la versión elegida: sin errores${msgs.length ? ` (${msgs.length} avisos)` : ""}.`);
88
+ const ops = diffLines(current, source);
89
+ if (!ops)
90
+ out.push("", "Cambio: el objeto se reescribe casi entero (demasiadas diferencias para mostrarlas como diff).");
91
+ else {
92
+ const u = unified(ops, 3);
93
+ const lines = u.text.split("\n");
94
+ out.push("", u.hunks ? `Cambio respecto a lo que hay ahora: +${u.added} −${u.removed} en ${u.hunks} bloques` : "Cambio: ninguno (la versión elegida es idéntica a lo que hay ahora).");
95
+ if (u.hunks)
96
+ out.push(lines.slice(0, 400).join("\n") + (lines.length > 400 ? `\n[… ${lines.length - 400} líneas más de diff]` : ""));
97
+ }
98
+ return { text: out.join("\n"), state: stateOf(current) };
99
+ },
100
+ async run({ object_name, object_type, include, target, transport, activate }, ctx) {
101
+ const c = await ctx.sap.adt();
102
+ const obj = await resolveObject(c, object_name, object_type);
103
+ const { label, source } = await chosenSource(c, obj, include, target);
104
+ // La escritura real es la de write_source: mismo bloqueo, misma comprobación de que nada cambió desde la vista
105
+ // previa (ctx.confirmedState), misma decisión de orden y misma activación.
106
+ const r = await writeSource.run({ object_name, object_type, include, source, transport, activate, skip_syntax_check: false }, ctx);
107
+ const text = typeof r === "string" ? r : r.text;
108
+ return { text: `Revertido a la ${label}.\n${text}`, isError: typeof r === "string" ? false : !!r.isError };
109
+ },
110
+ });
111
+ //# sourceMappingURL=revert_source.js.map
@@ -10,9 +10,10 @@ import { defineTool } from "../../core/tool.js";
10
10
  const decode = decodeEntities;
11
11
  const strip = htmlToText;
12
12
  /** Ejecuta el ATC del alcance y lo recuerda bajo ese alcance. */
13
- async function execute(c, sap, systemId, t, o) {
13
+ async function execute(c, sap, systemId, t, o, ctx) {
14
14
  const variant = o.variant ?? (await defaultVariant(c));
15
15
  let res;
16
+ ctx?.progress?.(`ATC de ${t.scope} con la variante ${variant}…`);
16
17
  try {
17
18
  res = await runAtc(c, t.uri, variant, o.max_findings, o.include_exempted);
18
19
  res.scope = t.scope;
@@ -35,6 +36,8 @@ async function execute(c, sap, systemId, t, o) {
35
36
  const uris = (await Promise.all(refs.map((r) => resolveByTypePrefix(c, r.name, r.types)))).filter(Boolean).map((r) => r.uri);
36
37
  if (!uris.length)
37
38
  throw new ToolError("NOT_FOUND", `La orden ${tr} no contiene objetos que el ATC pueda revisar.`);
39
+ ctx?.signal?.throwIfAborted();
40
+ ctx?.progress?.(`Este release no admite la orden como conjunto: ATC sobre sus ${uris.length} objetos en una corrida…`);
38
41
  res = await runAtc(c, uris, variant, o.max_findings, o.include_exempted);
39
42
  res.scope = `orden ${tr} (sus ${uris.length} objetos: este release no admite la orden como conjunto ATC)`;
40
43
  }
@@ -44,6 +47,7 @@ async function execute(c, sap, systemId, t, o) {
44
47
  }
45
48
  export default defineTool({
46
49
  name: "run_atc",
50
+ timeoutMs: 180_000,
47
51
  title: "Ejecutar ATC",
48
52
  description: "Ejecuta el ATC sobre un objeto o una orden de transporte y lista los hallazgos numerados (prioridad, línea, " +
49
53
  "check, mensaje), con los totales P1/P2/P3 que da SAP. El resultado queda recordado POR OBJETO U ORDEN: " +
@@ -66,7 +70,8 @@ export default defineTool({
66
70
  .optional()
67
71
  .describe("Documentación del hallazgo N del ATC del objeto u orden indicados (sin ellos: del último ATC, y lo dice)"),
68
72
  },
69
- async run(a, { sap, system }) {
73
+ async run(a, ctx) {
74
+ const { sap, system } = ctx;
70
75
  const c = await sap.adt();
71
76
  // El alcance (objeto u orden nombrados) se resuelve antes que nada: explain=N es SIEMPRE de ese alcance.
72
77
  let target;
@@ -83,7 +88,7 @@ export default defineTool({
83
88
  let run = target ? recallRun(system.id, target.key) : recallRun(system.id);
84
89
  let ranNow = false;
85
90
  if (!run && target) {
86
- run = await execute(c, sap, system.id, target, opts);
91
+ run = await execute(c, sap, system.id, target, opts, ctx);
87
92
  ranNow = true;
88
93
  }
89
94
  if (!run)
@@ -99,7 +104,7 @@ export default defineTool({
99
104
  }
100
105
  if (!target)
101
106
  throw new ToolError("INPUT", "Indica object_name o transport.");
102
- const res = await execute(c, sap, system.id, target, opts);
107
+ const res = await execute(c, sap, system.id, target, opts, ctx);
103
108
  const shown = res.findings.filter((f) => !a.priorities || a.priorities.includes(f.priority));
104
109
  const byPrio = [1, 2, 3, 4].map((p) => res.findings.filter((f) => f.priority === p).length);
105
110
  const stats = res.stats ? `SAP: P1 ${res.stats.p1} · P2 ${res.stats.p2} · P3 ${res.stats.p3}` : `P1 ${byPrio[0]} · P2 ${byPrio[1]} · P3 ${byPrio[2]}`;
@@ -9,6 +9,7 @@ import { defineTool } from "../../core/tool.js";
9
9
  export const SAFE_TEST_FLAGS = { harmless: true, dangerous: false, critical: false, short: true, medium: false, long: false };
10
10
  export default defineTool({
11
11
  name: "run_unit_tests",
12
+ timeoutMs: 120_000,
12
13
  title: "Ejecutar ABAP Unit",
13
14
  description: "Ejecuta los tests ABAP Unit de una clase o programa y devuelve el resultado por método, con el detalle de cada " +
14
15
  "fallo. Solo corre tests RISK LEVEL HARMLESS y DURATION SHORT. Si no hay clases de test lo dice: cero tests no es un éxito.",
@@ -20,9 +21,11 @@ export default defineTool({
20
21
  object_name: z.string().min(1),
21
22
  object_type: z.string().default("CLAS").describe(TYPE_HELP),
22
23
  },
23
- async run({ object_name, object_type }, { sap }) {
24
+ async run({ object_name, object_type }, ctx) {
25
+ const { sap } = ctx;
24
26
  const c = await sap.adt();
25
27
  const obj = await resolveObject(c, object_name, object_type);
28
+ ctx.progress?.(`Ejecutando ABAP Unit de ${obj.name} (solo tests harmless y short)…`);
26
29
  const classes = await c.unitTestRun(obj.uri, { ...SAFE_TEST_FLAGS });
27
30
  if (!classes.length)
28
31
  return { text: `${obj.name}: no se encontraron clases de test. No se ejecutó nada.`, isError: true };