arc-1 0.3.0 → 0.4.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 (152) hide show
  1. package/README.md +77 -199
  2. package/dist/adt/btp.d.ts +2 -2
  3. package/dist/adt/btp.d.ts.map +1 -1
  4. package/dist/adt/btp.js +1 -1
  5. package/dist/adt/btp.js.map +1 -1
  6. package/dist/adt/client.d.ts +13 -1
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +39 -1
  9. package/dist/adt/client.js.map +1 -1
  10. package/dist/adt/codeintel.d.ts +41 -0
  11. package/dist/adt/codeintel.d.ts.map +1 -1
  12. package/dist/adt/codeintel.js +99 -27
  13. package/dist/adt/codeintel.js.map +1 -1
  14. package/dist/adt/config.d.ts.map +1 -1
  15. package/dist/adt/config.js.map +1 -1
  16. package/dist/adt/cookies.d.ts.map +1 -1
  17. package/dist/adt/cookies.js.map +1 -1
  18. package/dist/adt/crud.d.ts.map +1 -1
  19. package/dist/adt/crud.js.map +1 -1
  20. package/dist/adt/devtools.d.ts +14 -0
  21. package/dist/adt/devtools.d.ts.map +1 -1
  22. package/dist/adt/devtools.js +28 -3
  23. package/dist/adt/devtools.js.map +1 -1
  24. package/dist/adt/diagnostics.d.ts +101 -0
  25. package/dist/adt/diagnostics.d.ts.map +1 -0
  26. package/dist/adt/diagnostics.js +349 -0
  27. package/dist/adt/diagnostics.js.map +1 -0
  28. package/dist/adt/errors.d.ts.map +1 -1
  29. package/dist/adt/errors.js.map +1 -1
  30. package/dist/adt/features.d.ts.map +1 -1
  31. package/dist/adt/features.js.map +1 -1
  32. package/dist/adt/http.d.ts +26 -7
  33. package/dist/adt/http.d.ts.map +1 -1
  34. package/dist/adt/http.js +140 -86
  35. package/dist/adt/http.js.map +1 -1
  36. package/dist/adt/oauth.d.ts.map +1 -1
  37. package/dist/adt/oauth.js.map +1 -1
  38. package/dist/adt/safety.d.ts.map +1 -1
  39. package/dist/adt/safety.js.map +1 -1
  40. package/dist/adt/transport.d.ts.map +1 -1
  41. package/dist/adt/transport.js.map +1 -1
  42. package/dist/adt/types.d.ts +139 -0
  43. package/dist/adt/types.d.ts.map +1 -1
  44. package/dist/adt/types.js.map +1 -1
  45. package/dist/adt/xml-parser.d.ts +37 -1
  46. package/dist/adt/xml-parser.d.ts.map +1 -1
  47. package/dist/adt/xml-parser.js +147 -0
  48. package/dist/adt/xml-parser.js.map +1 -1
  49. package/dist/cache/cache.d.ts +47 -4
  50. package/dist/cache/cache.d.ts.map +1 -1
  51. package/dist/cache/cache.js +16 -5
  52. package/dist/cache/cache.js.map +1 -1
  53. package/dist/cache/caching-layer.d.ts +82 -0
  54. package/dist/cache/caching-layer.d.ts.map +1 -0
  55. package/dist/cache/caching-layer.js +134 -0
  56. package/dist/cache/caching-layer.js.map +1 -0
  57. package/dist/cache/memory.d.ts +14 -2
  58. package/dist/cache/memory.d.ts.map +1 -1
  59. package/dist/cache/memory.js +72 -5
  60. package/dist/cache/memory.js.map +1 -1
  61. package/dist/cache/sqlite.d.ts +10 -1
  62. package/dist/cache/sqlite.d.ts.map +1 -1
  63. package/dist/cache/sqlite.js +90 -2
  64. package/dist/cache/sqlite.js.map +1 -1
  65. package/dist/cache/warmup.d.ts +41 -0
  66. package/dist/cache/warmup.d.ts.map +1 -0
  67. package/dist/cache/warmup.js +286 -0
  68. package/dist/cache/warmup.js.map +1 -0
  69. package/dist/cli.d.ts.map +1 -1
  70. package/dist/cli.js.map +1 -1
  71. package/dist/context/cds-deps.d.ts +35 -0
  72. package/dist/context/cds-deps.d.ts.map +1 -0
  73. package/dist/context/cds-deps.js +201 -0
  74. package/dist/context/cds-deps.js.map +1 -0
  75. package/dist/context/compressor.d.ts +20 -1
  76. package/dist/context/compressor.d.ts.map +1 -1
  77. package/dist/context/compressor.js +178 -17
  78. package/dist/context/compressor.js.map +1 -1
  79. package/dist/context/contract.d.ts.map +1 -1
  80. package/dist/context/contract.js.map +1 -1
  81. package/dist/context/deps.d.ts.map +1 -1
  82. package/dist/context/deps.js.map +1 -1
  83. package/dist/context/method-surgery.d.ts +91 -0
  84. package/dist/context/method-surgery.d.ts.map +1 -0
  85. package/dist/context/method-surgery.js +441 -0
  86. package/dist/context/method-surgery.js.map +1 -0
  87. package/dist/context/types.d.ts +7 -0
  88. package/dist/context/types.d.ts.map +1 -1
  89. package/dist/context/types.js.map +1 -1
  90. package/dist/handlers/hyperfocused.d.ts +35 -0
  91. package/dist/handlers/hyperfocused.d.ts.map +1 -0
  92. package/dist/handlers/hyperfocused.js +104 -0
  93. package/dist/handlers/hyperfocused.js.map +1 -0
  94. package/dist/handlers/intent.d.ts +2 -1
  95. package/dist/handlers/intent.d.ts.map +1 -1
  96. package/dist/handlers/intent.js +440 -43
  97. package/dist/handlers/intent.js.map +1 -1
  98. package/dist/handlers/tools.d.ts.map +1 -1
  99. package/dist/handlers/tools.js +130 -39
  100. package/dist/handlers/tools.js.map +1 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js.map +1 -1
  103. package/dist/lint/config-builder.d.ts +60 -0
  104. package/dist/lint/config-builder.d.ts.map +1 -0
  105. package/dist/lint/config-builder.js +187 -0
  106. package/dist/lint/config-builder.js.map +1 -0
  107. package/dist/lint/lint.d.ts +43 -0
  108. package/dist/lint/lint.d.ts.map +1 -1
  109. package/dist/lint/lint.js +77 -2
  110. package/dist/lint/lint.js.map +1 -1
  111. package/dist/lint/presets/cloud.d.ts +16 -0
  112. package/dist/lint/presets/cloud.d.ts.map +1 -0
  113. package/dist/lint/presets/cloud.js +111 -0
  114. package/dist/lint/presets/cloud.js.map +1 -0
  115. package/dist/lint/presets/onprem.d.ts +17 -0
  116. package/dist/lint/presets/onprem.d.ts.map +1 -0
  117. package/dist/lint/presets/onprem.js +82 -0
  118. package/dist/lint/presets/onprem.js.map +1 -0
  119. package/dist/server/audit.d.ts +1 -0
  120. package/dist/server/audit.d.ts.map +1 -1
  121. package/dist/server/audit.js.map +1 -1
  122. package/dist/server/config.d.ts.map +1 -1
  123. package/dist/server/config.js +12 -0
  124. package/dist/server/config.js.map +1 -1
  125. package/dist/server/context.d.ts.map +1 -1
  126. package/dist/server/context.js.map +1 -1
  127. package/dist/server/elicit.d.ts.map +1 -1
  128. package/dist/server/elicit.js.map +1 -1
  129. package/dist/server/http.d.ts.map +1 -1
  130. package/dist/server/http.js +4 -1
  131. package/dist/server/http.js.map +1 -1
  132. package/dist/server/logger.d.ts.map +1 -1
  133. package/dist/server/logger.js.map +1 -1
  134. package/dist/server/server.d.ts +3 -2
  135. package/dist/server/server.d.ts.map +1 -1
  136. package/dist/server/server.js +90 -5
  137. package/dist/server/server.js.map +1 -1
  138. package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
  139. package/dist/server/sinks/btp-auditlog.js.map +1 -1
  140. package/dist/server/sinks/file.d.ts.map +1 -1
  141. package/dist/server/sinks/file.js.map +1 -1
  142. package/dist/server/sinks/stderr.d.ts.map +1 -1
  143. package/dist/server/sinks/stderr.js.map +1 -1
  144. package/dist/server/sinks/types.d.ts.map +1 -1
  145. package/dist/server/sinks/types.js.map +1 -1
  146. package/dist/server/types.d.ts +14 -0
  147. package/dist/server/types.d.ts.map +1 -1
  148. package/dist/server/types.js +6 -0
  149. package/dist/server/types.js.map +1 -1
  150. package/dist/server/xsuaa.d.ts.map +1 -1
  151. package/dist/server/xsuaa.js.map +1 -1
  152. package/package.json +13 -6
@@ -9,17 +9,23 @@
9
9
  * responses. Internal details (stack traces, SAP XML) are NOT
10
10
  * leaked to the LLM — only user-friendly error messages.
11
11
  */
12
- import { findDefinition, findReferences, getCompletion } from '../adt/codeintel.js';
12
+ import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
13
13
  import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
14
- import { activate, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
14
+ import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
15
+ import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
15
16
  import { AdtApiError, AdtNetworkError, AdtSafetyError } from '../adt/errors.js';
16
17
  import { mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
18
+ import { isOperationAllowed, OperationType } from '../adt/safety.js';
17
19
  import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
18
- import { compressContext } from '../context/compressor.js';
19
- import { detectFilename, lintAbapSource } from '../lint/lint.js';
20
+ import { extractCdsElements } from '../context/cds-deps.js';
21
+ import { compressCdsContext, compressContext } from '../context/compressor.js';
22
+ import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
23
+ import { buildLintConfig, listRulesFromConfig, } from '../lint/config-builder.js';
24
+ import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from '../lint/lint.js';
20
25
  import { sanitizeArgs } from '../server/audit.js';
21
26
  import { generateRequestId, requestContext } from '../server/context.js';
22
27
  import { logger } from '../server/logger.js';
28
+ import { expandHyperfocusedArgs, getHyperfocusedScope } from './hyperfocused.js';
23
29
  /**
24
30
  * Scope required for each tool.
25
31
  *
@@ -86,7 +92,7 @@ function classifyError(err) {
86
92
  * all tools are allowed (backward compatibility).
87
93
  * @param server - MCP Server instance for elicitation support.
88
94
  */
89
- export async function handleToolCall(client, _config, toolName, args, authInfo, _server) {
95
+ export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer) {
90
96
  const reqId = generateRequestId();
91
97
  const start = Date.now();
92
98
  // Build user context for audit logging
@@ -127,7 +133,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
127
133
  let result;
128
134
  switch (toolName) {
129
135
  case 'SAPRead':
130
- result = await handleSAPRead(client, args);
136
+ result = await handleSAPRead(client, args, cachingLayer);
131
137
  break;
132
138
  case 'SAPSearch':
133
139
  result = await handleSAPSearch(client, args);
@@ -136,7 +142,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
136
142
  result = await handleSAPQuery(client, args);
137
143
  break;
138
144
  case 'SAPWrite':
139
- result = await handleSAPWrite(client, args);
145
+ result = await handleSAPWrite(client, args, config, cachingLayer);
140
146
  break;
141
147
  case 'SAPActivate':
142
148
  result = await handleSAPActivate(client, args);
@@ -145,7 +151,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
145
151
  result = await handleSAPNavigate(client, args);
146
152
  break;
147
153
  case 'SAPLint':
148
- result = await handleSAPLint(client, args);
154
+ result = await handleSAPLint(client, args, config);
149
155
  break;
150
156
  case 'SAPDiagnose':
151
157
  result = await handleSAPDiagnose(client, args);
@@ -154,11 +160,30 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
154
160
  result = await handleSAPTransport(client, args);
155
161
  break;
156
162
  case 'SAPContext':
157
- result = await handleSAPContext(client, args);
163
+ result = await handleSAPContext(client, args, cachingLayer);
158
164
  break;
159
165
  case 'SAPManage':
160
- result = await handleSAPManage(client, _config, args);
166
+ result = await handleSAPManage(client, config, args, cachingLayer);
161
167
  break;
168
+ case 'SAP': {
169
+ // Hyperfocused mode: route to the appropriate handler
170
+ const expanded = expandHyperfocusedArgs(args);
171
+ if ('error' in expanded) {
172
+ result = errorResult(expanded.error);
173
+ break;
174
+ }
175
+ // Check scope for the delegated action
176
+ if (authInfo) {
177
+ const requiredScope = getHyperfocusedScope(String(args.action ?? ''));
178
+ if (!authInfo.scopes.includes(requiredScope)) {
179
+ result = errorResult(`Insufficient scope: '${requiredScope}' required for SAP(action="${args.action}"). Your scopes: [${authInfo.scopes.join(', ')}]`);
180
+ break;
181
+ }
182
+ }
183
+ // Delegate to the real handler (recursive call, but with the mapped tool name)
184
+ result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer);
185
+ break;
186
+ }
162
187
  default:
163
188
  result = errorResult(`Unknown tool: ${toolName}`);
164
189
  }
@@ -168,7 +193,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
168
193
  const resultPreview = fullText.length > 500 ? `${fullText.slice(0, 500)}...` : fullText;
169
194
  logger.emitAudit({
170
195
  timestamp: new Date().toISOString(),
171
- level: 'info',
196
+ level: result.isError ? 'error' : 'info',
172
197
  event: 'tool_call_end',
173
198
  requestId: reqId,
174
199
  user,
@@ -177,6 +202,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
177
202
  durationMs,
178
203
  status: result.isError ? 'error' : 'success',
179
204
  errorMessage: result.isError ? result.content[0]?.text : undefined,
205
+ errorClass: result.isError ? 'result-path' : undefined,
180
206
  resultSize,
181
207
  resultPreview,
182
208
  });
@@ -215,31 +241,64 @@ const BTP_HINTS = {
215
241
  TEXT_ELEMENTS: 'Text elements are not available on BTP ABAP Environment (no classic programs). Use message classes or constant classes instead.',
216
242
  VARIANTS: 'Variants are not available on BTP ABAP Environment (no classic programs).',
217
243
  SOBJ: 'BOR business objects (SOBJ) are not available on BTP ABAP Environment. Use RAP behavior definitions (BDEF) instead.',
244
+ TRAN: 'Transaction codes (TRAN) are not available on BTP ABAP Environment. Use SAPSearch to find apps and services instead.',
218
245
  };
219
- async function handleSAPRead(client, args) {
246
+ async function handleSAPRead(client, args, cachingLayer) {
220
247
  const type = String(args.type ?? '');
221
248
  const name = String(args.name ?? '');
222
249
  // BTP: return helpful error for unavailable types
223
250
  if (isBtpSystem() && BTP_HINTS[type]) {
224
251
  return errorResult(BTP_HINTS[type]);
225
252
  }
253
+ // Helper: get source with cache support
254
+ const cachedGet = async (objType, objName, fetcher) => {
255
+ if (!cachingLayer)
256
+ return fetcher();
257
+ const { source } = await cachingLayer.getSource(objType, objName, fetcher);
258
+ return source;
259
+ };
226
260
  switch (type) {
227
261
  case 'PROG':
228
- return textResult(await client.getProgram(name));
229
- case 'CLAS':
262
+ return textResult(await cachedGet('PROG', name, () => client.getProgram(name)));
263
+ case 'CLAS': {
264
+ const methodParam = args.method;
265
+ if (methodParam && !args.include) {
266
+ // Method-level read — fetch full source then extract
267
+ const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
268
+ const abaplintVer = cachedFeatures?.abapRelease
269
+ ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
270
+ : undefined;
271
+ if (methodParam === '*') {
272
+ const listing = listMethods(fullSource, name, abaplintVer);
273
+ return textResult(formatMethodListing(listing));
274
+ }
275
+ const extracted = extractMethod(fullSource, name, methodParam, abaplintVer);
276
+ if (!extracted.success) {
277
+ return errorResult(extracted.error ?? `Method "${methodParam}" not found in ${name}.`);
278
+ }
279
+ return textResult(extracted.methodSource);
280
+ }
281
+ // Only cache the full merged source (no include param), not individual includes
282
+ if (!args.include) {
283
+ return textResult(await cachedGet('CLAS', name, () => client.getClass(name)));
284
+ }
230
285
  return textResult(await client.getClass(name, args.include));
286
+ }
231
287
  case 'INTF':
232
- return textResult(await client.getInterface(name));
288
+ return textResult(await cachedGet('INTF', name, () => client.getInterface(name)));
233
289
  case 'FUNC': {
234
290
  let group = String(args.group ?? '');
235
291
  if (!group) {
236
- const resolved = await client.resolveFunctionGroup(name);
292
+ // Use cached func group resolution if available
293
+ const resolved = cachingLayer
294
+ ? await cachingLayer.resolveFuncGroup(client, name)
295
+ : await client.resolveFunctionGroup(name);
237
296
  if (!resolved) {
238
297
  return errorResult(`Cannot resolve function group for "${name}". Provide the group parameter explicitly, or use SAPSearch("${name}") to find the function group.`);
239
298
  }
240
299
  group = resolved;
241
300
  }
242
- return textResult(await client.getFunction(group, name));
301
+ return textResult(await cachedGet('FUNC', name, () => client.getFunction(group, name)));
243
302
  }
244
303
  case 'FUGR': {
245
304
  const expand = Boolean(args.expand_includes);
@@ -265,17 +324,53 @@ async function handleSAPRead(client, args) {
265
324
  return textResult(JSON.stringify(fg, null, 2));
266
325
  }
267
326
  case 'INCL':
268
- return textResult(await client.getInclude(name));
269
- case 'DDLS':
270
- return textResult(await client.getDdls(name));
327
+ return textResult(await cachedGet('INCL', name, () => client.getInclude(name)));
328
+ case 'DDLS': {
329
+ const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
330
+ if (args.include?.toLowerCase() === 'elements') {
331
+ return textResult(extractCdsElements(ddlSource, name));
332
+ }
333
+ return textResult(ddlSource);
334
+ }
271
335
  case 'BDEF':
272
- return textResult(await client.getBdef(name));
336
+ return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
273
337
  case 'SRVD':
274
- return textResult(await client.getSrvd(name));
338
+ return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
339
+ case 'DDLX':
340
+ return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
341
+ case 'SRVB':
342
+ return textResult(await cachedGet('SRVB', name, () => client.getSrvb(name)));
275
343
  case 'TABL':
276
- return textResult(await client.getTable(name));
344
+ return textResult(await cachedGet('TABL', name, () => client.getTable(name)));
277
345
  case 'VIEW':
278
- return textResult(await client.getView(name));
346
+ return textResult(await cachedGet('VIEW', name, () => client.getView(name)));
347
+ case 'STRU':
348
+ return textResult(await cachedGet('STRU', name, () => client.getStructure(name)));
349
+ case 'DOMA': {
350
+ const domain = await client.getDomain(name);
351
+ return textResult(JSON.stringify(domain, null, 2));
352
+ }
353
+ case 'DTEL': {
354
+ const dtel = await client.getDataElement(name);
355
+ return textResult(JSON.stringify(dtel, null, 2));
356
+ }
357
+ case 'TRAN': {
358
+ const tran = await client.getTransaction(name);
359
+ // Enrich with program name via SQL — only if free SQL is allowed by safety config
360
+ if (isOperationAllowed(client.safety, OperationType.FreeSQL)) {
361
+ try {
362
+ const safeName = name.toUpperCase().replace(/[^A-Z0-9_/]/g, '');
363
+ const data = await client.runQuery(`SELECT TCODE, PGMNA FROM TSTC WHERE TCODE = '${safeName}'`, 1);
364
+ if (data.rows.length > 0) {
365
+ tran.program = String(data.rows[0].PGMNA ?? '').trim();
366
+ }
367
+ }
368
+ catch {
369
+ // SQL failed (e.g., TSTC not found on BTP) — still return metadata
370
+ }
371
+ }
372
+ return textResult(JSON.stringify(tran, null, 2));
373
+ }
279
374
  case 'TABLE_CONTENTS': {
280
375
  const maxRows = Number(args.maxRows ?? 100);
281
376
  const data = await client.getTableContents(name, maxRows, args.sqlFilter);
@@ -326,7 +421,7 @@ async function handleSAPRead(client, args) {
326
421
  case 'VARIANTS':
327
422
  return textResult(await client.getVariants(name));
328
423
  default:
329
- return errorResult(`Unknown SAPRead type: ${type}. Supported: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, BDEF, SRVD, TABL, VIEW, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS`);
424
+ return errorResult(`Unknown SAPRead type: ${type}. Supported: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS`);
330
425
  }
331
426
  }
332
427
  async function handleSAPSearch(client, args) {
@@ -382,20 +477,69 @@ async function handleSAPQuery(client, args) {
382
477
  throw err;
383
478
  }
384
479
  }
385
- async function handleSAPLint(_client, args) {
480
+ // _client unused: SAPLint runs offline via @abaplint/core (no SAP round-trip).
481
+ // Signature matches other handlers for consistency with handleToolCall dispatch.
482
+ async function handleSAPLint(_client, args, config) {
386
483
  const action = String(args.action ?? '');
484
+ const ruleOverrides = args.rules;
485
+ const configOptions = buildLintConfigOptions(config, ruleOverrides);
387
486
  switch (action) {
388
487
  case 'lint': {
389
488
  const source = String(args.source ?? '');
489
+ if (!source)
490
+ return errorResult('"source" is required for lint action.');
390
491
  const name = String(args.name ?? 'UNKNOWN');
391
492
  const filename = detectFilename(source, name);
392
- const issues = lintAbapSource(source, filename);
493
+ const lintConfig = buildLintConfig(configOptions);
494
+ const issues = lintAbapSource(source, filename, lintConfig);
393
495
  return textResult(JSON.stringify(issues, null, 2));
394
496
  }
497
+ case 'lint_and_fix': {
498
+ const source = String(args.source ?? '');
499
+ if (!source)
500
+ return errorResult('"source" is required for lint_and_fix action.');
501
+ const name = String(args.name ?? 'UNKNOWN');
502
+ const filename = detectFilename(source, name);
503
+ const lintConfig = buildLintConfig(configOptions);
504
+ const result = lintAndFix(source, filename, lintConfig);
505
+ return textResult(JSON.stringify(result, null, 2));
506
+ }
507
+ case 'list_rules': {
508
+ const lintConfig = buildLintConfig(configOptions);
509
+ const rules = listRulesFromConfig(lintConfig);
510
+ const enabled = rules.filter((r) => r.enabled);
511
+ const disabled = rules.filter((r) => !r.enabled);
512
+ return textResult(JSON.stringify({
513
+ preset: configOptions.systemType === 'btp' ? 'cloud' : 'onprem',
514
+ abapVersion: cachedFeatures?.abapRelease ?? 'unknown',
515
+ enabledRules: enabled.length,
516
+ disabledRules: disabled.length,
517
+ rules: enabled,
518
+ disabledRuleNames: disabled.map((r) => r.rule),
519
+ }, null, 2));
520
+ }
395
521
  default:
396
- return errorResult(`Unknown SAPLint action: ${action}. Supported: lint, atc, syntax`);
522
+ return errorResult(`Unknown SAPLint action: "${action}". Supported: lint, lint_and_fix, list_rules. For atc/syntax/unittest, use SAPDiagnose instead.`);
397
523
  }
398
524
  }
525
+ /**
526
+ * Build LintConfigOptions from server config and cached features.
527
+ *
528
+ * Uses cachedFeatures (from SAPManage probe) when available, but falls back
529
+ * to config.systemType so that --system-type btp works even before the first
530
+ * probe. Without this fallback, cloud lint rules wouldn't apply until a probe
531
+ * populates cachedFeatures.
532
+ */
533
+ function buildLintConfigOptions(config, ruleOverrides) {
534
+ // Probe-detected system type is most accurate; fall back to CLI config
535
+ const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
536
+ return {
537
+ systemType,
538
+ abapRelease: cachedFeatures?.abapRelease,
539
+ configFile: config.abaplintConfig,
540
+ ruleOverrides,
541
+ };
542
+ }
399
543
  // ─── Object URL Mapping ──────────────────────────────────────────────
400
544
  /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
401
545
  function objectUrlForType(type, name) {
@@ -419,8 +563,20 @@ function objectUrlForType(type, name) {
419
563
  return `/sap/bc/adt/bo/behaviordefinitions/${encoded}`;
420
564
  case 'SRVD':
421
565
  return `/sap/bc/adt/ddic/srvd/sources/${encoded}`;
566
+ case 'DDLX':
567
+ return `/sap/bc/adt/ddic/ddlx/sources/${encoded}`;
568
+ case 'SRVB':
569
+ return `/sap/bc/adt/businessservices/bindings/${encoded}`;
422
570
  case 'TABL':
423
571
  return `/sap/bc/adt/ddic/tables/${encoded}`;
572
+ case 'STRU':
573
+ return `/sap/bc/adt/ddic/structures/${encoded}`;
574
+ case 'DOMA':
575
+ return `/sap/bc/adt/ddic/domains/${encoded}`;
576
+ case 'DTEL':
577
+ return `/sap/bc/adt/ddic/dataelements/${encoded}`;
578
+ case 'TRAN':
579
+ return `/sap/bc/adt/vit/wb/object_type/trant/object_name/${encoded}`;
424
580
  default:
425
581
  return `/sap/bc/adt/programs/programs/${encoded}`;
426
582
  }
@@ -430,7 +586,7 @@ function sourceUrlForType(type, name) {
430
586
  return `${objectUrlForType(type, name)}/source/main`;
431
587
  }
432
588
  // ─── SAPWrite Handler ────────────────────────────────────────────────
433
- async function handleSAPWrite(client, args) {
589
+ async function handleSAPWrite(client, args, config, cachingLayer) {
434
590
  const action = String(args.action ?? '');
435
591
  const type = String(args.type ?? '');
436
592
  const name = String(args.name ?? '');
@@ -440,8 +596,14 @@ async function handleSAPWrite(client, args) {
440
596
  const srcUrl = sourceUrlForType(type, name);
441
597
  switch (action) {
442
598
  case 'update': {
599
+ // Pre-write lint validation
600
+ const lintWarnings = runPreWriteLint(source, type, name, config);
601
+ if (lintWarnings.blocked)
602
+ return lintWarnings.result;
443
603
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
444
- return textResult(`Successfully updated ${type} ${name}.`);
604
+ cachingLayer?.invalidate(type, name);
605
+ const msg = `Successfully updated ${type} ${name}.`;
606
+ return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
445
607
  }
446
608
  case 'create': {
447
609
  const pkg = String(args.package ?? '$TMP');
@@ -453,6 +615,37 @@ async function handleSAPWrite(client, args) {
453
615
  const result = await createObject(client.http, client.safety, objectUrl, body, 'application/xml', transport);
454
616
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
455
617
  }
618
+ case 'edit_method': {
619
+ const method = String(args.method ?? '');
620
+ if (!method)
621
+ return errorResult('"method" is required for edit_method action.');
622
+ if (!source)
623
+ return errorResult('"source" (new method body) is required for edit_method action.');
624
+ if (type !== 'CLAS')
625
+ return errorResult('edit_method is only supported for type=CLAS.');
626
+ // Fetch current full source (use cache if available)
627
+ const currentSource = cachingLayer
628
+ ? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
629
+ : await client.getClass(name);
630
+ // Use detected ABAP version from probe if available
631
+ const abaplintVer = cachedFeatures?.abapRelease
632
+ ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
633
+ : undefined;
634
+ // Splice in the new method body
635
+ const spliced = spliceMethod(currentSource, name, method, source, abaplintVer);
636
+ if (!spliced.success) {
637
+ return errorResult(spliced.error ?? `Failed to splice method "${method}" in ${name}.`);
638
+ }
639
+ // Pre-write lint validation on the full spliced source
640
+ const lintWarnings = runPreWriteLint(spliced.newSource, type, name, config);
641
+ if (lintWarnings.blocked)
642
+ return lintWarnings.result;
643
+ // Write the full source back (existing lock/modify/unlock flow)
644
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport);
645
+ cachingLayer?.invalidate(type, name);
646
+ const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
647
+ return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
648
+ }
456
649
  case 'delete': {
457
650
  // Lock, delete, unlock pattern
458
651
  await client.http.withStatefulSession(async (session) => {
@@ -469,16 +662,81 @@ async function handleSAPWrite(client, args) {
469
662
  }
470
663
  }
471
664
  });
665
+ cachingLayer?.invalidate(type, name);
472
666
  return textResult(`Deleted ${type} ${name}.`);
473
667
  }
474
668
  default:
475
- return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete`);
669
+ return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method`);
670
+ }
671
+ }
672
+ /**
673
+ * Run pre-write lint validation on source code.
674
+ *
675
+ * This is a "lint-before-lock" optimization (pattern from vibing-steampunk):
676
+ * by validating locally before acquiring the SAP object lock, we avoid
677
+ * holding locks on objects that would fail validation anyway.
678
+ *
679
+ * Only runs a strict subset of correctness rules (parser_error, cloud_types, etc.)
680
+ * — not style/formatting rules. This prevents false rejections from opinionated
681
+ * style checks while catching genuine errors that would fail server-side anyway.
682
+ *
683
+ * If lint itself throws (e.g., abaplint bug on unusual syntax), we don't block
684
+ * the write — we let the SAP server-side syntax check handle it instead.
685
+ */
686
+ function runPreWriteLint(source, type, name, config) {
687
+ if (!config.lintBeforeWrite || !source) {
688
+ return { blocked: false };
689
+ }
690
+ try {
691
+ const filename = detectFilename(source, name);
692
+ const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
693
+ const configOptions = {
694
+ systemType,
695
+ abapRelease: cachedFeatures?.abapRelease,
696
+ configFile: config.abaplintConfig,
697
+ };
698
+ const result = validateBeforeWrite(source, filename, configOptions);
699
+ if (!result.pass) {
700
+ const errorLines = result.errors.map((e) => ` Line ${e.line}: [${e.rule}] ${e.message}`).join('\n');
701
+ return {
702
+ blocked: true,
703
+ result: errorResult(`Pre-write lint check failed for ${type} ${name}. Fix these errors before writing:\n${errorLines}\n\n` +
704
+ 'Use SAPLint action="lint_and_fix" to auto-fix, or disable with --lint-before-write=false.'),
705
+ };
706
+ }
707
+ if (result.warnings.length > 0) {
708
+ const warningLines = result.warnings.map((w) => ` Line ${w.line}: [${w.rule}] ${w.message}`).join('\n');
709
+ return {
710
+ blocked: false,
711
+ warnings: `Lint warnings:\n${warningLines}`,
712
+ };
713
+ }
714
+ return { blocked: false };
715
+ }
716
+ catch {
717
+ // If lint itself fails, don't block the write
718
+ return { blocked: false };
476
719
  }
477
720
  }
478
721
  // ─── SAPActivate Handler ─────────────────────────────────────────────
479
722
  async function handleSAPActivate(client, args) {
480
- const name = String(args.name ?? '');
481
723
  const type = String(args.type ?? '');
724
+ // Batch activation: multiple objects at once (for RAP stacks etc.)
725
+ if (args.objects && Array.isArray(args.objects)) {
726
+ const objects = args.objects.map((o) => {
727
+ const objType = String(o.type ?? type);
728
+ const objName = String(o.name ?? '');
729
+ return { url: objectUrlForType(objType, objName), name: objName };
730
+ });
731
+ const result = await activateBatch(client.http, client.safety, objects);
732
+ const names = objects.map((o) => o.name).join(', ');
733
+ if (result.success) {
734
+ return textResult(`Successfully activated ${objects.length} objects: ${names}.${result.messages.length > 0 ? `\nMessages: ${result.messages.join('; ')}` : ''}`);
735
+ }
736
+ return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
737
+ }
738
+ // Single activation (existing behavior)
739
+ const name = String(args.name ?? '');
482
740
  const objectUrl = objectUrlForType(type, name);
483
741
  const result = await activate(client.http, client.safety, objectUrl);
484
742
  if (result.success) {
@@ -526,7 +784,31 @@ async function handleSAPNavigate(client, args) {
526
784
  if (!uri) {
527
785
  return errorResult('Provide uri or type+name to find references.');
528
786
  }
529
- const results = await findReferences(client.http, client.safety, uri);
787
+ const objectType = args.objectType ? String(args.objectType) : undefined;
788
+ let results;
789
+ try {
790
+ results = await findWhereUsed(client.http, client.safety, uri, objectType);
791
+ }
792
+ catch (err) {
793
+ // Only fall back for HTTP errors indicating the endpoint is not available (older SAP systems)
794
+ if (err instanceof AdtApiError && [404, 405, 415, 501].includes(err.statusCode)) {
795
+ results = await findReferences(client.http, client.safety, uri);
796
+ if (results.length === 0) {
797
+ return textResult('No references found.');
798
+ }
799
+ const json = JSON.stringify(results, null, 2);
800
+ if (objectType) {
801
+ return textResult(JSON.stringify({
802
+ note: `This SAP system does not support scope-based Where-Used. The objectType filter "${objectType}" was ignored — results below are unfiltered.`,
803
+ results,
804
+ }, null, 2));
805
+ }
806
+ return textResult(json);
807
+ }
808
+ else {
809
+ throw err;
810
+ }
811
+ }
530
812
  if (results.length === 0) {
531
813
  return textResult('No references found.');
532
814
  }
@@ -545,23 +827,64 @@ async function handleSAPDiagnose(client, args) {
545
827
  const action = String(args.action ?? '');
546
828
  const name = String(args.name ?? '');
547
829
  const type = String(args.type ?? '');
548
- const objectUrl = objectUrlForType(type, name);
549
830
  switch (action) {
550
831
  case 'syntax': {
832
+ const objectUrl = objectUrlForType(type, name);
551
833
  const result = await syntaxCheck(client.http, client.safety, objectUrl);
552
834
  return textResult(JSON.stringify(result, null, 2));
553
835
  }
554
836
  case 'unittest': {
837
+ const objectUrl = objectUrlForType(type, name);
555
838
  const results = await runUnitTests(client.http, client.safety, objectUrl);
556
839
  return textResult(JSON.stringify(results, null, 2));
557
840
  }
558
841
  case 'atc': {
842
+ const objectUrl = objectUrlForType(type, name);
559
843
  const variant = args.variant;
560
844
  const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
561
845
  return textResult(JSON.stringify(result, null, 2));
562
846
  }
847
+ case 'dumps': {
848
+ const id = args.id;
849
+ if (id) {
850
+ // Get single dump detail
851
+ const detail = await getDump(client.http, client.safety, id);
852
+ return textResult(JSON.stringify(detail, null, 2));
853
+ }
854
+ // List dumps
855
+ const user = args.user;
856
+ const maxResults = args.maxResults ? Number(args.maxResults) : undefined;
857
+ const dumps = await listDumps(client.http, client.safety, { user, maxResults });
858
+ return textResult(JSON.stringify(dumps, null, 2));
859
+ }
860
+ case 'traces': {
861
+ const id = args.id;
862
+ if (id) {
863
+ // Get trace analysis
864
+ const analysis = String(args.analysis ?? 'hitlist');
865
+ switch (analysis) {
866
+ case 'hitlist': {
867
+ const hitlist = await getTraceHitlist(client.http, client.safety, id);
868
+ return textResult(JSON.stringify(hitlist, null, 2));
869
+ }
870
+ case 'statements': {
871
+ const statements = await getTraceStatements(client.http, client.safety, id);
872
+ return textResult(JSON.stringify(statements, null, 2));
873
+ }
874
+ case 'dbAccesses': {
875
+ const dbAccesses = await getTraceDbAccesses(client.http, client.safety, id);
876
+ return textResult(JSON.stringify(dbAccesses, null, 2));
877
+ }
878
+ default:
879
+ return errorResult(`Unknown trace analysis type: ${analysis}. Supported: hitlist, statements, dbAccesses`);
880
+ }
881
+ }
882
+ // List traces
883
+ const traces = await listTraces(client.http, client.safety);
884
+ return textResult(JSON.stringify(traces, null, 2));
885
+ }
563
886
  default:
564
- return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc`);
887
+ return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, dumps, traces`);
565
888
  }
566
889
  }
567
890
  // ─── SAPTransport Handler ────────────────────────────────────────────
@@ -601,14 +924,44 @@ async function handleSAPTransport(client, args) {
601
924
  }
602
925
  }
603
926
  // ─── SAPContext Handler ───────────────────────────────────────────────
604
- async function handleSAPContext(client, args) {
927
+ async function handleSAPContext(client, args, cachingLayer) {
928
+ const action = String(args.action ?? '');
605
929
  const type = String(args.type ?? '');
606
930
  const name = String(args.name ?? '');
607
931
  const maxDeps = Number(args.maxDeps ?? 20);
608
932
  const depth = Math.min(Math.max(Number(args.depth ?? 1), 1), 3);
933
+ // ─── Reverse dep lookup (pre-warmer only) ─────────────────────────
934
+ if (action === 'usages') {
935
+ if (!name)
936
+ return errorResult('"name" is required for usages action.');
937
+ if (!cachingLayer) {
938
+ return errorResult('Reverse dependency lookup requires object caching. Cache is disabled (ARC1_CACHE=none). ' +
939
+ 'Enable caching and run cache warmup to use this feature.');
940
+ }
941
+ const usages = cachingLayer.getUsages(name);
942
+ if (usages === null) {
943
+ return errorResult(`Reverse dependency lookup requires a pre-warmed cache. The cache warmup has not been run yet.\n\n` +
944
+ `To enable this feature:\n` +
945
+ `1. Start ARC-1 with --cache-warmup (or set ARC1_CACHE_WARMUP=true)\n` +
946
+ `2. Wait for the warmup to complete (indexes all custom objects)\n` +
947
+ `3. Then retry SAPContext(action="usages", name="${name}")\n\n` +
948
+ `Alternative: Use SAPNavigate(action="references", type="CLAS", name="${name}") for a live ADT lookup (slower, but works without warmup).`);
949
+ }
950
+ if (usages.length === 0) {
951
+ return textResult(`No objects found that depend on "${name}" in the cached index.`);
952
+ }
953
+ return textResult(JSON.stringify({ name, usageCount: usages.length, usages }, null, 2));
954
+ }
609
955
  if (!type || !name) {
610
956
  return errorResult('Both "type" and "name" are required for SAPContext.');
611
957
  }
958
+ // Helper: get source with cache support
959
+ const cachedGet = async (objType, objName, fetcher) => {
960
+ if (!cachingLayer)
961
+ return fetcher();
962
+ const { source } = await cachingLayer.getSource(objType, objName, fetcher);
963
+ return source;
964
+ };
612
965
  // Get source — either provided or fetched from SAP
613
966
  let source;
614
967
  if (args.source) {
@@ -617,37 +970,70 @@ async function handleSAPContext(client, args) {
617
970
  else {
618
971
  switch (type) {
619
972
  case 'CLAS':
620
- source = await client.getClass(name);
973
+ source = await cachedGet('CLAS', name, () => client.getClass(name));
621
974
  break;
622
975
  case 'INTF':
623
- source = await client.getInterface(name);
976
+ source = await cachedGet('INTF', name, () => client.getInterface(name));
624
977
  break;
625
978
  case 'PROG':
626
- source = await client.getProgram(name);
979
+ source = await cachedGet('PROG', name, () => client.getProgram(name));
627
980
  break;
628
981
  case 'FUNC': {
629
982
  const group = String(args.group ?? '');
630
983
  if (!group) {
631
984
  return errorResult('The "group" parameter is required for FUNC type. Use SAPSearch to find the function group.');
632
985
  }
633
- source = await client.getFunction(group, name);
986
+ source = await cachedGet('FUNC', name, () => client.getFunction(group, name));
634
987
  break;
635
988
  }
989
+ case 'DDLS': {
990
+ const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
991
+ const cdsResult = await compressCdsContext(client, ddlSource, name, maxDeps, depth, cachingLayer);
992
+ return textResult(cdsResult.output);
993
+ }
636
994
  default:
637
- return errorResult(`SAPContext supports types: CLAS, INTF, PROG, FUNC. Got: ${type}`);
995
+ return errorResult(`SAPContext supports types: CLAS, INTF, PROG, FUNC, DDLS. Got: ${type}`);
996
+ }
997
+ }
998
+ // Check dep graph cache — if source hash matches, return cached contracts
999
+ if (cachingLayer) {
1000
+ const cachedGraph = cachingLayer.getCachedDepGraph(source);
1001
+ if (cachedGraph) {
1002
+ const successful = cachedGraph.contracts.filter((c) => c.success);
1003
+ const failed = cachedGraph.contracts.filter((c) => !c.success);
1004
+ const lines = [];
1005
+ lines.push(`* === Dependency context for ${name} (${successful.length} deps resolved${failed.length > 0 ? `, ${failed.length} failed` : ''}) [cached] ===`);
1006
+ lines.push('');
1007
+ for (const contract of successful) {
1008
+ const typeLabel = contract.type.toLowerCase();
1009
+ const methodLabel = contract.methodCount > 0 ? `, ${contract.methodCount} methods` : '';
1010
+ lines.push(`* --- ${contract.name} (${typeLabel}${methodLabel}) ---`);
1011
+ lines.push(contract.source.trim());
1012
+ lines.push('');
1013
+ }
1014
+ if (failed.length > 0) {
1015
+ lines.push('* --- Failed dependencies ---');
1016
+ for (const f of failed) {
1017
+ lines.push(`* ${f.name}: ${f.error}`);
1018
+ }
1019
+ lines.push('');
1020
+ }
1021
+ const totalLines = lines.length;
1022
+ lines.push(`* Stats: ${successful.length + failed.length} deps found, ${successful.length} resolved, ${failed.length} failed, ${totalLines} lines [from cache]`);
1023
+ return textResult(lines.join('\n'));
638
1024
  }
639
1025
  }
640
1026
  // Use detected ABAP version from probe if available, otherwise Cloud (superset)
641
1027
  const abaplintVersion = cachedFeatures?.abapRelease
642
1028
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
643
1029
  : undefined;
644
- const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion);
1030
+ const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion, cachingLayer);
645
1031
  return textResult(result.output);
646
1032
  }
647
1033
  // ─── SAPManage Handler ────────────────────────────────────────────────
648
1034
  /** Cached feature status — populated on first probe */
649
1035
  let cachedFeatures;
650
- async function handleSAPManage(client, config, args) {
1036
+ async function handleSAPManage(client, config, args, cachingLayer) {
651
1037
  const action = String(args.action ?? '');
652
1038
  switch (action) {
653
1039
  case 'features': {
@@ -656,6 +1042,17 @@ async function handleSAPManage(client, config, args) {
656
1042
  }
657
1043
  return textResult(JSON.stringify(cachedFeatures, null, 2));
658
1044
  }
1045
+ case 'cache_stats': {
1046
+ if (!cachingLayer) {
1047
+ return textResult(JSON.stringify({ enabled: false, message: 'Object cache is disabled (ARC1_CACHE=none).' }));
1048
+ }
1049
+ const stats = cachingLayer.stats();
1050
+ return textResult(JSON.stringify({
1051
+ enabled: true,
1052
+ warmupAvailable: cachingLayer.isWarmupAvailable,
1053
+ ...stats,
1054
+ }, null, 2));
1055
+ }
659
1056
  case 'probe': {
660
1057
  const { defaultFeatureConfig } = await import('../adt/config.js');
661
1058
  const featureConfig = defaultFeatureConfig();