@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.
- package/CHANGELOG.md +76 -0
- package/README.es.md +13 -9
- package/README.md +13 -9
- package/dist/core/catalog.en.js +4 -0
- package/dist/core/catalog.js +4 -0
- package/dist/core/concurrency.js +13 -0
- package/dist/core/connection.js +41 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/packages.js +26 -0
- package/dist/core/registry.js +80 -7
- package/dist/core/transport.js +7 -0
- package/dist/tools/core/dumps.js +133 -10
- package/dist/tools/core/enhancements.js +135 -0
- package/dist/tools/core/package_contents.js +3 -14
- package/dist/tools/core/revert_source.js +111 -0
- package/dist/tools/core/run_atc.js +9 -4
- package/dist/tools/core/run_unit_tests.js +4 -1
- package/dist/tools/core/sap_notes.js +148 -0
- package/dist/tools/core/source_search.js +188 -0
- package/dist/tools/core/sql_query.js +18 -2
- package/dist/tools/core/syntax_check.js +17 -2
- package/dist/tools/core/transport_diff.js +11 -13
- package/dist/tools/core/where_used.js +6 -1
- package/dist/tools/local/sap_systems.js +4 -0
- package/docs/TOOLS.md +155 -5
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { dataClassOf, 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
|
+
* Estado de las notas SAP en ESTE sistema (lo que muestra SNOTE), leído de las tablas de la herramienta de notas:
|
|
9
|
+
* CWBNTCUST (estado por nota), CWBNTHEAD (versión y componente) y CWBNTSTXT (título). No consulta el portal de SAP
|
|
10
|
+
* ni necesita S-user: solo sabe de las notas descargadas en el sistema. Los códigos de estado no son valores fijos
|
|
11
|
+
* del diccionario; su significado sale de las constantes de SAP (IF_SCWN_NA_CONSTANTS), verificadas en un 7.50.
|
|
12
|
+
*/
|
|
13
|
+
/** CWBNTCUST-PRSTATUS, «Implementation State». */
|
|
14
|
+
export const IMPL_STATE = {
|
|
15
|
+
E: "implementada completamente",
|
|
16
|
+
U: "implementada de forma incompleta",
|
|
17
|
+
V: "implementada una versión anterior",
|
|
18
|
+
N: "se puede implementar (no implementada)",
|
|
19
|
+
O: "obsoleta",
|
|
20
|
+
"-": "no se puede implementar (sin instrucción de corrección válida)",
|
|
21
|
+
"": "sin determinar",
|
|
22
|
+
};
|
|
23
|
+
/** CWBNTCUST-NTSTATUS, «Processing Status». */
|
|
24
|
+
export const PROC_STATE = { N: "nueva", I: "en tratamiento", A: "terminada", R: "no relevante" };
|
|
25
|
+
const describe = (map, code) => map[code.trim()] ?? `código «${code}» desconocido`;
|
|
26
|
+
const noteKey = (n) => String(Number(n));
|
|
27
|
+
/** Una consulta por tramo: un IN con miles de literales supera lo que acepta la vista previa de datos ADT. */
|
|
28
|
+
export async function inChunks(sap, sql, keys, size = 200) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (let i = 0; i < keys.length; i += size)
|
|
31
|
+
out.push(...(await sap.query(sql(keys.slice(i, i + size).map(sqlLiteral).join(", ")), 5000)).values);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
export async function noteDetails(sap, notes) {
|
|
35
|
+
const heads = { values: await inChunks(sap, (l) => `SELECT numm, versno, themk FROM cwbnthead WHERE numm IN ( ${l} )`, notes) };
|
|
36
|
+
const latest = new Map();
|
|
37
|
+
for (const h of heads.values) {
|
|
38
|
+
const k = noteKey(h.NUMM);
|
|
39
|
+
const v = Number(h.VERSNO);
|
|
40
|
+
if (!latest.has(k) || v > latest.get(k).version)
|
|
41
|
+
latest.set(k, { version: v, component: String(h.THEMK ?? "").trim() });
|
|
42
|
+
}
|
|
43
|
+
const texts = { values: await inChunks(sap, (l) => `SELECT numm, versno, langu, stext FROM cwbntstxt WHERE numm IN ( ${l} ) AND ( langu = 'S' OR langu = 'E' )`, notes) };
|
|
44
|
+
const title = new Map();
|
|
45
|
+
for (const t of texts.values) {
|
|
46
|
+
const k = noteKey(t.NUMM);
|
|
47
|
+
const cand = { v: Number(t.VERSNO), lang: String(t.LANGU), text: String(t.STEXT ?? "").trim() };
|
|
48
|
+
const cur = title.get(k);
|
|
49
|
+
// Versión más alta; a igual versión, español antes que inglés.
|
|
50
|
+
if (!cur || cand.v > cur.v || (cand.v === cur.v && cand.lang === "S" && cur.lang !== "S"))
|
|
51
|
+
title.set(k, cand);
|
|
52
|
+
}
|
|
53
|
+
return { latest, title };
|
|
54
|
+
}
|
|
55
|
+
const NOTE = z.object({
|
|
56
|
+
note: z.string(),
|
|
57
|
+
downloaded: z.boolean().describe("false = la nota no está en este sistema (nunca se descargó con SNOTE)"),
|
|
58
|
+
implementation: z.string().optional(),
|
|
59
|
+
implementation_code: z.string().optional(),
|
|
60
|
+
processing: z.string().optional(),
|
|
61
|
+
version: z.number().int().optional(),
|
|
62
|
+
component: z.string().optional(),
|
|
63
|
+
title: z.string().optional(),
|
|
64
|
+
processor: z.string().optional().describe("Usuario que la trató (solo en sistemas con datos de prueba)"),
|
|
65
|
+
});
|
|
66
|
+
export default defineTool({
|
|
67
|
+
name: "sap_notes",
|
|
68
|
+
title: "Notas SAP en el sistema (SNOTE)",
|
|
69
|
+
description: "Estado de notas SAP en este sistema, como en SNOTE: si está descargada, estado de implementación (completa, " +
|
|
70
|
+
"incompleta, versión anterior, se puede implementar, obsoleta, no se puede implementar), estado de tratamiento, " +
|
|
71
|
+
"versión, componente y título. Con notes=[…] responde por notas concretas (p. ej. «¿está la 2198647 en PRD?»); " +
|
|
72
|
+
"sin notas, lista las del estado pedido. Solo conoce las notas descargadas en el sistema: no consulta el portal.",
|
|
73
|
+
access: "read",
|
|
74
|
+
input: {
|
|
75
|
+
notes: z.array(z.string().regex(/^\d{1,10}$/, "número de nota: solo dígitos")).max(200).optional(),
|
|
76
|
+
implementation: z
|
|
77
|
+
.array(z.enum(["E", "U", "V", "N", "O", "-"]))
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Sin notes: filtrar por estado de implementación (E completa, U incompleta, V versión anterior, N se puede implementar, O obsoleta, - no se puede)"),
|
|
80
|
+
component: z.string().regex(/^[A-Z0-9-]{2,24}$/i).optional().describe("Sin notes: prefijo de componente, p. ej. SD-BF o MM-PUR"),
|
|
81
|
+
max: z.number().int().min(1).max(1000).default(100),
|
|
82
|
+
},
|
|
83
|
+
output: { notes: z.array(NOTE), total: z.number().int(), truncated: z.boolean() },
|
|
84
|
+
async run({ notes, implementation, component, max }, { sap, system }) {
|
|
85
|
+
const showUser = dataClassOf(system) === "test";
|
|
86
|
+
const cap = Math.min(max, rowCap(system));
|
|
87
|
+
let custRows;
|
|
88
|
+
let counted;
|
|
89
|
+
if (notes?.length) {
|
|
90
|
+
const r = await sap.query(`SELECT numm, ntstatus, prstatus, cwbuser FROM cwbntcust WHERE numm IN ( ${notes.map(sqlLiteral).join(", ")} )`, 1000);
|
|
91
|
+
custRows = r.values;
|
|
92
|
+
}
|
|
93
|
+
else if (component) {
|
|
94
|
+
// El componente está en la cabecera: se filtra ahí y luego se trae el estado de esas notas, por tramos.
|
|
95
|
+
const h = await sap.query(`SELECT DISTINCT numm FROM cwbnthead WHERE themk LIKE ${sqlLiteral(`${component.toUpperCase()}%`)}`, 5000);
|
|
96
|
+
const ids = h.values.map((v) => noteKey(v.NUMM));
|
|
97
|
+
const impl = implementation?.length ? ` AND prstatus IN ( ${implementation.map(sqlLiteral).join(", ")} )` : "";
|
|
98
|
+
custRows = await inChunks(sap, (l) => `SELECT numm, ntstatus, prstatus, cwbuser FROM cwbntcust WHERE numm IN ( ${l} )${impl}`, ids);
|
|
99
|
+
custRows.sort((a, b) => Number(b.NUMM) - Number(a.NUMM));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (!implementation?.length)
|
|
103
|
+
throw new ToolError("INPUT", "Indica notes, o un filtro: implementation y/o component.");
|
|
104
|
+
const where = `WHERE prstatus IN ( ${implementation.map(sqlLiteral).join(", ")} )`;
|
|
105
|
+
const r = await sap.query(`SELECT numm, ntstatus, prstatus, cwbuser FROM cwbntcust ${where} ORDER BY numm DESCENDING`, cap + 1);
|
|
106
|
+
custRows = r.values;
|
|
107
|
+
counted = Number((await sap.query(`SELECT COUNT(*) AS n FROM cwbntcust ${where}`, 1)).values[0]?.N ?? custRows.length);
|
|
108
|
+
}
|
|
109
|
+
// Sin notas concretas solo hace falta el detalle de las que se van a mostrar.
|
|
110
|
+
const shownRows = notes?.length ? custRows : custRows.slice(0, cap);
|
|
111
|
+
const keys = [...new Set([...(notes ?? []).map(noteKey), ...shownRows.map((c) => noteKey(c.NUMM))])];
|
|
112
|
+
const { latest, title } = keys.length ? await noteDetails(sap, keys) : { latest: new Map(), title: new Map() };
|
|
113
|
+
const byNote = new Map(custRows.map((c) => [noteKey(c.NUMM), c]));
|
|
114
|
+
let rows = (notes?.length ? notes.map(noteKey) : [...byNote.keys()]).map((k) => {
|
|
115
|
+
const c = byNote.get(k);
|
|
116
|
+
const h = latest.get(k);
|
|
117
|
+
if (!c && !h)
|
|
118
|
+
return { note: k, downloaded: false };
|
|
119
|
+
const impl = String(c?.PRSTATUS ?? "").trim();
|
|
120
|
+
return {
|
|
121
|
+
note: k,
|
|
122
|
+
downloaded: true,
|
|
123
|
+
implementation: c ? describe(IMPL_STATE, impl) : "sin estado en SNOTE",
|
|
124
|
+
implementation_code: c ? impl || " " : undefined,
|
|
125
|
+
processing: c ? describe(PROC_STATE, String(c.NTSTATUS ?? "")) : undefined,
|
|
126
|
+
version: h?.version,
|
|
127
|
+
component: h?.component,
|
|
128
|
+
title: title.get(k)?.text,
|
|
129
|
+
...(showUser && c?.CWBUSER ? { processor: String(c.CWBUSER).trim() } : {}),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
const total = notes?.length ? rows.length : (counted ?? custRows.length);
|
|
133
|
+
const truncated = !notes?.length && total > cap;
|
|
134
|
+
rows = rows.slice(0, notes?.length ? rows.length : cap);
|
|
135
|
+
const structured = { notes: rows, total, truncated };
|
|
136
|
+
if (!rows.length)
|
|
137
|
+
return { text: "La consulta se hizo y ninguna nota de este sistema cumple el filtro.", structured };
|
|
138
|
+
const header = ["nota", "descargada", "implementación", "tratamiento", "versión", "componente", "título", ...(showUser ? ["tratada por"] : [])];
|
|
139
|
+
const table = tsv(header, rows.map((r) => [r.note, r.downloaded ? "sí" : "NO", r.implementation ?? "—", r.processing ?? "—", r.version ?? "", r.component ?? "", r.title ?? "", ...(showUser ? [r.processor ?? ""] : [])]));
|
|
140
|
+
const missing = rows.filter((r) => !r.downloaded).map((r) => r.note);
|
|
141
|
+
return {
|
|
142
|
+
text: `${rows.length} notas${truncated ? ` (TOPE: hay ${total}; sube max o afina el filtro)` : ""}\n\n${table}` +
|
|
143
|
+
(missing.length ? `\n\nNo descargadas en este sistema: ${missing.join(", ")}. Que no esté no significa que no aplique: SNOTE solo conoce lo descargado.` : ""),
|
|
144
|
+
structured,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
//# sourceMappingURL=sap_notes.js.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ADTClient } from "abap-adt-api";
|
|
3
|
+
import { pool } from "../../core/concurrency.js";
|
|
4
|
+
import { normalizeError, ToolError } from "../../core/errors.js";
|
|
5
|
+
import { resolveByTypePrefix, sqlLiteral } from "../../core/objects.js";
|
|
6
|
+
import { packageObjects, packageTree } from "../../core/packages.js";
|
|
7
|
+
import { assertTrkorr } from "../../core/policy.js";
|
|
8
|
+
import { sourceObjectsOf } from "../../core/revisions.js";
|
|
9
|
+
import { defineTool } from "../../core/tool.js";
|
|
10
|
+
import { orderEntries, orderHeaders } from "../../core/transport.js";
|
|
11
|
+
/**
|
|
12
|
+
* Búsqueda de texto en el código fuente de un alcance acotado (paquete, orden u objetos). ADT tiene un endpoint de
|
|
13
|
+
* búsqueda de texto (`informationsystem/textsearch`), pero en NW 7.50 no existe: responde 404 y no figura en el
|
|
14
|
+
* discovery. Así que se leen las fuentes del alcance y se busca aquí. Sirve donde where_used no llega: el índice de
|
|
15
|
+
* referencias de un sistema convertido puede no cubrir el código Z.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_PATTERN = 200;
|
|
18
|
+
const MAX_LINE = 200;
|
|
19
|
+
/** Programa principal e includes de un grupo de funciones, con namespace: /DZ/GRP → /DZ/SAPLGRP y /DZ/LGRP. */
|
|
20
|
+
export function groupNames(group) {
|
|
21
|
+
const g = group.trim().toUpperCase();
|
|
22
|
+
const m = /^(\/[^/]+\/)(.+)$/.exec(g);
|
|
23
|
+
return m ? { main: `${m[1]}SAPL${m[2]}`, includePrefix: `${m[1]}L${m[2]}` } : { main: `SAPL${g}`, includePrefix: `L${g}` };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Un grupo de funciones se busca en sus módulos (TFDIR) y en sus includes propios (TOP, F, O, I…), no en los U (son
|
|
27
|
+
* los módulos) ni en los generados con «$». ADT tipa esos includes como FUGR/I, no PROG/I (verificado en 7.50).
|
|
28
|
+
*/
|
|
29
|
+
async function expandGroup(sap, group) {
|
|
30
|
+
const { main, includePrefix } = groupNames(group);
|
|
31
|
+
const fms = await sap.query(`SELECT funcname FROM tfdir WHERE pname = ${sqlLiteral(main)}`, 500);
|
|
32
|
+
const incs = await sap.query(`SELECT name FROM trdir WHERE name LIKE ${sqlLiteral(`${includePrefix}%`)} AND name NOT LIKE ${sqlLiteral(`${includePrefix}U%`)}`, 500);
|
|
33
|
+
return [
|
|
34
|
+
...fms.values.map((v) => ({ name: String(v.FUNCNAME).trim(), types: ["FUGR/FF"], from: [`R3TR FUGR ${group}`] })),
|
|
35
|
+
...incs.values
|
|
36
|
+
.map((v) => String(v.NAME).trim())
|
|
37
|
+
.filter((n) => !n.includes("$"))
|
|
38
|
+
.map((name) => ({ name, types: ["FUGR/I", "PROG/"], from: [`R3TR FUGR ${group}`] })),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
async function scopeRefs(sap, a, max) {
|
|
42
|
+
let rows;
|
|
43
|
+
let label;
|
|
44
|
+
if (a.package) {
|
|
45
|
+
const root = a.package.trim().toUpperCase();
|
|
46
|
+
const { packages } = await packageTree(sap, root, a.include_subpackages);
|
|
47
|
+
rows = await packageObjects(sap, packages, 5000);
|
|
48
|
+
label = `paquete ${root}${a.include_subpackages && packages.length > 1 ? ` y ${packages.length - 1} subpaquetes` : ""}`;
|
|
49
|
+
}
|
|
50
|
+
else if (a.transport) {
|
|
51
|
+
const tr = assertTrkorr(a.transport);
|
|
52
|
+
const head = (await orderHeaders(sap, [tr])).get(tr);
|
|
53
|
+
if (!head)
|
|
54
|
+
throw new ToolError("NOT_FOUND", `La orden ${tr} no existe en este sistema.`);
|
|
55
|
+
const root = head.parent || tr;
|
|
56
|
+
rows = await orderEntries(sap, root);
|
|
57
|
+
label = `orden ${root}${root !== tr ? ` (pediste la tarea ${tr})` : ""}`;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Lista explícita: cada nombre se resuelve con cualquier tipo con fuente.
|
|
61
|
+
const refs = a.objects.map((n) => ({ name: n.trim().toUpperCase(), types: ["PROG/", "CLAS/", "INTF/", "FUGR/FF", "DDLS/"], from: ["objects"] }));
|
|
62
|
+
return { refs: refs.slice(0, max), total: refs.length, label: `${refs.length} objetos indicados` };
|
|
63
|
+
}
|
|
64
|
+
const { source, other } = sourceObjectsOf(rows);
|
|
65
|
+
const groups = other.filter((o) => o.startsWith("R3TR FUGR ")).map((o) => o.slice("R3TR FUGR ".length));
|
|
66
|
+
const refs = [...source];
|
|
67
|
+
for (const g of groups)
|
|
68
|
+
refs.push(...(await expandGroup(sap, g)));
|
|
69
|
+
return { refs: refs.slice(0, max), total: refs.length, label };
|
|
70
|
+
}
|
|
71
|
+
/** Fuentes de un objeto: una sola, salvo clases (main y sus includes locales, los que existan). */
|
|
72
|
+
async function sourcesOf(c, uri, type) {
|
|
73
|
+
if (!type.startsWith("CLAS")) {
|
|
74
|
+
const url = ADTClient.isMainInclude(uri) ? uri : `${uri}/source/main`;
|
|
75
|
+
return [{ include: "main", text: await c.getObjectSource(url) }];
|
|
76
|
+
}
|
|
77
|
+
const includes = ADTClient.classIncludes((await c.objectStructure(uri)));
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const inc of ["main", "definitions", "implementations", "testclasses"]) {
|
|
80
|
+
const url = includes.get(inc);
|
|
81
|
+
if (url)
|
|
82
|
+
out.push({ include: inc, text: await c.getObjectSource(url) });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function matcher(text, regex) {
|
|
87
|
+
if (text.length > MAX_PATTERN)
|
|
88
|
+
throw new ToolError("INPUT", `El patrón tiene ${text.length} caracteres; el máximo es ${MAX_PATTERN}.`);
|
|
89
|
+
if (!regex) {
|
|
90
|
+
const needle = text.toUpperCase();
|
|
91
|
+
return (line) => line.toUpperCase().includes(needle);
|
|
92
|
+
}
|
|
93
|
+
let re;
|
|
94
|
+
try {
|
|
95
|
+
re = new RegExp(text, "i");
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
throw new ToolError("INPUT", `Expresión regular inválida: ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
return (line) => re.test(line);
|
|
101
|
+
}
|
|
102
|
+
const isComment = (line) => line.startsWith("*") || line.trimStart().startsWith('"');
|
|
103
|
+
const HIT = z.object({ object: z.string(), type: z.string(), include: z.string(), line: z.number().int(), text: z.string() });
|
|
104
|
+
export default defineTool({
|
|
105
|
+
name: "source_search",
|
|
106
|
+
title: "Buscar texto en el código",
|
|
107
|
+
description: "Busca un texto (o una expresión regular) en el código fuente de un paquete (con subpaquetes), de una orden de " +
|
|
108
|
+
"transporte o de una lista de objetos, y devuelve objeto, include, línea y la línea encontrada. Sirve donde " +
|
|
109
|
+
"where_used no llega: llamadas dinámicas, literales, código Z que el índice de referencias no cubre. Lee cada " +
|
|
110
|
+
"fuente, así que exige un alcance y tiene tope de objetos; los objetos que no se pudieron leer se listan aparte.",
|
|
111
|
+
access: "read",
|
|
112
|
+
timeoutMs: 180_000,
|
|
113
|
+
input: {
|
|
114
|
+
text: z.string().min(2).describe("Texto a buscar (sin distinguir mayúsculas), p. ej. RV_FLOW o 'CALL FUNCTION'"),
|
|
115
|
+
regex: z.boolean().default(false).describe("Tratar text como expresión regular (máx. 200 caracteres)"),
|
|
116
|
+
package: z.string().optional().describe("Alcance: paquete de desarrollo"),
|
|
117
|
+
include_subpackages: z.boolean().default(true),
|
|
118
|
+
transport: z.string().optional().describe("Alcance: orden (o tarea) de transporte"),
|
|
119
|
+
objects: z.array(z.string().min(1)).max(200).optional().describe("Alcance: lista de objetos por nombre"),
|
|
120
|
+
ignore_comments: z.boolean().default(false).describe("No contar líneas de comentario"),
|
|
121
|
+
// ~0,8 s por objeto medido en un 7.50 con 4 lecturas en paralelo: 150 objetos caben en el tiempo máximo de la tool.
|
|
122
|
+
max_objects: z.number().int().min(1).max(1000).default(150),
|
|
123
|
+
max_hits: z.number().int().min(1).max(2000).default(300),
|
|
124
|
+
},
|
|
125
|
+
output: {
|
|
126
|
+
scope: z.string(),
|
|
127
|
+
objects_in_scope: z.number().int(),
|
|
128
|
+
scanned: z.number().int().describe("Objetos leídos"),
|
|
129
|
+
truncated: z.boolean().describe("true si el alcance tenía más objetos que max_objects o más coincidencias que max_hits"),
|
|
130
|
+
hits: z.array(HIT),
|
|
131
|
+
skipped: z.array(z.object({ object: z.string(), reason: z.string() })).describe("Objetos que no se pudieron leer"),
|
|
132
|
+
},
|
|
133
|
+
async run(a, ctx) {
|
|
134
|
+
const scopes = [a.package, a.transport, a.objects?.length ? "x" : undefined].filter(Boolean).length;
|
|
135
|
+
if (scopes !== 1) {
|
|
136
|
+
throw new ToolError("INPUT", "Indica exactamente un alcance: package, transport u objects. Buscar en todo el repositorio no es viable leyendo fuentes.");
|
|
137
|
+
}
|
|
138
|
+
const match = matcher(a.text, a.regex);
|
|
139
|
+
const { sap } = ctx;
|
|
140
|
+
const { refs, total, label } = await scopeRefs(sap, a, a.max_objects);
|
|
141
|
+
const c = await sap.adt();
|
|
142
|
+
const hits = [];
|
|
143
|
+
const skipped = [];
|
|
144
|
+
let done = 0;
|
|
145
|
+
let scanned = 0;
|
|
146
|
+
ctx.progress?.(`Leyendo ${refs.length} objetos de ${label}…`, 0, refs.length);
|
|
147
|
+
await pool(refs, 4, async (ref) => {
|
|
148
|
+
ctx.signal?.throwIfAborted();
|
|
149
|
+
try {
|
|
150
|
+
const res = await resolveByTypePrefix(c, ref.name, ref.types);
|
|
151
|
+
if (!res) {
|
|
152
|
+
skipped.push({ object: ref.name, reason: "no se encontró con un tipo con fuente" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
for (const src of await sourcesOf(c, res.uri, res.type)) {
|
|
156
|
+
src.text.split(/\r?\n/).forEach((line, i) => {
|
|
157
|
+
if (hits.length >= a.max_hits || (a.ignore_comments && isComment(line)) || !match(line))
|
|
158
|
+
return;
|
|
159
|
+
hits.push({ object: ref.name, type: res.type, include: src.include, line: i + 1, text: line.trim().slice(0, MAX_LINE) });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
scanned++;
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const te = normalizeError(e);
|
|
166
|
+
if (te.kind === "NETWORK" || te.kind === "AUTH" || te.kind === "CANCELLED")
|
|
167
|
+
throw te;
|
|
168
|
+
skipped.push({ object: ref.name, reason: `${te.kind}: ${te.message.slice(0, 120)}` });
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
ctx.progress?.(`${++done} de ${refs.length} objetos leídos`, done, refs.length);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
hits.sort((x, y) => x.object.localeCompare(y.object) || x.include.localeCompare(y.include) || x.line - y.line);
|
|
175
|
+
const truncated = total > refs.length || hits.length >= a.max_hits;
|
|
176
|
+
const structured = { scope: label, objects_in_scope: total, scanned, truncated, hits, skipped };
|
|
177
|
+
const head = `«${a.text}»${a.regex ? " (regex)" : ""} en ${label}: ${hits.length} coincidencias en ${new Set(hits.map((h) => h.object)).size} objetos · ` +
|
|
178
|
+
`${scanned} de ${total} objetos leídos` +
|
|
179
|
+
(total > refs.length ? ` — TOPE ALCANZADO: ${total - refs.length} objetos sin leer (sube max_objects o acota el alcance)` : "") +
|
|
180
|
+
(hits.length >= a.max_hits ? ` — tope de ${a.max_hits} coincidencias: puede haber más` : "");
|
|
181
|
+
const lines = hits.map((h) => `${h.object}${h.include !== "main" ? ` (${h.include})` : ""} L${h.line}: ${h.text}`);
|
|
182
|
+
const skip = skipped.length ? `\n\nNo se pudieron leer (${skipped.length}):\n${skipped.map((s) => ` ${s.object}: ${s.reason}`).join("\n")}` : "";
|
|
183
|
+
if (!hits.length)
|
|
184
|
+
return { text: `${head}.\nSe leyó el código y no aparece.${skip}`, structured };
|
|
185
|
+
return { text: `${head}\n\n${lines.join("\n")}${skip}`, structured };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=source_search.js.map
|
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { guardedQuery } from "../../core/datapolicy.js";
|
|
3
3
|
import { tsv } from "../../core/output.js";
|
|
4
4
|
import { defineTool } from "../../core/tool.js";
|
|
5
|
+
/** La salida estructurada duplica el contenido del texto: se acota para no doblar respuestas enormes. */
|
|
6
|
+
const STRUCTURED_ROWS = 500;
|
|
5
7
|
export default defineTool({
|
|
6
8
|
name: "sql_query",
|
|
7
9
|
title: "Consulta ABAP SQL",
|
|
@@ -14,12 +16,26 @@ export default defineTool({
|
|
|
14
16
|
query: z.string().min(1).describe("SELECT ... FROM ... WHERE ..."),
|
|
15
17
|
max_rows: z.number().int().min(1).max(5000).default(100),
|
|
16
18
|
},
|
|
19
|
+
output: {
|
|
20
|
+
rows: z.number().int().describe("Filas devueltas"),
|
|
21
|
+
columns: z.array(z.string()),
|
|
22
|
+
values: z.array(z.record(z.unknown())).describe(`Filas como objetos columna→valor (hasta ${STRUCTURED_ROWS}; el texto las trae todas)`),
|
|
23
|
+
truncated: z.boolean().describe("true si values no incluye todas las filas"),
|
|
24
|
+
notes: z.array(z.string()).describe("Avisos de la política de datos (enmascarado, tope de filas)"),
|
|
25
|
+
},
|
|
17
26
|
async run({ query, max_rows }, { sap, system }) {
|
|
18
27
|
const r = await guardedQuery(sap, system, query, max_rows);
|
|
28
|
+
const structured = {
|
|
29
|
+
rows: r.values.length,
|
|
30
|
+
columns: r.columns,
|
|
31
|
+
values: r.values.slice(0, STRUCTURED_ROWS),
|
|
32
|
+
truncated: r.values.length > STRUCTURED_ROWS,
|
|
33
|
+
notes: r.notes,
|
|
34
|
+
};
|
|
19
35
|
if (!r.values.length)
|
|
20
|
-
return `La consulta se ejecutó y no devolvió filas
|
|
36
|
+
return { text: `La consulta se ejecutó y no devolvió filas.`, structured };
|
|
21
37
|
const table = tsv(r.columns, r.values.map((v) => r.columns.map((c) => v[c])));
|
|
22
|
-
return `${r.values.length} filas\n\n${table}${r.notes.length ? `\n\n${r.notes.join("\n")}` : ""}
|
|
38
|
+
return { text: `${r.values.length} filas\n\n${table}${r.notes.length ? `\n\n${r.notes.join("\n")}` : ""}`, structured };
|
|
23
39
|
},
|
|
24
40
|
});
|
|
25
41
|
//# sourceMappingURL=sql_query.js.map
|
|
@@ -16,6 +16,13 @@ export default defineTool({
|
|
|
16
16
|
source: z.string().optional().describe("Fuente completa a comprobar (sin guardar)"),
|
|
17
17
|
main_program: z.string().optional().describe("URI del programa principal, solo para includes ambiguos"),
|
|
18
18
|
},
|
|
19
|
+
output: {
|
|
20
|
+
object: z.string(),
|
|
21
|
+
checked: z.enum(["proposed", "saved"]).describe("proposed = la fuente pasada sin guardar; saved = lo último guardado"),
|
|
22
|
+
errors: z.number().int(),
|
|
23
|
+
warnings: z.number().int(),
|
|
24
|
+
messages: z.array(z.object({ severity: z.string(), line: z.number(), offset: z.number(), text: z.string(), uri: z.string() })),
|
|
25
|
+
},
|
|
19
26
|
async run({ object_name, object_type, include, source, main_program }, { sap }) {
|
|
20
27
|
const c = await sap.adt();
|
|
21
28
|
const obj = await resolveObject(c, object_name, object_type);
|
|
@@ -23,12 +30,20 @@ export default defineTool({
|
|
|
23
30
|
const content = source ?? (await c.getObjectSource(url, { version: "inactive" }));
|
|
24
31
|
const msgs = await syntaxCheck(c, obj, url, content, main_program);
|
|
25
32
|
const what = source ? "fuente propuesta (no guardada)" : "última versión guardada";
|
|
26
|
-
if (!msgs.length)
|
|
27
|
-
return `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`;
|
|
28
33
|
const errs = msgs.filter(isError).length;
|
|
34
|
+
const structured = {
|
|
35
|
+
object: obj.name,
|
|
36
|
+
checked: source ? "proposed" : "saved",
|
|
37
|
+
errors: errs,
|
|
38
|
+
warnings: msgs.length - errs,
|
|
39
|
+
messages: msgs.map((m) => ({ severity: m.severity ?? "", line: m.line, offset: m.offset, text: m.text, uri: m.uri })),
|
|
40
|
+
};
|
|
41
|
+
if (!msgs.length)
|
|
42
|
+
return { text: `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`, structured };
|
|
29
43
|
return {
|
|
30
44
|
text: `${obj.name}: ${errs} errores, ${msgs.length - errs} avisos (${what}).\n\n${renderSyntax(msgs)}`,
|
|
31
45
|
isError: errs > 0,
|
|
46
|
+
structured,
|
|
32
47
|
};
|
|
33
48
|
},
|
|
34
49
|
});
|
|
@@ -3,25 +3,15 @@ import { diffLines, unified } from "../../core/diff.js";
|
|
|
3
3
|
import { normalizeError, ToolError } from "../../core/errors.js";
|
|
4
4
|
import { resolveByTypePrefix, sqlLiteral } from "../../core/objects.js";
|
|
5
5
|
import { assertTrkorr } from "../../core/policy.js";
|
|
6
|
+
import { pool } from "../../core/concurrency.js";
|
|
6
7
|
import { selectRevisionPair, sourceObjectsOf, versionNumber } from "../../core/revisions.js";
|
|
7
8
|
import { describeOrder, orderHeaders } from "../../core/transport.js";
|
|
8
9
|
import { defineTool } from "../../core/tool.js";
|
|
9
10
|
const resolveRef = (c, ref) => resolveByTypePrefix(c, ref.name, ref.types);
|
|
10
11
|
const label = (r) => r ? `${r.version || "sin orden"} (v${versionNumber(r) || "?"}, ${r.date.slice(0, 10)}, ${r.author})` : "—";
|
|
11
|
-
/** Limita cuántas promesas corren a la vez (no saturar SAP). */
|
|
12
|
-
async function pool(items, n, fn) {
|
|
13
|
-
const out = new Array(items.length);
|
|
14
|
-
let next = 0;
|
|
15
|
-
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
16
|
-
while (next < items.length) {
|
|
17
|
-
const i = next++;
|
|
18
|
-
out[i] = await fn(items[i]);
|
|
19
|
-
}
|
|
20
|
-
}));
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
23
12
|
export default defineTool({
|
|
24
13
|
name: "transport_diff",
|
|
14
|
+
timeoutMs: 180_000,
|
|
25
15
|
title: "Qué cambió una orden (diff de código)",
|
|
26
16
|
description: "Revisión de código de una orden: por cada objeto con fuente (programas, includes, clases, interfaces, FM, CDS) " +
|
|
27
17
|
"compara la versión grabada con esa orden (o sus tareas) contra la versión anterior, y muestra el diff unificado. " +
|
|
@@ -36,7 +26,8 @@ export default defineTool({
|
|
|
36
26
|
max_objects: z.number().int().min(1).max(60).default(20),
|
|
37
27
|
max_diff_lines: z.number().int().min(20).max(3000).default(300).describe("Tope de líneas de diff por objeto"),
|
|
38
28
|
},
|
|
39
|
-
async run({ transport, objects, context, summary_only, max_objects, max_diff_lines },
|
|
29
|
+
async run({ transport, objects, context, summary_only, max_objects, max_diff_lines }, ctx) {
|
|
30
|
+
const { sap } = ctx;
|
|
40
31
|
const tr = assertTrkorr(transport);
|
|
41
32
|
const heads = await orderHeaders(sap, [tr]);
|
|
42
33
|
const head = heads.get(tr);
|
|
@@ -51,7 +42,11 @@ export default defineTool({
|
|
|
51
42
|
const selected = source.filter((s) => !wanted || wanted.includes(s.name.toUpperCase()));
|
|
52
43
|
const shown = selected.slice(0, max_objects);
|
|
53
44
|
const c = await sap.adt();
|
|
45
|
+
let done = 0;
|
|
46
|
+
ctx.progress?.(`Comparando ${shown.length} objetos de ${root}…`, 0, shown.length);
|
|
54
47
|
const blocks = await pool(shown, 4, async (ref) => {
|
|
48
|
+
// Cancelación entre objetos: el que está en curso termina, el siguiente ya no empieza.
|
|
49
|
+
ctx.signal?.throwIfAborted();
|
|
55
50
|
try {
|
|
56
51
|
const res = await resolveRef(c, ref);
|
|
57
52
|
if (!res)
|
|
@@ -91,6 +86,9 @@ export default defineTool({
|
|
|
91
86
|
throw te;
|
|
92
87
|
return { name: ref.name, text: `■ ${ref.name}: no se pudo comparar (${te.kind}: ${te.message})`, added: 0, removed: 0 };
|
|
93
88
|
}
|
|
89
|
+
finally {
|
|
90
|
+
ctx.progress?.(`${++done} de ${shown.length} objetos comparados`, done, shown.length);
|
|
91
|
+
}
|
|
94
92
|
});
|
|
95
93
|
const tot = blocks.reduce((a, b) => ({ added: a.added + b.added, removed: a.removed + b.removed }), { added: 0, removed: 0 });
|
|
96
94
|
const out = [
|
|
@@ -4,6 +4,7 @@ import { tsv } from "../../core/output.js";
|
|
|
4
4
|
import { defineTool } from "../../core/tool.js";
|
|
5
5
|
export default defineTool({
|
|
6
6
|
name: "where_used",
|
|
7
|
+
timeoutMs: 120_000,
|
|
7
8
|
title: "Dónde se usa",
|
|
8
9
|
description: "Lista de uso (where-used) de un objeto: quién lo referencia, con paquete y responsable. Con snippets=true añade " +
|
|
9
10
|
"las líneas de código de cada uso (más lento). Ojo: no ve usos dinámicos ni exits que no declaran tipos.",
|
|
@@ -15,10 +16,13 @@ export default defineTool({
|
|
|
15
16
|
max_results: z.number().int().min(1).max(1000).default(100),
|
|
16
17
|
snippets: z.boolean().default(false),
|
|
17
18
|
},
|
|
18
|
-
async run({ object_name, object_type, max_results, snippets },
|
|
19
|
+
async run({ object_name, object_type, max_results, snippets }, ctx) {
|
|
20
|
+
const { sap } = ctx;
|
|
19
21
|
const c = await sap.adt();
|
|
20
22
|
const obj = await resolveObject(c, object_name, object_type);
|
|
23
|
+
ctx.progress?.(`Buscando usos de ${obj.name} (en objetos muy usados tarda hasta un minuto)…`);
|
|
21
24
|
const refs = (await c.usageReferences(obj.uri)).filter((r) => r.isResult);
|
|
25
|
+
ctx.signal?.throwIfAborted();
|
|
22
26
|
if (!refs.length)
|
|
23
27
|
return `${obj.name} (${obj.type}): el where-used se ejecutó y no encontró usos estáticos.`;
|
|
24
28
|
const shown = refs.slice(0, max_results);
|
|
@@ -27,6 +31,7 @@ export default defineTool({
|
|
|
27
31
|
"\n\n" +
|
|
28
32
|
tsv(["nombre", "tipo", "paquete", "responsable", "uso"], shown.map((r) => [r["adtcore:name"], r["adtcore:type"], r.packageRef?.["adtcore:name"], r["adtcore:responsible"], r.usageInformation]));
|
|
29
33
|
if (snippets) {
|
|
34
|
+
ctx.progress?.(`${refs.length} usos; leyendo fragmentos de los primeros ${Math.min(shown.length, 30)}…`);
|
|
30
35
|
const sn = await c.usageReferenceSnippets(shown.slice(0, 30));
|
|
31
36
|
const lines = sn.flatMap((s) => s.snippets.map((x) => `${s.objectIdentifier} L${x.uri?.start?.line ?? "?"}: ${x.content.trim()}`));
|
|
32
37
|
out += `\n\nFragmentos (hasta 30 objetos):\n${lines.join("\n")}`;
|
|
@@ -30,6 +30,10 @@ export default defineTool({
|
|
|
30
30
|
if (!check)
|
|
31
31
|
continue;
|
|
32
32
|
const sap = pool.get(s);
|
|
33
|
+
if (sap.circuitOpenFor()) {
|
|
34
|
+
out.push(" circuito abierto por fallos de red seguidos: se cierra y se reintenta ahora");
|
|
35
|
+
sap.resetCircuit();
|
|
36
|
+
}
|
|
33
37
|
try {
|
|
34
38
|
const caps = await sap.capabilities(refresh_discovery);
|
|
35
39
|
const rel = await sap.release();
|