@dozimple/abap-adt 1.0.0-rc.1

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +19 -0
  4. package/README.es.md +356 -0
  5. package/README.md +356 -0
  6. package/SECURITY.md +68 -0
  7. package/THIRD_PARTY_NOTICES.md +134 -0
  8. package/config/systems.example.json +39 -0
  9. package/dist/core/activation.js +18 -0
  10. package/dist/core/atc.js +94 -0
  11. package/dist/core/audit.js +93 -0
  12. package/dist/core/catalog.en.js +92 -0
  13. package/dist/core/catalog.js +139 -0
  14. package/dist/core/checks.js +19 -0
  15. package/dist/core/config.js +136 -0
  16. package/dist/core/confirm.js +43 -0
  17. package/dist/core/connection.js +159 -0
  18. package/dist/core/credentials.js +44 -0
  19. package/dist/core/datapolicy.js +175 -0
  20. package/dist/core/diff.js +115 -0
  21. package/dist/core/edits.js +31 -0
  22. package/dist/core/errors.js +68 -0
  23. package/dist/core/feeds.js +60 -0
  24. package/dist/core/notes.js +18 -0
  25. package/dist/core/objects.js +143 -0
  26. package/dist/core/output.js +27 -0
  27. package/dist/core/policy.js +126 -0
  28. package/dist/core/prompts.js +89 -0
  29. package/dist/core/registry.js +316 -0
  30. package/dist/core/revisions.js +97 -0
  31. package/dist/core/sidecar.js +85 -0
  32. package/dist/core/sql.js +54 -0
  33. package/dist/core/telemetry.js +42 -0
  34. package/dist/core/tool.js +5 -0
  35. package/dist/core/transport.js +109 -0
  36. package/dist/index.js +55 -0
  37. package/dist/scripts/audit-verify.js +19 -0
  38. package/dist/scripts/smoke.js +87 -0
  39. package/dist/tools/core/activate.js +27 -0
  40. package/dist/tools/core/api_release_state.js +58 -0
  41. package/dist/tools/core/atc_quickfix.js +159 -0
  42. package/dist/tools/core/co_change.js +73 -0
  43. package/dist/tools/core/create_transport.js +44 -0
  44. package/dist/tools/core/ddic_type_info.js +81 -0
  45. package/dist/tools/core/dumps.js +55 -0
  46. package/dist/tools/core/edit_preflight.js +112 -0
  47. package/dist/tools/core/function_modules.js +75 -0
  48. package/dist/tools/core/gateway_errors.js +45 -0
  49. package/dist/tools/core/get_source.js +70 -0
  50. package/dist/tools/core/inactive_objects.js +29 -0
  51. package/dist/tools/core/object_versions.js +35 -0
  52. package/dist/tools/core/package_contents.js +59 -0
  53. package/dist/tools/core/run_atc.js +117 -0
  54. package/dist/tools/core/run_unit_tests.js +60 -0
  55. package/dist/tools/core/search_objects.js +26 -0
  56. package/dist/tools/core/sql_query.js +25 -0
  57. package/dist/tools/core/support.js +95 -0
  58. package/dist/tools/core/syntax_check.js +35 -0
  59. package/dist/tools/core/table_contents.js +57 -0
  60. package/dist/tools/core/text_elements.js +108 -0
  61. package/dist/tools/core/transaction_info.js +43 -0
  62. package/dist/tools/core/transport_contents.js +49 -0
  63. package/dist/tools/core/transport_diff.js +111 -0
  64. package/dist/tools/core/where_used.js +37 -0
  65. package/dist/tools/core/write_source.js +95 -0
  66. package/dist/tools/docs/docs.js +159 -0
  67. package/dist/tools/local/growth.js +104 -0
  68. package/dist/tools/local/sap_systems.js +65 -0
  69. package/dist/tools/transport-risk/_service.js +63 -0
  70. package/dist/tools/transport-risk/risk_extras.js +90 -0
  71. package/dist/tools/transport-risk/transport_risk.js +42 -0
  72. package/docs/THREAT_MODEL.md +70 -0
  73. package/docs/TOOLS.md +980 -0
  74. package/package.json +68 -0
  75. package/scripts/set-password.sh +31 -0
@@ -0,0 +1,109 @@
1
+ import { sqlLiteral } from "./objects.js";
2
+ export const TRFUNCTION = {
3
+ K: "orden workbench",
4
+ W: "orden customizing",
5
+ T: "transporte de copias",
6
+ S: "tarea de desarrollo/corrección",
7
+ R: "tarea de reparación",
8
+ Q: "tarea de customizing",
9
+ X: "tarea no clasificada",
10
+ C: "reubicación",
11
+ };
12
+ export const TRSTATUS = {
13
+ D: "modificable",
14
+ L: "modificable, protegida",
15
+ O: "liberación iniciada",
16
+ R: "liberada",
17
+ N: "liberada (protección de importación)",
18
+ };
19
+ export const isOpen = (trstatus) => trstatus === "D" || trstatus === "L";
20
+ const chunks = (a, n) => Array.from({ length: Math.ceil(a.length / n) }, (_, i) => a.slice(i * n, i * n + n));
21
+ /** Cabeceras E070 + texto E07T de un conjunto de órdenes/tareas. */
22
+ export async function orderHeaders(sap, trkorrs) {
23
+ const out = new Map();
24
+ const uniq = [...new Set(trkorrs.filter(Boolean))];
25
+ for (const part of chunks(uniq, 40)) {
26
+ const inList = part.map(sqlLiteral).join(", ");
27
+ const h = await sap.query(`SELECT trkorr, trfunction, trstatus, tarsystem, as4user, as4date, strkorr FROM e070 WHERE trkorr IN ( ${inList} )`, part.length);
28
+ const t = await sap.query(`SELECT trkorr, as4text FROM e07t WHERE trkorr IN ( ${inList} )`, part.length * 4);
29
+ const texts = new Map();
30
+ for (const r of t.values)
31
+ if (!texts.has(r.TRKORR))
32
+ texts.set(r.TRKORR, r.AS4TEXT);
33
+ for (const r of h.values) {
34
+ out.set(r.TRKORR, {
35
+ trkorr: r.TRKORR,
36
+ trfunction: r.TRFUNCTION,
37
+ trstatus: r.TRSTATUS,
38
+ tarsystem: r.TARSYSTEM,
39
+ owner: r.AS4USER,
40
+ date: r.AS4DATE,
41
+ parent: r.STRKORR,
42
+ text: texts.get(r.TRKORR) ?? "",
43
+ });
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ export function describeOrder(h) {
49
+ return (`${h.trkorr} «${h.text}» · ${TRFUNCTION[h.trfunction] ?? h.trfunction} · ${TRSTATUS[h.trstatus] ?? h.trstatus} · ` +
50
+ `${h.owner}` + (h.tarsystem ? ` · destino ${h.tarsystem}` : ""));
51
+ }
52
+ /**
53
+ * Qué orden usar al guardar, a partir de lo que devuelve el bloqueo ADT.
54
+ * Regla: nunca se elige una orden por el usuario. Si el objeto ya está
55
+ * bloqueado en otra orden, SAP obliga a usar esa: se para y se explica.
56
+ */
57
+ export function decideTransport(lock, requested, lockParent) {
58
+ const req = requested?.trim().toUpperCase() || undefined;
59
+ if (lock.IS_LOCAL === "X" || lock.IS_LOCAL === "true") {
60
+ return { ok: true, corrNr: "", note: "Objeto local: no se registra en ninguna orden." };
61
+ }
62
+ const held = lock.CORRNR?.trim();
63
+ if (held) {
64
+ const heldDesc = `${held}${lockParent && lockParent !== held ? ` (orden ${lockParent})` : ""}${lock.CORRUSER ? ` de ${lock.CORRUSER}` : ""}`;
65
+ if (!req) {
66
+ return {
67
+ ok: false,
68
+ reason: `El objeto ya está bloqueado en ${heldDesc}. Si es la orden correcta, repite con transport=${lockParent || held}.`,
69
+ };
70
+ }
71
+ if (req === held || req === lockParent) {
72
+ return { ok: true, corrNr: held, note: `Se guarda en ${heldDesc}.` };
73
+ }
74
+ return {
75
+ ok: false,
76
+ reason: `Pediste ${req}, pero el objeto está bloqueado en ${heldDesc}: SAP obliga a guardar ahí. No se escribió nada.`,
77
+ };
78
+ }
79
+ if (!req)
80
+ return { ok: false, reason: "El objeto no es local: indica transport (la orden donde debe ir el cambio)." };
81
+ return { ok: true, corrNr: req, note: `Se guarda en ${req}.` };
82
+ }
83
+ /**
84
+ * Lo que conviene saber de una orden ANTES de escribir en ella. Cada aviso corresponde a un daño real: una orden
85
+ * sin sistema destino no viaja (lo guardado se queda en desarrollo) y una orden ajena mezcla el cambio con el
86
+ * trabajo de otra persona. Si se da una tarea, se mira también su orden.
87
+ */
88
+ export async function transportWarnings(sap, trkorr, myUser) {
89
+ const heads = await orderHeaders(sap, [trkorr]);
90
+ const h = heads.get(trkorr);
91
+ if (!h)
92
+ return [`${trkorr} no existe en este sistema.`];
93
+ const order = h.parent ? (await orderHeaders(sap, [h.parent])).get(h.parent) ?? h : h;
94
+ const out = [];
95
+ const me = myUser.trim().toUpperCase();
96
+ if (!isOpen(h.trstatus))
97
+ out.push(`${trkorr} no está modificable (${TRSTATUS[h.trstatus] ?? h.trstatus}).`);
98
+ if (order !== h && !isOpen(order.trstatus))
99
+ out.push(`Su orden ${order.trkorr} no está modificable (${TRSTATUS[order.trstatus] ?? order.trstatus}).`);
100
+ if (!order.tarsystem?.trim() && order.trfunction !== "T") {
101
+ out.push(`SIN SISTEMA DESTINO: ${order.trkorr} no tiene destino de transporte; lo que se guarde en ella no viajará a calidad ni a productivo.`);
102
+ }
103
+ for (const x of order === h ? [h] : [h, order]) {
104
+ if (x.owner && x.owner.trim().toUpperCase() !== me)
105
+ out.push(`${x.trkorr} es de ${x.owner}, no del usuario de esta conexión: el cambio se mezclaría con su trabajo.`);
106
+ }
107
+ return out;
108
+ }
109
+ //# sourceMappingURL=transport.js.map
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readFileSync } from "node:fs";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { configPath, loadConfig } from "./core/config.js";
8
+ import { ConnectionPool } from "./core/connection.js";
9
+ import { registerPrompts } from "./core/prompts.js";
10
+ import { loadTools, registerAll } from "./core/registry.js";
11
+ import { SidecarPool } from "./core/sidecar.js";
12
+ // stdout es el canal MCP: cualquier log va a stderr.
13
+ const log = (...a) => console.error("[abap-adt-doZimple]", ...a);
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ /** Versión única: la de package.json (dist/index.js → ../package.json, también dentro del paquete npm). */
16
+ function packageVersion() {
17
+ try {
18
+ return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version;
19
+ }
20
+ catch {
21
+ return "0.0.0";
22
+ }
23
+ }
24
+ async function main() {
25
+ const server = new McpServer({ name: "abap-adt-doZimple", version: packageVersion() });
26
+ let config;
27
+ try {
28
+ config = loadConfig();
29
+ }
30
+ catch (e) {
31
+ // Sin configuración se arranca igual, con una sola tool que explica el
32
+ // problema: así se ve en el cliente en vez de un servidor «caído».
33
+ const msg = e.message;
34
+ log(msg);
35
+ server.registerTool("sap_systems", { title: "Sistemas SAP", description: "Estado de la configuración de sistemas SAP." }, async () => ({ content: [{ type: "text", text: `Configuración inválida (${configPath()}):\n${msg}` }], isError: true }));
36
+ await server.connect(new StdioServerTransport());
37
+ return;
38
+ }
39
+ const defs = await loadTools(join(here, "tools"));
40
+ const sidecars = new SidecarPool(config);
41
+ const published = registerAll(server, defs, config, new ConnectionPool(), sidecars);
42
+ // Los componentes auxiliares son procesos hijo: se cierran con el servidor.
43
+ const shutdown = () => void sidecars.closeAll().finally(() => process.exit(0));
44
+ process.stdin.on("close", shutdown);
45
+ process.on("SIGTERM", shutdown);
46
+ process.on("SIGINT", shutdown);
47
+ const prompts = registerPrompts(server, config);
48
+ log(`${published.length} tools y ${prompts.length} prompts para ${config.systems.map((s) => s.id).join(", ")}`);
49
+ await server.connect(new StdioServerTransport());
50
+ }
51
+ main().catch((e) => {
52
+ log("fallo al arrancar:", e);
53
+ process.exit(1);
54
+ });
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { AUDIT_FILE, verifyAudit } from "../core/audit.js";
4
+ import { stateDir } from "../core/telemetry.js";
5
+ /** npm run audit:verify [ruta] — comprueba que el registro de auditoría no se ha retocado. */
6
+ const path = process.argv[2] ?? join(stateDir(), AUDIT_FILE);
7
+ if (!existsSync(path)) {
8
+ console.log(`No hay registro de auditoría en ${path} (todavía no se ha escrito ni ejecutado nada en SAP).`);
9
+ process.exit(0);
10
+ }
11
+ const v = verifyAudit(readFileSync(path, "utf8"));
12
+ if (v.ok) {
13
+ console.log(`Registro íntegro: ${v.entries} entradas encadenadas (${path}).`);
14
+ }
15
+ else {
16
+ console.error(`REGISTRO ALTERADO en la entrada ${v.brokenAt} de ${v.entries}: ${v.reason} (${path}).`);
17
+ process.exit(1);
18
+ }
19
+ //# sourceMappingURL=audit-verify.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Validación de extremo a extremo: arranca el servidor por stdio como lo haría
3
+ * un cliente MCP, lista tools y prompts y, si se da un sistema, ejecuta SOLO
4
+ * lecturas contra él y resume el resultado por tool.
5
+ *
6
+ * npm run smoke → handshake, tools y prompts
7
+ * npm run smoke -- MI_DEV → batería de lecturas con objetos estándar de SAP
8
+ * SMOKE_TRANSPORT=DEVK900123 SMOKE_OBJECT=ZREPORTE npm run smoke -- MI_DEV
9
+ * → añade diff, ATC, correcciones y co-cambio sobre datos reales
10
+ *
11
+ * La orden y el objeto propios se pasan por entorno a propósito: nunca quedan en el repositorio.
12
+ */
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const system = process.argv[2];
19
+ const transport = process.env.SMOKE_TRANSPORT;
20
+ const object = process.env.SMOKE_OBJECT;
21
+ const client = new Client({ name: "smoke", version: "0" });
22
+ await client.connect(new StdioClientTransport({ command: process.execPath, args: [join(here, "..", "index.js")], stderr: "ignore" }));
23
+ const { tools } = await client.listTools();
24
+ const { prompts } = await client.listPrompts();
25
+ console.log(`${tools.length} tools · ${prompts.length} prompts`);
26
+ if (!system) {
27
+ console.log(tools.map((t) => t.name).join(", "));
28
+ await client.close();
29
+ process.exit(0);
30
+ }
31
+ const results = [];
32
+ async function call(tool, args, local = false) {
33
+ if (!tools.some((t) => t.name === tool))
34
+ return results.push({ tool, ms: 0, ok: false, line: "no publicada en esta configuración" });
35
+ const t0 = Date.now();
36
+ let r;
37
+ try {
38
+ r = await client.callTool({ name: tool, arguments: local ? args : { system, ...args } }, undefined, { timeout: 180_000 });
39
+ }
40
+ catch (e) {
41
+ // Una llamada lenta o fallida no aborta la batería: se anota y se sigue.
42
+ return results.push({ tool, ms: Date.now() - t0, ok: false, line: `sin respuesta: ${e.message}` });
43
+ }
44
+ const text = r.content?.[0]?.text ?? "";
45
+ const first = text.split("\n").find((l) => l.trim() && !l.startsWith("Sistema:") && !l.startsWith("Contenido de documentación")) ?? "";
46
+ results.push({ tool, ms: Date.now() - t0, ok: !r.isError, line: first.slice(0, 110) });
47
+ }
48
+ await call("sap_systems", { check: true, only: system }, true);
49
+ await call("search_objects", { query: "RSPARAM*", max_results: 3 });
50
+ await call("get_source", { object_name: "RSPARAM", object_type: "PROG", line_count: 5 });
51
+ await call("get_source", { object_name: "RFC_READ_TABLE", object_type: "FUNC", line_count: 5 });
52
+ await call("sql_query", { query: "SELECT mandt, mtext FROM t000", max_rows: 5 });
53
+ await call("table_contents", { table: "T000", columns: ["MANDT", "MTEXT"] });
54
+ await call("where_used", { object_name: "RSPARAM", object_type: "PROG", max_results: 3 });
55
+ await call("package_contents", { package: "SEUA_TRAN", max_objects: 10 });
56
+ await call("ddic_type_info", { name: "MANDT" });
57
+ await call("transaction_info", { tcode: "SE38" });
58
+ await call("syntax_check", { object_name: "RSPARAM", object_type: "PROG" });
59
+ await call("object_versions", { object_name: "RSPARAM", object_type: "PROG" });
60
+ await call("text_elements", { object_name: "RSPARAM", object_type: "PROG" });
61
+ await call("api_release_state", { object_name: "BAPI_SALESORDER_CREATEFROMDAT2", object_type: "FUNC" });
62
+ await call("dumps", { max: 3 });
63
+ await call("jobs", { max: 3 });
64
+ await call("application_log", { max: 3 });
65
+ await call("gateway_errors", { max: 3 });
66
+ await call("inactive_objects", {});
67
+ await call("abap_feature_matrix", { query: "COND", limit: 2 }, true);
68
+ await call("docs_search", { query: "inline declaration", k: 3 }, true);
69
+ await call("clean_core_objects", { query: "BAPI_SALESORDER", limit: 3 }, true);
70
+ await call("abap_lint", { code: "REPORT zdemo.\nDATA lv TYPE i.\nlv = 1.", version: "Standard" }, true);
71
+ if (transport) {
72
+ await call("transport_contents", { transport, max_objects: 20 });
73
+ await call("transport_diff", { transport, summary_only: true });
74
+ await call("run_atc", { transport, priorities: [1, 2] });
75
+ await call("atc_quickfix", { finding: 1 });
76
+ }
77
+ if (object) {
78
+ await call("edit_preflight", { object_name: object });
79
+ await call("co_change", { object_name: object, top: 5 });
80
+ }
81
+ const w = Math.max(...results.map((r) => r.tool.length));
82
+ for (const r of results)
83
+ console.log(`${r.ok ? "✓" : "✗"} ${r.tool.padEnd(w)} ${String(r.ms).padStart(6)} ms ${r.line}`);
84
+ const ok = results.filter((r) => r.ok).length;
85
+ console.log(`\n${ok}/${results.length} correctas`);
86
+ await client.close();
87
+ //# sourceMappingURL=smoke.js.map
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { activateObject } from "../../core/activation.js";
3
+ import { resolveObject, TYPE_HELP } from "../../core/objects.js";
4
+ import { defineTool } from "../../core/tool.js";
5
+ export default defineTool({
6
+ name: "activate",
7
+ title: "Activar objeto",
8
+ description: "Activa un objeto y devuelve los mensajes de SAP tal cual (errores con línea, avisos, objetos que quedan inactivos).",
9
+ access: "write",
10
+ requires: { adt: ["/sap/bc/adt/activation"] },
11
+ input: {
12
+ object_name: z.string().min(1),
13
+ object_type: z.string().optional().describe(TYPE_HELP),
14
+ },
15
+ async preview({ object_name, object_type }, { sap }) {
16
+ const c = await sap.adt();
17
+ const obj = await resolveObject(c, object_name, object_type);
18
+ return `Se activará ${obj.name} (${obj.type}) · paquete ${obj.packageName ?? "?"}.\nLa versión inactiva pasa a ser la activa: quien ejecute el objeto usará el código nuevo.`;
19
+ },
20
+ async run({ object_name, object_type }, { sap }) {
21
+ const c = await sap.adt();
22
+ const obj = await resolveObject(c, object_name, object_type);
23
+ const r = await activateObject(c, obj);
24
+ return { text: r.text, isError: !r.ok };
25
+ },
26
+ });
27
+ //# sourceMappingURL=activate.js.map
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { resolveObject, TYPE_HELP } from "../../core/objects.js";
3
+ import { defineTool } from "../../core/tool.js";
4
+ const CONTRACTS = {
5
+ c0: "C0 extensión",
6
+ c1: "C1 uso interno (ABAP Cloud / key user)",
7
+ c2: "C2 uso remoto",
8
+ c3: "C3 contenido de configuración",
9
+ c4: "C4 uso en ABAP-managed DB procedures",
10
+ };
11
+ /** Por contrato: estado y sucesores, del documento apiRelease de ADT. */
12
+ export function parseApiRelease(xml) {
13
+ const out = [];
14
+ const parts = xml.split(/<atom:link [^>]*href="[^"]*\/(c\d)" rel="self"[^>]*\/>/);
15
+ for (let i = 1; i < parts.length; i += 2) {
16
+ const id = parts[i];
17
+ const body = parts[i + 1] ?? "";
18
+ const state = /<ars:status ars:state="([^"]+)" ars:stateDescription="([^"]*)"/.exec(body);
19
+ const succ = /<ars:successors>([\s\S]*?)<\/ars:successors>/.exec(body)?.[1] ?? "";
20
+ const successors = [...succ.matchAll(/adtcore:type="([^"]+)" adtcore:name="([^"]+)"/g)].map((m) => `${m[2]} (${m[1]})`);
21
+ const concept = /<ars:successorConceptName>([^<]*)<\/ars:successorConceptName>/.exec(body)?.[1] ?? "";
22
+ out.push({ contract: CONTRACTS[id] ?? id, state: state ? `${state[2]} (${state[1]})` : "?", successors, concept });
23
+ }
24
+ return out;
25
+ }
26
+ export default defineTool({
27
+ name: "api_release_state",
28
+ title: "¿Está liberada esta API? ¿Cuál es su sucesor?",
29
+ description: "Estado de liberación de un objeto SAP (clase, FM/BAPI, tabla, CDS…) por contrato C0–C4 y su sucesor liberado, " +
30
+ "leído del propio sistema. Clave en remediación ATC S/4: «VBUK no está liberada», «BAPI_X → I_SALESORDERTP». " +
31
+ "Solo S/4 y releases con API Release State (no existe en ECC 7.50).",
32
+ access: "read",
33
+ requires: { adt: ["/sap/bc/adt/apireleases"] },
34
+ input: {
35
+ object_name: z.string().min(1),
36
+ object_type: z.string().optional().describe(TYPE_HELP),
37
+ },
38
+ async run({ object_name, object_type }, { sap }) {
39
+ const c = await sap.adt();
40
+ const obj = await resolveObject(c, object_name, object_type);
41
+ const r = await c.httpClient.request(`/sap/bc/adt/apireleases/${encodeURIComponent(obj.uri)}`, {
42
+ headers: { Accept: "application/vnd.sap.adt.apirelease.v10+xml" },
43
+ });
44
+ const contracts = parseApiRelease(String(r.body));
45
+ if (!contracts.length)
46
+ return `${obj.name} (${obj.type}): SAP no devuelve contratos de liberación para este objeto.`;
47
+ const released = contracts.filter((k) => k.state.includes("(RELEASED)"));
48
+ const successors = [...new Set(contracts.flatMap((k) => k.successors))];
49
+ return [
50
+ `${obj.name} (${obj.type})${obj.packageName ? ` · paquete ${obj.packageName}` : ""}`,
51
+ released.length ? `Liberado para: ${released.map((k) => k.contract).join("; ")}` : "No liberado en ningún contrato.",
52
+ successors.length ? `Sucesor(es) indicados por SAP: ${successors.join(", ")}` : "SAP no indica sucesor.",
53
+ "",
54
+ ...contracts.map((k) => ` ${k.contract}: ${k.state}${k.successors.length ? ` → ${k.successors.join(", ")}` : ""}${k.concept ? ` · concepto ${k.concept}` : ""}`),
55
+ ].join("\n");
56
+ },
57
+ });
58
+ //# sourceMappingURL=api_release_state.js.map
@@ -0,0 +1,159 @@
1
+ import { z } from "zod";
2
+ import { objectScope, recallRun, runAge, transportScope } from "../../core/atc.js";
3
+ import { assertTrkorr } from "../../core/policy.js";
4
+ import { isError, renderSyntax, syntaxCheck } from "../../core/checks.js";
5
+ import { diffLines, unified } from "../../core/diff.js";
6
+ import { applyEdits } from "../../core/edits.js";
7
+ import { ToolError } from "../../core/errors.js";
8
+ import { decodeEntities, stripTags } from "../../core/feeds.js";
9
+ import { resolveObject, sourceUrl, TYPE_HELP } from "../../core/objects.js";
10
+ import { defineTool } from "../../core/tool.js";
11
+ const decode = decodeEntities;
12
+ /** ADT entrega estas descripciones con el HTML escapado dentro del XML: se decodifica dos veces, a propósito, y luego se quitan etiquetas. */
13
+ const plain = (html) => stripTags(decode(decode(html)), "").replace(/\s+/g, " ").trim();
14
+ /** Columnas donde probar: la indicada y el inicio de cada token de la línea (literales primero). */
15
+ export function candidateColumns(line, given) {
16
+ const cols = new Set();
17
+ if (given > 0)
18
+ cols.add(given);
19
+ const quote = line.indexOf("'");
20
+ if (quote >= 0)
21
+ cols.add(quote + 1);
22
+ const re = /[A-Za-z_\/][\w\/~>=-]*/g;
23
+ let m;
24
+ while ((m = re.exec(line)) && cols.size < 12)
25
+ cols.add(m.index + 1);
26
+ return [...cols];
27
+ }
28
+ export default defineTool({
29
+ name: "atc_quickfix",
30
+ title: "Correcciones propuestas por SAP",
31
+ description: "Correcciones que SAP ofrece (las mismas de Ctrl+1 en Eclipse) para un hallazgo ATC o una línea: crear símbolo de " +
32
+ "texto, extraer constante, etc. Sin apply lista las propuestas; con apply=N calcula el cambio SIN GUARDAR y " +
33
+ "devuelve el diff y el chequeo de sintaxis. Para guardar, pasa el fuente resultante a write_source (return_source=true). " +
34
+ "Uso típico: run_atc → atc_quickfix(finding=N) → atc_quickfix(finding=N, apply=K).",
35
+ access: "read",
36
+ requires: { adt: ["/sap/bc/adt/quickfixes"] },
37
+ input: {
38
+ finding: z
39
+ .number()
40
+ .int()
41
+ .min(1)
42
+ .optional()
43
+ .describe("Número de hallazgo del run_atc de object_name (o transport); sin ellos, del último run_atc del sistema"),
44
+ transport: z.string().optional().describe("Con finding: el hallazgo es del run_atc de esta orden"),
45
+ object_name: z.string().optional(),
46
+ object_type: z.string().optional().describe(TYPE_HELP),
47
+ line: z.number().int().min(1).optional(),
48
+ column: z.number().int().min(0).optional(),
49
+ apply: z.number().int().min(1).optional().describe("Propuesta a calcular (de la lista)"),
50
+ return_source: z.boolean().default(false).describe("Incluir el fuente completo resultante (para write_source)"),
51
+ },
52
+ async run(a, { sap, system }) {
53
+ const c = await sap.adt();
54
+ let obj;
55
+ let src;
56
+ let line;
57
+ let column = a.column ?? 0;
58
+ let context = "";
59
+ if (a.finding) {
60
+ // El hallazgo N es del ATC del objeto u orden que se nombran, nunca de otro analizado antes.
61
+ let key;
62
+ let named = "";
63
+ if (a.transport) {
64
+ const tr = assertTrkorr(a.transport);
65
+ key = transportScope(tr);
66
+ named = `la orden ${tr}`;
67
+ }
68
+ else if (a.object_name) {
69
+ const named0 = await resolveObject(c, a.object_name, a.object_type);
70
+ key = objectScope(named0.name);
71
+ named = named0.name;
72
+ }
73
+ const run = recallRun(system.id, key);
74
+ if (!run) {
75
+ throw new ToolError("INPUT", key ? `No hay un run_atc de ${named} en esta sesión: ejecuta run_atc sobre ese objeto primero.` : "No hay un run_atc en esta sesión: ejecútalo primero.");
76
+ }
77
+ const f = run.findings.find((x) => x.n === a.finding);
78
+ if (!f)
79
+ throw new ToolError("INPUT", `El ATC de ${run.scope} tiene ${run.findings.length} hallazgos: no existe el ${a.finding}.`);
80
+ obj = { name: f.objectName, type: f.objectType, uri: f.objectUri };
81
+ src = f.sourceUri;
82
+ line = f.line;
83
+ column = a.column ?? f.column;
84
+ context = `Hallazgo ${f.n} del ATC de ${run.scope} (${runAge(run)}): P${f.priority} [${f.checkTitle}] ${decode(f.messageTitle)}\n`;
85
+ }
86
+ else {
87
+ if (!a.object_name || !a.line)
88
+ throw new ToolError("INPUT", "Indica finding, o object_name + line.");
89
+ obj = await resolveObject(c, a.object_name, a.object_type);
90
+ src = await sourceUrl(c, obj);
91
+ line = a.line;
92
+ }
93
+ // Las posiciones del ATC son de la versión ACTIVA. Si hay cambios sin activar, no se mezcla.
94
+ const active = await c.getObjectSource(src, { version: "active" });
95
+ const latest = await c.getObjectSource(src, { version: "inactive" });
96
+ if (active !== latest) {
97
+ throw new ToolError("INPUT", `${obj.name} tiene cambios guardados sin activar: las líneas del ATC ya no corresponden. Activa y vuelve a ejecutar run_atc.`);
98
+ }
99
+ const lines = active.replace(/\r\n/g, "\n").split("\n");
100
+ const text = lines[line - 1];
101
+ if (text === undefined)
102
+ throw new ToolError("INPUT", `${obj.name} tiene ${lines.length} líneas.`);
103
+ const found = [];
104
+ const seen = new Set();
105
+ for (const col of candidateColumns(text, column)) {
106
+ const props = await c.fixProposals(src, active, line, col);
107
+ for (const p of props) {
108
+ const key = `${p["adtcore:uri"]}|${plain(p["adtcore:name"])}`;
109
+ if (!seen.has(key)) {
110
+ seen.add(key);
111
+ found.push({ p, col });
112
+ }
113
+ }
114
+ }
115
+ const head = `${context}${obj.name} L${line}: ${text.trim()}\n`;
116
+ if (!found.length) {
117
+ return `${head}\nSAP no ofrece correcciones automáticas en esta línea (se probaron ${candidateColumns(text, column).length} posiciones). Hay que corregir a mano.`;
118
+ }
119
+ if (!a.apply) {
120
+ return (`${head}\nCorrecciones que ofrece SAP:\n` +
121
+ found.map((f, i) => ` ${i + 1}. ${plain(f.p["adtcore:name"])} — ${plain(f.p["adtcore:description"]).slice(0, 220)}`).join("\n") +
122
+ `\n\nPara ver el cambio sin guardar: atc_quickfix(... apply=N).`);
123
+ }
124
+ const chosen = found[a.apply - 1];
125
+ if (!chosen)
126
+ throw new ToolError("INPUT", `Solo hay ${found.length} propuestas.`);
127
+ const deltas = await c.fixEdits(chosen.p, active);
128
+ if (!deltas.length) {
129
+ return {
130
+ text: `${head}\nPropuesta ${a.apply}: ${plain(chosen.p["adtcore:name"])}\n\nSAP no devuelve cambios aplicables para esta ` +
131
+ `propuesta: las de tipo «Rename» abren el asistente de renombrado de Eclipse y no se aplican por aquí.`,
132
+ isError: true,
133
+ };
134
+ }
135
+ const here = deltas.filter((d) => d.uri.split("#")[0] === src);
136
+ const elsewhere = deltas.filter((d) => d.uri.split("#")[0] !== src);
137
+ const next = applyEdits(active, here);
138
+ const ops = diffLines(active, next);
139
+ const u = ops ? unified(ops, 2) : undefined;
140
+ const msgs = await syntaxCheck(c, obj, src, next);
141
+ const errs = msgs.filter(isError);
142
+ const out = [
143
+ `${head}\nPropuesta ${a.apply}: ${plain(chosen.p["adtcore:name"])} (NO guardado)`,
144
+ "",
145
+ u?.text || "(la corrección no cambia este fuente)",
146
+ ];
147
+ if (elsewhere.length) {
148
+ out.push("", `⚠ Esta corrección también modifica otros objetos que atc_quickfix NO aplica: ${elsewhere.map((d) => d.name || d.uri).join(", ")}. ` +
149
+ `Si es el pool de textos, crea el símbolo con abap-fs (manage_text_elements) antes de guardar.`);
150
+ }
151
+ out.push("", errs.length ? `Sintaxis con el cambio: ${errs.length} errores\n${renderSyntax(msgs)}` : "Sintaxis con el cambio: sin errores (comprobado por SAP).");
152
+ if (a.return_source)
153
+ out.push("", "Fuente resultante completo:", "```abap", next, "```");
154
+ else
155
+ out.push("", "Para guardarlo: repite con return_source=true y pasa ese fuente a write_source con la orden.");
156
+ return { text: out.join("\n"), isError: errs.length > 0 };
157
+ },
158
+ });
159
+ //# sourceMappingURL=atc_quickfix.js.map
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { ToolError } from "../../core/errors.js";
3
+ import { sqlLiteral } from "../../core/objects.js";
4
+ import { assertTrkorr } from "../../core/policy.js";
5
+ import { defineTool } from "../../core/tool.js";
6
+ /** Entradas que transportan datos o son de gestión: no dicen nada de acoplamiento de código. */
7
+ const NOISE = new Set(["TABU", "CDAT", "TDAT", "VDAT", "ACGR", "RELE", "COMM", "MERG", "ADIR", "DEVC", "NOTE"]);
8
+ export default defineTool({
9
+ name: "co_change",
10
+ title: "¿Con qué suele viajar este objeto?",
11
+ description: "Mira las órdenes que tocaron un objeto y cuenta qué otros objetos viajaron con él, de más a menos frecuente. " +
12
+ "Con transport, marca los compañeros habituales que NO van en esa orden: la pista de «te falta X en el pase». " +
13
+ "Es estadística de E071, no dependencia técnica: confírmalo con where_used.",
14
+ access: "read",
15
+ input: {
16
+ object_name: z.string().min(1).describe("Nombre como aparece en E071 (programa, clase, tabla…)"),
17
+ transport: z.string().optional().describe("Orden a revisar contra los compañeros habituales"),
18
+ last_orders: z.number().int().min(2).max(200).default(40),
19
+ top: z.number().int().min(1).max(100).default(20),
20
+ },
21
+ async run({ object_name, transport, last_orders, top }, { sap }) {
22
+ const name = object_name.trim().toUpperCase();
23
+ // Órdenes (padre) que contienen el objeto, más recientes primero.
24
+ const hits = await sap.query(`SELECT DISTINCT e071~trkorr, e070~strkorr, e070~as4date FROM e071 INNER JOIN e070 ON e070~trkorr = e071~trkorr ` +
25
+ `WHERE e071~obj_name = ${sqlLiteral(name)} ORDER BY e070~as4date DESCENDING`, 2000);
26
+ const orders = [...new Set(hits.values.map((v) => v.STRKORR?.trim() || v.TRKORR))]
27
+ .filter((t) => /^[A-Z0-9]{3}K\d{6}$/.test(t))
28
+ .slice(0, last_orders);
29
+ if (!orders.length)
30
+ throw new ToolError("NOT_FOUND", `${name} no aparece en ninguna orden de este sistema.`);
31
+ const tasks = await sap.query(`SELECT trkorr, strkorr FROM e070 WHERE strkorr IN ( ${orders.map(sqlLiteral).join(", ")} )`, 5000);
32
+ const parentOf = new Map(orders.map((o) => [o, o]));
33
+ for (const t of tasks.values)
34
+ parentOf.set(t.TRKORR, t.STRKORR);
35
+ const all = [...parentOf.keys()];
36
+ const rows = await sap.query(`SELECT trkorr, object, obj_name FROM e071 WHERE trkorr IN ( ${all.map(sqlLiteral).join(", ")} ) AND pgmid <> 'CORR'`, 5000);
37
+ const count = new Map();
38
+ for (const r of rows.values) {
39
+ const obj = String(r.OBJECT).trim();
40
+ const n = String(r.OBJ_NAME).trim();
41
+ if (NOISE.has(obj) || n === name || n.startsWith(name + " "))
42
+ continue;
43
+ const key = `${obj} ${obj === "METH" ? n.replace(/\s+/g, "->") : n}`;
44
+ const order = parentOf.get(r.TRKORR) ?? r.TRKORR;
45
+ if (!count.has(key))
46
+ count.set(key, new Set());
47
+ count.get(key).add(order);
48
+ }
49
+ const ranked = [...count].map(([k, s]) => ({ k, n: s.size })).sort((a, b) => b.n - a.n).slice(0, top);
50
+ let missing = [];
51
+ let trInfo = "";
52
+ if (transport) {
53
+ const tr = assertTrkorr(transport);
54
+ const trTasks = await sap.query(`SELECT trkorr FROM e070 WHERE strkorr = ${sqlLiteral(tr)}`, 500);
55
+ const ids = [tr, ...trTasks.values.map((v) => v.TRKORR)];
56
+ const inTr = await sap.query(`SELECT object, obj_name FROM e071 WHERE trkorr IN ( ${ids.map(sqlLiteral).join(", ")} )`, 5000);
57
+ const present = new Set(inTr.values.map((v) => `${String(v.OBJECT).trim()} ${String(v.OBJ_NAME).trim()}`));
58
+ const presentNames = new Set(inTr.values.map((v) => String(v.OBJ_NAME).trim()));
59
+ // Frecuente = aparece en al menos la mitad de las órdenes del objeto.
60
+ missing = ranked
61
+ .filter((r) => r.n >= Math.max(2, Math.ceil(orders.length / 2)))
62
+ .filter((r) => !present.has(r.k) && !presentNames.has(r.k.split(" ").slice(1).join(" ")))
63
+ .map((r) => `${r.k} (${r.n}/${orders.length})`);
64
+ trInfo = `\n\nContra ${tr}: ` + (missing.length ? `faltan compañeros habituales → ${missing.join(", ")}` : "no falta ninguno de los compañeros frecuentes.");
65
+ }
66
+ return (`${name} aparece en ${orders.length} órdenes (analizadas las ${Math.min(orders.length, last_orders)} más recientes).\n` +
67
+ `Viajaron con él (objeto · nº de órdenes compartidas):\n` +
68
+ ranked.map((r) => ` ${r.k} · ${r.n}`).join("\n") +
69
+ (rows.values.length >= 5000 ? "\n(tope de 5000 filas de E071 alcanzado: reduce last_orders para un recuento completo)" : "") +
70
+ trInfo);
71
+ },
72
+ });
73
+ //# sourceMappingURL=co_change.js.map
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { ToolError } from "../../core/errors.js";
3
+ import { resolveObject, TYPE_HELP } from "../../core/objects.js";
4
+ import { transportWarnings } from "../../core/transport.js";
5
+ import { defineTool } from "../../core/tool.js";
6
+ export default defineTool({
7
+ name: "create_transport",
8
+ title: "Crear orden de transporte",
9
+ description: "Crea una orden workbench para el paquete de un objeto, ANTES de la primera edición, para que el cambio caiga en " +
10
+ "la orden del ticket y no en una tarea reutilizada. Sigue la convención de texto del cliente " +
11
+ "(p. ej. «TICKET-123 - AAAAMMDD - tipo de cambio»). No libera nada.",
12
+ access: "write",
13
+ requires: { adt: ["/sap/bc/adt/cts/transports"] },
14
+ input: {
15
+ object_name: z.string().min(1).describe("Un objeto del paquete: define paquete y capa de transporte"),
16
+ object_type: z.string().optional().describe(TYPE_HELP),
17
+ text: z.string().min(5).max(60).describe("Texto de la orden (máx. 60, E07T-AS4TEXT)"),
18
+ transport_layer: z.string().optional(),
19
+ },
20
+ async preview({ object_name, object_type, text, transport_layer }, { sap }) {
21
+ const c = await sap.adt();
22
+ const obj = await resolveObject(c, object_name, object_type);
23
+ if (!obj.packageName)
24
+ throw new ToolError("INPUT", `No se pudo determinar el paquete de ${obj.name}.`);
25
+ if (obj.packageName.startsWith("$"))
26
+ throw new ToolError("INPUT", `${obj.name} es local (${obj.packageName}): no necesita orden.`);
27
+ return `Se creará una orden workbench nueva:\n Texto: «${text}»\n Paquete: ${obj.packageName} (de ${obj.name})\n Capa: ${transport_layer ?? "la del paquete"}\nNo se libera nada.`;
28
+ },
29
+ async run({ object_name, object_type, text, transport_layer }, { sap, system }) {
30
+ const c = await sap.adt();
31
+ const obj = await resolveObject(c, object_name, object_type);
32
+ if (!obj.packageName)
33
+ throw new ToolError("INPUT", `No se pudo determinar el paquete de ${obj.name}.`);
34
+ if (obj.packageName.startsWith("$"))
35
+ throw new ToolError("INPUT", `${obj.name} es local (${obj.packageName}): no necesita orden.`);
36
+ const tr = await c.createTransport(obj.uri, text, obj.packageName, transport_layer);
37
+ const warn = await transportWarnings(sap, tr, system.user).catch(() => []);
38
+ const base = `Orden ${tr} creada («${text}») para el paquete ${obj.packageName}.`;
39
+ if (warn.length)
40
+ return { text: `${base}\n\nAVISOS:\n${warn.map((w) => " ⚠ " + w).join("\n")}\n\nRevísala en SE01 antes de guardar nada en ella.`, isError: true };
41
+ return `${base} Tiene sistema destino. Úsala en write_source(transport=${tr}).`;
42
+ },
43
+ });
44
+ //# sourceMappingURL=create_transport.js.map