arc-1 0.9.6 → 0.9.7
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 +2 -2
- package/dist/adt/abapgit.d.ts +2 -1
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +2 -2
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +7 -3
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/class-structure.d.ts +176 -0
- package/dist/adt/class-structure.d.ts.map +1 -0
- package/dist/adt/class-structure.js +317 -0
- package/dist/adt/class-structure.js.map +1 -0
- package/dist/adt/client.d.ts +112 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +245 -3
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/crud.d.ts +38 -0
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +73 -1
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/errors.d.ts +2 -2
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +50 -6
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/gcts.d.ts +3 -2
- package/dist/adt/gcts.d.ts.map +1 -1
- package/dist/adt/gcts.js +4 -4
- package/dist/adt/gcts.js.map +1 -1
- package/dist/adt/http.d.ts +18 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +50 -43
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/package-hierarchy.d.ts +67 -0
- package/dist/adt/package-hierarchy.d.ts.map +1 -0
- package/dist/adt/package-hierarchy.js +100 -0
- package/dist/adt/package-hierarchy.js.map +1 -0
- package/dist/adt/release.d.ts +35 -0
- package/dist/adt/release.d.ts.map +1 -0
- package/dist/adt/release.js +48 -0
- package/dist/adt/release.js.map +1 -0
- package/dist/adt/safety.d.ts +39 -3
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +136 -15
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/types.d.ts +74 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +46 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +231 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +12 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/context/grep.d.ts +48 -0
- package/dist/context/grep.d.ts.map +1 -0
- package/dist/context/grep.js +146 -0
- package/dist/context/grep.js.map +1 -0
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +430 -24
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +42 -4
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +85 -9
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +68 -12
- package/dist/handlers/tools.js.map +1 -1
- package/dist/server/server.d.ts +20 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +45 -11
- package/dist/server/server.js.map +1 -1
- package/package.json +1 -1
package/dist/handlers/intent.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
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
13
|
import { buildSiblingExtensionFinding, classifyCdsImpact, deriveSiblingStem, isSiblingNameMatch, } from '../adt/cds-impact.js';
|
|
14
|
+
import { diffMethodSets, extractMethodNameFromClause, findSectionAnchor, insertMethodPair, moveMethodDefinition, removeMethodPair, spliceClassDefinition, spliceMethodSignature, } from '../adt/class-structure.js';
|
|
14
15
|
import { findDefinition, findInterfaceImplementersViaSeoMetaRel, findReferences, findWhereUsed, getCompletion, getWhereUsedScope, } from '../adt/codeintel.js';
|
|
15
|
-
import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, updateSource, } from '../adt/crud.js';
|
|
16
|
+
import { createObject, deleteObject, lockObject, safeUpdateClassInclude, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, updateSource, } from '../adt/crud.js';
|
|
16
17
|
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, decodeKtdText, rewriteKtdText, } from '../adt/ddic-xml.js';
|
|
17
18
|
import { activate, activateBatch, applyFixProposal, getFixProposals, getPrettyPrinterSettings, prettyPrint, publishServiceBinding, runAtcCheck, runUnitTests, setPrettyPrinterSettings, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
18
19
|
import { getDump, getGatewayErrorDetail, getObjectState, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraces, } from '../adt/diagnostics.js';
|
|
@@ -31,6 +32,7 @@ import { getAppInfo } from '../adt/ui5-repository.js';
|
|
|
31
32
|
import { validateAffHeader } from '../aff/validator.js';
|
|
32
33
|
import { extractCdsDependencies, extractCdsElements } from '../context/cds-deps.js';
|
|
33
34
|
import { compressCdsContext, compressContext } from '../context/compressor.js';
|
|
35
|
+
import { grepSource } from '../context/grep.js';
|
|
34
36
|
import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
|
|
35
37
|
import { buildLintConfig, listRulesFromConfig, } from '../lint/config-builder.js';
|
|
36
38
|
import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from '../lint/lint.js';
|
|
@@ -227,7 +229,11 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
227
229
|
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
228
230
|
const enriched = enrichWithSapDetails(err, message);
|
|
229
231
|
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
230
|
-
|
|
232
|
+
// Pass the detected SAP_BASIS release so the 423 lock-handle hint can specialize
|
|
233
|
+
// (< 7.51 → point at abapfs_extensions; see issue #293). cachedFeatures is set by the
|
|
234
|
+
// startup probe; config.abapRelease is the manual SAP_ABAP_RELEASE override fallback.
|
|
235
|
+
const abapRelease = cachedFeatures?.abapRelease ?? config.abapRelease;
|
|
236
|
+
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path, abapRelease);
|
|
231
237
|
if (classification) {
|
|
232
238
|
const transactionLine = classification.transaction ? `\nSAP Transaction: ${classification.transaction}` : '';
|
|
233
239
|
return `${enriched}\n\nHint: ${classification.hint}${transactionLine}`;
|
|
@@ -263,6 +269,14 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
263
269
|
`sqlFilter="MANDT = '100'" or sqlFilter="MATNR LIKE 'Z%'".`);
|
|
264
270
|
}
|
|
265
271
|
}
|
|
272
|
+
if (tool === 'SAPRead' && argType === 'TABLE_QUERY' && err.statusCode === 400) {
|
|
273
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}`;
|
|
274
|
+
if (/is invalid here|due to grammar/i.test(combined)) {
|
|
275
|
+
return (`${enriched}\n\nHint: TABLE_QUERY parser error — check field names match the actual column names ` +
|
|
276
|
+
'exposed by the table/CDS view (use SAPRead(type="DDLS", include="elements") to inspect CDS view fields). ' +
|
|
277
|
+
'Also verify value formats (e.g. FiscalPeriod is C(2,0) so use "01" not "001").');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
266
280
|
const behaviorPoolHint = getBehaviorPoolSaveFailureHint(err, args);
|
|
267
281
|
if (behaviorPoolHint) {
|
|
268
282
|
return `${enriched}\n\nHint: ${behaviorPoolHint}`;
|
|
@@ -1105,6 +1119,11 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1105
1119
|
const indicator = cacheHit && revalidated ? '[cached:revalidated]\n' : '';
|
|
1106
1120
|
return textResult(`${note}${indicator}${source}`);
|
|
1107
1121
|
};
|
|
1122
|
+
/** When args.grep is set, return only matching source lines (+context) instead of full source. */
|
|
1123
|
+
const grepText = (source) => {
|
|
1124
|
+
const g = grepSource(source, String(args.grep));
|
|
1125
|
+
return g.invalidPattern ? errorResult(g.output) : textResult(g.output);
|
|
1126
|
+
};
|
|
1108
1127
|
// Structured format is only supported for CLAS type
|
|
1109
1128
|
if (args.format === 'structured' && type !== 'CLAS') {
|
|
1110
1129
|
return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
|
|
@@ -1112,9 +1131,45 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1112
1131
|
switch (type) {
|
|
1113
1132
|
case 'PROG': {
|
|
1114
1133
|
const { source, cacheHit, revalidated } = await cachedGet('PROG', name, effectiveVersion, (ifNoneMatch) => client.getProgram(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1134
|
+
if (args.grep)
|
|
1135
|
+
return grepText(source);
|
|
1115
1136
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1116
1137
|
}
|
|
1117
1138
|
case 'CLAS': {
|
|
1139
|
+
// grep: return only matching source lines (+context), annotated with the owning class/method.
|
|
1140
|
+
if (args.grep) {
|
|
1141
|
+
if (args.method) {
|
|
1142
|
+
return errorResult('Do not combine grep with method. Use grep to find code, then method="<name>" to read the full method.');
|
|
1143
|
+
}
|
|
1144
|
+
const rawSection = args.include;
|
|
1145
|
+
// 'main' (and the default) live at /source/main, not /includes/main — read via the
|
|
1146
|
+
// cached main path; only real sub-includes go through the raw getClassInclude endpoint.
|
|
1147
|
+
const section = rawSection && rawSection.toLowerCase() !== 'main' ? rawSection : undefined;
|
|
1148
|
+
let clasSource;
|
|
1149
|
+
if (section) {
|
|
1150
|
+
try {
|
|
1151
|
+
clasSource = (await client.getClassInclude(name, section, { version: effectiveVersion })).source;
|
|
1152
|
+
}
|
|
1153
|
+
catch (err) {
|
|
1154
|
+
if (isNotFoundError(err)) {
|
|
1155
|
+
return textResult(`Include "${section}" is not available for class ${name}. Run grep without include= to search the full class source.`);
|
|
1156
|
+
}
|
|
1157
|
+
throw err;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
clasSource = (await cachedGet('CLAS', name, effectiveVersion, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch, version: effectiveVersion }))).source;
|
|
1162
|
+
}
|
|
1163
|
+
const abaplintVer = cachedFeatures?.abapRelease
|
|
1164
|
+
? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
|
|
1165
|
+
: undefined;
|
|
1166
|
+
// MethodInfo is a structural superset of grepSource's MethodRange — pass through directly.
|
|
1167
|
+
const listing = listMethods(clasSource, name, abaplintVer);
|
|
1168
|
+
const g = grepSource(clasSource, String(args.grep), listing.success ? { methods: listing.methods } : undefined);
|
|
1169
|
+
return g.invalidPattern
|
|
1170
|
+
? errorResult(g.output)
|
|
1171
|
+
: textResult(`[${name} section=${rawSection ?? 'main'}]\n${g.output}`);
|
|
1172
|
+
}
|
|
1118
1173
|
// Structured format: return JSON with metadata + decomposed source
|
|
1119
1174
|
if (args.format === 'structured') {
|
|
1120
1175
|
const structured = await client.getClassStructured(name);
|
|
@@ -1149,6 +1204,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1149
1204
|
}
|
|
1150
1205
|
case 'INTF': {
|
|
1151
1206
|
const { source, cacheHit, revalidated } = await cachedGet('INTF', name, effectiveVersion, (ifNoneMatch) => client.getInterface(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1207
|
+
if (args.grep)
|
|
1208
|
+
return grepText(source);
|
|
1152
1209
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1153
1210
|
}
|
|
1154
1211
|
case 'FUNC': {
|
|
@@ -1184,6 +1241,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1184
1241
|
};
|
|
1185
1242
|
return textResult(JSON.stringify(payload, null, 2));
|
|
1186
1243
|
}
|
|
1244
|
+
if (args.grep)
|
|
1245
|
+
return grepText(source);
|
|
1187
1246
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1188
1247
|
}
|
|
1189
1248
|
case 'FUGR': {
|
|
@@ -1211,6 +1270,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1211
1270
|
}
|
|
1212
1271
|
case 'INCL': {
|
|
1213
1272
|
const { source, cacheHit, revalidated } = await cachedGet('INCL', name, effectiveVersion, (ifNoneMatch) => client.getInclude(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1273
|
+
if (args.grep)
|
|
1274
|
+
return grepText(source);
|
|
1214
1275
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1215
1276
|
}
|
|
1216
1277
|
case 'DDLS': {
|
|
@@ -1223,23 +1284,33 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1223
1284
|
// Elements extraction is derived from source — no cache indicator
|
|
1224
1285
|
return cachedTextResult(extractCdsElements(ddlSource, name), false, false, versionWarning);
|
|
1225
1286
|
}
|
|
1287
|
+
if (args.grep)
|
|
1288
|
+
return grepText(ddlSource);
|
|
1226
1289
|
return cachedTextResult(ddlSource, cacheHit, revalidated, versionWarning);
|
|
1227
1290
|
}
|
|
1228
1291
|
case 'DCLS': {
|
|
1229
1292
|
const { source, cacheHit, revalidated } = await cachedGet('DCLS', name, effectiveVersion, (ifNoneMatch) => client.getDcl(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1293
|
+
if (args.grep)
|
|
1294
|
+
return grepText(source);
|
|
1230
1295
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1231
1296
|
}
|
|
1232
1297
|
case 'BDEF': {
|
|
1233
1298
|
const { source, cacheHit, revalidated } = await cachedGet('BDEF', name, effectiveVersion, (ifNoneMatch) => client.getBdef(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1299
|
+
if (args.grep)
|
|
1300
|
+
return grepText(source);
|
|
1234
1301
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1235
1302
|
}
|
|
1236
1303
|
case 'SRVD': {
|
|
1237
1304
|
const { source, cacheHit, revalidated } = await cachedGet('SRVD', name, effectiveVersion, (ifNoneMatch) => client.getSrvd(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1305
|
+
if (args.grep)
|
|
1306
|
+
return grepText(source);
|
|
1238
1307
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1239
1308
|
}
|
|
1240
1309
|
case 'DDLX': {
|
|
1241
1310
|
try {
|
|
1242
1311
|
const { source, cacheHit, revalidated } = await cachedGet('DDLX', name, effectiveVersion, (ifNoneMatch) => client.getDdlx(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1312
|
+
if (args.grep)
|
|
1313
|
+
return grepText(source);
|
|
1243
1314
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1244
1315
|
}
|
|
1245
1316
|
catch (err) {
|
|
@@ -1273,10 +1344,14 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1273
1344
|
// client.getTabl() handles the /tables/ → /structures/ fallback internally
|
|
1274
1345
|
// and caches the resolved URL for subsequent write/activate paths.
|
|
1275
1346
|
const { source, cacheHit, revalidated } = await cachedGet('TABL', name, effectiveVersion, (ifNoneMatch) => client.getTabl(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1347
|
+
if (args.grep)
|
|
1348
|
+
return grepText(source);
|
|
1276
1349
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1277
1350
|
}
|
|
1278
1351
|
case 'VIEW': {
|
|
1279
1352
|
const { source, cacheHit, revalidated } = await cachedGet('VIEW', name, effectiveVersion, (ifNoneMatch) => client.getView(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1353
|
+
if (args.grep)
|
|
1354
|
+
return grepText(source);
|
|
1280
1355
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1281
1356
|
}
|
|
1282
1357
|
case 'DOMA': {
|
|
@@ -1383,6 +1458,15 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1383
1458
|
const data = await client.getTableContents(name, maxRows, args.sqlFilter);
|
|
1384
1459
|
return textResult(JSON.stringify(data, null, 2));
|
|
1385
1460
|
}
|
|
1461
|
+
case 'TABLE_QUERY': {
|
|
1462
|
+
const maxRows = Number(args.maxRows ?? 100);
|
|
1463
|
+
const columns = Array.isArray(args.columns) ? args.columns : undefined;
|
|
1464
|
+
const where = Array.isArray(args.where)
|
|
1465
|
+
? args.where
|
|
1466
|
+
: undefined;
|
|
1467
|
+
const data = await client.runTableQuery(name, { columns, where, maxRows });
|
|
1468
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
1469
|
+
}
|
|
1386
1470
|
case 'SOBJ': {
|
|
1387
1471
|
const method = String(args.method ?? '');
|
|
1388
1472
|
// Sanitize inputs to prevent SQL injection — BOR names are alphanumeric + underscore only
|
|
@@ -1542,7 +1626,7 @@ async function handleSAPSearch(client, args) {
|
|
|
1542
1626
|
// keeps the same logical object from appearing twice in the merged matches.
|
|
1543
1627
|
// Preserve the more-specific slash form when both originate from ADT+DB.
|
|
1544
1628
|
const seen = new Map();
|
|
1545
|
-
const baseKey = (m) => `${(m.objectType.split('/')[0] || m.objectType).toUpperCase()}
|
|
1629
|
+
const baseKey = (m) => `${(m.objectType.split('/')[0] || m.objectType).toUpperCase()}\x00${m.objectName.toUpperCase()}`;
|
|
1546
1630
|
for (const m of adtMatches)
|
|
1547
1631
|
seen.set(baseKey(m), m);
|
|
1548
1632
|
for (const m of dbMatches) {
|
|
@@ -2924,6 +3008,12 @@ function stripIncludeHeader(source) {
|
|
|
2924
3008
|
return source.replace(/^=== \w+ ===\n/, '');
|
|
2925
3009
|
}
|
|
2926
3010
|
// ─── SAPWrite Handler ────────────────────────────────────────────────
|
|
3011
|
+
/**
|
|
3012
|
+
* Single-object actions whose top-level `name` is an SAP object name and must be
|
|
3013
|
+
* uppercase (TADIR convention). `batch_create` is excluded — its names live in
|
|
3014
|
+
* the `objects[]` items and are validated per item in the batch_create branch.
|
|
3015
|
+
*/
|
|
3016
|
+
const NAME_CASE_GUARD_ACTIONS = new Set(['create', 'update', 'edit_method', 'delete']);
|
|
2927
3017
|
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
2928
3018
|
const action = String(args.action ?? '');
|
|
2929
3019
|
const type = normalizeWriteObjectType(String(args.type ?? ''));
|
|
@@ -2931,6 +3021,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2931
3021
|
const source = String(args.source ?? '');
|
|
2932
3022
|
const hasSource = typeof args.source === 'string';
|
|
2933
3023
|
const include = normalizeClassWriteInclude(args.include);
|
|
3024
|
+
// Whether a non-empty include was actually requested. Some MCP clients serialize
|
|
3025
|
+
// an omitted optional string as "" — treat empty/whitespace as "not provided" so
|
|
3026
|
+
// those clients aren't rejected with a bogus "Invalid CLAS include" on the MAIN path.
|
|
3027
|
+
const includeProvided = typeof args.include === 'string' && args.include.trim() !== '';
|
|
2934
3028
|
const transport = args.transport;
|
|
2935
3029
|
const lintOverride = args.lintBeforeWrite;
|
|
2936
3030
|
const preflightOverride = args.preflightBeforeWrite;
|
|
@@ -2940,12 +3034,16 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2940
3034
|
return errorResult('"type" and "name" are required for this action.');
|
|
2941
3035
|
}
|
|
2942
3036
|
// SAP TADIR stores object names uppercase. Mixed-case names cause silent corruption
|
|
2943
|
-
// (e.g. DDLS
|
|
2944
|
-
// still contains "Zc_MyView", confusing every downstream tool)
|
|
2945
|
-
//
|
|
3037
|
+
// on create (e.g. DDLS "Zc_MyView" registers as "ZC_MYVIEW" in TADIR but the source body
|
|
3038
|
+
// still contains "Zc_MyView", confusing every downstream tool) and broken URL lookups on
|
|
3039
|
+
// mutate/delete — the lock is held against the canonical uppercase name while the request
|
|
3040
|
+
// URL carries the mixed-case one, which surfaces on ECC as 423 "... is not locked" (issue
|
|
3041
|
+
// #293, original report used name "Z_HELLO_world"). Reject pre-flight for every name-bearing
|
|
3042
|
+
// single-object action — universal SAP convention, not a 7.50 quirk. (batch_create validates
|
|
3043
|
+
// each item separately below.)
|
|
2946
3044
|
// Note: source code INSIDE the object can use mixed case (e.g. for DDLS: name="ZC_MYVIEW"
|
|
2947
3045
|
// but `define view entity Zc_MyView` is fine inside the source body).
|
|
2948
|
-
if (action
|
|
3046
|
+
if (NAME_CASE_GUARD_ACTIONS.has(action) && name && name !== name.toUpperCase()) {
|
|
2949
3047
|
return errorResult(`Object name "${name}" contains lowercase characters. SAP object names must be uppercase (e.g. "${name.toUpperCase()}").\n\n` +
|
|
2950
3048
|
`Note: the object NAME in TADIR must be uppercase, but the source code inside the object can use mixed case ` +
|
|
2951
3049
|
`(e.g. for DDLS: name="${name.toUpperCase()}" but source can contain "define view entity ${name}").`);
|
|
@@ -3023,14 +3121,35 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3023
3121
|
};
|
|
3024
3122
|
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
|
|
3025
3123
|
// Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
|
|
3124
|
+
// Fail-closed: if the package cannot be determined from ADT metadata, refuse the write
|
|
3125
|
+
// rather than silently passing through the allowlist gate.
|
|
3026
3126
|
async function enforcePackageForExistingObject() {
|
|
3027
3127
|
if (client.safety.allowedPackages.length === 0)
|
|
3028
3128
|
return undefined;
|
|
3029
3129
|
const pkg = await client.resolveObjectPackage(objectUrl);
|
|
3030
|
-
if (pkg)
|
|
3031
|
-
|
|
3130
|
+
if (!pkg) {
|
|
3131
|
+
throw new AdtSafetyError(`Operations on ${type} '${name}' blocked: ARC-1 could not determine the object's package ` +
|
|
3132
|
+
`from ADT metadata (no adtcore:packageRef in response). Fail-closed because allowedPackages is restricted.`);
|
|
3133
|
+
}
|
|
3134
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
3032
3135
|
return pkg;
|
|
3033
3136
|
}
|
|
3137
|
+
// Helper for class-section surgery (issue #303): fetch the class structure AND
|
|
3138
|
+
// /source/main at the SAME effective version, so the spliced line ranges line
|
|
3139
|
+
// up with the bytes being edited. resolveVersionAndDraftInfo picks 'inactive'
|
|
3140
|
+
// when an unactivated draft exists. We pass that version to BOTH getClassStructure
|
|
3141
|
+
// (the /objectstructure?version= read) and the source read, AND to the cache opts
|
|
3142
|
+
// (so inactive bytes aren't cached under the 'active' key). Without this, a chained
|
|
3143
|
+
// surgery call on a draft would splice active-version line ranges into inactive
|
|
3144
|
+
// source and silently corrupt the draft.
|
|
3145
|
+
async function fetchClassStructureAndMain(clsName) {
|
|
3146
|
+
const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', clsName, 'auto');
|
|
3147
|
+
const structure = await client.getClassStructure(clsName, effectiveVersion);
|
|
3148
|
+
const main = cachingLayer
|
|
3149
|
+
? (await cachingLayer.getSource('CLAS', clsName, (ifNoneMatch) => client.getClass(clsName, undefined, { ifNoneMatch, version: effectiveVersion }), { version: effectiveVersion })).source
|
|
3150
|
+
: (await client.getClass(clsName, undefined, { version: effectiveVersion })).source;
|
|
3151
|
+
return { structure, main, effectiveVersion };
|
|
3152
|
+
}
|
|
3034
3153
|
switch (action) {
|
|
3035
3154
|
case 'update': {
|
|
3036
3155
|
const existingPackage = await enforcePackageForExistingObject();
|
|
@@ -3047,9 +3166,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3047
3166
|
if (!hasSource) {
|
|
3048
3167
|
return errorResult('"source" is required when updating a CLAS include.');
|
|
3049
3168
|
}
|
|
3050
|
-
|
|
3169
|
+
// Auto-initialise the include if it doesn't exist yet. On a fresh class
|
|
3170
|
+
// the testclasses (CCAU) include is absent — a content PUT alone fails
|
|
3171
|
+
// with HTTP 500 "…CCAU does not have any inactive version". safeUpdateClassInclude
|
|
3172
|
+
// probes the include and POST-creates it (under the same lock) before the PUT.
|
|
3173
|
+
const { initialized } = await safeUpdateClassInclude(client.http, client.safety, objectUrl, classIncludeUrl(name, include), source, transport, cachedFeatures?.abapRelease);
|
|
3051
3174
|
invalidateWrittenObject(type, name);
|
|
3052
|
-
|
|
3175
|
+
const initNote = initialized ? ` (initialised the ${include} include first)` : '';
|
|
3176
|
+
return textResult(`Successfully updated ${type} ${name} include ${include}${initNote}. Active version remains unchanged until activation; read with SAPRead(version="inactive") to verify the draft.`);
|
|
3053
3177
|
}
|
|
3054
3178
|
if (type === 'SKTD') {
|
|
3055
3179
|
// KTD update requires the full <sktd:docu> XML envelope with the Markdown
|
|
@@ -3150,7 +3274,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3150
3274
|
}
|
|
3151
3275
|
case 'create': {
|
|
3152
3276
|
const pkg = String(args.package ?? '$TMP');
|
|
3153
|
-
checkPackage(client.safety, pkg);
|
|
3277
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
3154
3278
|
const description = String(args.description ?? name);
|
|
3155
3279
|
// Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
|
|
3156
3280
|
// SAP requires a transport number for objects in transportable packages.
|
|
@@ -3466,6 +3590,261 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3466
3590
|
const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
|
|
3467
3591
|
return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
|
|
3468
3592
|
}
|
|
3593
|
+
// ─── Class-section surgery actions (issue #303) ─────────────────────
|
|
3594
|
+
//
|
|
3595
|
+
// Four actions share a common shape: fetch objectstructure → optional
|
|
3596
|
+
// diff/refuse → splice into /source/main (or /includes/<inc> when
|
|
3597
|
+
// include= is set) → PUT under lock → no auto-activate.
|
|
3598
|
+
//
|
|
3599
|
+
// Pre-write lint runs on the SPLICED FULL source (not the partial input
|
|
3600
|
+
// fragment) because a raw DEFINITION block alone fails abaplint with
|
|
3601
|
+
// "Expected CLASSIMPLEMENTATION" — verified live on a4h. Lint is skipped
|
|
3602
|
+
// for include= writes (same precedent as `update include=` path).
|
|
3603
|
+
case 'edit_class_definition': {
|
|
3604
|
+
if (type !== 'CLAS')
|
|
3605
|
+
return errorResult('edit_class_definition is only supported for type=CLAS.');
|
|
3606
|
+
if (!hasSource)
|
|
3607
|
+
return errorResult('"source" (new CLASS DEFINITION block) is required for edit_class_definition.');
|
|
3608
|
+
if (includeProvided && !include) {
|
|
3609
|
+
return errorResult(`Invalid CLAS include "${String(args.include)}". Valid values: ${CLASS_WRITE_INCLUDES.join(', ')}.`);
|
|
3610
|
+
}
|
|
3611
|
+
await enforcePackageForExistingObject();
|
|
3612
|
+
const writeUrl = include ? classIncludeUrl(name, include) : srcUrl;
|
|
3613
|
+
let spliced;
|
|
3614
|
+
if (include) {
|
|
3615
|
+
// include= path: whole-replace the local include (CCDEF/CCIMP/macros/
|
|
3616
|
+
// testclasses). The structure-based diff/refuse doesn't apply — the
|
|
3617
|
+
// /objectstructure endpoint reports the GLOBAL class, not the local
|
|
3618
|
+
// include's split DEFINITION/IMPLEMENTATION halves. SAP activation is the
|
|
3619
|
+
// validator here (same precedent as `update include=`). No structure or
|
|
3620
|
+
// source fetch is needed: the caller's `source` IS the new include body.
|
|
3621
|
+
spliced = source.endsWith('\n') ? source : `${source}\n`;
|
|
3622
|
+
}
|
|
3623
|
+
else {
|
|
3624
|
+
// MAIN path: fetch structure + source at the same effective version so
|
|
3625
|
+
// the spliced line ranges align with the bytes being edited.
|
|
3626
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3627
|
+
// Refuse-policy: compute the method-set diff against the NEW DEFINITION.
|
|
3628
|
+
const diff = diffMethodSets(structure, source);
|
|
3629
|
+
const missingImpls = [];
|
|
3630
|
+
const orphanImpls = [];
|
|
3631
|
+
for (const add of diff.added) {
|
|
3632
|
+
// Exempt declarations that never have a METHOD…ENDMETHOD body.
|
|
3633
|
+
if (add.isAbstract || add.isEvent || add.isInterface || add.isAlias)
|
|
3634
|
+
continue;
|
|
3635
|
+
// Does IMPLEMENTATION already have a METHOD <name> header? Match the
|
|
3636
|
+
// method name followed by a word-boundary so AMDP / event-handler /
|
|
3637
|
+
// multi-line headers (`METHOD x BY DATABASE PROCEDURE…`, `METHOD x FOR
|
|
3638
|
+
// EVENT…`, `METHOD x\n IMPORTING…`) are recognized — NOT only the bare
|
|
3639
|
+
// `METHOD x.` form. \b after the name prevents matching a longer name
|
|
3640
|
+
// with the same prefix (METHOD x_helper for added X).
|
|
3641
|
+
const re = new RegExp(`^\\s*METHOD\\s+${add.name}\\b`, 'im');
|
|
3642
|
+
if (!re.test(main))
|
|
3643
|
+
missingImpls.push(add.name);
|
|
3644
|
+
}
|
|
3645
|
+
for (const rem of diff.removed) {
|
|
3646
|
+
if (rem.implementation) {
|
|
3647
|
+
// Was concrete, still has impl range — caller didn't remove the body.
|
|
3648
|
+
orphanImpls.push(rem.name);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
if (missingImpls.length > 0 || orphanImpls.length > 0) {
|
|
3652
|
+
const parts = [];
|
|
3653
|
+
if (missingImpls.length > 0) {
|
|
3654
|
+
parts.push(`Cannot apply edit_class_definition: the new DEFINITION declares method(s) ${missingImpls.join(', ')} but the existing IMPLEMENTATION block has no matching METHOD…ENDMETHOD body. Either include a METHOD <name>. ENDMETHOD. block per added method in your new source, or use SAPWrite(action="add_method", name="${name}", method="<METHODS clause>") to insert each one atomically.`);
|
|
3655
|
+
}
|
|
3656
|
+
if (orphanImpls.length > 0) {
|
|
3657
|
+
parts.push(`Cannot apply edit_class_definition: the new DEFINITION removes method(s) ${orphanImpls.join(', ')} but the existing IMPLEMENTATION block still has METHOD…ENDMETHOD bodies for them (orphan implementation). Either remove those METHOD blocks in your edit, or use SAPWrite(action="delete_method", name="${name}", method="<name>") to drop each one atomically.`);
|
|
3658
|
+
}
|
|
3659
|
+
return errorResult(parts.join('\n\n'));
|
|
3660
|
+
}
|
|
3661
|
+
spliced = spliceClassDefinition(main, structure, source);
|
|
3662
|
+
}
|
|
3663
|
+
// Pre-write lint on the spliced full source (MAIN path only — include=
|
|
3664
|
+
// fragments can't be lint-parsed standalone).
|
|
3665
|
+
if (!include) {
|
|
3666
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3667
|
+
if (lintWarnings.blocked)
|
|
3668
|
+
return lintWarnings.result;
|
|
3669
|
+
}
|
|
3670
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, writeUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3671
|
+
invalidateWrittenObject(type, name);
|
|
3672
|
+
const whereLabel = include ? ` (include: ${include})` : '';
|
|
3673
|
+
return textResult(`Successfully updated DEFINITION of ${type} ${name}${whereLabel}. Active version unchanged until activation; read with SAPRead(version="inactive") to verify, then SAPActivate.`);
|
|
3674
|
+
}
|
|
3675
|
+
case 'edit_method_signature': {
|
|
3676
|
+
if (type !== 'CLAS')
|
|
3677
|
+
return errorResult('edit_method_signature is only supported for type=CLAS.');
|
|
3678
|
+
const methodSpecifier = String(args.method ?? '').trim();
|
|
3679
|
+
if (!methodSpecifier) {
|
|
3680
|
+
return errorResult('"method" (the method NAME to re-sign) is required for edit_method_signature.');
|
|
3681
|
+
}
|
|
3682
|
+
if (!hasSource) {
|
|
3683
|
+
return errorResult('"source" (the new METHODS clause) is required for edit_method_signature.');
|
|
3684
|
+
}
|
|
3685
|
+
// MAIN-only action: include= is rejected at the schema layer (this action is
|
|
3686
|
+
// not in SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls
|
|
3687
|
+
// that bypass Zod.
|
|
3688
|
+
if (includeProvided) {
|
|
3689
|
+
return errorResult('edit_method_signature targets the global class DEFINITION (/source/main). For local-class (CCDEF) signatures, use edit_class_definition with include=definitions.');
|
|
3690
|
+
}
|
|
3691
|
+
await enforcePackageForExistingObject();
|
|
3692
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3693
|
+
const upperName = methodSpecifier.toUpperCase();
|
|
3694
|
+
const method = structure.methods.find((m) => m.name === upperName);
|
|
3695
|
+
if (!method) {
|
|
3696
|
+
const available = structure.methods.map((m) => m.name).join(', ');
|
|
3697
|
+
const hint = methodSpecifier.includes('~')
|
|
3698
|
+
? ' Interface-qualified names (e.g. "zif_x~m") are not addressable here — objectstructure lists the implementing method under its bare name; for interface/local-handler bodies use edit_method.'
|
|
3699
|
+
: '';
|
|
3700
|
+
return errorResult(`Method "${methodSpecifier}" not found in CLAS ${name}. Available methods: ${available || '(none)'}.${hint}`);
|
|
3701
|
+
}
|
|
3702
|
+
const spliced = spliceMethodSignature(main, method, source);
|
|
3703
|
+
// No pre-write lint: edit_method_signature changes ONLY the declaration; the
|
|
3704
|
+
// method body still references the old signature until the caller follows up
|
|
3705
|
+
// with edit_method. Linting the spliced full source here would flag legitimate
|
|
3706
|
+
// in-progress renames (e.g. "param `name` not declared"). SAP activation is the
|
|
3707
|
+
// authoritative check — same rationale as the include= lint skip on edit_method.
|
|
3708
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3709
|
+
invalidateWrittenObject(type, name);
|
|
3710
|
+
return textResult(`Successfully updated signature of method "${method.name}" in ${type} ${name}. Active version unchanged until activation; if the body still references the old signature, follow up with edit_method, then SAPActivate.`);
|
|
3711
|
+
}
|
|
3712
|
+
case 'add_method': {
|
|
3713
|
+
if (type !== 'CLAS')
|
|
3714
|
+
return errorResult('add_method is only supported for type=CLAS.');
|
|
3715
|
+
const clause = String(args.method ?? '');
|
|
3716
|
+
if (!clause.trim()) {
|
|
3717
|
+
return errorResult('"method" (the full METHODS clause, e.g. "METHODS greet IMPORTING who TYPE string.") is required for add_method.');
|
|
3718
|
+
}
|
|
3719
|
+
const methodName = extractMethodNameFromClause(clause);
|
|
3720
|
+
if (!methodName) {
|
|
3721
|
+
return errorResult('Could not extract method name from the METHODS clause. Provide a clause starting with "METHODS <name>" or "CLASS-METHODS <name>".');
|
|
3722
|
+
}
|
|
3723
|
+
// Interface-qualified names (lhc_x~y, zif_x~m) can't be added to a global
|
|
3724
|
+
// class's DEFINITION/IMPLEMENTATION — `~` is interface-method scope and would
|
|
3725
|
+
// produce invalid ABAP in the METHOD stub. Reject with a clear pointer.
|
|
3726
|
+
if (methodName.includes('~')) {
|
|
3727
|
+
return errorResult(`add_method cannot add the interface-qualified method "${methodName}" to a global class. Implement the interface via "INTERFACES <name>." in the DEFINITION (use edit_class_definition), then provide the body with edit_method.`);
|
|
3728
|
+
}
|
|
3729
|
+
const visibility = args.visibility ?? 'public';
|
|
3730
|
+
const isAbstract = args.abstract === true;
|
|
3731
|
+
// MAIN-only action: include= is rejected at the schema layer (not in
|
|
3732
|
+
// SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls.
|
|
3733
|
+
if (includeProvided) {
|
|
3734
|
+
return errorResult('add_method targets the global class DEFINITION (/source/main). For local-class (CCDEF) method additions, use edit_class_definition with include=definitions.');
|
|
3735
|
+
}
|
|
3736
|
+
await enforcePackageForExistingObject();
|
|
3737
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3738
|
+
// Refuse if method already exists (would silently duplicate).
|
|
3739
|
+
if (structure.methods.some((m) => m.name === methodName)) {
|
|
3740
|
+
return errorResult(`Method "${methodName}" already exists in CLAS ${name}. Use SAPWrite(action="edit_method_signature", method="${methodName}", source="<new METHODS clause>") to change its signature.`);
|
|
3741
|
+
}
|
|
3742
|
+
// A concrete (non-abstract) method needs an IMPLEMENTATION block to receive
|
|
3743
|
+
// its METHOD…ENDMETHOD stub. A purely-abstract class has no IMPLEMENTATION
|
|
3744
|
+
// half, so inserting a concrete declaration there would leave it unimplemented.
|
|
3745
|
+
if (!isAbstract && !structure.classImplementationBlock) {
|
|
3746
|
+
return errorResult(`CLAS ${name} has no IMPLEMENTATION block (purely abstract class). Pass abstract=true to add an abstract method, or add the IMPLEMENTATION half first via edit_class_definition.`);
|
|
3747
|
+
}
|
|
3748
|
+
// Refuse with hint if the target visibility section header is missing.
|
|
3749
|
+
const anchor = findSectionAnchor(main, structure, visibility);
|
|
3750
|
+
if (!anchor) {
|
|
3751
|
+
return errorResult(`No ${visibility.toUpperCase()} SECTION exists in CLAS ${name}. Use SAPWrite(action="edit_class_definition") to add the section header first, then re-run add_method.`);
|
|
3752
|
+
}
|
|
3753
|
+
const spliced = insertMethodPair(main, structure, {
|
|
3754
|
+
decl: clause,
|
|
3755
|
+
visibility,
|
|
3756
|
+
methodName,
|
|
3757
|
+
isAbstract,
|
|
3758
|
+
});
|
|
3759
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3760
|
+
if (lintWarnings.blocked)
|
|
3761
|
+
return lintWarnings.result;
|
|
3762
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3763
|
+
invalidateWrittenObject(type, name);
|
|
3764
|
+
const stubNote = isAbstract ? ' (abstract — no IMPL stub inserted)' : '';
|
|
3765
|
+
return textResult(`Successfully added method "${methodName}" (${visibility}) to ${type} ${name}${stubNote}. Active version unchanged until activation; SAPActivate next.`);
|
|
3766
|
+
}
|
|
3767
|
+
case 'delete_method': {
|
|
3768
|
+
if (type !== 'CLAS')
|
|
3769
|
+
return errorResult('delete_method is only supported for type=CLAS.');
|
|
3770
|
+
const methodSpecifier = String(args.method ?? '').trim();
|
|
3771
|
+
if (!methodSpecifier) {
|
|
3772
|
+
return errorResult('"method" (the method NAME to delete) is required for delete_method.');
|
|
3773
|
+
}
|
|
3774
|
+
// MAIN-only action: include= is rejected at the schema layer (not in
|
|
3775
|
+
// SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls.
|
|
3776
|
+
if (includeProvided) {
|
|
3777
|
+
return errorResult('delete_method targets the global class DEFINITION (/source/main). For local-class (CCDEF/CCIMP) method removal, use edit_class_definition with include=...');
|
|
3778
|
+
}
|
|
3779
|
+
await enforcePackageForExistingObject();
|
|
3780
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3781
|
+
const upperName = methodSpecifier.toUpperCase();
|
|
3782
|
+
const method = structure.methods.find((m) => m.name === upperName);
|
|
3783
|
+
if (!method) {
|
|
3784
|
+
const available = structure.methods.map((m) => m.name).join(', ');
|
|
3785
|
+
const hint = methodSpecifier.includes('~')
|
|
3786
|
+
? ' Interface-qualified names (e.g. "zif_x~m") are not addressable here; objectstructure lists methods under their bare names.'
|
|
3787
|
+
: '';
|
|
3788
|
+
return errorResult(`Method "${methodSpecifier}" not found in CLAS ${name}. Available methods: ${available || '(none)'}.${hint}`);
|
|
3789
|
+
}
|
|
3790
|
+
const spliced = removeMethodPair(main, method);
|
|
3791
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3792
|
+
if (lintWarnings.blocked)
|
|
3793
|
+
return lintWarnings.result;
|
|
3794
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3795
|
+
invalidateWrittenObject(type, name);
|
|
3796
|
+
const where = method.implementation ? ' (DEFINITION + IMPLEMENTATION)' : ' (DEFINITION only — was ABSTRACT)';
|
|
3797
|
+
return textResult(`Successfully deleted method "${method.name}" from ${type} ${name}${where}. Active version unchanged until activation; SAPActivate next.`);
|
|
3798
|
+
}
|
|
3799
|
+
case 'change_method_visibility': {
|
|
3800
|
+
// Body-preserving visibility move (issue #303 follow-up). Moves the METHODS
|
|
3801
|
+
// clause from its current section to the target section; the IMPLEMENTATION
|
|
3802
|
+
// block is never touched, so the method body survives. This is the safe
|
|
3803
|
+
// alternative to delete_method + add_method (which discards the body).
|
|
3804
|
+
if (type !== 'CLAS')
|
|
3805
|
+
return errorResult('change_method_visibility is only supported for type=CLAS.');
|
|
3806
|
+
const methodSpecifier = String(args.method ?? '').trim();
|
|
3807
|
+
if (!methodSpecifier) {
|
|
3808
|
+
return errorResult('"method" (the method NAME to move) is required for change_method_visibility.');
|
|
3809
|
+
}
|
|
3810
|
+
const target = args.visibility;
|
|
3811
|
+
if (!target) {
|
|
3812
|
+
return errorResult('"visibility" (target section: public, protected, or private) is required for change_method_visibility.');
|
|
3813
|
+
}
|
|
3814
|
+
// MAIN-only action: include= is rejected at the schema layer (not in
|
|
3815
|
+
// SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls.
|
|
3816
|
+
if (includeProvided) {
|
|
3817
|
+
return errorResult('change_method_visibility targets the global class DEFINITION (/source/main). For local-class (CCDEF) methods, use edit_class_definition with include=definitions.');
|
|
3818
|
+
}
|
|
3819
|
+
await enforcePackageForExistingObject();
|
|
3820
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3821
|
+
const upperName = methodSpecifier.toUpperCase();
|
|
3822
|
+
const method = structure.methods.find((m) => m.name === upperName);
|
|
3823
|
+
if (!method) {
|
|
3824
|
+
const available = structure.methods.map((m) => m.name).join(', ');
|
|
3825
|
+
const hint = methodSpecifier.includes('~')
|
|
3826
|
+
? ' Interface-qualified names (e.g. "zif_x~m") are not addressable here; objectstructure lists methods under their bare names.'
|
|
3827
|
+
: '';
|
|
3828
|
+
return errorResult(`Method "${methodSpecifier}" not found in CLAS ${name}. Available methods: ${available || '(none)'}.${hint}`);
|
|
3829
|
+
}
|
|
3830
|
+
// Idempotent: already in the requested section → no write.
|
|
3831
|
+
if (method.visibility === target) {
|
|
3832
|
+
return textResult(`Method "${method.name}" is already in the ${target.toUpperCase()} SECTION of ${type} ${name}. No change made.`);
|
|
3833
|
+
}
|
|
3834
|
+
// The target section header must already exist (same constraint as add_method).
|
|
3835
|
+
const anchor = findSectionAnchor(main, structure, target);
|
|
3836
|
+
if (!anchor) {
|
|
3837
|
+
return errorResult(`No ${target.toUpperCase()} SECTION exists in CLAS ${name}. Use SAPWrite(action="edit_class_definition") to add the section header first, then re-run change_method_visibility.`);
|
|
3838
|
+
}
|
|
3839
|
+
// DEFINITION-only move — IMPLEMENTATION (the method body) is preserved verbatim.
|
|
3840
|
+
const spliced = moveMethodDefinition(main, method, anchor.afterLine);
|
|
3841
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3842
|
+
if (lintWarnings.blocked)
|
|
3843
|
+
return lintWarnings.result;
|
|
3844
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3845
|
+
invalidateWrittenObject(type, name);
|
|
3846
|
+
return textResult(`Successfully moved method "${method.name}" from ${method.visibility.toUpperCase()} to ${target.toUpperCase()} SECTION of ${type} ${name} (IMPLEMENTATION preserved). Active version unchanged until activation; SAPActivate next.`);
|
|
3847
|
+
}
|
|
3469
3848
|
case 'scaffold_rap_handlers': {
|
|
3470
3849
|
// What this action does:
|
|
3471
3850
|
// Given a behavior-pool class (ZBP_*) and its interface BDEF, inspect
|
|
@@ -3745,8 +4124,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3745
4124
|
return { obj, type: objType, name: objName, packageName: objPackage, explicitTransport };
|
|
3746
4125
|
});
|
|
3747
4126
|
// Check every target package before starting any creates.
|
|
3748
|
-
|
|
3749
|
-
|
|
4127
|
+
// Resolver is shared across the loop so subtree BFS happens once even when
|
|
4128
|
+
// many objects target descendants of the same `ZFOO/**` root.
|
|
4129
|
+
{
|
|
4130
|
+
const resolver = client.getPackageHierarchyResolver();
|
|
4131
|
+
for (const pkg of new Set(batchPlan.map((item) => item.packageName))) {
|
|
4132
|
+
await checkPackage(client.safety, pkg, resolver);
|
|
4133
|
+
}
|
|
3750
4134
|
}
|
|
3751
4135
|
// Pre-flight transport check for batch_create (same logic as single create),
|
|
3752
4136
|
// but keyed by each effective package because objects can override package.
|
|
@@ -4020,11 +4404,11 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
4020
4404
|
const batchStatuses = buildBatchActivationStatuses(writtenObjects, activationOutcome);
|
|
4021
4405
|
const statusDetails = formatBatchActivationStatuses(batchStatuses);
|
|
4022
4406
|
terminalActivationFailure = statusDetails;
|
|
4023
|
-
const statusByName = new Map(batchStatuses.map((s) => [`${s.type}
|
|
4407
|
+
const statusByName = new Map(batchStatuses.map((s) => [`${s.type}\x00${s.name}`, s]));
|
|
4024
4408
|
for (const result of results) {
|
|
4025
4409
|
if (result.status !== 'success')
|
|
4026
4410
|
continue;
|
|
4027
|
-
const key = `${result.type}
|
|
4411
|
+
const key = `${result.type}\x00${result.name}`;
|
|
4028
4412
|
const matched = statusByName.get(key);
|
|
4029
4413
|
if (!matched)
|
|
4030
4414
|
continue;
|
|
@@ -5064,7 +5448,7 @@ async function handleSAPGit(client, args, _authInfo) {
|
|
|
5064
5448
|
password,
|
|
5065
5449
|
token,
|
|
5066
5450
|
};
|
|
5067
|
-
result = await gctsCloneRepo(client.http, client.safety, params);
|
|
5451
|
+
result = await gctsCloneRepo(client.http, client.safety, params, client.getPackageHierarchyResolver());
|
|
5068
5452
|
}
|
|
5069
5453
|
else {
|
|
5070
5454
|
if (!packageName)
|
|
@@ -5076,7 +5460,7 @@ async function handleSAPGit(client, args, _authInfo) {
|
|
|
5076
5460
|
transportRequest: String(args.transport ?? '').trim() || undefined,
|
|
5077
5461
|
user,
|
|
5078
5462
|
password,
|
|
5079
|
-
});
|
|
5463
|
+
}, client.getPackageHierarchyResolver());
|
|
5080
5464
|
}
|
|
5081
5465
|
break;
|
|
5082
5466
|
case 'pull':
|
|
@@ -5134,7 +5518,7 @@ async function handleSAPGit(client, args, _authInfo) {
|
|
|
5134
5518
|
result = await gctsCreateBranch(client.http, client.safety, repoId, {
|
|
5135
5519
|
branch,
|
|
5136
5520
|
...(packageName ? { package: packageName } : {}),
|
|
5137
|
-
});
|
|
5521
|
+
}, client.getPackageHierarchyResolver());
|
|
5138
5522
|
}
|
|
5139
5523
|
else {
|
|
5140
5524
|
await abapGitCreateBranch(client.http, client.safety, repoId, branch);
|
|
@@ -5635,10 +6019,19 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5635
6019
|
if (!description)
|
|
5636
6020
|
return errorResult('"description" is required for create_package action.');
|
|
5637
6021
|
checkOperation(client.safety, OperationType.Create, 'CreatePackage');
|
|
5638
|
-
// Package allowlist
|
|
5639
|
-
//
|
|
6022
|
+
// Package allowlist gate:
|
|
6023
|
+
// - When `superPackage` is set, gate the parent. This enables creating
|
|
6024
|
+
// children in allowed parents like $TMP. With subtree (`X/**`) rules,
|
|
6025
|
+
// the new child will automatically be inside its parent's subtree.
|
|
6026
|
+
// - When `superPackage` is omitted, the new package is created at the
|
|
6027
|
+
// root and IS the gateable name itself — otherwise an admin's
|
|
6028
|
+
// allowedPackages restriction would be bypassed by simply omitting
|
|
6029
|
+
// the parent. Gate the new name in that case.
|
|
5640
6030
|
if (superPackage) {
|
|
5641
|
-
checkPackage(client.safety, superPackage);
|
|
6031
|
+
await checkPackage(client.safety, superPackage, client.getPackageHierarchyResolver());
|
|
6032
|
+
}
|
|
6033
|
+
else {
|
|
6034
|
+
await checkPackage(client.safety, name, client.getPackageHierarchyResolver());
|
|
5642
6035
|
}
|
|
5643
6036
|
let effectiveTransport = transport || undefined;
|
|
5644
6037
|
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
@@ -5681,6 +6074,9 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5681
6074
|
packageType,
|
|
5682
6075
|
});
|
|
5683
6076
|
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
6077
|
+
// Hierarchy changed: invalidate any cached subtree that could contain
|
|
6078
|
+
// the new package. Conservative: clear all (cheap; per-call cost is one BFS).
|
|
6079
|
+
client.invalidatePackageHierarchy();
|
|
5684
6080
|
return textResult(`Created package ${name}.`);
|
|
5685
6081
|
}
|
|
5686
6082
|
case 'delete_package': {
|
|
@@ -5689,6 +6085,9 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5689
6085
|
if (!name)
|
|
5690
6086
|
return errorResult('"name" is required for delete_package action.');
|
|
5691
6087
|
checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
|
|
6088
|
+
// Gate by allowedPackages: deletion targets the package itself, so the
|
|
6089
|
+
// package name must be in the allowed set (or in an allowed subtree).
|
|
6090
|
+
await checkPackage(client.safety, name, client.getPackageHierarchyResolver());
|
|
5692
6091
|
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
5693
6092
|
await client.http.withStatefulSession(async (session) => {
|
|
5694
6093
|
const lock = await lockObject(session, client.safety, packageUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
@@ -5705,6 +6104,8 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5705
6104
|
}
|
|
5706
6105
|
}
|
|
5707
6106
|
});
|
|
6107
|
+
// Hierarchy changed: invalidate cached subtrees.
|
|
6108
|
+
client.invalidatePackageHierarchy();
|
|
5708
6109
|
return textResult(`Deleted package ${name}.`);
|
|
5709
6110
|
}
|
|
5710
6111
|
case 'change_package': {
|
|
@@ -5723,8 +6124,11 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5723
6124
|
if (!newPackage)
|
|
5724
6125
|
return errorResult('"newPackage" is required for change_package action.');
|
|
5725
6126
|
checkOperation(client.safety, OperationType.Update, 'ChangePackage');
|
|
5726
|
-
|
|
5727
|
-
|
|
6127
|
+
{
|
|
6128
|
+
const resolver = client.getPackageHierarchyResolver();
|
|
6129
|
+
await checkPackage(client.safety, oldPackage, resolver);
|
|
6130
|
+
await checkPackage(client.safety, newPackage, resolver);
|
|
6131
|
+
}
|
|
5728
6132
|
// Resolve object URI via search if not provided
|
|
5729
6133
|
if (!objectUri) {
|
|
5730
6134
|
const searchResp = await client.http.get(`/sap/bc/adt/repository/informationsystem/search?operation=quickSearch&query=${encodeURIComponent(objectName)}&maxResults=10`);
|
|
@@ -5770,6 +6174,8 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5770
6174
|
newPackage,
|
|
5771
6175
|
transport: effectiveTransport,
|
|
5772
6176
|
});
|
|
6177
|
+
// Hierarchy may have shifted (object moved between packages); invalidate cache.
|
|
6178
|
+
client.invalidatePackageHierarchy();
|
|
5773
6179
|
const transportNote = result.transport ? ` (transport: ${result.transport})` : '';
|
|
5774
6180
|
return textResult(`Moved ${objectName} from package ${oldPackage} to ${newPackage}${transportNote}.`);
|
|
5775
6181
|
}
|