arc-1 0.6.10 → 0.7.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/README.md +8 -7
- package/bin/arc1-cli.js +10 -0
- package/bin/arc1.js +1 -1
- package/dist/adt/cds-impact.d.ts +35 -0
- package/dist/adt/cds-impact.d.ts.map +1 -1
- package/dist/adt/cds-impact.js +71 -0
- package/dist/adt/cds-impact.js.map +1 -1
- package/dist/adt/client.d.ts +4 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +18 -5
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +32 -5
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/devtools.d.ts +39 -3
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +237 -25
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +69 -7
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +694 -36
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +14 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +40 -9
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +86 -1
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/rap-handlers.d.ts +165 -0
- package/dist/adt/rap-handlers.d.ts.map +1 -0
- package/dist/adt/rap-handlers.js +835 -0
- package/dist/adt/rap-handlers.js.map +1 -0
- package/dist/adt/rap-preflight.d.ts +43 -0
- package/dist/adt/rap-preflight.d.ts.map +1 -0
- package/dist/adt/rap-preflight.js +405 -0
- package/dist/adt/rap-preflight.js.map +1 -0
- package/dist/adt/safety.d.ts +60 -36
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +202 -120
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/transport.d.ts +1 -1
- package/dist/adt/transport.js +2 -2
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +88 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +13 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +26 -15
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts +53 -0
- package/dist/authz/policy.d.ts.map +1 -0
- package/dist/authz/policy.js +199 -0
- package/dist/authz/policy.js.map +1 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +62 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +13 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +252 -55
- package/dist/cli.js.map +1 -1
- package/dist/extract-sap-cookies.d.ts +24 -0
- package/dist/extract-sap-cookies.d.ts.map +1 -0
- package/dist/extract-sap-cookies.js +317 -0
- package/dist/extract-sap-cookies.js.map +1 -0
- package/dist/handlers/hyperfocused.d.ts +4 -3
- package/dist/handlers/hyperfocused.d.ts.map +1 -1
- package/dist/handlers/hyperfocused.js +25 -16
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/intent.d.ts +4 -12
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +1238 -114
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +38 -10
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +69 -4
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +251 -164
- package/dist/handlers/tools.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/server/audit.d.ts +26 -3
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts +34 -19
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +320 -193
- package/dist/server/config.js.map +1 -1
- package/dist/server/deny-actions.d.ts +31 -0
- package/dist/server/deny-actions.d.ts.map +1 -0
- package/dist/server/deny-actions.js +156 -0
- package/dist/server/deny-actions.js.map +1 -0
- package/dist/server/effective-policy-log.d.ts +27 -0
- package/dist/server/effective-policy-log.d.ts.map +1 -0
- package/dist/server/effective-policy-log.js +103 -0
- package/dist/server/effective-policy-log.js.map +1 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +15 -16
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +37 -3
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +231 -30
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +29 -13
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +10 -11
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +1 -2
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +13 -14
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +6 -3
package/dist/handlers/intent.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Intent-based tool handler for ARC-1.
|
|
3
3
|
*
|
|
4
4
|
* Routes MCP tool calls to the appropriate ADT client methods.
|
|
5
|
-
* Each of the
|
|
5
|
+
* Each of the 12 tools (SAPRead, SAPSearch, etc.) dispatches
|
|
6
6
|
* based on its `type` or `action` parameter.
|
|
7
7
|
*
|
|
8
8
|
* Error handling: all errors are caught and returned as MCP error
|
|
@@ -10,16 +10,18 @@
|
|
|
10
10
|
* leaked to the LLM — only user-friendly error messages.
|
|
11
11
|
*/
|
|
12
12
|
import { checkRepo as abapGitCheckRepo, createBranch as abapGitCreateBranch, createRepo as abapGitCreateRepo, getExternalInfo as abapGitGetExternalInfo, listRepos as abapGitListRepos, pullRepo as abapGitPullRepo, pushRepo as abapGitPushRepo, stageRepo as abapGitStageRepo, switchBranch as abapGitSwitchBranch, unlinkRepo as abapGitUnlinkRepo, } from '../adt/abapgit.js';
|
|
13
|
-
import { classifyCdsImpact } from '../adt/cds-impact.js';
|
|
14
|
-
import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
|
|
15
|
-
import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../adt/crud.js';
|
|
13
|
+
import { buildSiblingExtensionFinding, classifyCdsImpact, deriveSiblingStem, isSiblingNameMatch, } from '../adt/cds-impact.js';
|
|
14
|
+
import { findDefinition, findReferences, findWhereUsed, getCompletion, getWhereUsedScope, } from '../adt/codeintel.js';
|
|
15
|
+
import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, updateSource, } from '../adt/crud.js';
|
|
16
16
|
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, decodeKtdText, rewriteKtdText, } from '../adt/ddic-xml.js';
|
|
17
17
|
import { activate, activateBatch, applyFixProposal, getFixProposals, getPrettyPrinterSettings, prettyPrint, publishServiceBinding, runAtcCheck, runUnitTests, setPrettyPrinterSettings, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
18
|
-
import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
|
|
18
|
+
import { getDump, getGatewayErrorDetail, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraces, } from '../adt/diagnostics.js';
|
|
19
19
|
import { AdtApiError, AdtNetworkError, AdtSafetyError, classifySapDomainError, isNotFoundError, } from '../adt/errors.js';
|
|
20
20
|
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
21
21
|
import { addTileToGroup, createCatalog, createGroup, createTile, deleteCatalog, listCatalogs, listGroups, listTiles, } from '../adt/flp.js';
|
|
22
22
|
import { cloneRepo as gctsCloneRepo, commitRepo as gctsCommitRepo, createBranch as gctsCreateBranch, deleteRepo as gctsDeleteRepo, getCommitHistory as gctsGetCommitHistory, getConfig as gctsGetConfig, getUserInfo as gctsGetUserInfo, listBranches as gctsListBranches, listRepoObjects as gctsListRepoObjects, listRepos as gctsListRepos, pullRepo as gctsPullRepo, switchBranch as gctsSwitchBranch, } from '../adt/gcts.js';
|
|
23
|
+
import { applyRapHandlerScaffold, extractRapHandlerRequirements, findMissingRapHandlerImplementationStubs, findMissingRapHandlerRequirements, } from '../adt/rap-handlers.js';
|
|
24
|
+
import { formatRapPreflightFindings, validateRapSource } from '../adt/rap-preflight.js';
|
|
23
25
|
import { changePackage } from '../adt/refactoring.js';
|
|
24
26
|
import { checkOperation, checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
25
27
|
import { createTransport, deleteTransport, getObjectTransports, getTransport, getTransportInfo, listTransports, reassignTransport, releaseTransport, releaseTransportRecursive, } from '../adt/transport.js';
|
|
@@ -33,66 +35,46 @@ import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from
|
|
|
33
35
|
import { sanitizeArgs } from '../server/audit.js';
|
|
34
36
|
import { generateRequestId, requestContext } from '../server/context.js';
|
|
35
37
|
import { logger } from '../server/logger.js';
|
|
36
|
-
import { expandHyperfocusedArgs
|
|
38
|
+
import { expandHyperfocusedArgs } from './hyperfocused.js';
|
|
37
39
|
import { getToolSchema } from './schemas.js';
|
|
38
40
|
import { formatZodError } from './zod-errors.js';
|
|
39
41
|
/**
|
|
40
42
|
* Scope required for each tool.
|
|
41
43
|
*
|
|
42
44
|
* Scope enforcement is ADDITIVE to the safety system:
|
|
43
|
-
* - Safety system (
|
|
45
|
+
* - Safety system (allowWrites, allowedPackages, etc.) gates operations at the ADT client level
|
|
44
46
|
* - Scopes gate operations at the MCP tool level (only enforced when authInfo is present)
|
|
45
47
|
* - Both must pass for an operation to succeed
|
|
46
48
|
*
|
|
47
|
-
* A user with `write` scope but `
|
|
49
|
+
* A user with `write` scope but `allowWrites=false` in config still can't write.
|
|
50
|
+
*
|
|
51
|
+
* Scope lookup and implication rules are defined in `src/authz/policy.ts` (ACTION_POLICY,
|
|
52
|
+
* getActionPolicy, hasRequiredScope). This module routes through them.
|
|
48
53
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
external_info: 'read',
|
|
69
|
-
history: 'read',
|
|
70
|
-
objects: 'read',
|
|
71
|
-
check: 'read',
|
|
72
|
-
clone: 'write',
|
|
73
|
-
pull: 'write',
|
|
74
|
-
push: 'write',
|
|
75
|
-
commit: 'write',
|
|
76
|
-
stage: 'write',
|
|
77
|
-
switch_branch: 'write',
|
|
78
|
-
create_branch: 'write',
|
|
79
|
-
unlink: 'write',
|
|
80
|
-
};
|
|
54
|
+
import { getActionPolicy, hasRequiredScope as hasScopeHelper } from '../authz/policy.js';
|
|
55
|
+
/**
|
|
56
|
+
* Back-compat re-export of a tool→scope map derived from ACTION_POLICY.
|
|
57
|
+
* New code should use `getActionPolicy(tool, action)` directly.
|
|
58
|
+
*/
|
|
59
|
+
export const TOOL_SCOPES = Object.fromEntries([
|
|
60
|
+
'SAPRead',
|
|
61
|
+
'SAPSearch',
|
|
62
|
+
'SAPQuery',
|
|
63
|
+
'SAPGit',
|
|
64
|
+
'SAPNavigate',
|
|
65
|
+
'SAPContext',
|
|
66
|
+
'SAPLint',
|
|
67
|
+
'SAPDiagnose',
|
|
68
|
+
'SAPWrite',
|
|
69
|
+
'SAPActivate',
|
|
70
|
+
'SAPManage',
|
|
71
|
+
'SAPTransport',
|
|
72
|
+
].map((t) => [t, getActionPolicy(t)?.scope ?? 'read']));
|
|
81
73
|
/**
|
|
82
|
-
* Check if authInfo has the required scope,
|
|
83
|
-
* - `write` implies `read`
|
|
84
|
-
* - `sql` implies `data`
|
|
74
|
+
* Check if authInfo has the required scope, routing through policy.hasRequiredScope.
|
|
85
75
|
*/
|
|
86
76
|
export function hasRequiredScope(authInfo, requiredScope) {
|
|
87
|
-
|
|
88
|
-
if (scopes.includes(requiredScope))
|
|
89
|
-
return true;
|
|
90
|
-
// Implied scopes
|
|
91
|
-
if (requiredScope === 'read' && scopes.includes('write'))
|
|
92
|
-
return true;
|
|
93
|
-
if (requiredScope === 'data' && scopes.includes('sql'))
|
|
94
|
-
return true;
|
|
95
|
-
return false;
|
|
77
|
+
return hasScopeHelper(authInfo.scopes, requiredScope);
|
|
96
78
|
}
|
|
97
79
|
function textResult(text) {
|
|
98
80
|
return { content: [{ type: 'text', text }] };
|
|
@@ -102,6 +84,64 @@ function errorResult(message) {
|
|
|
102
84
|
}
|
|
103
85
|
const DDIC_SAVE_HINT_TYPES = new Set(['TABL', 'DDLS', 'DCLS', 'BDEF', 'SRVD', 'SRVB', 'DDLX', 'DOMA', 'DTEL']);
|
|
104
86
|
const DDIC_POST_SAVE_CHECK_TYPES = new Set(['TABL', 'DDLS', 'DCLS', 'BDEF', 'SRVD', 'SRVB', 'DDLX']);
|
|
87
|
+
const CDS_DEPENDENCY_SENSITIVE_TYPES = new Set(['DDLS', 'DCLS', 'DDLX', 'BDEF', 'SRVD', 'SRVB', 'TABL']);
|
|
88
|
+
const CDS_IMPACT_BUCKET_ORDER = [
|
|
89
|
+
'projectionViews',
|
|
90
|
+
'bdefs',
|
|
91
|
+
'serviceDefinitions',
|
|
92
|
+
'serviceBindings',
|
|
93
|
+
'accessControls',
|
|
94
|
+
'metadataExtensions',
|
|
95
|
+
'abapConsumers',
|
|
96
|
+
'tables',
|
|
97
|
+
'documentation',
|
|
98
|
+
'other',
|
|
99
|
+
];
|
|
100
|
+
const CDS_IMPACT_BUCKET_LABEL = {
|
|
101
|
+
projectionViews: 'Projection views (DDLS)',
|
|
102
|
+
bdefs: 'Behavior definitions (BDEF)',
|
|
103
|
+
serviceDefinitions: 'Service definitions (SRVD)',
|
|
104
|
+
serviceBindings: 'Service bindings (SRVB)',
|
|
105
|
+
accessControls: 'Access controls (DCLS)',
|
|
106
|
+
metadataExtensions: 'Metadata extensions (DDLX)',
|
|
107
|
+
abapConsumers: 'ABAP consumers',
|
|
108
|
+
tables: 'Tables',
|
|
109
|
+
documentation: 'Documentation (SKTD)',
|
|
110
|
+
other: 'Other',
|
|
111
|
+
};
|
|
112
|
+
const CDS_REACTIVATION_BUCKET_ORDER = [
|
|
113
|
+
'projectionViews',
|
|
114
|
+
'accessControls',
|
|
115
|
+
'metadataExtensions',
|
|
116
|
+
'bdefs',
|
|
117
|
+
'serviceDefinitions',
|
|
118
|
+
'serviceBindings',
|
|
119
|
+
'other',
|
|
120
|
+
];
|
|
121
|
+
const CDS_DELETE_BUCKET_ORDER = [
|
|
122
|
+
'serviceBindings',
|
|
123
|
+
'serviceDefinitions',
|
|
124
|
+
'bdefs',
|
|
125
|
+
'metadataExtensions',
|
|
126
|
+
'accessControls',
|
|
127
|
+
'projectionViews',
|
|
128
|
+
'other',
|
|
129
|
+
];
|
|
130
|
+
const CDS_ORDERABLE_TYPES = new Set(['DDLS', 'DCLS', 'DDLX', 'BDEF', 'SRVD', 'SRVB']);
|
|
131
|
+
const CDS_IMPACT_WHERE_USED_TYPES = new Set([
|
|
132
|
+
'DDLS',
|
|
133
|
+
'DCLS',
|
|
134
|
+
'DDLX',
|
|
135
|
+
'BDEF',
|
|
136
|
+
'SRVD',
|
|
137
|
+
'SRVB',
|
|
138
|
+
'CLAS',
|
|
139
|
+
'INTF',
|
|
140
|
+
'PROG',
|
|
141
|
+
'FUGR',
|
|
142
|
+
'TABL',
|
|
143
|
+
'SKTD',
|
|
144
|
+
]);
|
|
105
145
|
// ─── Search Helpers ─────────────────────────────────────────────────
|
|
106
146
|
/**
|
|
107
147
|
* Transliterate non-ASCII characters in search queries.
|
|
@@ -142,8 +182,44 @@ export function looksLikeFieldName(query) {
|
|
|
142
182
|
return false;
|
|
143
183
|
return true;
|
|
144
184
|
}
|
|
185
|
+
function hasSqlParserSignature(text) {
|
|
186
|
+
const normalized = text.toLowerCase();
|
|
187
|
+
return (normalized.includes('only one select statement is allowed') ||
|
|
188
|
+
normalized.includes('only select statement is allowed') ||
|
|
189
|
+
normalized.includes('invalid query string') ||
|
|
190
|
+
normalized.includes('due to grammar') ||
|
|
191
|
+
normalized.includes('is invalid here') ||
|
|
192
|
+
normalized.includes('is invalid at this position'));
|
|
193
|
+
}
|
|
194
|
+
function getWriteInfrastructureHint(err, tool, args) {
|
|
195
|
+
if (tool !== 'SAPWrite')
|
|
196
|
+
return undefined;
|
|
197
|
+
const action = String(args.action ?? '').toLowerCase();
|
|
198
|
+
if (!['create', 'update', 'batch_create', 'edit_method', 'delete'].includes(action))
|
|
199
|
+
return undefined;
|
|
200
|
+
// These failures happen around ADT session management, often after SAP has
|
|
201
|
+
// already accepted a mutation. They need cleanup guidance, not DDIC syntax hints.
|
|
202
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}\n${err.path}`.toLowerCase();
|
|
203
|
+
const failedDuringCsrfFetch = err.path.includes('/sap/bc/adt/core/discovery') || combined.includes('no csrf token');
|
|
204
|
+
const failedDuringUnlock = combined.includes('_action=unlock');
|
|
205
|
+
const serviceRoutingFailure = combined.includes('service cannot be reached');
|
|
206
|
+
if (!failedDuringCsrfFetch && !failedDuringUnlock && !serviceRoutingFailure)
|
|
207
|
+
return undefined;
|
|
208
|
+
return ('SAP ADT write/session infrastructure failed, not a DDIC source save failure. ' +
|
|
209
|
+
'The object may have been partially created or changed before the session failed; verify with SAPRead/SAPSearch, ' +
|
|
210
|
+
'wait briefly, then retry cleanup. If an edit lock remains, release it in ADT/SM12 or ask Basis to clear it.');
|
|
211
|
+
}
|
|
145
212
|
/** Format error messages with LLM-friendly remediation hints */
|
|
146
|
-
function formatErrorForLLM(err, message,
|
|
213
|
+
function formatErrorForLLM(err, message, tool, args) {
|
|
214
|
+
const base = buildBaseErrorMessage(err, message, tool, args);
|
|
215
|
+
// Handler-attached remediation hints (e.g., CDS delete blocker list) always
|
|
216
|
+
// appear last so the message reads "what happened → diagnostics → how to fix".
|
|
217
|
+
if (err instanceof AdtApiError && err.extraHint && !base.includes(err.extraHint)) {
|
|
218
|
+
return `${base}\n\n${err.extraHint}`;
|
|
219
|
+
}
|
|
220
|
+
return base;
|
|
221
|
+
}
|
|
222
|
+
function buildBaseErrorMessage(err, message, tool, args) {
|
|
147
223
|
if (err instanceof AdtApiError) {
|
|
148
224
|
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
149
225
|
const enriched = enrichWithSapDetails(err, message);
|
|
@@ -154,6 +230,10 @@ function formatErrorForLLM(err, message, _tool, args) {
|
|
|
154
230
|
return `${enriched}\n\nHint: ${classification.hint}${transactionLine}`;
|
|
155
231
|
}
|
|
156
232
|
if (err.isNotFound) {
|
|
233
|
+
const diagnosticsHint = buildDiagnosticsNotFoundHint(tool, args);
|
|
234
|
+
if (diagnosticsHint) {
|
|
235
|
+
return `${enriched}\n\nHint: ${diagnosticsHint}`;
|
|
236
|
+
}
|
|
157
237
|
const name = String(args.name ?? '');
|
|
158
238
|
const type = String(args.type ?? '');
|
|
159
239
|
return `${enriched}\n\nHint: Object "${name}" (type ${type}) was not found. Use SAPSearch with query "${name}" to verify the name exists and check the correct type.`;
|
|
@@ -166,7 +246,31 @@ function formatErrorForLLM(err, message, _tool, args) {
|
|
|
166
246
|
if (transportHint) {
|
|
167
247
|
return `${enriched}\n\nHint: ${transportHint}`;
|
|
168
248
|
}
|
|
169
|
-
if (
|
|
249
|
+
if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS' && err.statusCode === 400) {
|
|
250
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}`;
|
|
251
|
+
if (hasSqlParserSignature(combined)) {
|
|
252
|
+
return (`${enriched}\n\nHint: TABLE_CONTENTS sqlFilter must be a condition expression only ` +
|
|
253
|
+
'(no WHERE, no SELECT, no semicolon). Examples: ' +
|
|
254
|
+
`sqlFilter="MANDT = '100'" or sqlFilter="MATNR LIKE 'Z%'".`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const behaviorPoolHint = getBehaviorPoolSaveFailureHint(err, args);
|
|
258
|
+
if (behaviorPoolHint) {
|
|
259
|
+
return `${enriched}\n\nHint: ${behaviorPoolHint}`;
|
|
260
|
+
}
|
|
261
|
+
const writeInfrastructureHint = getWriteInfrastructureHint(err, tool, args);
|
|
262
|
+
if (writeInfrastructureHint) {
|
|
263
|
+
return `${enriched}\n\nHint: ${writeInfrastructureHint}`;
|
|
264
|
+
}
|
|
265
|
+
// Save hint — applies to create/update/batch_create/edit_method, not delete.
|
|
266
|
+
// Delete failures on DDIC types have different remediation (dependency resolution, not annotation fixes).
|
|
267
|
+
const action = String(args.action ?? '').toLowerCase();
|
|
268
|
+
const isSaveAction = action === '' ||
|
|
269
|
+
action === 'create' ||
|
|
270
|
+
action === 'update' ||
|
|
271
|
+
action === 'batch_create' ||
|
|
272
|
+
action === 'edit_method';
|
|
273
|
+
if ((err.statusCode === 400 || err.statusCode === 409) && DDIC_SAVE_HINT_TYPES.has(argType) && isSaveAction) {
|
|
170
274
|
return (`${enriched}\n\nHint: DDIC save failed. Check the diagnostic details above for specific field or annotation errors. ` +
|
|
171
275
|
'Common fixes: add missing @AbapCatalog annotations, fix field type names, check key field definitions.');
|
|
172
276
|
}
|
|
@@ -182,11 +286,92 @@ function formatErrorForLLM(err, message, _tool, args) {
|
|
|
182
286
|
}
|
|
183
287
|
return enriched;
|
|
184
288
|
}
|
|
289
|
+
if (err instanceof AdtSafetyError) {
|
|
290
|
+
const argType = String(args.type ?? '').toUpperCase();
|
|
291
|
+
if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS') {
|
|
292
|
+
return (`${message}\n\nHint: TABLE_CONTENTS is blocked by safety configuration or missing data scope. ` +
|
|
293
|
+
'Set SAP_ALLOW_DATA_PREVIEW=true at the server level and, in authenticated HTTP mode, ' +
|
|
294
|
+
'ensure the token includes data (or sql) scope.');
|
|
295
|
+
}
|
|
296
|
+
return message;
|
|
297
|
+
}
|
|
185
298
|
if (err instanceof AdtNetworkError) {
|
|
186
|
-
|
|
299
|
+
if (tool === 'SAPRead' && String(args.type ?? '').toUpperCase() === 'SYSTEM') {
|
|
300
|
+
return (`${message}\n\nHint: Connectivity probe failed. Fix connectivity first, then retry ` +
|
|
301
|
+
'SAPRead(type="SYSTEM") before running any batch or parallel tool calls.');
|
|
302
|
+
}
|
|
303
|
+
return (`${message}\n\nHint: Cannot reach the SAP system. Run SAPRead(type="SYSTEM") once as a connectivity ` +
|
|
304
|
+
'probe before retrying batch/parallel calls.');
|
|
187
305
|
}
|
|
188
306
|
return message;
|
|
189
307
|
}
|
|
308
|
+
function buildDiagnosticsNotFoundHint(tool, args) {
|
|
309
|
+
if (tool !== 'SAPDiagnose')
|
|
310
|
+
return undefined;
|
|
311
|
+
const action = String(args.action ?? '');
|
|
312
|
+
const id = String(args.id ?? '').trim();
|
|
313
|
+
const detailUrl = String(args.detailUrl ?? '').trim();
|
|
314
|
+
if (action === 'dumps' && id) {
|
|
315
|
+
return `Dump ID "${id}" was not found. Re-list dumps with SAPDiagnose(action="dumps", maxResults=50), then retry with a fresh ID from that list.`;
|
|
316
|
+
}
|
|
317
|
+
if (action === 'traces' && id) {
|
|
318
|
+
return `Trace ID "${id}" was not found. Re-list traces with SAPDiagnose(action="traces") and retry using an existing trace ID.`;
|
|
319
|
+
}
|
|
320
|
+
if (action === 'gateway_errors' && (detailUrl || id)) {
|
|
321
|
+
return 'Gateway error detail was not found. Re-list SAPDiagnose(action="gateway_errors") and reuse a current detailUrl from the list output.';
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
function buildAuditResultPreview(toolName, args, fullText) {
|
|
326
|
+
const maxLen = 500;
|
|
327
|
+
const truncate = (value) => (value.length > maxLen ? `${value.slice(0, maxLen)}...` : value);
|
|
328
|
+
if (toolName !== 'SAPDiagnose')
|
|
329
|
+
return truncate(fullText);
|
|
330
|
+
const action = String(args.action ?? '');
|
|
331
|
+
const isDetailDump = action === 'dumps' && Boolean(args.id);
|
|
332
|
+
const isDetailGateway = action === 'gateway_errors' && (Boolean(args.detailUrl) || (Boolean(args.id) && Boolean(args.errorType)));
|
|
333
|
+
if (!isDetailDump && !isDetailGateway)
|
|
334
|
+
return truncate(fullText);
|
|
335
|
+
try {
|
|
336
|
+
const payload = JSON.parse(fullText);
|
|
337
|
+
if (isDetailDump) {
|
|
338
|
+
const sections = payload.sections && typeof payload.sections === 'object' ? payload.sections : {};
|
|
339
|
+
const compact = {
|
|
340
|
+
id: payload.id,
|
|
341
|
+
error: payload.error,
|
|
342
|
+
program: payload.program,
|
|
343
|
+
user: payload.user,
|
|
344
|
+
timestamp: payload.timestamp,
|
|
345
|
+
selectedSectionIds: payload.selectedSectionIds,
|
|
346
|
+
sections: Object.fromEntries(Object.entries(sections).map(([key, value]) => {
|
|
347
|
+
if (typeof value === 'string')
|
|
348
|
+
return [key, `[omitted ${value.length} chars]`];
|
|
349
|
+
return [key, '[omitted]'];
|
|
350
|
+
})),
|
|
351
|
+
formattedText: typeof payload.formattedText === 'string' ? `[omitted ${payload.formattedText.length} chars]` : undefined,
|
|
352
|
+
};
|
|
353
|
+
return truncate(JSON.stringify(compact));
|
|
354
|
+
}
|
|
355
|
+
if (isDetailGateway && payload.sourceCode && typeof payload.sourceCode === 'object') {
|
|
356
|
+
const sourceCode = payload.sourceCode;
|
|
357
|
+
const lines = Array.isArray(sourceCode.lines) ? sourceCode.lines.length : 0;
|
|
358
|
+
const compact = {
|
|
359
|
+
type: payload.type,
|
|
360
|
+
shortText: payload.shortText,
|
|
361
|
+
transactionId: payload.transactionId,
|
|
362
|
+
username: payload.username,
|
|
363
|
+
dateTime: payload.dateTime,
|
|
364
|
+
sourceCode: `[omitted ${lines} lines]`,
|
|
365
|
+
callStackCount: Array.isArray(payload.callStack) ? payload.callStack.length : 0,
|
|
366
|
+
};
|
|
367
|
+
return truncate(JSON.stringify(compact));
|
|
368
|
+
}
|
|
369
|
+
return truncate(JSON.stringify(payload));
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return truncate(fullText);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
190
375
|
/** Enrich error message with additional SAP XML diagnostic detail (extra messages, properties) */
|
|
191
376
|
function enrichWithSapDetails(err, message) {
|
|
192
377
|
if (!err.responseBody)
|
|
@@ -218,9 +403,225 @@ function enrichWithSapDetails(err, message) {
|
|
|
218
403
|
}
|
|
219
404
|
return parts.join('\n');
|
|
220
405
|
}
|
|
221
|
-
|
|
222
|
-
|
|
406
|
+
function isDeleteDependencyError(err) {
|
|
407
|
+
const clean = AdtApiError.extractCleanMessage(err.responseBody ?? err.message).toLowerCase();
|
|
408
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
409
|
+
const diagnostics = err.responseBody ? AdtApiError.extractDdicDiagnostics(err.responseBody) : [];
|
|
410
|
+
if (diagnostics.some((diag) => diag.messageNumber === '039'))
|
|
411
|
+
return true;
|
|
412
|
+
return /could not be deleted|cannot be deleted|still in use|used by|dependent object|existing reference/.test(`${clean}\n${body}`);
|
|
413
|
+
}
|
|
414
|
+
function formatCdsImpactBuckets(downstream, maxNames = 4) {
|
|
415
|
+
const lines = [];
|
|
416
|
+
for (const bucket of CDS_IMPACT_BUCKET_ORDER) {
|
|
417
|
+
const entries = downstream[bucket];
|
|
418
|
+
if (entries.length === 0)
|
|
419
|
+
continue;
|
|
420
|
+
const unique = Array.from(new Set(entries.map((entry) => {
|
|
421
|
+
const mainType = entry.type.split('/')[0] || entry.type || '?';
|
|
422
|
+
return `${entry.name} (${mainType})`;
|
|
423
|
+
})));
|
|
424
|
+
const listed = unique.slice(0, maxNames).join(', ');
|
|
425
|
+
const more = unique.length > maxNames ? ` (+${unique.length - maxNames} more)` : '';
|
|
426
|
+
lines.push(`- ${CDS_IMPACT_BUCKET_LABEL[bucket]}: ${listed}${more}`);
|
|
427
|
+
}
|
|
428
|
+
return lines;
|
|
429
|
+
}
|
|
430
|
+
function mainObjectType(type) {
|
|
431
|
+
return type.split('/')[0]?.toUpperCase() ?? '';
|
|
432
|
+
}
|
|
433
|
+
function collectOrderedCdsObjects(downstream, bucketOrder) {
|
|
434
|
+
const seen = new Set();
|
|
435
|
+
const ordered = [];
|
|
436
|
+
for (const bucket of bucketOrder) {
|
|
437
|
+
for (const entry of downstream[bucket]) {
|
|
438
|
+
const type = mainObjectType(entry.type);
|
|
439
|
+
const name = String(entry.name ?? '').toUpperCase();
|
|
440
|
+
if (!type || !name || !CDS_ORDERABLE_TYPES.has(type))
|
|
441
|
+
continue;
|
|
442
|
+
const key = `${type}:${name}`;
|
|
443
|
+
if (seen.has(key))
|
|
444
|
+
continue;
|
|
445
|
+
seen.add(key);
|
|
446
|
+
ordered.push({ type, name });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return ordered;
|
|
450
|
+
}
|
|
451
|
+
function dedupeCdsObjects(objects) {
|
|
452
|
+
const seen = new Set();
|
|
453
|
+
const deduped = [];
|
|
454
|
+
for (const obj of objects) {
|
|
455
|
+
const key = `${obj.type}:${obj.name.toUpperCase()}`;
|
|
456
|
+
if (seen.has(key))
|
|
457
|
+
continue;
|
|
458
|
+
seen.add(key);
|
|
459
|
+
deduped.push(obj);
|
|
460
|
+
}
|
|
461
|
+
return deduped;
|
|
462
|
+
}
|
|
463
|
+
function formatCdsObjectList(objects, max = 8) {
|
|
464
|
+
if (objects.length === 0)
|
|
223
465
|
return '';
|
|
466
|
+
const listed = objects
|
|
467
|
+
.slice(0, max)
|
|
468
|
+
.map((obj) => `${obj.type} ${obj.name}`)
|
|
469
|
+
.join(', ');
|
|
470
|
+
return objects.length > max ? `${listed} (+${objects.length - max} more)` : listed;
|
|
471
|
+
}
|
|
472
|
+
function formatCdsActivationPayload(objects, max = 8) {
|
|
473
|
+
if (objects.length === 0)
|
|
474
|
+
return '[]';
|
|
475
|
+
const listed = objects
|
|
476
|
+
.slice(0, max)
|
|
477
|
+
.map((obj) => `{type:"${obj.type}",name:"${obj.name}"}`)
|
|
478
|
+
.join(', ');
|
|
479
|
+
return objects.length > max ? `[${listed}, ...] (+${objects.length - max} more)` : `[${listed}]`;
|
|
480
|
+
}
|
|
481
|
+
function dedupeWhereUsedResults(results) {
|
|
482
|
+
const seen = new Set();
|
|
483
|
+
const deduped = [];
|
|
484
|
+
for (const result of results) {
|
|
485
|
+
const uriKey = result.uri.toLowerCase();
|
|
486
|
+
const fallbackKey = `${mainObjectType(result.type)}:${String(result.name ?? '').toUpperCase()}`;
|
|
487
|
+
const key = uriKey || fallbackKey;
|
|
488
|
+
if (!key || seen.has(key))
|
|
489
|
+
continue;
|
|
490
|
+
seen.add(key);
|
|
491
|
+
deduped.push(result);
|
|
492
|
+
}
|
|
493
|
+
return deduped;
|
|
494
|
+
}
|
|
495
|
+
function isCdsImpactWhereUsedType(objectType) {
|
|
496
|
+
return CDS_IMPACT_WHERE_USED_TYPES.has(mainObjectType(objectType));
|
|
497
|
+
}
|
|
498
|
+
async function loadScopedCdsWhereUsedResults(client, objectUrl) {
|
|
499
|
+
try {
|
|
500
|
+
const scope = await getWhereUsedScope(client.http, client.safety, objectUrl);
|
|
501
|
+
const scopedTypes = Array.from(new Set(scope.entries
|
|
502
|
+
.filter((entry) => entry.count > 0 && isCdsImpactWhereUsedType(entry.objectType))
|
|
503
|
+
.map((entry) => entry.objectType)));
|
|
504
|
+
const scopedResults = [];
|
|
505
|
+
for (const objectType of scopedTypes) {
|
|
506
|
+
try {
|
|
507
|
+
scopedResults.push(...(await findWhereUsed(client.http, client.safety, objectUrl, objectType)));
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Scoped results only enrich guidance; one unsupported filter must not
|
|
511
|
+
// make the write/delete/activate path fail.
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return scopedResults;
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
if (err instanceof AdtApiError && [404, 405, 415, 501].includes(err.statusCode)) {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
// Where-used enrichment is advisory; the original write/delete/activate
|
|
521
|
+
// result should not fail just because a scoped lookup is unavailable.
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function loadCdsImpactDownstream(client, objectUrl) {
|
|
526
|
+
try {
|
|
527
|
+
const whereUsed = await findWhereUsed(client.http, client.safety, objectUrl);
|
|
528
|
+
// Some SAP releases return a shallow/default result set for unfiltered
|
|
529
|
+
// usageReferences. Scope + object-type filters usually expose the full
|
|
530
|
+
// bucket fan-out, which is exactly what CRUD guidance needs.
|
|
531
|
+
const scopedWhereUsed = await loadScopedCdsWhereUsedResults(client, objectUrl);
|
|
532
|
+
const combinedWhereUsed = dedupeWhereUsedResults([...whereUsed, ...scopedWhereUsed]);
|
|
533
|
+
return classifyCdsImpact(combinedWhereUsed, { includeIndirect: true });
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
if (err instanceof AdtApiError && [404, 405, 415, 501].includes(err.statusCode)) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function buildCdsUpdateCrudHint(client, name, objectUrl) {
|
|
543
|
+
const lines = [];
|
|
544
|
+
lines.push(`CDS update follow-up for ${name}:`);
|
|
545
|
+
const downstream = await loadCdsImpactDownstream(client, objectUrl);
|
|
546
|
+
let orderedReactivation = [];
|
|
547
|
+
if (downstream) {
|
|
548
|
+
const bucketLines = formatCdsImpactBuckets(downstream);
|
|
549
|
+
if (bucketLines.length > 0) {
|
|
550
|
+
lines.push(`- Downstream consumers in ADT where-used index: ${downstream.summary.total}`);
|
|
551
|
+
lines.push(...bucketLines);
|
|
552
|
+
orderedReactivation = collectOrderedCdsObjects(downstream, CDS_REACTIVATION_BUCKET_ORDER);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
lines.push('- No downstream consumers found in the current ADT where-used index.');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
lines.push('- Where-used index is unavailable on this system (impact list could not be fetched).');
|
|
560
|
+
}
|
|
561
|
+
lines.push(`- SAPWrite(update) stores inactive source only. Run SAPActivate(type="DDLS", name="${name}").`);
|
|
562
|
+
lines.push('- Field/alias/signature changes may require re-activation of dependent DDLS/BDEF/SRVD/DDLX objects.');
|
|
563
|
+
if (orderedReactivation.length > 0) {
|
|
564
|
+
const activationPlan = dedupeCdsObjects([{ type: 'DDLS', name }, ...orderedReactivation]);
|
|
565
|
+
lines.push(`- Suggested re-activation order: ${formatCdsObjectList(activationPlan)}.`);
|
|
566
|
+
lines.push(`- Batch call template: SAPActivate(objects=${formatCdsActivationPayload(activationPlan)}).`);
|
|
567
|
+
}
|
|
568
|
+
return lines.join('\n');
|
|
569
|
+
}
|
|
570
|
+
async function buildCdsDeleteDependencyHint(client, type, name, objectUrl) {
|
|
571
|
+
const downstream = await loadCdsImpactDownstream(client, objectUrl);
|
|
572
|
+
if (!downstream || downstream.summary.total === 0) {
|
|
573
|
+
const lines = [];
|
|
574
|
+
lines.push(`Delete dependency follow-up for ${type} ${name}:`);
|
|
575
|
+
if (!downstream) {
|
|
576
|
+
lines.push('- ADT where-used lookup is unavailable on this system or failed during error enrichment.');
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
lines.push('- No current ADT where-used dependents were returned, but SAP still rejected delete with a DDIC dependency error.');
|
|
580
|
+
}
|
|
581
|
+
lines.push('- If dependents were just deleted, wait briefly and retry; SAP active dependency/index state can lag in the same cleanup session.');
|
|
582
|
+
lines.push(`- If source was stripped or restored, run SAPActivate(type="${type}", name="${name}") first; delete checks active DDIC dependencies.`);
|
|
583
|
+
lines.push(`- If it keeps failing, run SAPNavigate(action="references", type="${type}", name="${name}") and check for edit locks/inactive objects before retrying.`);
|
|
584
|
+
return lines.join('\n');
|
|
585
|
+
}
|
|
586
|
+
const lines = [];
|
|
587
|
+
lines.push(`Blocking dependents for ${type} ${name} (ADT where-used):`);
|
|
588
|
+
lines.push(...formatCdsImpactBuckets(downstream));
|
|
589
|
+
const orderedDelete = collectOrderedCdsObjects(downstream, CDS_DELETE_BUCKET_ORDER);
|
|
590
|
+
if (orderedDelete.length > 0) {
|
|
591
|
+
lines.push(`Suggested delete order: ${formatCdsObjectList(orderedDelete)}, then ${type} ${name}.`);
|
|
592
|
+
}
|
|
593
|
+
lines.push(`Delete/refactor these dependents first, then retry SAPWrite(action="delete", type="${type}", name="${name}").`);
|
|
594
|
+
lines.push('If the listed dependents were just deleted, wait briefly and retry; SAP active dependency/index state can lag in the same cleanup session.');
|
|
595
|
+
lines.push('For cyclic CDS projection graphs, temporarily strip redirected/composition associations, activate stripped DDLS, then delete.');
|
|
596
|
+
lines.push('If source was already stripped, activate first — delete checks active version dependencies.');
|
|
597
|
+
return lines.join('\n');
|
|
598
|
+
}
|
|
599
|
+
async function buildCdsActivationDependencyHint(client, name, objectUrl) {
|
|
600
|
+
const lines = [];
|
|
601
|
+
const downstream = await loadCdsImpactDownstream(client, objectUrl);
|
|
602
|
+
let orderedReactivation = [];
|
|
603
|
+
lines.push(`CDS activation impact for ${name}:`);
|
|
604
|
+
if (!downstream || downstream.summary.total === 0) {
|
|
605
|
+
lines.push('- No downstream consumers found in ADT where-used index, or index is unavailable.');
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
lines.push(...formatCdsImpactBuckets(downstream));
|
|
609
|
+
orderedReactivation = collectOrderedCdsObjects(downstream, CDS_REACTIVATION_BUCKET_ORDER);
|
|
610
|
+
}
|
|
611
|
+
lines.push('- When fields/elements change, dependents may fail until re-activated in dependency order.');
|
|
612
|
+
if (orderedReactivation.length > 0) {
|
|
613
|
+
const activationPlan = dedupeCdsObjects([{ type: 'DDLS', name }, ...orderedReactivation]);
|
|
614
|
+
lines.push(`- Suggested re-activation order: ${formatCdsObjectList(activationPlan)}.`);
|
|
615
|
+
lines.push(`- Batch call template: SAPActivate(objects=${formatCdsActivationPayload(activationPlan)}).`);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
lines.push(`- Try SAPActivate(objects=[{type:"DDLS",name:"${name}"}, ...dependents...]).`);
|
|
619
|
+
}
|
|
620
|
+
return lines.join('\n');
|
|
621
|
+
}
|
|
622
|
+
/** Run a syntax check on the inactive version and format the errors for appending to an
|
|
623
|
+
* error message. Returns '' on any failure or when no errors are reported. */
|
|
624
|
+
async function inactiveSyntaxDiagnostic(client, type, name) {
|
|
224
625
|
try {
|
|
225
626
|
const checkResult = await syntaxCheck(client.http, client.safety, objectUrlForType(type, name), {
|
|
226
627
|
version: 'inactive',
|
|
@@ -231,8 +632,9 @@ async function tryPostSaveSyntaxCheck(client, type, name) {
|
|
|
231
632
|
if (errors.length === 0)
|
|
232
633
|
return '';
|
|
233
634
|
const lines = errors.map((msg) => {
|
|
234
|
-
const
|
|
235
|
-
|
|
635
|
+
const prefix = msg.line ? `[line ${msg.line}] ` : '';
|
|
636
|
+
const suffix = msg.uri ? ` (${msg.uri})` : '';
|
|
637
|
+
return `- ${prefix}${msg.text}${suffix}`;
|
|
236
638
|
});
|
|
237
639
|
return `\nServer syntax check (inactive):\n${lines.join('\n')}`;
|
|
238
640
|
}
|
|
@@ -240,6 +642,11 @@ async function tryPostSaveSyntaxCheck(client, type, name) {
|
|
|
240
642
|
return '';
|
|
241
643
|
}
|
|
242
644
|
}
|
|
645
|
+
async function tryPostSaveSyntaxCheck(client, type, name) {
|
|
646
|
+
if (!DDIC_POST_SAVE_CHECK_TYPES.has(type.toUpperCase()))
|
|
647
|
+
return '';
|
|
648
|
+
return inactiveSyntaxDiagnostic(client, type, name);
|
|
649
|
+
}
|
|
243
650
|
/** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
|
|
244
651
|
function getTransportHint(err) {
|
|
245
652
|
const body = (err.responseBody ?? '').toLowerCase();
|
|
@@ -275,6 +682,34 @@ function getTransportHint(err) {
|
|
|
275
682
|
}
|
|
276
683
|
return undefined;
|
|
277
684
|
}
|
|
685
|
+
function inferBdefNameFromBehaviorPoolSource(source) {
|
|
686
|
+
const match = source.match(/\bfor\s+behavior\s+of\s+([A-Za-z_][\w/]+)/i);
|
|
687
|
+
return match?.[1];
|
|
688
|
+
}
|
|
689
|
+
function getBehaviorPoolSaveFailureHint(err, args) {
|
|
690
|
+
const type = normalizeObjectType(String(args.type ?? ''));
|
|
691
|
+
if (type !== 'CLAS')
|
|
692
|
+
return undefined;
|
|
693
|
+
const name = String(args.name ?? '');
|
|
694
|
+
const source = String(args.source ?? '');
|
|
695
|
+
const clean = AdtApiError.extractCleanMessage(err.responseBody ?? '').toLowerCase();
|
|
696
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
697
|
+
const isGenericSaveFailure = clean.includes('an error occured during the save operation') ||
|
|
698
|
+
clean.includes('an error occurred during the save operation') ||
|
|
699
|
+
body.includes('an error occured during the save operation') ||
|
|
700
|
+
body.includes('an error occurred during the save operation');
|
|
701
|
+
if (!isGenericSaveFailure)
|
|
702
|
+
return undefined;
|
|
703
|
+
const looksLikeBehaviorPool = /\bfor\s+behavior\s+of\b/i.test(source) || /^zbp_/i.test(name) || /^ybp_/i.test(name);
|
|
704
|
+
if (!looksLikeBehaviorPool)
|
|
705
|
+
return undefined;
|
|
706
|
+
const inferredBdef = inferBdefNameFromBehaviorPoolSource(source);
|
|
707
|
+
const bdefHint = inferredBdef ? `, bdefName="${inferredBdef}"` : ', bdefName="<interface_bdef_name>"';
|
|
708
|
+
return (`Behavior-pool class save failed on handler declarations. Use ` +
|
|
709
|
+
`SAPWrite(action="scaffold_rap_handlers", type="CLAS", name="${name}"${bdefHint}) ` +
|
|
710
|
+
`to list missing RAP handler signatures, then rerun with autoApply=true to inject declarations. ` +
|
|
711
|
+
`If SAP still rejects the full-class write, use ADT quick-fix to stamp signatures and continue with SAPWrite(action="edit_method").`);
|
|
712
|
+
}
|
|
278
713
|
function classifyError(err) {
|
|
279
714
|
if (err instanceof AdtApiError) {
|
|
280
715
|
const classification = classifySapDomainError(err.statusCode, err.responseBody);
|
|
@@ -313,10 +748,20 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
313
748
|
tool: toolName,
|
|
314
749
|
args: sanitizeArgs(args),
|
|
315
750
|
});
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
751
|
+
// Unified scope enforcement via ACTION_POLICY — routes through action/type-aware lookup.
|
|
752
|
+
// For SAPRead, the policy key is Tool.{type}; for other action-bearing tools, Tool.{action};
|
|
753
|
+
// for tools without an action/type enum (SAPSearch, SAPQuery), the tool-level default applies.
|
|
754
|
+
// Runs BEFORE Zod validation so scope errors don't leak schema details to unauthorized callers.
|
|
755
|
+
const actionOrType = toolName === 'SAPRead'
|
|
756
|
+
? typeof args.type === 'string'
|
|
757
|
+
? args.type
|
|
758
|
+
: undefined
|
|
759
|
+
: typeof args.action === 'string'
|
|
760
|
+
? args.action
|
|
761
|
+
: undefined;
|
|
762
|
+
const policy = getActionPolicy(toolName, actionOrType);
|
|
763
|
+
if (authInfo && policy) {
|
|
764
|
+
if (!hasRequiredScope(authInfo, policy.scope)) {
|
|
320
765
|
logger.emitAudit({
|
|
321
766
|
timestamp: new Date().toISOString(),
|
|
322
767
|
level: 'warn',
|
|
@@ -325,16 +770,32 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
325
770
|
user,
|
|
326
771
|
clientId,
|
|
327
772
|
tool: toolName,
|
|
328
|
-
requiredScope,
|
|
773
|
+
requiredScope: policy.scope,
|
|
329
774
|
availableScopes: authInfo.scopes,
|
|
330
775
|
});
|
|
331
|
-
|
|
776
|
+
const actionLabel = actionOrType
|
|
777
|
+
? `${toolName}(${toolName === 'SAPRead' ? 'type' : 'action'}="${actionOrType}")`
|
|
778
|
+
: toolName;
|
|
779
|
+
return errorResult(`Insufficient scope: '${policy.scope}' required for ${actionLabel}. Your scopes: [${authInfo.scopes.join(', ')}]`);
|
|
332
780
|
}
|
|
333
781
|
}
|
|
334
|
-
//
|
|
782
|
+
// Server-level denyActions (SAP_DENY_ACTIONS) — blocks before any per-user scope allows it.
|
|
783
|
+
const { isActionDenied } = await import('../server/deny-actions.js');
|
|
784
|
+
if (isActionDenied(toolName, actionOrType, config.denyActions ?? [])) {
|
|
785
|
+
logger.emitAudit({
|
|
786
|
+
timestamp: new Date().toISOString(),
|
|
787
|
+
level: 'warn',
|
|
788
|
+
event: 'safety_blocked',
|
|
789
|
+
requestId: reqId,
|
|
790
|
+
user,
|
|
791
|
+
clientId,
|
|
792
|
+
operation: `${toolName}${actionOrType ? `.${actionOrType}` : ''}`,
|
|
793
|
+
reason: 'Action denied by SAP_DENY_ACTIONS',
|
|
794
|
+
});
|
|
795
|
+
return errorResult(`Action '${toolName}${actionOrType ? `.${actionOrType}` : ''}' is denied by server policy (SAP_DENY_ACTIONS).`);
|
|
796
|
+
}
|
|
797
|
+
// Validate tool arguments with Zod schema (runs AFTER scope + deny check).
|
|
335
798
|
const isBtp = config.systemType === 'btp';
|
|
336
|
-
// Always use the full search schema for validation — the handler checks text search availability
|
|
337
|
-
// and returns a proper error message with the probe reason when source_code search is unavailable
|
|
338
799
|
const schema = getToolSchema(toolName, isBtp);
|
|
339
800
|
if (schema) {
|
|
340
801
|
args = normalizeTypeArgsForValidation(toolName, args);
|
|
@@ -403,15 +864,8 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
403
864
|
result = errorResult(expanded.error);
|
|
404
865
|
break;
|
|
405
866
|
}
|
|
406
|
-
// Check scope for the delegated action
|
|
407
|
-
if (authInfo) {
|
|
408
|
-
const requiredScope = getHyperfocusedScope(String(args.action ?? ''));
|
|
409
|
-
if (!hasRequiredScope(authInfo, requiredScope)) {
|
|
410
|
-
result = errorResult(`Insufficient scope: '${requiredScope}' required for SAP(action="${args.action}"). Your scopes: [${authInfo.scopes.join(', ')}]`);
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
867
|
// Delegate to the real handler (recursive call, but with the mapped tool name)
|
|
868
|
+
// The concrete tool/action policy is enforced by the recursive call.
|
|
415
869
|
result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer, isPerUserClient);
|
|
416
870
|
break;
|
|
417
871
|
}
|
|
@@ -421,7 +875,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
421
875
|
const durationMs = Date.now() - start;
|
|
422
876
|
const fullText = result.content.map((c) => c.text).join('');
|
|
423
877
|
const resultSize = fullText.length;
|
|
424
|
-
const resultPreview =
|
|
878
|
+
const resultPreview = buildAuditResultPreview(toolName, args, fullText);
|
|
425
879
|
logger.emitAudit({
|
|
426
880
|
timestamp: new Date().toISOString(),
|
|
427
881
|
level: result.isError ? 'error' : 'info',
|
|
@@ -893,6 +1347,25 @@ async function handleSAPSearch(client, args) {
|
|
|
893
1347
|
}
|
|
894
1348
|
return textResult(transliterationNote + JSON.stringify(results, null, 2));
|
|
895
1349
|
}
|
|
1350
|
+
function classifySapQueryParserError(err, sql) {
|
|
1351
|
+
if (err.statusCode !== 400)
|
|
1352
|
+
return undefined;
|
|
1353
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}`;
|
|
1354
|
+
if (!hasSqlParserSignature(combined))
|
|
1355
|
+
return undefined;
|
|
1356
|
+
const hints = [
|
|
1357
|
+
'ADT freestyle SQL parser rejected this query on this backend/version.',
|
|
1358
|
+
'Submit exactly one SELECT statement (no semicolons, no multi-statement scripts).',
|
|
1359
|
+
'Remove ABAP target clauses from SQL text (INTO, APPENDING, PACKAGE SIZE).',
|
|
1360
|
+
];
|
|
1361
|
+
if (/\bJOIN\b/i.test(sql)) {
|
|
1362
|
+
hints.push('JOIN parsing can fail on some systems (SAP Note 3605050); split into staged single-table queries.');
|
|
1363
|
+
}
|
|
1364
|
+
if (/\bINTO\b|\bAPPENDING\b|\bPACKAGE\s+SIZE\b/i.test(sql)) {
|
|
1365
|
+
hints.push('Use the MCP maxRows parameter for row limits instead of ABAP target-table clauses.');
|
|
1366
|
+
}
|
|
1367
|
+
return `${err.message}\n\nHint: ${hints.join(' ')}`;
|
|
1368
|
+
}
|
|
896
1369
|
async function handleSAPQuery(client, args) {
|
|
897
1370
|
const sql = String(args.sql ?? '');
|
|
898
1371
|
const maxRows = Number(args.maxRows ?? 100);
|
|
@@ -921,9 +1394,10 @@ async function handleSAPQuery(client, args) {
|
|
|
921
1394
|
}
|
|
922
1395
|
}
|
|
923
1396
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1397
|
+
if (err instanceof AdtApiError) {
|
|
1398
|
+
const parserHint = classifySapQueryParserError(err, sql);
|
|
1399
|
+
if (parserHint)
|
|
1400
|
+
return errorResult(parserHint);
|
|
927
1401
|
}
|
|
928
1402
|
throw err;
|
|
929
1403
|
}
|
|
@@ -1701,6 +2175,10 @@ function objectUrlForTypeRaw(type, name) {
|
|
|
1701
2175
|
function sourceUrlForType(type, name) {
|
|
1702
2176
|
return `${objectUrlForType(type, name)}/source/main`;
|
|
1703
2177
|
}
|
|
2178
|
+
/** Get a CLAS include URL (definitions/implementations/macros/testclasses) */
|
|
2179
|
+
function classIncludeUrl(name, include) {
|
|
2180
|
+
return `/sap/bc/adt/oo/classes/${encodeURIComponent(name)}/includes/${include}`;
|
|
2181
|
+
}
|
|
1704
2182
|
// ─── SAPWrite Handler ────────────────────────────────────────────────
|
|
1705
2183
|
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
1706
2184
|
const action = String(args.action ?? '');
|
|
@@ -1709,13 +2187,15 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1709
2187
|
const source = String(args.source ?? '');
|
|
1710
2188
|
const transport = args.transport;
|
|
1711
2189
|
const lintOverride = args.lintBeforeWrite;
|
|
2190
|
+
const preflightOverride = args.preflightBeforeWrite;
|
|
2191
|
+
const checkOverride = args.checkBeforeWrite;
|
|
1712
2192
|
// type and name are required for all actions except batch_create
|
|
1713
2193
|
if (action !== 'batch_create' && (!type || !name)) {
|
|
1714
2194
|
return errorResult('"type" and "name" are required for this action.');
|
|
1715
2195
|
}
|
|
1716
2196
|
const objectUrl = objectUrlForType(type, name);
|
|
1717
2197
|
const srcUrl = sourceUrlForType(type, name);
|
|
1718
|
-
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method).
|
|
2198
|
+
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
|
|
1719
2199
|
// Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
|
|
1720
2200
|
async function enforcePackageForExistingObject() {
|
|
1721
2201
|
if (client.safety.allowedPackages.length === 0)
|
|
@@ -1754,6 +2234,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1754
2234
|
cachingLayer?.invalidate(type, name);
|
|
1755
2235
|
return textResult(`Successfully updated ${type} ${name}.`);
|
|
1756
2236
|
}
|
|
2237
|
+
// RAP deterministic preflight validation
|
|
2238
|
+
const preflightWarnings = runRapPreflightValidation(source, type, name, cachedFeatures, config.systemType, preflightOverride);
|
|
2239
|
+
if (preflightWarnings.blocked)
|
|
2240
|
+
return preflightWarnings.result;
|
|
1757
2241
|
// CDS pre-write validation: reject unsupported syntax early
|
|
1758
2242
|
const cdsGuardUpdate = guardCdsSyntax(type, source, cachedFeatures);
|
|
1759
2243
|
if (cdsGuardUpdate)
|
|
@@ -1762,10 +2246,16 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1762
2246
|
const lintWarnings = runPreWriteLint(source, type, name, config, lintOverride);
|
|
1763
2247
|
if (lintWarnings.blocked)
|
|
1764
2248
|
return lintWarnings.result;
|
|
2249
|
+
// Pre-write server-side syntax check (opt-in; never blocks — warnings only).
|
|
2250
|
+
const checkNotes = await runPreWriteSyntaxCheck(client, type, source, objectUrl, config, checkOverride);
|
|
2251
|
+
// If safeUpdateSource throws (lock conflict, network error, etc.), checkNotes
|
|
2252
|
+
// is intentionally discarded — pre-check warnings only matter when the write succeeded.
|
|
1765
2253
|
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
|
|
1766
2254
|
cachingLayer?.invalidate(type, name);
|
|
1767
2255
|
const msg = `Successfully updated ${type} ${name}.`;
|
|
1768
|
-
|
|
2256
|
+
const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
|
|
2257
|
+
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint);
|
|
2258
|
+
return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
|
|
1769
2259
|
}
|
|
1770
2260
|
case 'create': {
|
|
1771
2261
|
const pkg = String(args.package ?? '$TMP');
|
|
@@ -1809,6 +2299,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1809
2299
|
const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
|
|
1810
2300
|
if (cdsGuard)
|
|
1811
2301
|
return cdsGuard;
|
|
2302
|
+
// RAP deterministic preflight validation (before object creation to avoid stubs)
|
|
2303
|
+
const preflightWarnings = runRapPreflightValidation(source, type, name, cachedFeatures, config.systemType, preflightOverride);
|
|
2304
|
+
if (preflightWarnings.blocked)
|
|
2305
|
+
return preflightWarnings.result;
|
|
1812
2306
|
// AFF header validation (if schema available for this type)
|
|
1813
2307
|
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
1814
2308
|
if (!affResult.valid) {
|
|
@@ -1925,7 +2419,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1925
2419
|
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
|
|
1926
2420
|
cachingLayer?.invalidate(type, name);
|
|
1927
2421
|
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
1928
|
-
|
|
2422
|
+
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings);
|
|
2423
|
+
return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
|
|
1929
2424
|
}
|
|
1930
2425
|
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
|
|
1931
2426
|
}
|
|
@@ -1955,31 +2450,229 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1955
2450
|
const lintWarnings = runPreWriteLint(spliced.newSource, type, name, config, lintOverride);
|
|
1956
2451
|
if (lintWarnings.blocked)
|
|
1957
2452
|
return lintWarnings.result;
|
|
2453
|
+
// Pre-write server-side syntax check on the full spliced source (opt-in; warnings only).
|
|
2454
|
+
const checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
|
|
1958
2455
|
// Write the full source back (existing lock/modify/unlock flow)
|
|
1959
2456
|
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport);
|
|
1960
2457
|
cachingLayer?.invalidate(type, name);
|
|
1961
2458
|
const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
//
|
|
2459
|
+
const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
|
|
2460
|
+
return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
|
|
2461
|
+
}
|
|
2462
|
+
case 'scaffold_rap_handlers': {
|
|
2463
|
+
// What this action does:
|
|
2464
|
+
// Given a behavior-pool class (ZBP_*) and its interface BDEF, inspect
|
|
2465
|
+
// the class for every `lhc_<alias>` local handler class and make
|
|
2466
|
+
// sure it declares a METHOD for every action / determination /
|
|
2467
|
+
// validation / authorization master the BDEF requires. When autoApply
|
|
2468
|
+
// is true, missing METHODS signatures plus empty METHOD stubs are
|
|
2469
|
+
// inserted directly and the class is saved.
|
|
2470
|
+
//
|
|
2471
|
+
// Why this exists:
|
|
2472
|
+
// Without it, the LLM agent trying to author a RAP behavior pool has
|
|
2473
|
+
// to manually read the BDEF, compute the required handler signatures,
|
|
2474
|
+
// paste them into the correct local class, and then save — a
|
|
2475
|
+
// boilerplate-heavy step that is easy to get wrong (alias case,
|
|
2476
|
+
// RESULT vs no RESULT, factory/static modifiers). The activation
|
|
2477
|
+
// errors for an incomplete pool are particularly unhelpful. See
|
|
2478
|
+
// docs/plans/completed/rap-onprem-agent-gap-closure.md.
|
|
2479
|
+
if (type !== 'CLAS') {
|
|
2480
|
+
return errorResult('scaffold_rap_handlers is only supported for type=CLAS behavior pool classes.');
|
|
2481
|
+
}
|
|
2482
|
+
const bdefName = String(args.bdefName ?? '').trim();
|
|
2483
|
+
if (!bdefName) {
|
|
2484
|
+
return errorResult('"bdefName" is required for scaffold_rap_handlers (interface behavior definition name).');
|
|
2485
|
+
}
|
|
2486
|
+
const autoApply = Boolean(args.autoApply ?? false);
|
|
2487
|
+
const targetAlias = String(args.targetAlias ?? '')
|
|
2488
|
+
.trim()
|
|
2489
|
+
.toLowerCase();
|
|
2490
|
+
if (autoApply) {
|
|
2491
|
+
await enforcePackageForExistingObject();
|
|
2492
|
+
}
|
|
2493
|
+
// Why scan all three CLAS includes (main, definitions, implementations):
|
|
2494
|
+
// Behavior-pool handler classes CAN live in any of the three, and
|
|
2495
|
+
// which include they occupy depends on how the pool was generated:
|
|
2496
|
+
// - "main" (source/main) — unusual; some hand-written pools put
|
|
2497
|
+
// lhc_* alongside the global class definition
|
|
2498
|
+
// - "definitions" (CCDEF) — the ADT "Create Behavior Impl Class"
|
|
2499
|
+
// wizard default target
|
|
2500
|
+
// - "implementations" (CCIMP) — older SAP templates and every
|
|
2501
|
+
// example under /DMO/* ship the handler classes here
|
|
2502
|
+
// We read all three so the diff (findMissingRapHandlerRequirements)
|
|
2503
|
+
// reflects what's actually declared anywhere in the class, and the
|
|
2504
|
+
// apply flow can fall through main → definitions → implementations.
|
|
2505
|
+
const classStructured = await client.getClassStructured(name);
|
|
2506
|
+
const classMainSource = classStructured.main ?? '';
|
|
2507
|
+
const classDefinitionsSource = classStructured.definitions ?? '';
|
|
2508
|
+
const classImplementationsSource = classStructured.implementations ?? '';
|
|
2509
|
+
const classCombinedSource = [classMainSource, classDefinitionsSource, classImplementationsSource]
|
|
2510
|
+
.filter(Boolean)
|
|
2511
|
+
.join('\n\n');
|
|
2512
|
+
const bdefSource = cachingLayer
|
|
2513
|
+
? (await cachingLayer.getSource('BDEF', bdefName, () => client.getBdef(bdefName))).source
|
|
2514
|
+
: await client.getBdef(bdefName);
|
|
2515
|
+
let requirements = extractRapHandlerRequirements(bdefSource);
|
|
2516
|
+
if (targetAlias) {
|
|
2517
|
+
requirements = requirements.filter((req) => req.entityAlias.toLowerCase() === targetAlias);
|
|
2518
|
+
}
|
|
2519
|
+
if (requirements.length === 0) {
|
|
2520
|
+
const allAliases = Array.from(new Set(extractRapHandlerRequirements(bdefSource).map((req) => req.entityAlias)));
|
|
2521
|
+
const aliasHint = targetAlias && allAliases.length > 0
|
|
2522
|
+
? ` Available aliases in ${bdefName}: ${allAliases.join(', ')}.`
|
|
2523
|
+
: ' No RAP action/determination/validation/auth handler declarations were found in the BDEF source.';
|
|
2524
|
+
return errorResult(`No RAP handler requirements were found for the requested scope.${aliasHint}`);
|
|
2525
|
+
}
|
|
2526
|
+
const missing = findMissingRapHandlerRequirements(requirements, classCombinedSource);
|
|
2527
|
+
const missingImplementationStubs = findMissingRapHandlerImplementationStubs(requirements, classCombinedSource);
|
|
2528
|
+
const summary = {
|
|
2529
|
+
className: name,
|
|
2530
|
+
bdefName,
|
|
2531
|
+
targetAlias: targetAlias || undefined,
|
|
2532
|
+
scannedSections: [
|
|
2533
|
+
'main',
|
|
2534
|
+
classDefinitionsSource ? 'definitions' : undefined,
|
|
2535
|
+
classImplementationsSource ? 'implementations' : undefined,
|
|
2536
|
+
].filter(Boolean),
|
|
2537
|
+
requiredCount: requirements.length,
|
|
2538
|
+
missingCount: missing.length,
|
|
2539
|
+
missing,
|
|
2540
|
+
missingImplementationStubCount: missingImplementationStubs.length,
|
|
2541
|
+
missingImplementationStubs,
|
|
2542
|
+
};
|
|
2543
|
+
if (!autoApply || (missing.length === 0 && missingImplementationStubs.length === 0)) {
|
|
2544
|
+
return textResult(JSON.stringify({ ...summary, applied: false }, null, 2));
|
|
2545
|
+
}
|
|
2546
|
+
// Pure RAP transformation planning lives in rap-handlers.ts. Keep this
|
|
2547
|
+
// handler focused on MCP/ADT concerns: safety, linting, locking, writes.
|
|
2548
|
+
const scaffoldPlan = applyRapHandlerScaffold({
|
|
2549
|
+
main: classMainSource,
|
|
2550
|
+
definitions: classDefinitionsSource || undefined,
|
|
2551
|
+
implementations: classImplementationsSource || undefined,
|
|
2552
|
+
}, missing, missingImplementationStubs);
|
|
2553
|
+
if (scaffoldPlan.changedSections.length === 0) {
|
|
2554
|
+
const unresolvedHandlerClasses = Array.from(new Set(scaffoldPlan.unresolved.map((req) => req.targetHandlerClass)));
|
|
2555
|
+
const unresolvedHint = unresolvedHandlerClasses.length > 0
|
|
2556
|
+
? `No source changes were applied because handler class skeleton(s) ${unresolvedHandlerClasses.join(', ')} were not found in main, definitions, or implementations. Create the local handler class skeleton(s) first (for example with the ADT quick fix "Create local handler class"), then rerun with autoApply=true.`
|
|
2557
|
+
: undefined;
|
|
2558
|
+
return textResult(JSON.stringify({
|
|
2559
|
+
...summary,
|
|
2560
|
+
applied: false,
|
|
2561
|
+
hint: unresolvedHint,
|
|
2562
|
+
applyResult: {
|
|
2563
|
+
main: scaffoldPlan.signatures.main,
|
|
2564
|
+
definitions: scaffoldPlan.signatures.definitions,
|
|
2565
|
+
implementations: scaffoldPlan.signatures.implementations,
|
|
2566
|
+
implementationStubs: scaffoldPlan.implementationStubs,
|
|
2567
|
+
unresolved: scaffoldPlan.unresolved,
|
|
2568
|
+
},
|
|
2569
|
+
}, null, 2));
|
|
2570
|
+
}
|
|
2571
|
+
const finalMainSource = scaffoldPlan.sections.main;
|
|
2572
|
+
const finalDefinitionsSource = scaffoldPlan.sections.definitions;
|
|
2573
|
+
const finalImplementationsSource = scaffoldPlan.sections.implementations;
|
|
2574
|
+
const { changed } = scaffoldPlan;
|
|
2575
|
+
// Run lint for every section we are about to update; block before any write to avoid partial state.
|
|
2576
|
+
let lintWarningsMain;
|
|
2577
|
+
if (changed.main) {
|
|
2578
|
+
lintWarningsMain = runPreWriteLint(finalMainSource, type, name, config, lintOverride);
|
|
2579
|
+
if (lintWarningsMain.blocked)
|
|
2580
|
+
return lintWarningsMain.result;
|
|
2581
|
+
}
|
|
2582
|
+
let lintWarningsDefinitions;
|
|
2583
|
+
if (changed.definitions && finalDefinitionsSource) {
|
|
2584
|
+
lintWarningsDefinitions = runPreWriteLint(finalDefinitionsSource, type, name, config, lintOverride);
|
|
2585
|
+
if (lintWarningsDefinitions.blocked)
|
|
2586
|
+
return lintWarningsDefinitions.result;
|
|
2587
|
+
}
|
|
2588
|
+
let lintWarningsImplementations;
|
|
2589
|
+
if (changed.implementations && finalImplementationsSource) {
|
|
2590
|
+
lintWarningsImplementations = runPreWriteLint(finalImplementationsSource, type, name, config, lintOverride);
|
|
2591
|
+
if (lintWarningsImplementations.blocked)
|
|
2592
|
+
return lintWarningsImplementations.result;
|
|
2593
|
+
}
|
|
2594
|
+
// All modified includes share one lock so we never end up in a partial-state
|
|
2595
|
+
// (e.g. main written, implementations errored → handler class declares but
|
|
2596
|
+
// doesn't implement methods → class cannot activate). The lock is taken once
|
|
2597
|
+
// at the class object URL, and every include PUT carries the same lockHandle.
|
|
2598
|
+
// This mirrors how ADT-in-Eclipse saves a multi-include class in one commit.
|
|
1967
2599
|
await client.http.withStatefulSession(async (session) => {
|
|
1968
2600
|
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1969
2601
|
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
1970
2602
|
try {
|
|
1971
|
-
|
|
2603
|
+
if (changed.main) {
|
|
2604
|
+
await updateSource(session, client.safety, srcUrl, finalMainSource, lock.lockHandle, effectiveTransport);
|
|
2605
|
+
}
|
|
2606
|
+
if (changed.definitions && finalDefinitionsSource) {
|
|
2607
|
+
await updateSource(session, client.safety, classIncludeUrl(name, 'definitions'), finalDefinitionsSource, lock.lockHandle, effectiveTransport);
|
|
2608
|
+
}
|
|
2609
|
+
if (changed.implementations && finalImplementationsSource) {
|
|
2610
|
+
await updateSource(session, client.safety, classIncludeUrl(name, 'implementations'), finalImplementationsSource, lock.lockHandle, effectiveTransport);
|
|
2611
|
+
}
|
|
1972
2612
|
}
|
|
1973
2613
|
finally {
|
|
2614
|
+
// Best-effort unlock — if the object was already removed or the session
|
|
2615
|
+
// expired, we still want to surface the original error instead of masking
|
|
2616
|
+
// it with an unlock failure.
|
|
1974
2617
|
try {
|
|
1975
2618
|
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
1976
2619
|
}
|
|
1977
2620
|
catch {
|
|
1978
|
-
//
|
|
2621
|
+
// Swallowed intentionally; see comment above.
|
|
1979
2622
|
}
|
|
1980
2623
|
}
|
|
1981
2624
|
});
|
|
1982
2625
|
cachingLayer?.invalidate(type, name);
|
|
2626
|
+
const msg = `Scaffolded ${scaffoldPlan.insertedSignatureCount} RAP handler signature(s) and ${scaffoldPlan.insertedImplementationStubCount} implementation stub(s) in ${type} ${name} from BDEF ${bdefName}. ` +
|
|
2627
|
+
`Updated section(s): ${scaffoldPlan.changedSections.join(', ')}.`;
|
|
2628
|
+
const warnings = mergePreWriteWarnings(lintWarningsMain?.warnings, lintWarningsDefinitions?.warnings, lintWarningsImplementations?.warnings);
|
|
2629
|
+
const details = JSON.stringify({
|
|
2630
|
+
...summary,
|
|
2631
|
+
applied: true,
|
|
2632
|
+
applyResult: {
|
|
2633
|
+
main: scaffoldPlan.signatures.main,
|
|
2634
|
+
definitions: scaffoldPlan.signatures.definitions,
|
|
2635
|
+
implementations: scaffoldPlan.signatures.implementations,
|
|
2636
|
+
implementationStubs: scaffoldPlan.implementationStubs,
|
|
2637
|
+
unresolved: scaffoldPlan.unresolved,
|
|
2638
|
+
},
|
|
2639
|
+
}, null, 2);
|
|
2640
|
+
return warnings ? textResult(`${msg}\n\n${warnings}\n\n${details}`) : textResult(`${msg}\n\n${details}`);
|
|
2641
|
+
}
|
|
2642
|
+
case 'delete': {
|
|
2643
|
+
await enforcePackageForExistingObject();
|
|
2644
|
+
// Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
|
|
2645
|
+
try {
|
|
2646
|
+
await client.http.withStatefulSession(async (session) => {
|
|
2647
|
+
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2648
|
+
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
2649
|
+
try {
|
|
2650
|
+
await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
|
|
2651
|
+
}
|
|
2652
|
+
finally {
|
|
2653
|
+
try {
|
|
2654
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
2655
|
+
}
|
|
2656
|
+
catch {
|
|
2657
|
+
// Object may already be deleted — unlock failure is expected
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
catch (err) {
|
|
2663
|
+
if (err instanceof AdtApiError && CDS_DEPENDENCY_SENSITIVE_TYPES.has(type) && isDeleteDependencyError(err)) {
|
|
2664
|
+
const hint = await buildCdsDeleteDependencyHint(client, type, name, objectUrl);
|
|
2665
|
+
if (hint) {
|
|
2666
|
+
// Attach via extraHint so the LLM-facing formatter renders it after
|
|
2667
|
+
// DDIC diagnostics ("what happened → diagnostics → how to fix").
|
|
2668
|
+
// Mutating err.message would surface the hint before diagnostics and
|
|
2669
|
+
// leak into any other consumer of the same error instance.
|
|
2670
|
+
err.extraHint = hint;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
throw err;
|
|
2674
|
+
}
|
|
2675
|
+
cachingLayer?.invalidate(type, name);
|
|
1983
2676
|
return textResult(`Deleted ${type} ${name}.`);
|
|
1984
2677
|
}
|
|
1985
2678
|
case 'batch_create': {
|
|
@@ -2022,6 +2715,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2022
2715
|
}
|
|
2023
2716
|
}
|
|
2024
2717
|
const results = [];
|
|
2718
|
+
const batchWarnings = [];
|
|
2025
2719
|
for (const obj of objects) {
|
|
2026
2720
|
const objType = normalizeObjectType(String(obj.type ?? ''));
|
|
2027
2721
|
const objName = String(obj.name ?? '');
|
|
@@ -2043,6 +2737,19 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2043
2737
|
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects.
|
|
2044
2738
|
// Metadata objects (DOMA/DTEL) are XML-only and intentionally skip source lint.
|
|
2045
2739
|
if (!metadataObject && objSource) {
|
|
2740
|
+
const preflightWarnings = runRapPreflightValidation(objSource, objType, objName, cachedFeatures, config.systemType, preflightOverride);
|
|
2741
|
+
if (preflightWarnings.blocked) {
|
|
2742
|
+
results.push({
|
|
2743
|
+
type: objType,
|
|
2744
|
+
name: objName,
|
|
2745
|
+
status: 'failed',
|
|
2746
|
+
error: preflightWarnings.result.content[0].text,
|
|
2747
|
+
});
|
|
2748
|
+
break;
|
|
2749
|
+
}
|
|
2750
|
+
if (preflightWarnings.warnings) {
|
|
2751
|
+
batchWarnings.push(`${objType} ${objName}: ${preflightWarnings.warnings}`);
|
|
2752
|
+
}
|
|
2046
2753
|
const lintWarnings = runPreWriteLint(objSource, objType, objName, config, lintOverride);
|
|
2047
2754
|
if (lintWarnings.blocked) {
|
|
2048
2755
|
results.push({
|
|
@@ -2130,18 +2837,59 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2130
2837
|
.join(', ');
|
|
2131
2838
|
const successCount = results.filter((r) => r.status === 'success').length;
|
|
2132
2839
|
const hasFailure = results.some((r) => r.status === 'failed');
|
|
2840
|
+
const warningSuffix = batchWarnings.length > 0 ? `\n\nRAP preflight warnings:\n- ${batchWarnings.join('\n- ')}` : '';
|
|
2133
2841
|
if (hasFailure) {
|
|
2134
2842
|
const cleanupHint = successCount > 0
|
|
2135
2843
|
? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
|
|
2136
2844
|
: '';
|
|
2137
|
-
return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}`);
|
|
2845
|
+
return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}${warningSuffix}`);
|
|
2138
2846
|
}
|
|
2139
|
-
return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}`);
|
|
2847
|
+
return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}${warningSuffix}`);
|
|
2140
2848
|
}
|
|
2141
2849
|
default:
|
|
2142
|
-
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create`);
|
|
2850
|
+
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create, scaffold_rap_handlers`);
|
|
2143
2851
|
}
|
|
2144
2852
|
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Run deterministic RAP preflight checks for non-ABAP RAP artifact types.
|
|
2855
|
+
*
|
|
2856
|
+
* Unlike lint, this check is intentionally narrow and rule-based. It focuses on
|
|
2857
|
+
* known activation churn patterns (TABL curr/quan semantics, BDEF enum/header
|
|
2858
|
+
* misuse, DDLX scope/duplicate annotations) and can cover types that offline
|
|
2859
|
+
* abaplint does not parse well.
|
|
2860
|
+
*/
|
|
2861
|
+
function runRapPreflightValidation(source, type, name, features, configSystemType, perCallOverride) {
|
|
2862
|
+
const enabled = perCallOverride ?? true;
|
|
2863
|
+
if (!enabled || !source) {
|
|
2864
|
+
return { blocked: false };
|
|
2865
|
+
}
|
|
2866
|
+
const systemType = features?.systemType ?? (configSystemType !== 'auto' ? configSystemType : undefined);
|
|
2867
|
+
const result = validateRapSource(type, source, {
|
|
2868
|
+
systemType,
|
|
2869
|
+
abapRelease: features?.abapRelease,
|
|
2870
|
+
});
|
|
2871
|
+
if (result.errors.length > 0) {
|
|
2872
|
+
const details = formatRapPreflightFindings(result.errors);
|
|
2873
|
+
return {
|
|
2874
|
+
blocked: true,
|
|
2875
|
+
result: errorResult(`RAP preflight validation failed for ${type} ${name}. Fix these issues before writing:\n${details}\n\n` +
|
|
2876
|
+
'Set preflightBeforeWrite=false only when you intentionally need to bypass these checks.'),
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
if (result.warnings.length > 0) {
|
|
2880
|
+
return {
|
|
2881
|
+
blocked: false,
|
|
2882
|
+
warnings: `RAP preflight warnings:\n${formatRapPreflightFindings(result.warnings)}`,
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
return { blocked: false };
|
|
2886
|
+
}
|
|
2887
|
+
function mergePreWriteWarnings(...warnings) {
|
|
2888
|
+
const parts = warnings.filter((w) => Boolean(w));
|
|
2889
|
+
if (parts.length === 0)
|
|
2890
|
+
return undefined;
|
|
2891
|
+
return parts.join('\n\n');
|
|
2892
|
+
}
|
|
2145
2893
|
/**
|
|
2146
2894
|
* Run pre-write lint validation on source code.
|
|
2147
2895
|
*
|
|
@@ -2203,6 +2951,56 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
|
|
|
2203
2951
|
return { blocked: false };
|
|
2204
2952
|
}
|
|
2205
2953
|
}
|
|
2954
|
+
/** Types that carry source code that SAP's /checkruns endpoint can meaningfully compile.
|
|
2955
|
+
* Metadata-write types (DOMA/DTEL/TABL/STRU/MSAG/DEVC/SKTD) have no /source/main artifact. */
|
|
2956
|
+
const SYNTAX_CHECKABLE_TYPES = new Set([
|
|
2957
|
+
'PROG',
|
|
2958
|
+
'CLAS',
|
|
2959
|
+
'INTF',
|
|
2960
|
+
'FUNC',
|
|
2961
|
+
'FUGR',
|
|
2962
|
+
'INCL',
|
|
2963
|
+
'DDLS',
|
|
2964
|
+
'DCLS',
|
|
2965
|
+
'DDLX',
|
|
2966
|
+
'BDEF',
|
|
2967
|
+
'SRVD',
|
|
2968
|
+
]);
|
|
2969
|
+
/** Pre-write SAP server-side syntax check via /checkruns with inline <chkrun:content>.
|
|
2970
|
+
* Sends the proposed source to SAP's compiler without writing. Surfaces errors AND
|
|
2971
|
+
* warnings as informational text appended to the write's success message — never
|
|
2972
|
+
* blocks the write. Rationale: multi-file edits have inter-object dependencies, so
|
|
2973
|
+
* intermediate writes legitimately trip compile errors that resolve once the whole
|
|
2974
|
+
* sequence lands. Real blocking is deferred to SAPActivate, which runs after all
|
|
2975
|
+
* dependencies are in place. Best-effort: network/endpoint failures return ''. */
|
|
2976
|
+
async function runPreWriteSyntaxCheck(client, type, source, objectUrl, config, perCallOverride) {
|
|
2977
|
+
const enabled = perCallOverride ?? config.checkBeforeWrite;
|
|
2978
|
+
if (!enabled || !source)
|
|
2979
|
+
return '';
|
|
2980
|
+
if (!SYNTAX_CHECKABLE_TYPES.has(type.toUpperCase()))
|
|
2981
|
+
return '';
|
|
2982
|
+
try {
|
|
2983
|
+
const result = await syntaxCheck(client.http, client.safety, objectUrl, { content: source, version: 'active' });
|
|
2984
|
+
if (result.messages.length === 0)
|
|
2985
|
+
return '';
|
|
2986
|
+
const errors = result.messages.filter((m) => m.severity === 'error');
|
|
2987
|
+
const warnings = result.messages.filter((m) => m.severity === 'warning');
|
|
2988
|
+
const parts = [];
|
|
2989
|
+
if (errors.length > 0) {
|
|
2990
|
+
const lines = errors.map((m) => ` Line ${m.line || '?'}${m.column ? `:${m.column}` : ''}: ${m.text}`).join('\n');
|
|
2991
|
+
parts.push(`Server syntax check errors (source was still written — activate to confirm whether these resolve once dependencies are in place):\n${lines}`);
|
|
2992
|
+
}
|
|
2993
|
+
if (warnings.length > 0) {
|
|
2994
|
+
const lines = warnings.map((m) => ` Line ${m.line || '?'}: ${m.text}`).join('\n');
|
|
2995
|
+
parts.push(`Server syntax check warnings:\n${lines}`);
|
|
2996
|
+
}
|
|
2997
|
+
return parts.join('\n\n');
|
|
2998
|
+
}
|
|
2999
|
+
catch {
|
|
3000
|
+
// Best-effort: never let a failing pre-check fail the write.
|
|
3001
|
+
return '';
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
2206
3004
|
// ─── SAPActivate Handler ─────────────────────────────────────────────
|
|
2207
3005
|
async function handleSAPActivate(client, args) {
|
|
2208
3006
|
const action = String(args.action ?? 'activate');
|
|
@@ -2299,25 +3097,45 @@ async function handleSAPActivate(client, args) {
|
|
|
2299
3097
|
const preaudit = args.preaudit !== undefined ? Boolean(args.preaudit) : undefined;
|
|
2300
3098
|
const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
|
|
2301
3099
|
if (args.objects && Array.isArray(args.objects)) {
|
|
2302
|
-
const
|
|
3100
|
+
const rawObjects = args.objects;
|
|
3101
|
+
const objects = rawObjects.map((o) => {
|
|
2303
3102
|
const objType = normalizeObjectType(String(o.type ?? type));
|
|
2304
3103
|
const objName = String(o.name ?? '');
|
|
2305
|
-
return {
|
|
3104
|
+
return { type: objType, name: objName, url: objectUrlForType(objType, objName) };
|
|
2306
3105
|
});
|
|
2307
3106
|
const result = await activateBatch(client.http, client.safety, objects, activateOpts);
|
|
2308
3107
|
const names = objects.map((o) => o.name).join(', ');
|
|
3108
|
+
const batchStatuses = buildBatchActivationStatuses(objects, result);
|
|
3109
|
+
const statusDetails = formatBatchActivationStatuses(batchStatuses);
|
|
2309
3110
|
if (result.success) {
|
|
2310
|
-
return textResult(`Successfully activated ${objects.length} objects: ${names}.${
|
|
2311
|
-
}
|
|
2312
|
-
|
|
3111
|
+
return textResult(`Successfully activated ${objects.length} objects: ${names}.${statusDetails}`);
|
|
3112
|
+
}
|
|
3113
|
+
// On batch failure enrich with per-object inactive-version syntax errors —
|
|
3114
|
+
// only for objects whose activation returned no error details, to avoid duplicating messages.
|
|
3115
|
+
const objectsNeedingSyntaxCheck = objects.filter((_o, i) => batchStatuses[i].status !== 'error');
|
|
3116
|
+
const diagnostics = await Promise.all(objectsNeedingSyntaxCheck.map((o) => inactiveSyntaxDiagnostic(client, o.type, o.name)));
|
|
3117
|
+
const combinedDiag = diagnostics
|
|
3118
|
+
.map((d, i) => (d ? `\n[${objectsNeedingSyntaxCheck[i].name}]${d}` : ''))
|
|
3119
|
+
.filter(Boolean)
|
|
3120
|
+
.join('');
|
|
3121
|
+
return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
|
|
2313
3122
|
}
|
|
2314
3123
|
// Single activation (existing behavior)
|
|
2315
3124
|
const objectUrl = objectUrlForType(type, name);
|
|
2316
|
-
const result = await activate(client.http, client.safety, objectUrl, activateOpts);
|
|
3125
|
+
const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
|
|
2317
3126
|
if (result.success) {
|
|
2318
3127
|
return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
|
|
2319
3128
|
}
|
|
2320
|
-
|
|
3129
|
+
// On failure, try to enrich with the actual compiler errors from the inactive version —
|
|
3130
|
+
// especially useful when SAP returned <ioc:inactiveObjects> with no <msg> detail.
|
|
3131
|
+
// Skip when activation already returned error details to avoid duplicating the same messages.
|
|
3132
|
+
const hasActivationErrors = result.details.some((d) => d.severity === 'error');
|
|
3133
|
+
const syntaxDetail = hasActivationErrors ? '' : await inactiveSyntaxDiagnostic(client, type, name);
|
|
3134
|
+
let activationError = `Activation failed for ${type} ${name}.\n${formatActivationMessages(result)}${syntaxDetail}`;
|
|
3135
|
+
if (type === 'DDLS') {
|
|
3136
|
+
activationError += `\n\n${await buildCdsActivationDependencyHint(client, name, objectUrl)}`;
|
|
3137
|
+
}
|
|
3138
|
+
return errorResult(activationError);
|
|
2321
3139
|
}
|
|
2322
3140
|
/** Format activation result messages with structured detail (line numbers, URIs) when available */
|
|
2323
3141
|
function formatActivationMessages(result) {
|
|
@@ -2347,6 +3165,67 @@ function formatActivationMessages(result) {
|
|
|
2347
3165
|
}
|
|
2348
3166
|
return parts.length > 0 ? `\n${parts.join('\n')}` : '';
|
|
2349
3167
|
}
|
|
3168
|
+
function normalizeActivationUri(uri) {
|
|
3169
|
+
if (!uri)
|
|
3170
|
+
return undefined;
|
|
3171
|
+
return uri.replace(/#.*$/, '').replace(/\/+$/, '').toLowerCase();
|
|
3172
|
+
}
|
|
3173
|
+
function buildBatchActivationStatuses(objects, result) {
|
|
3174
|
+
// Group error details by object. SAP error URIs may be subpaths of the object URL
|
|
3175
|
+
// (e.g. .../classes/zcl_demo/source/main for object .../classes/ZCL_DEMO) and may
|
|
3176
|
+
// differ in case, so we lowercase and use startsWith for matching.
|
|
3177
|
+
const objectKeys = objects.map((obj) => normalizeActivationUri(obj.url) ?? '');
|
|
3178
|
+
const perObject = objects.map(() => []);
|
|
3179
|
+
const unassigned = [];
|
|
3180
|
+
for (const detail of result.details) {
|
|
3181
|
+
const detailUri = normalizeActivationUri(detail.uri);
|
|
3182
|
+
const prefix = detail.line ? `[line ${detail.line}] ` : '';
|
|
3183
|
+
const suffix = detail.uri ? ` (${detail.uri})` : '';
|
|
3184
|
+
if (!detailUri) {
|
|
3185
|
+
unassigned.push(`${prefix}${detail.text}${suffix}`);
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
const matchIdx = objectKeys.findIndex((k) => k && detailUri.startsWith(k));
|
|
3189
|
+
if (matchIdx >= 0) {
|
|
3190
|
+
perObject[matchIdx].push({ severity: detail.severity, text: `${prefix}${detail.text}${suffix}` });
|
|
3191
|
+
}
|
|
3192
|
+
else {
|
|
3193
|
+
unassigned.push(`${prefix}${detail.text}${suffix}`);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
return objects.map((obj, index) => {
|
|
3197
|
+
const details = perObject[index];
|
|
3198
|
+
const hasError = details.some((detail) => detail.severity === 'error');
|
|
3199
|
+
const hasWarning = details.some((detail) => detail.severity === 'warning');
|
|
3200
|
+
const status = hasError ? 'error' : hasWarning ? 'warning' : 'active';
|
|
3201
|
+
const messages = details.map((detail) => detail.text);
|
|
3202
|
+
if (index === 0 && unassigned.length > 0) {
|
|
3203
|
+
messages.push(...unassigned);
|
|
3204
|
+
}
|
|
3205
|
+
return {
|
|
3206
|
+
type: obj.type,
|
|
3207
|
+
name: obj.name,
|
|
3208
|
+
status,
|
|
3209
|
+
messages,
|
|
3210
|
+
};
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
function formatBatchActivationStatuses(statuses) {
|
|
3214
|
+
if (statuses.length === 0)
|
|
3215
|
+
return '';
|
|
3216
|
+
const lines = [];
|
|
3217
|
+
for (const status of statuses) {
|
|
3218
|
+
if (status.messages.length === 0) {
|
|
3219
|
+
lines.push(`- ${status.name} (${status.type}): ${status.status}`);
|
|
3220
|
+
}
|
|
3221
|
+
else {
|
|
3222
|
+
for (const msg of status.messages) {
|
|
3223
|
+
lines.push(`- ${status.name} (${status.type}) ${msg}`);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
return `\n${lines.join('\n')}`;
|
|
3228
|
+
}
|
|
2350
3229
|
// ─── SAPNavigate Handler ─────────────────────────────────────────────
|
|
2351
3230
|
async function handleSAPNavigate(client, args) {
|
|
2352
3231
|
const action = String(args.action ?? '');
|
|
@@ -2437,7 +3316,8 @@ async function handleSAPNavigate(client, args) {
|
|
|
2437
3316
|
const canQuery = isOperationAllowed(client.safety, OperationType.Query);
|
|
2438
3317
|
if (!canFreeSQL && !canQuery) {
|
|
2439
3318
|
return errorResult('Class hierarchy requires data access permissions. ' +
|
|
2440
|
-
'Enable free SQL (--
|
|
3319
|
+
'Enable free SQL (SAP_ALLOW_FREE_SQL=true / --allow-free-sql=true) or table preview ' +
|
|
3320
|
+
'(SAP_ALLOW_DATA_PREVIEW=true / --allow-data-preview=true), and grant the matching sql/data scope in HTTP auth mode.');
|
|
2441
3321
|
}
|
|
2442
3322
|
try {
|
|
2443
3323
|
let ownRels;
|
|
@@ -2490,7 +3370,14 @@ async function handleSAPDiagnose(client, args) {
|
|
|
2490
3370
|
switch (action) {
|
|
2491
3371
|
case 'syntax': {
|
|
2492
3372
|
const objectUrl = objectUrlForType(type, name);
|
|
2493
|
-
const
|
|
3373
|
+
const version = args.version === 'inactive' ? 'inactive' : args.version === 'active' ? 'active' : undefined;
|
|
3374
|
+
const content = typeof args.source === 'string' ? args.source : undefined;
|
|
3375
|
+
const opts = {};
|
|
3376
|
+
if (version)
|
|
3377
|
+
opts.version = version;
|
|
3378
|
+
if (content !== undefined)
|
|
3379
|
+
opts.content = content;
|
|
3380
|
+
const result = await syntaxCheck(client.http, client.safety, objectUrl, Object.keys(opts).length > 0 ? opts : undefined);
|
|
2494
3381
|
return textResult(JSON.stringify(result, null, 2));
|
|
2495
3382
|
}
|
|
2496
3383
|
case 'unittest': {
|
|
@@ -2553,11 +3440,31 @@ async function handleSAPDiagnose(client, args) {
|
|
|
2553
3440
|
case 'dumps': {
|
|
2554
3441
|
const id = args.id;
|
|
2555
3442
|
if (id) {
|
|
2556
|
-
// Get single dump detail
|
|
2557
3443
|
const detail = await getDump(client.http, client.safety, id);
|
|
2558
|
-
|
|
3444
|
+
const includeFullText = args.includeFullText === true || String(args.includeFullText ?? '') === 'true';
|
|
3445
|
+
const selectedSections = selectDumpSections(detail, args.sections);
|
|
3446
|
+
const payload = {
|
|
3447
|
+
id: detail.id,
|
|
3448
|
+
error: detail.error,
|
|
3449
|
+
exception: detail.exception,
|
|
3450
|
+
program: detail.program,
|
|
3451
|
+
user: detail.user,
|
|
3452
|
+
timestamp: detail.timestamp,
|
|
3453
|
+
chapters: detail.chapters,
|
|
3454
|
+
terminationUri: detail.terminationUri,
|
|
3455
|
+
sections: selectedSections,
|
|
3456
|
+
selectedSectionIds: Object.keys(selectedSections),
|
|
3457
|
+
availableSections: detail.chapters.map((chapter) => ({
|
|
3458
|
+
id: chapter.name,
|
|
3459
|
+
title: chapter.title,
|
|
3460
|
+
line: chapter.line,
|
|
3461
|
+
})),
|
|
3462
|
+
};
|
|
3463
|
+
if (includeFullText) {
|
|
3464
|
+
payload.formattedText = detail.formattedText;
|
|
3465
|
+
}
|
|
3466
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
2559
3467
|
}
|
|
2560
|
-
// List dumps
|
|
2561
3468
|
const user = args.user;
|
|
2562
3469
|
const maxResults = args.maxResults ? Number(args.maxResults) : undefined;
|
|
2563
3470
|
const dumps = await listDumps(client.http, client.safety, { user, maxResults });
|
|
@@ -2589,10 +3496,95 @@ async function handleSAPDiagnose(client, args) {
|
|
|
2589
3496
|
const traces = await listTraces(client.http, client.safety);
|
|
2590
3497
|
return textResult(JSON.stringify(traces, null, 2));
|
|
2591
3498
|
}
|
|
3499
|
+
case 'system_messages': {
|
|
3500
|
+
const user = args.user;
|
|
3501
|
+
const maxResults = args.maxResults ? Number(args.maxResults) : undefined;
|
|
3502
|
+
const from = args.from;
|
|
3503
|
+
const to = args.to;
|
|
3504
|
+
const messages = await listSystemMessages(client.http, client.safety, { user, maxResults, from, to });
|
|
3505
|
+
return textResult(JSON.stringify(messages, null, 2));
|
|
3506
|
+
}
|
|
3507
|
+
case 'gateway_errors': {
|
|
3508
|
+
if (isBtpSystem()) {
|
|
3509
|
+
return errorResult('SAP Gateway error log is not available on BTP ABAP Environment. Use this action on on-prem systems.');
|
|
3510
|
+
}
|
|
3511
|
+
const user = args.user;
|
|
3512
|
+
const maxResults = args.maxResults ? Number(args.maxResults) : undefined;
|
|
3513
|
+
const from = args.from;
|
|
3514
|
+
const to = args.to;
|
|
3515
|
+
const detailUrl = args.detailUrl;
|
|
3516
|
+
const id = args.id;
|
|
3517
|
+
const errorType = args.errorType;
|
|
3518
|
+
if (detailUrl || id) {
|
|
3519
|
+
const detail = await getGatewayErrorDetail(client.http, client.safety, { detailUrl, id, errorType });
|
|
3520
|
+
return textResult(JSON.stringify(detail, null, 2));
|
|
3521
|
+
}
|
|
3522
|
+
const errors = await listGatewayErrors(client.http, client.safety, { user, maxResults, from, to });
|
|
3523
|
+
return textResult(JSON.stringify(errors, null, 2));
|
|
3524
|
+
}
|
|
2592
3525
|
default:
|
|
2593
|
-
return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, quickfix, apply_quickfix, dumps, traces`);
|
|
3526
|
+
return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, quickfix, apply_quickfix, dumps, traces, system_messages, gateway_errors`);
|
|
2594
3527
|
}
|
|
2595
3528
|
}
|
|
3529
|
+
function selectDumpSections(detail, requestedSections) {
|
|
3530
|
+
const availableSections = detail.sections ?? {};
|
|
3531
|
+
const availableIds = Object.keys(availableSections);
|
|
3532
|
+
if (availableIds.length === 0)
|
|
3533
|
+
return {};
|
|
3534
|
+
const requestedIds = resolveRequestedDumpSectionIds(detail, requestedSections);
|
|
3535
|
+
const selectedIds = requestedIds.length > 0 ? requestedIds : pickDefaultDumpSectionIds(detail);
|
|
3536
|
+
const finalIds = selectedIds.length > 0 ? selectedIds : availableIds.slice(0, 5);
|
|
3537
|
+
return Object.fromEntries(finalIds.map((id) => [id, availableSections[id] ?? '']));
|
|
3538
|
+
}
|
|
3539
|
+
function resolveRequestedDumpSectionIds(detail, requestedSections) {
|
|
3540
|
+
if (!Array.isArray(requestedSections))
|
|
3541
|
+
return [];
|
|
3542
|
+
const availableIds = new Set(Object.keys(detail.sections ?? {}));
|
|
3543
|
+
const resolved = requestedSections
|
|
3544
|
+
.map((entry) => resolveDumpSectionId(detail, String(entry ?? '')))
|
|
3545
|
+
.filter((entry) => typeof entry === 'string' && availableIds.has(entry));
|
|
3546
|
+
return Array.from(new Set(resolved));
|
|
3547
|
+
}
|
|
3548
|
+
function resolveDumpSectionId(detail, candidate) {
|
|
3549
|
+
const normalizedCandidate = normalizeDumpSectionKey(candidate);
|
|
3550
|
+
if (!normalizedCandidate)
|
|
3551
|
+
return undefined;
|
|
3552
|
+
const direct = detail.chapters.find((chapter) => normalizeDumpSectionKey(chapter.name) === normalizedCandidate)?.name;
|
|
3553
|
+
if (direct)
|
|
3554
|
+
return direct;
|
|
3555
|
+
const exactTitle = detail.chapters.find((chapter) => normalizeDumpSectionKey(chapter.title) === normalizedCandidate)?.name;
|
|
3556
|
+
if (exactTitle)
|
|
3557
|
+
return exactTitle;
|
|
3558
|
+
const fuzzyTitle = detail.chapters.find((chapter) => normalizeDumpSectionKey(chapter.title).includes(normalizedCandidate))?.name;
|
|
3559
|
+
return fuzzyTitle;
|
|
3560
|
+
}
|
|
3561
|
+
function pickDefaultDumpSectionIds(detail) {
|
|
3562
|
+
const wanted = ['short text', 'what happened', 'error analysis', 'source code extract', 'active calls', 'call stack'];
|
|
3563
|
+
const selected = [];
|
|
3564
|
+
for (const pattern of wanted) {
|
|
3565
|
+
const found = detail.chapters.find((chapter) => normalizeDumpSectionKey(chapter.title).includes(normalizeDumpSectionKey(pattern)) && chapter.name);
|
|
3566
|
+
if (found?.name && !selected.includes(found.name) && detail.sections[found.name]) {
|
|
3567
|
+
selected.push(found.name);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
if (selected.length > 0)
|
|
3571
|
+
return selected;
|
|
3572
|
+
const ordered = [...detail.chapters]
|
|
3573
|
+
.sort((a, b) => {
|
|
3574
|
+
if (a.line !== b.line)
|
|
3575
|
+
return a.line - b.line;
|
|
3576
|
+
return a.chapterOrder - b.chapterOrder;
|
|
3577
|
+
})
|
|
3578
|
+
.map((chapter) => chapter.name)
|
|
3579
|
+
.filter((name) => Boolean(name) && Boolean(detail.sections[name]));
|
|
3580
|
+
return Array.from(new Set(ordered)).slice(0, 5);
|
|
3581
|
+
}
|
|
3582
|
+
function normalizeDumpSectionKey(value) {
|
|
3583
|
+
return value
|
|
3584
|
+
.toLowerCase()
|
|
3585
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
3586
|
+
.trim();
|
|
3587
|
+
}
|
|
2596
3588
|
function resolveSapGitBackend(args) {
|
|
2597
3589
|
const forced = args.backend;
|
|
2598
3590
|
const hasGcts = Boolean(cachedFeatures?.gcts?.available);
|
|
@@ -2619,15 +3611,13 @@ async function loadAbapGitRepo(client, repoId) {
|
|
|
2619
3611
|
}
|
|
2620
3612
|
return repo;
|
|
2621
3613
|
}
|
|
2622
|
-
async function handleSAPGit(client, args,
|
|
3614
|
+
async function handleSAPGit(client, args, _authInfo) {
|
|
3615
|
+
// Scope enforcement happens at handleToolCall level via ACTION_POLICY.
|
|
3616
|
+
// This handler only dispatches action logic.
|
|
2623
3617
|
const action = String(args.action ?? '');
|
|
2624
|
-
|
|
2625
|
-
if (!scope) {
|
|
3618
|
+
if (!getActionPolicy('SAPGit', action)) {
|
|
2626
3619
|
return errorResult(`Unknown SAPGit action: ${action}`);
|
|
2627
3620
|
}
|
|
2628
|
-
if (authInfo && !hasRequiredScope(authInfo, scope)) {
|
|
2629
|
-
return errorResult(`Insufficient scope: '${scope}' required for SAPGit(action="${action}"). Your scopes: [${authInfo.scopes.join(', ')}]`);
|
|
2630
|
-
}
|
|
2631
3621
|
const resolved = resolveSapGitBackend(args);
|
|
2632
3622
|
if (!resolved.backend) {
|
|
2633
3623
|
return errorResult(resolved.error ?? 'Unable to resolve SAPGit backend.');
|
|
@@ -2868,7 +3858,7 @@ async function handleSAPTransport(client, args) {
|
|
|
2868
3858
|
}
|
|
2869
3859
|
case 'check': {
|
|
2870
3860
|
// Check transport requirements for an object/package combination.
|
|
2871
|
-
// Does NOT require
|
|
3861
|
+
// Does NOT require allowTransportWrites — this is a read-only check.
|
|
2872
3862
|
const objectType = String(args.type ?? '');
|
|
2873
3863
|
const objectName = String(args.name ?? '');
|
|
2874
3864
|
const pkg = String(args.package ?? '');
|
|
@@ -2936,6 +3926,15 @@ async function handleSAPTransport(client, args) {
|
|
|
2936
3926
|
}
|
|
2937
3927
|
}
|
|
2938
3928
|
// ─── SAPContext Handler ───────────────────────────────────────────────
|
|
3929
|
+
const DEFAULT_SIBLING_MAX_CANDIDATES = 4;
|
|
3930
|
+
const HARD_MAX_SIBLING_MAX_CANDIDATES = 10;
|
|
3931
|
+
function parseSiblingMaxCandidates(value) {
|
|
3932
|
+
const parsed = Number(value ?? DEFAULT_SIBLING_MAX_CANDIDATES);
|
|
3933
|
+
if (!Number.isFinite(parsed))
|
|
3934
|
+
return DEFAULT_SIBLING_MAX_CANDIDATES;
|
|
3935
|
+
const rounded = Math.trunc(parsed);
|
|
3936
|
+
return Math.min(Math.max(rounded, 1), HARD_MAX_SIBLING_MAX_CANDIDATES);
|
|
3937
|
+
}
|
|
2939
3938
|
async function handleSAPContext(client, args, cachingLayer) {
|
|
2940
3939
|
const action = String(args.action ?? '');
|
|
2941
3940
|
// action="impact" is DDLS-only on the server side — default the type so LLMs
|
|
@@ -2985,8 +3984,12 @@ async function handleSAPContext(client, args, cachingLayer) {
|
|
|
2985
3984
|
const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
2986
3985
|
const upstream = buildCdsUpstream(extractCdsDependencies(ddlSource));
|
|
2987
3986
|
const includeIndirect = args.includeIndirect === true;
|
|
3987
|
+
const siblingCheck = args.siblingCheck !== false;
|
|
3988
|
+
const siblingMaxCandidates = parseSiblingMaxCandidates(args.siblingMaxCandidates);
|
|
2988
3989
|
let downstream = classifyCdsImpact([], { includeIndirect });
|
|
2989
3990
|
const warnings = [];
|
|
3991
|
+
const consistencyHints = [];
|
|
3992
|
+
let siblingExtensionAnalysis;
|
|
2990
3993
|
try {
|
|
2991
3994
|
const whereUsed = await findWhereUsed(client.http, client.safety, objectUrlForType('DDLS', name));
|
|
2992
3995
|
downstream = classifyCdsImpact(whereUsed, { includeIndirect });
|
|
@@ -2999,6 +4002,125 @@ async function handleSAPContext(client, args, cachingLayer) {
|
|
|
2999
4002
|
throw err;
|
|
3000
4003
|
}
|
|
3001
4004
|
}
|
|
4005
|
+
if (siblingCheck && warnings.length === 0) {
|
|
4006
|
+
try {
|
|
4007
|
+
const targetName = name.toUpperCase();
|
|
4008
|
+
const stem = deriveSiblingStem(targetName);
|
|
4009
|
+
// Guard against over-broad sibling searches for short/degenerate stems
|
|
4010
|
+
// (e.g., target "Z1" -> stem "Z" -> searchQuery "Z*" would scan the full Z namespace).
|
|
4011
|
+
if (stem.length < 3) {
|
|
4012
|
+
warnings.push(`Sibling consistency check skipped: derived stem "${stem}" is too short to identify siblings safely.`);
|
|
4013
|
+
}
|
|
4014
|
+
else {
|
|
4015
|
+
const targetMatches = await client.searchObject(targetName, 25);
|
|
4016
|
+
const targetMatch = targetMatches.find((candidate) => normalizeObjectType(candidate.objectType) === 'DDLS' && candidate.objectName.toUpperCase() === targetName);
|
|
4017
|
+
const targetPackageName = targetMatch?.packageName;
|
|
4018
|
+
if (!targetPackageName) {
|
|
4019
|
+
warnings.push(`Sibling consistency check skipped: could not resolve package for DDLS "${targetName}".`);
|
|
4020
|
+
}
|
|
4021
|
+
else {
|
|
4022
|
+
const searchQuery = `${stem}*`;
|
|
4023
|
+
const searchMaxResults = Math.min(100, Math.max(siblingMaxCandidates * 4, siblingMaxCandidates + 4));
|
|
4024
|
+
const siblingCandidates = await client.searchObject(searchQuery, searchMaxResults);
|
|
4025
|
+
const skipped = {
|
|
4026
|
+
self: 0,
|
|
4027
|
+
nonDdls: 0,
|
|
4028
|
+
packageMismatch: 0,
|
|
4029
|
+
nameMismatch: 0,
|
|
4030
|
+
overLimit: 0,
|
|
4031
|
+
};
|
|
4032
|
+
const filteredCandidates = [];
|
|
4033
|
+
const seenNames = new Set();
|
|
4034
|
+
for (const candidate of siblingCandidates) {
|
|
4035
|
+
if (normalizeObjectType(candidate.objectType) !== 'DDLS') {
|
|
4036
|
+
skipped.nonDdls += 1;
|
|
4037
|
+
continue;
|
|
4038
|
+
}
|
|
4039
|
+
const candidateName = candidate.objectName.toUpperCase();
|
|
4040
|
+
if (candidateName === targetName) {
|
|
4041
|
+
skipped.self += 1;
|
|
4042
|
+
continue;
|
|
4043
|
+
}
|
|
4044
|
+
if (candidate.packageName !== targetPackageName) {
|
|
4045
|
+
skipped.packageMismatch += 1;
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
if (!isSiblingNameMatch(targetName, candidateName, stem)) {
|
|
4049
|
+
skipped.nameMismatch += 1;
|
|
4050
|
+
continue;
|
|
4051
|
+
}
|
|
4052
|
+
if (seenNames.has(candidateName)) {
|
|
4053
|
+
continue;
|
|
4054
|
+
}
|
|
4055
|
+
seenNames.add(candidateName);
|
|
4056
|
+
filteredCandidates.push({ name: candidateName, packageName: candidate.packageName });
|
|
4057
|
+
}
|
|
4058
|
+
const selectedCandidates = filteredCandidates.slice(0, siblingMaxCandidates);
|
|
4059
|
+
skipped.overLimit = Math.max(filteredCandidates.length - selectedCandidates.length, 0);
|
|
4060
|
+
const checkedCandidates = [];
|
|
4061
|
+
let skippedWhereUsedCandidates = 0;
|
|
4062
|
+
for (const candidate of selectedCandidates) {
|
|
4063
|
+
try {
|
|
4064
|
+
const siblingWhereUsed = await findWhereUsed(client.http, client.safety, objectUrlForType('DDLS', candidate.name));
|
|
4065
|
+
const siblingDownstream = classifyCdsImpact(siblingWhereUsed, { includeIndirect });
|
|
4066
|
+
checkedCandidates.push({
|
|
4067
|
+
name: candidate.name,
|
|
4068
|
+
packageName: candidate.packageName,
|
|
4069
|
+
metadataExtensions: siblingDownstream.metadataExtensions.length,
|
|
4070
|
+
downstreamTotal: siblingDownstream.summary.total,
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
catch (err) {
|
|
4074
|
+
if (err instanceof AdtApiError && [404, 405, 415, 501].includes(err.statusCode)) {
|
|
4075
|
+
skippedWhereUsedCandidates += 1;
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
throw err;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
if (skippedWhereUsedCandidates > 0) {
|
|
4082
|
+
warnings.push(`Sibling consistency check skipped ${skippedWhereUsedCandidates} candidate(s) due to where-used endpoint errors.`);
|
|
4083
|
+
}
|
|
4084
|
+
const siblingFinding = buildSiblingExtensionFinding({
|
|
4085
|
+
targetName,
|
|
4086
|
+
targetPackageName,
|
|
4087
|
+
stem,
|
|
4088
|
+
targetMetadataExtensions: downstream.metadataExtensions.length,
|
|
4089
|
+
siblings: checkedCandidates,
|
|
4090
|
+
});
|
|
4091
|
+
if (siblingFinding) {
|
|
4092
|
+
consistencyHints.push(siblingFinding.message);
|
|
4093
|
+
}
|
|
4094
|
+
siblingExtensionAnalysis = {
|
|
4095
|
+
enabled: true,
|
|
4096
|
+
stem,
|
|
4097
|
+
searchQuery,
|
|
4098
|
+
includeIndirect,
|
|
4099
|
+
maxCandidates: siblingMaxCandidates,
|
|
4100
|
+
filters: {
|
|
4101
|
+
samePackage: true,
|
|
4102
|
+
siblingStem: stem,
|
|
4103
|
+
},
|
|
4104
|
+
target: {
|
|
4105
|
+
name: targetName,
|
|
4106
|
+
packageName: targetPackageName,
|
|
4107
|
+
metadataExtensions: downstream.metadataExtensions.length,
|
|
4108
|
+
},
|
|
4109
|
+
consideredCandidates: filteredCandidates.length,
|
|
4110
|
+
checkedCandidates,
|
|
4111
|
+
skipped,
|
|
4112
|
+
};
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
catch (err) {
|
|
4117
|
+
logger.debug('Sibling consistency check aborted', {
|
|
4118
|
+
name,
|
|
4119
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4120
|
+
});
|
|
4121
|
+
warnings.push('Sibling consistency check skipped due to search or where-used processing errors.');
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
3002
4124
|
const upstreamCount = upstream.tables.length + upstream.views.length + upstream.associations.length + upstream.compositions.length;
|
|
3003
4125
|
const response = {
|
|
3004
4126
|
name,
|
|
@@ -3010,6 +4132,8 @@ async function handleSAPContext(client, args, cachingLayer) {
|
|
|
3010
4132
|
downstreamTotal: downstream.summary.total,
|
|
3011
4133
|
downstreamDirect: downstream.summary.direct,
|
|
3012
4134
|
},
|
|
4135
|
+
...(consistencyHints.length > 0 ? { consistencyHints } : {}),
|
|
4136
|
+
...(siblingExtensionAnalysis ? { siblingExtensionAnalysis } : {}),
|
|
3013
4137
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
3014
4138
|
};
|
|
3015
4139
|
return textResult(JSON.stringify(response, null, 2));
|
|
@@ -3449,7 +4573,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
3449
4573
|
return textResult(JSON.stringify(probed, null, 2));
|
|
3450
4574
|
}
|
|
3451
4575
|
default:
|
|
3452
|
-
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats, create_package, delete_package, flp_list_catalogs, flp_list_groups, flp_list_tiles, flp_create_catalog, flp_create_group, flp_create_tile, flp_add_tile_to_group, flp_delete_catalog`);
|
|
4576
|
+
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats, create_package, delete_package, change_package, flp_list_catalogs, flp_list_groups, flp_list_tiles, flp_create_catalog, flp_create_group, flp_create_tile, flp_add_tile_to_group, flp_delete_catalog`);
|
|
3453
4577
|
}
|
|
3454
4578
|
}
|
|
3455
4579
|
/** Reset cached features (for testing) */
|