arc-1 0.7.2 → 0.9.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 +32 -6
- package/dist/adt/btp.d.ts +14 -3
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +18 -3
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/client.d.ts +30 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +72 -2
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/codeintel.d.ts +23 -0
- package/dist/adt/codeintel.d.ts.map +1 -1
- package/dist/adt/codeintel.js +39 -0
- package/dist/adt/codeintel.js.map +1 -1
- package/dist/adt/config.d.ts +4 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +28 -4
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +74 -28
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +7 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +38 -9
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +1 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +47 -2
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts +43 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +93 -0
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/transport.d.ts +22 -2
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +41 -13
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/xml-parser.d.ts +4 -0
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +6 -2
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/context/compressor.d.ts +1 -1
- package/dist/context/compressor.js +7 -11
- package/dist/context/compressor.js.map +1 -1
- package/dist/handlers/intent.d.ts +21 -0
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +378 -78
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +5 -2
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +14 -4
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +42 -10
- package/dist/handlers/tools.js.map +1 -1
- package/dist/probe/catalog.d.ts.map +1 -1
- package/dist/probe/catalog.js +19 -11
- package/dist/probe/catalog.js.map +1 -1
- package/dist/server/audit.d.ts +41 -1
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +32 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts +25 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +149 -7
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +40 -3
- package/dist/server/server.js.map +1 -1
- package/dist/server/stateless-client-store.d.ts +97 -0
- package/dist/server/stateless-client-store.d.ts.map +1 -0
- package/dist/server/stateless-client-store.js +334 -0
- package/dist/server/stateless-client-store.js.map +1 -0
- package/dist/server/types.d.ts +12 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +11 -43
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +12 -177
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +10 -4
package/dist/handlers/intent.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
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 { findDefinition, findReferences, findWhereUsed, getCompletion, getWhereUsedScope, } from '../adt/codeintel.js';
|
|
14
|
+
import { findDefinition, findInterfaceImplementersViaSeoMetaRel, findReferences, findWhereUsed, getCompletion, getWhereUsedScope, } from '../adt/codeintel.js';
|
|
15
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';
|
|
@@ -210,8 +210,8 @@ function getWriteInfrastructureHint(err, tool, args) {
|
|
|
210
210
|
'wait briefly, then retry cleanup. If an edit lock remains, release it in ADT/SM12 or ask Basis to clear it.');
|
|
211
211
|
}
|
|
212
212
|
/** Format error messages with LLM-friendly remediation hints */
|
|
213
|
-
function formatErrorForLLM(err, message, tool, args) {
|
|
214
|
-
const base = buildBaseErrorMessage(err, message, tool, args);
|
|
213
|
+
function formatErrorForLLM(err, message, tool, args, config) {
|
|
214
|
+
const base = buildBaseErrorMessage(err, message, tool, args, config);
|
|
215
215
|
// Handler-attached remediation hints (e.g., CDS delete blocker list) always
|
|
216
216
|
// appear last so the message reads "what happened → diagnostics → how to fix".
|
|
217
217
|
if (err instanceof AdtApiError && err.extraHint && !base.includes(err.extraHint)) {
|
|
@@ -219,12 +219,12 @@ function formatErrorForLLM(err, message, tool, args) {
|
|
|
219
219
|
}
|
|
220
220
|
return base;
|
|
221
221
|
}
|
|
222
|
-
function buildBaseErrorMessage(err, message, tool, args) {
|
|
222
|
+
function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
223
223
|
if (err instanceof AdtApiError) {
|
|
224
224
|
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
225
225
|
const enriched = enrichWithSapDetails(err, message);
|
|
226
226
|
const argType = String(args.type ?? '').toUpperCase();
|
|
227
|
-
const classification = classifySapDomainError(err.statusCode, err.responseBody);
|
|
227
|
+
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path);
|
|
228
228
|
if (classification) {
|
|
229
229
|
const transactionLine = classification.transaction ? `\nSAP Transaction: ${classification.transaction}` : '';
|
|
230
230
|
return `${enriched}\n\nHint: ${classification.hint}${transactionLine}`;
|
|
@@ -239,6 +239,12 @@ function buildBaseErrorMessage(err, message, tool, args) {
|
|
|
239
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.`;
|
|
240
240
|
}
|
|
241
241
|
if (err.isUnauthorized || err.isForbidden) {
|
|
242
|
+
if (config.cookieFile || config.cookieString) {
|
|
243
|
+
return (`${enriched}\n\n` +
|
|
244
|
+
'Hint: SAP cookies have expired. Ask the user to re-extract cookies ' +
|
|
245
|
+
'with `arc1-cli extract-cookies`. The next SAP call after extraction ' +
|
|
246
|
+
'will automatically reload the fresh cookies — no restart needed.');
|
|
247
|
+
}
|
|
242
248
|
return `${enriched}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
|
|
243
249
|
}
|
|
244
250
|
// Transport / corrNr specific hints
|
|
@@ -428,6 +434,13 @@ function formatCdsImpactBuckets(downstream, maxNames = 4) {
|
|
|
428
434
|
return lines;
|
|
429
435
|
}
|
|
430
436
|
function mainObjectType(type) {
|
|
437
|
+
// First consult SLASH_TYPE_MAP so collapsed types (TABL/DS → TABL, legacy
|
|
438
|
+
// STRU/DS → TABL) resolve to ARC-1's canonical short type. Then fall back to
|
|
439
|
+
// splitting on '/' so unknown slash forms (e.g. BDEF/BO from where-used
|
|
440
|
+
// results) still produce the parent type rather than the full slash form.
|
|
441
|
+
const normalized = normalizeObjectType(type);
|
|
442
|
+
if (normalized && !normalized.includes('/'))
|
|
443
|
+
return normalized;
|
|
431
444
|
return type.split('/')[0]?.toUpperCase() ?? '';
|
|
432
445
|
}
|
|
433
446
|
function collectOrderedCdsObjects(downstream, bucketOrder) {
|
|
@@ -712,7 +725,7 @@ function getBehaviorPoolSaveFailureHint(err, args) {
|
|
|
712
725
|
}
|
|
713
726
|
function classifyError(err) {
|
|
714
727
|
if (err instanceof AdtApiError) {
|
|
715
|
-
const classification = classifySapDomainError(err.statusCode, err.responseBody);
|
|
728
|
+
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path);
|
|
716
729
|
return classification ? `AdtApiError:${classification.category}` : 'AdtApiError';
|
|
717
730
|
}
|
|
718
731
|
if (err instanceof AdtNetworkError)
|
|
@@ -909,7 +922,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
909
922
|
errorClass: classifyError(err),
|
|
910
923
|
errorMessage: message,
|
|
911
924
|
});
|
|
912
|
-
return errorResult(formatErrorForLLM(err, message, toolName, args));
|
|
925
|
+
return errorResult(formatErrorForLLM(err, message, toolName, args, config));
|
|
913
926
|
}
|
|
914
927
|
});
|
|
915
928
|
}
|
|
@@ -943,7 +956,6 @@ const VERSIONED_SOURCE_READ_TYPES = new Set([
|
|
|
943
956
|
'SKTD',
|
|
944
957
|
'TABL',
|
|
945
958
|
'VIEW',
|
|
946
|
-
'STRU',
|
|
947
959
|
]);
|
|
948
960
|
function inactiveTypeMatches(readType, inactiveType) {
|
|
949
961
|
return (inactiveType.split('/')[0] ?? inactiveType).toUpperCase() === readType.toUpperCase();
|
|
@@ -1159,17 +1171,16 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1159
1171
|
}
|
|
1160
1172
|
}
|
|
1161
1173
|
case 'TABL': {
|
|
1162
|
-
|
|
1174
|
+
// Unified TABL: covers transparent tables and DDIC structures (Model B).
|
|
1175
|
+
// client.getTabl() handles the /tables/ → /structures/ fallback internally
|
|
1176
|
+
// and caches the resolved URL for subsequent write/activate paths.
|
|
1177
|
+
const { source, cacheHit, revalidated } = await cachedGet('TABL', name, effectiveVersion, (ifNoneMatch) => client.getTabl(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1163
1178
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1164
1179
|
}
|
|
1165
1180
|
case 'VIEW': {
|
|
1166
1181
|
const { source, cacheHit, revalidated } = await cachedGet('VIEW', name, effectiveVersion, (ifNoneMatch) => client.getView(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1167
1182
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1168
1183
|
}
|
|
1169
|
-
case 'STRU': {
|
|
1170
|
-
const { source, cacheHit, revalidated } = await cachedGet('STRU', name, effectiveVersion, (ifNoneMatch) => client.getStructure(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1171
|
-
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1172
|
-
}
|
|
1173
1184
|
case 'DOMA': {
|
|
1174
1185
|
const domain = await client.getDomain(name);
|
|
1175
1186
|
return textResult(JSON.stringify(domain, null, 2));
|
|
@@ -1182,7 +1193,17 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1182
1193
|
const authField = await client.getAuthorizationField(name);
|
|
1183
1194
|
return textResult(JSON.stringify(authField, null, 2));
|
|
1184
1195
|
}
|
|
1185
|
-
case 'FTG2':
|
|
1196
|
+
case 'FTG2':
|
|
1197
|
+
case 'FEATURE_TOGGLE': {
|
|
1198
|
+
// FEATURE_TOGGLE is the canonical short type. 'FTG2' is a deprecated alias —
|
|
1199
|
+
// see research/abap-types/types/ftg2.md (ARC-1-invented; zero hits in TADIR,
|
|
1200
|
+
// abap-file-formats, Eclipse apidoc). Removed in the next minor.
|
|
1201
|
+
if (type === 'FTG2') {
|
|
1202
|
+
logger.warn('SAPRead type "FTG2" is deprecated — use "FEATURE_TOGGLE" instead', {
|
|
1203
|
+
type: 'FTG2',
|
|
1204
|
+
replacement: 'FEATURE_TOGGLE',
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1186
1207
|
const toggle = await client.getFeatureToggle(name);
|
|
1187
1208
|
return textResult(JSON.stringify(toggle, null, 2));
|
|
1188
1209
|
}
|
|
@@ -1302,7 +1323,17 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1302
1323
|
const components = await client.getInstalledComponents();
|
|
1303
1324
|
return textResult(JSON.stringify(components, null, 2));
|
|
1304
1325
|
}
|
|
1305
|
-
case 'MESSAGES':
|
|
1326
|
+
case 'MESSAGES':
|
|
1327
|
+
case 'MSAG': {
|
|
1328
|
+
// MSAG is the canonical TADIR R3TR type for message classes; 'MESSAGES' is a
|
|
1329
|
+
// deprecated read alias kept for one minor release. See
|
|
1330
|
+
// research/abap-types/types/msag.md.
|
|
1331
|
+
if (type === 'MESSAGES') {
|
|
1332
|
+
logger.warn('SAPRead type "MESSAGES" is deprecated — use "MSAG" instead', {
|
|
1333
|
+
type: 'MESSAGES',
|
|
1334
|
+
replacement: 'MSAG',
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1306
1337
|
try {
|
|
1307
1338
|
const mcInfo = await client.getMessageClassInfo(name);
|
|
1308
1339
|
return textResult(JSON.stringify(mcInfo, null, 2));
|
|
@@ -1356,7 +1387,7 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1356
1387
|
return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
|
|
1357
1388
|
}
|
|
1358
1389
|
default:
|
|
1359
|
-
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DCLS, DDLX, BDEF, SRVD, SRVB, SKTD, TABL, VIEW,
|
|
1390
|
+
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DCLS, DDLX, BDEF, SRVD, SRVB, SKTD, TABL, VIEW, DOMA, DTEL, MSAG, AUTH, FEATURE_TOGGLE, ENHO, VERSIONS, VERSION_SOURCE, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. Deprecated aliases: MESSAGES (use MSAG), FTG2 (use FEATURE_TOGGLE). ` +
|
|
1360
1391
|
'Tip: Type aliases are auto-normalized (e.g., DDLS/DF → DDLS, DCLS/DL → DCLS, CLAS/OC → CLAS, PROG/P → PROG). ' +
|
|
1361
1392
|
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
1362
1393
|
}
|
|
@@ -2068,31 +2099,116 @@ function escapeXml(s) {
|
|
|
2068
2099
|
.replace(/>/g, '>');
|
|
2069
2100
|
}
|
|
2070
2101
|
// ─── Object URL Mapping ──────────────────────────────────────────────
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
'
|
|
2082
|
-
'
|
|
2083
|
-
'
|
|
2084
|
-
'
|
|
2085
|
-
|
|
2086
|
-
'
|
|
2087
|
-
'
|
|
2088
|
-
|
|
2089
|
-
'
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
'
|
|
2094
|
-
'
|
|
2102
|
+
// Every entry verified against either Eclipse ADT apidoc 3.58.1, live a4h S/4HANA
|
|
2103
|
+
// 2023 + npl NW 7.50 ADT responses (captured 2026-05-08 — both systems agree), or
|
|
2104
|
+
// abap-file-formats schemas. Per-entry evidence in research/abap-types/types/<x>.md.
|
|
2105
|
+
// SLASH_TYPE_EVIDENCE below MUST stay key-equal (anti-cargo-cult guard, enforced by
|
|
2106
|
+
// tests/unit/handlers/slash-type-map.test.ts — see issue #218 follow-up).
|
|
2107
|
+
// Exported for tests only — the citation guard
|
|
2108
|
+
// (tests/unit/handlers/slash-type-map.test.ts) needs to assert key-equality
|
|
2109
|
+
// against SLASH_TYPE_EVIDENCE so a new entry without evidence fails CI.
|
|
2110
|
+
// Production callers should keep using normalizeObjectType().
|
|
2111
|
+
export const SLASH_TYPE_MAP = {
|
|
2112
|
+
'PROG/P': 'PROG', // research/abap-types/types/prog.md
|
|
2113
|
+
'PROG/I': 'INCL', // research/abap-types/types/incl.md
|
|
2114
|
+
'CLAS/OC': 'CLAS', // research/abap-types/types/clas.md
|
|
2115
|
+
// 'CLAS/LI' removed — invented; absent from Eclipse apidoc; no live ADT response
|
|
2116
|
+
// emits it. Pass-through means schema validation rejects it loudly.
|
|
2117
|
+
'INTF/OI': 'INTF', // research/abap-types/types/intf.md
|
|
2118
|
+
// 'FUNC/FM' removed — invented; ADT emits FUGR/FF for function modules, not
|
|
2119
|
+
// FUNC/FM. Function modules are LIMU FUNC under R3TR FUGR.
|
|
2120
|
+
'FUGR/F': 'FUGR', // function group container — research/abap-types/types/fugr.md
|
|
2121
|
+
// FUGR/FF is a function module (LIMU FUNC under FUGR), not the function group.
|
|
2122
|
+
// Live a4h: GET .../groups/su_user/fmodules/bapi_user_getlist returns
|
|
2123
|
+
// adtcore:type="FUGR/FF" with <adtcore:containerRef adtcore:type="FUGR/F"/>.
|
|
2124
|
+
'FUGR/FF': 'FUNC', // research/abap-types/types/fugr.md + func.md
|
|
2125
|
+
'DDLS/DF': 'DDLS', // research/abap-types/types/ddls.md
|
|
2126
|
+
'DCLS/DL': 'DCLS', // research/abap-types/types/dcls.md
|
|
2127
|
+
'BDEF/BDO': 'BDEF', // research/abap-types/types/bdef.md
|
|
2128
|
+
'SRVD/SRV': 'SRVD', // research/abap-types/types/srvd.md
|
|
2129
|
+
'SRVB/SVB': 'SRVB', // research/abap-types/types/srvb.md
|
|
2130
|
+
'DDLX/EX': 'DDLX', // research/abap-types/types/ddlx.md (live a4h + npl 2026-05-08)
|
|
2131
|
+
// DDIC TABL: ADT exposes /DT (transparent table) and /DS (DDIC structure)
|
|
2132
|
+
// subtypes. Both share TADIR R3TR TABL (DD02L-TABCLASS = TRANSP vs INTTAB).
|
|
2133
|
+
// ARC-1 collapses both into the canonical short type 'TABL' (Model B — see
|
|
2134
|
+
// docs/plans/completed/collapse-stru-into-tabl.md).
|
|
2135
|
+
'TABL/DT': 'TABL', // research/abap-types/types/tabl.md
|
|
2136
|
+
'TABL/DS': 'TABL', // research/abap-types/types/tabl.md
|
|
2137
|
+
// Legacy slash-form alias — ADT never actually returns this, but pre-Model-B
|
|
2138
|
+
// ARC-1 prompts learned it from older docs. Kept so they normalize to TABL
|
|
2139
|
+
// instead of producing a schema error. Bare 'STRU' is NOT aliased.
|
|
2140
|
+
'STRU/DS': 'TABL', // research/abap-types/types/tabl.md (legacy alias)
|
|
2141
|
+
'DOMA/DD': 'DOMA', // research/abap-types/types/doma.md
|
|
2142
|
+
'DTEL/DE': 'DTEL', // research/abap-types/types/dtel.md
|
|
2143
|
+
'MSAG/N': 'MSAG', // research/abap-types/types/msag.md
|
|
2144
|
+
'DEVC/K': 'DEVC', // research/abap-types/types/devc.md
|
|
2145
|
+
// TRAN/T (was TRAN/O — invented). Live a4h + npl 2026-05-08 both return
|
|
2146
|
+
// adtcore:type="TRAN/T" for SE38, SU01, etc.
|
|
2147
|
+
'TRAN/T': 'TRAN', // research/abap-types/types/tran.md
|
|
2148
|
+
// VIEW/DV (was VIEW/V — invented). Live a4h + npl 2026-05-08 both return
|
|
2149
|
+
// adtcore:type="VIEW/DV" for V_USR_NAME.
|
|
2150
|
+
'VIEW/DV': 'VIEW', // research/abap-types/types/view.md
|
|
2151
|
+
'SKTD/TYP': 'SKTD', // research/abap-types/types/sktd.md
|
|
2095
2152
|
};
|
|
2153
|
+
/**
|
|
2154
|
+
* Citation guard companion for SLASH_TYPE_MAP. Keys MUST stay key-equal to
|
|
2155
|
+
* SLASH_TYPE_MAP (enforced by tests/unit/handlers/slash-type-map.test.ts). Each
|
|
2156
|
+
* value points at a research evidence file or a fixture that backs the slash code.
|
|
2157
|
+
* Adding an entry without evidence is the anti-cargo-cult guard.
|
|
2158
|
+
*/
|
|
2159
|
+
export const SLASH_TYPE_EVIDENCE = {
|
|
2160
|
+
'PROG/P': 'research/abap-types/types/prog.md',
|
|
2161
|
+
'PROG/I': 'research/abap-types/types/incl.md',
|
|
2162
|
+
'CLAS/OC': 'research/abap-types/types/clas.md',
|
|
2163
|
+
'INTF/OI': 'research/abap-types/types/intf.md',
|
|
2164
|
+
'FUGR/F': 'research/abap-types/types/fugr.md',
|
|
2165
|
+
'FUGR/FF': 'research/abap-types/types/fugr.md',
|
|
2166
|
+
'DDLS/DF': 'research/abap-types/types/ddls.md',
|
|
2167
|
+
'DCLS/DL': 'research/abap-types/types/dcls.md',
|
|
2168
|
+
'BDEF/BDO': 'research/abap-types/types/bdef.md',
|
|
2169
|
+
'SRVD/SRV': 'research/abap-types/types/srvd.md',
|
|
2170
|
+
'SRVB/SVB': 'research/abap-types/types/srvb.md',
|
|
2171
|
+
'DDLX/EX': 'research/abap-types/types/ddlx.md',
|
|
2172
|
+
'TABL/DT': 'research/abap-types/types/tabl.md',
|
|
2173
|
+
'TABL/DS': 'research/abap-types/types/tabl.md',
|
|
2174
|
+
'STRU/DS': 'research/abap-types/types/tabl.md',
|
|
2175
|
+
'DOMA/DD': 'research/abap-types/types/doma.md',
|
|
2176
|
+
'DTEL/DE': 'research/abap-types/types/dtel.md',
|
|
2177
|
+
'MSAG/N': 'research/abap-types/types/msag.md',
|
|
2178
|
+
'DEVC/K': 'research/abap-types/types/devc.md',
|
|
2179
|
+
'TRAN/T': 'research/abap-types/types/tran.md',
|
|
2180
|
+
'VIEW/DV': 'research/abap-types/types/view.md',
|
|
2181
|
+
'SKTD/TYP': 'research/abap-types/types/sktd.md',
|
|
2182
|
+
};
|
|
2183
|
+
/**
|
|
2184
|
+
* Set of canonical short types that MUST have a working `objectBasePath` case.
|
|
2185
|
+
* Drives the exhaustiveness guard inside `objectBasePath` so a new canonical type
|
|
2186
|
+
* added to SAPRead/SAPWrite enums without an URL builder fails loudly. The VIEW
|
|
2187
|
+
* silent-fallthrough bug (research/abap-types/types/view.md) is exactly what this
|
|
2188
|
+
* guard prevents from reoccurring.
|
|
2189
|
+
*/
|
|
2190
|
+
export const KNOWN_BASE_TYPES = new Set([
|
|
2191
|
+
'PROG',
|
|
2192
|
+
'CLAS',
|
|
2193
|
+
'INTF',
|
|
2194
|
+
'INCL',
|
|
2195
|
+
'FUGR',
|
|
2196
|
+
'FUNC',
|
|
2197
|
+
'DDLS',
|
|
2198
|
+
'DCLS',
|
|
2199
|
+
'BDEF',
|
|
2200
|
+
'SRVD',
|
|
2201
|
+
'SRVB',
|
|
2202
|
+
'DDLX',
|
|
2203
|
+
'TABL',
|
|
2204
|
+
'DOMA',
|
|
2205
|
+
'DTEL',
|
|
2206
|
+
'MSAG',
|
|
2207
|
+
'DEVC',
|
|
2208
|
+
'TRAN',
|
|
2209
|
+
'VIEW',
|
|
2210
|
+
'SKTD',
|
|
2211
|
+
]);
|
|
2096
2212
|
/** Normalize ADT type codes and aliases to ARC-1 canonical short types. */
|
|
2097
2213
|
export function normalizeObjectType(type) {
|
|
2098
2214
|
const normalized = String(type).trim().toUpperCase();
|
|
@@ -2157,12 +2273,26 @@ function normalizeTypeArgsForValidation(toolName, args) {
|
|
|
2157
2273
|
...args,
|
|
2158
2274
|
type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
|
|
2159
2275
|
};
|
|
2276
|
+
case 'SAPTransport':
|
|
2277
|
+
// Normalize `type` for SAPTransport actions that route through
|
|
2278
|
+
// objectBasePath (e.g. when a future action accepts a slash-form
|
|
2279
|
+
// workbench type). Codex review of PR #223 flagged this gap: without
|
|
2280
|
+
// normalization, a caller passing `type: 'FUNC/FM'` would slip past the
|
|
2281
|
+
// string-typed schema and hit the slash-form throw inside objectBasePath,
|
|
2282
|
+
// which is correct as a last-resort fence but not as a friendly error.
|
|
2283
|
+
return {
|
|
2284
|
+
...args,
|
|
2285
|
+
type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
|
|
2286
|
+
};
|
|
2160
2287
|
default:
|
|
2161
2288
|
return args;
|
|
2162
2289
|
}
|
|
2163
2290
|
}
|
|
2164
|
-
/**
|
|
2165
|
-
|
|
2291
|
+
/**
|
|
2292
|
+
* Base path for an object type. Returns path prefix without trailing name segment.
|
|
2293
|
+
* Exported for tests (Plan A Task 4 — exhaustiveness guard regression test).
|
|
2294
|
+
*/
|
|
2295
|
+
export function objectBasePath(type) {
|
|
2166
2296
|
switch (type) {
|
|
2167
2297
|
case 'PROG':
|
|
2168
2298
|
return '/sap/bc/adt/programs/programs/';
|
|
@@ -2171,7 +2301,23 @@ function objectBasePath(type) {
|
|
|
2171
2301
|
case 'INTF':
|
|
2172
2302
|
return '/sap/bc/adt/oo/interfaces/';
|
|
2173
2303
|
case 'FUNC':
|
|
2174
|
-
|
|
2304
|
+
// Codex review of PR #223 follow-up: function modules cannot be
|
|
2305
|
+
// addressed with a single base path — they live at
|
|
2306
|
+
// /sap/bc/adt/functions/groups/{group}/fmodules/{fm} and require the
|
|
2307
|
+
// parent function group. Returning the group prefix for FUNC was the
|
|
2308
|
+
// pre-PR behaviour and silently mis-routed a real ADT search result
|
|
2309
|
+
// `{ type: "FUGR/FF", name: "BAPI_USER_GETLIST" }` (which now
|
|
2310
|
+
// canonicalises to FUNC) to /functions/groups/BAPI_USER_GETLIST. Throw
|
|
2311
|
+
// so generic URL builders (SAPActivate / SAPDiagnose / SAPTransport via
|
|
2312
|
+
// objectUrlForType) fail loudly. SAPRead and SAPNavigate handle FUNC
|
|
2313
|
+
// through dedicated `case 'FUNC'` branches that take a `group` arg and
|
|
2314
|
+
// build the correct URL via client.getFunction(group, name) — those
|
|
2315
|
+
// paths do not call objectBasePath and remain unaffected.
|
|
2316
|
+
throw new Error(`objectBasePath: type 'FUNC' (function module) cannot be resolved to a ` +
|
|
2317
|
+
`single base path — it requires the parent function group via ` +
|
|
2318
|
+
`client.getFunction(group, name) or an explicit /sap/bc/adt/functions/` +
|
|
2319
|
+
`groups/{group}/fmodules/{name} URI. Caller must take the FUNC-aware ` +
|
|
2320
|
+
`path or pass 'uri' directly. See PR #223 codex follow-up.`);
|
|
2175
2321
|
case 'INCL':
|
|
2176
2322
|
return '/sap/bc/adt/programs/includes/';
|
|
2177
2323
|
case 'FUGR':
|
|
@@ -2189,9 +2335,10 @@ function objectBasePath(type) {
|
|
|
2189
2335
|
case 'SRVB':
|
|
2190
2336
|
return '/sap/bc/adt/businessservices/bindings/';
|
|
2191
2337
|
case 'TABL':
|
|
2338
|
+
// Default URL prefix for TABL: /tables/ (transparent tables). DDIC structures
|
|
2339
|
+
// live at /sap/bc/adt/ddic/structures/<name>; for those, callers must use
|
|
2340
|
+
// AdtClient.resolveTablObjectUrl(name) which falls back on 404.
|
|
2192
2341
|
return '/sap/bc/adt/ddic/tables/';
|
|
2193
|
-
case 'STRU':
|
|
2194
|
-
return '/sap/bc/adt/ddic/structures/';
|
|
2195
2342
|
case 'DOMA':
|
|
2196
2343
|
return '/sap/bc/adt/ddic/domains/';
|
|
2197
2344
|
case 'DTEL':
|
|
@@ -2201,10 +2348,45 @@ function objectBasePath(type) {
|
|
|
2201
2348
|
case 'DEVC':
|
|
2202
2349
|
return '/sap/bc/adt/packages/';
|
|
2203
2350
|
case 'TRAN':
|
|
2351
|
+
// VIT generic-object endpoint. The 'trant' infix is the ADT workbench type
|
|
2352
|
+
// for transactions; live a4h + npl 2026-05-08 confirm GET with this prefix
|
|
2353
|
+
// returns 200 for SE38/SU01.
|
|
2204
2354
|
return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
|
|
2355
|
+
case 'VIEW':
|
|
2356
|
+
// VIT generic-object endpoint for DDIC views. /sap/bc/adt/ddic/views/
|
|
2357
|
+
// returns HTTP 500 on a4h + npl (verified 2026-05-08); only the VIT URL
|
|
2358
|
+
// works. Without this case, VIEW reads silently fell through to
|
|
2359
|
+
// /programs/programs/ — see research/abap-types/types/view.md.
|
|
2360
|
+
return '/sap/bc/adt/vit/wb/object_type/viewdv/object_name/';
|
|
2205
2361
|
case 'SKTD':
|
|
2206
2362
|
return '/sap/bc/adt/documentation/ktd/documents/';
|
|
2207
2363
|
default:
|
|
2364
|
+
// Exhaustiveness guard: canonical types in KNOWN_BASE_TYPES MUST have a
|
|
2365
|
+
// switch case — that catches the silent-fallthrough bug class (VIEW pre-PR).
|
|
2366
|
+
if (KNOWN_BASE_TYPES.has(type)) {
|
|
2367
|
+
throw new Error(`objectBasePath: canonical type '${type}' is in KNOWN_BASE_TYPES but ` +
|
|
2368
|
+
`has no switch case. Add a case here or remove it from KNOWN_BASE_TYPES. ` +
|
|
2369
|
+
`See docs/plans/completed/audit-purge-invented-adt-types.md.`);
|
|
2370
|
+
}
|
|
2371
|
+
// Slash-form guard: a normalized slash code (e.g. 'FUNC/FM', 'CLAS/LI',
|
|
2372
|
+
// 'VIEW/V', 'TRAN/O') must NEVER reach here. If it did, normalizeObjectType
|
|
2373
|
+
// failed to map it and we'd silently route the request to the program
|
|
2374
|
+
// endpoint. Tools like SAPNavigate/SAPActivate/SAPDiagnose/SAPTransport
|
|
2375
|
+
// accept `type: string` (no enum), so the schema layer can't catch this
|
|
2376
|
+
// for them — only this guard can. Throw with a hint pointing at the
|
|
2377
|
+
// citation guard so the contributor adds the alias correctly. Codex
|
|
2378
|
+
// review of PR #223 caught that the previous default-fallback could
|
|
2379
|
+
// still silently route removed aliases via these non-enum tools.
|
|
2380
|
+
if (type.includes('/')) {
|
|
2381
|
+
throw new Error(`objectBasePath: refusing to build URL for slash-form type '${type}' — ` +
|
|
2382
|
+
`this normally indicates an invented or unmapped ADT slash code. Add ` +
|
|
2383
|
+
`it to SLASH_TYPE_MAP + SLASH_TYPE_EVIDENCE (with a research entry) ` +
|
|
2384
|
+
`if it is real, or correct the caller. See ` +
|
|
2385
|
+
`docs/plans/completed/audit-purge-invented-adt-types.md and ` +
|
|
2386
|
+
`tests/unit/handlers/slash-type-map.test.ts.`);
|
|
2387
|
+
}
|
|
2388
|
+
// Unknown raw inputs (no slash, not canonical) fall through to the
|
|
2389
|
+
// program path so legacy callers like inferObjectType keep working.
|
|
2208
2390
|
return '/sap/bc/adt/programs/programs/';
|
|
2209
2391
|
}
|
|
2210
2392
|
}
|
|
@@ -2255,8 +2437,31 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2255
2437
|
if (action !== 'batch_create' && (!type || !name)) {
|
|
2256
2438
|
return errorResult('"type" and "name" are required for this action.');
|
|
2257
2439
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2440
|
+
// SAP TADIR stores object names uppercase. Mixed-case names cause silent corruption
|
|
2441
|
+
// (e.g. DDLS created as "Zc_MyView" registers as "ZC_MYVIEW" in TADIR but the source body
|
|
2442
|
+
// still contains "Zc_MyView", confusing every downstream tool). Reject pre-flight on create —
|
|
2443
|
+
// applies on every SAP release; this is universal SAP convention, not a 7.50 quirk.
|
|
2444
|
+
// Note: source code INSIDE the object can use mixed case (e.g. for DDLS: name="ZC_MYVIEW"
|
|
2445
|
+
// but `define view entity Zc_MyView` is fine inside the source body).
|
|
2446
|
+
if (action === 'create' && name && name !== name.toUpperCase()) {
|
|
2447
|
+
return errorResult(`Object name "${name}" contains lowercase characters. SAP object names must be uppercase (e.g. "${name.toUpperCase()}").\n\n` +
|
|
2448
|
+
`Note: the object NAME in TADIR must be uppercase, but the source code inside the object can use mixed case ` +
|
|
2449
|
+
`(e.g. for DDLS: name="${name.toUpperCase()}" but source can contain "define view entity ${name}").`);
|
|
2450
|
+
}
|
|
2451
|
+
// For TABL update/delete/edit_method, the existing object may live at /tables/
|
|
2452
|
+
// (transparent) or /structures/ (DDIC structure). Resolve once via the client's
|
|
2453
|
+
// cached URL probe. For 'create' the default /tables/ URL is correct (we only
|
|
2454
|
+
// create transparent tables today; structure creation is out of scope).
|
|
2455
|
+
let objectUrl;
|
|
2456
|
+
let srcUrl;
|
|
2457
|
+
if (type === 'TABL' && action !== 'create' && action !== 'batch_create') {
|
|
2458
|
+
objectUrl = await client.resolveTablObjectUrl(name);
|
|
2459
|
+
srcUrl = `${objectUrl}/source/main`;
|
|
2460
|
+
}
|
|
2461
|
+
else {
|
|
2462
|
+
objectUrl = objectUrlForType(type, name);
|
|
2463
|
+
srcUrl = sourceUrlForType(type, name);
|
|
2464
|
+
}
|
|
2260
2465
|
const invalidateWrittenObject = (objType = type, objName = name) => {
|
|
2261
2466
|
cachingLayer?.invalidate(objType, objName, 'all');
|
|
2262
2467
|
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
@@ -2283,8 +2488,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2283
2488
|
// responsible/masterLanguage/packageRef/refObject metadata.
|
|
2284
2489
|
const { source: currentEnvelope } = await client.getKtd(name);
|
|
2285
2490
|
const body = rewriteKtdText(currentEnvelope, source);
|
|
2286
|
-
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, transport);
|
|
2287
|
-
invalidateWrittenObject();
|
|
2491
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, transport, cachedFeatures?.abapRelease);
|
|
2492
|
+
invalidateWrittenObject(type, name);
|
|
2288
2493
|
return textResult(`Successfully updated ${type} ${name}.`);
|
|
2289
2494
|
}
|
|
2290
2495
|
if (isMetadataWriteType(type)) {
|
|
@@ -2296,8 +2501,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2296
2501
|
const description = String(args.description ?? mergedProps._description ?? name);
|
|
2297
2502
|
const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
|
|
2298
2503
|
const body = buildCreateXml(type, name, pkg, description, mergedProps);
|
|
2299
|
-
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport);
|
|
2300
|
-
invalidateWrittenObject();
|
|
2504
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport, cachedFeatures?.abapRelease);
|
|
2505
|
+
invalidateWrittenObject(type, name);
|
|
2301
2506
|
return textResult(`Successfully updated ${type} ${name}.`);
|
|
2302
2507
|
}
|
|
2303
2508
|
// RAP deterministic preflight validation
|
|
@@ -2316,8 +2521,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2316
2521
|
const checkNotes = await runPreWriteSyntaxCheck(client, type, source, objectUrl, config, checkOverride);
|
|
2317
2522
|
// If safeUpdateSource throws (lock conflict, network error, etc.), checkNotes
|
|
2318
2523
|
// is intentionally discarded — pre-check warnings only matter when the write succeeded.
|
|
2319
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
|
|
2320
|
-
invalidateWrittenObject();
|
|
2524
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport, cachedFeatures?.abapRelease);
|
|
2525
|
+
invalidateWrittenObject(type, name);
|
|
2321
2526
|
const msg = `Successfully updated ${type} ${name}.`;
|
|
2322
2527
|
const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
|
|
2323
2528
|
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint);
|
|
@@ -2361,6 +2566,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2361
2566
|
// SAP will return its own error if a transport is actually needed.
|
|
2362
2567
|
}
|
|
2363
2568
|
}
|
|
2569
|
+
// MSAG transport-vs-task guard. Some SAP releases silently drop message inserts when
|
|
2570
|
+
// given a task number as corrNr — CL_ADT_MESSAGE_CLASS_API=>create() passes corrNr to
|
|
2571
|
+
// CTS_WBO_API_INSERT_OBJECTS which only accepts request numbers. The TADIR entry is
|
|
2572
|
+
// created but T100/T100A are never written, leaving a phantom MSAG. Confirmed on NW 7.50;
|
|
2573
|
+
// unclear whether later releases fixed it, so validate everywhere.
|
|
2574
|
+
// Cost: one extra HTTP roundtrip per MSAG create (negligible vs. the data loss risk).
|
|
2575
|
+
if (type === 'MSAG' && effectiveTransport) {
|
|
2576
|
+
const tr = await getTransport(client.http, client.safety, effectiveTransport);
|
|
2577
|
+
if (!tr) {
|
|
2578
|
+
return errorResult(`Transport "${effectiveTransport}" is not a valid transport request. ` +
|
|
2579
|
+
`MSAG creation requires a transport request number, not a task number. ` +
|
|
2580
|
+
`Use SAPTransport(action="get", id="<request>") to verify, or SAPTransport(action="list") to find modifiable requests.`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2364
2583
|
// CDS pre-write validation: reject unsupported syntax early
|
|
2365
2584
|
const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
|
|
2366
2585
|
if (cdsGuard)
|
|
@@ -2398,7 +2617,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2398
2617
|
<sktd:refObject adtcore:description="${escapeXml(refDescription)}" adtcore:name="${escapeXml(refName)}" adtcore:type="${escapeXml(refType)}" adtcore:uri="${escapeXml(refUri)}"/>
|
|
2399
2618
|
</sktd:docu>`;
|
|
2400
2619
|
const ktdCreateUrl = '/sap/bc/adt/documentation/ktd/documents';
|
|
2401
|
-
const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport);
|
|
2620
|
+
const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
2402
2621
|
// If initial Markdown was provided, follow up with an update PUT to write it.
|
|
2403
2622
|
// Same envelope contract as the update path: fetch-then-rewrite ensures we
|
|
2404
2623
|
// PUT back exactly the shape SAP gave us (with all the server-assigned
|
|
@@ -2406,8 +2625,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2406
2625
|
if (source) {
|
|
2407
2626
|
const { source: currentEnvelope } = await client.getKtd(name);
|
|
2408
2627
|
const body = rewriteKtdText(currentEnvelope, source);
|
|
2409
|
-
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport);
|
|
2410
|
-
invalidateWrittenObject();
|
|
2628
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport, cachedFeatures?.abapRelease);
|
|
2629
|
+
invalidateWrittenObject(type, name);
|
|
2411
2630
|
return textResult(`Created SKTD ${name} in package ${pkg} and wrote Markdown content.\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
|
|
2412
2631
|
}
|
|
2413
2632
|
invalidateWrittenObject();
|
|
@@ -2427,7 +2646,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2427
2646
|
const needsPackageParam = type === 'BDEF' || type === 'TABL';
|
|
2428
2647
|
let result;
|
|
2429
2648
|
try {
|
|
2430
|
-
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined);
|
|
2649
|
+
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
2431
2650
|
}
|
|
2432
2651
|
catch (createErr) {
|
|
2433
2652
|
if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
|
|
@@ -2445,7 +2664,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2445
2664
|
if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
|
|
2446
2665
|
const ct = vendorContentTypeForType(type);
|
|
2447
2666
|
await client.http.withStatefulSession(async (session) => {
|
|
2448
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2667
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2449
2668
|
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
2450
2669
|
try {
|
|
2451
2670
|
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
@@ -2459,7 +2678,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2459
2678
|
if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
|
|
2460
2679
|
const ct = vendorContentTypeForType(type);
|
|
2461
2680
|
await client.http.withStatefulSession(async (session) => {
|
|
2462
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2681
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2463
2682
|
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
2464
2683
|
try {
|
|
2465
2684
|
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
@@ -2482,8 +2701,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2482
2701
|
if (lintWarnings.blocked) {
|
|
2483
2702
|
return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
|
|
2484
2703
|
}
|
|
2485
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
|
|
2486
|
-
invalidateWrittenObject();
|
|
2704
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport, cachedFeatures?.abapRelease);
|
|
2705
|
+
invalidateWrittenObject(type, name);
|
|
2487
2706
|
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
2488
2707
|
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings);
|
|
2489
2708
|
return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
|
|
@@ -2519,8 +2738,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2519
2738
|
// Pre-write server-side syntax check on the full spliced source (opt-in; warnings only).
|
|
2520
2739
|
const checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
|
|
2521
2740
|
// Write the full source back (existing lock/modify/unlock flow)
|
|
2522
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport);
|
|
2523
|
-
invalidateWrittenObject();
|
|
2741
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport, cachedFeatures?.abapRelease);
|
|
2742
|
+
invalidateWrittenObject(type, name);
|
|
2524
2743
|
const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
|
|
2525
2744
|
const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
|
|
2526
2745
|
return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
|
|
@@ -2664,7 +2883,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2664
2883
|
// at the class object URL, and every include PUT carries the same lockHandle.
|
|
2665
2884
|
// This mirrors how ADT-in-Eclipse saves a multi-include class in one commit.
|
|
2666
2885
|
await client.http.withStatefulSession(async (session) => {
|
|
2667
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2886
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2668
2887
|
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
2669
2888
|
try {
|
|
2670
2889
|
if (changed.main) {
|
|
@@ -2711,7 +2930,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2711
2930
|
// Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
|
|
2712
2931
|
try {
|
|
2713
2932
|
await client.http.withStatefulSession(async (session) => {
|
|
2714
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2933
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2715
2934
|
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
2716
2935
|
try {
|
|
2717
2936
|
await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
|
|
@@ -2783,12 +3002,45 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2783
3002
|
}
|
|
2784
3003
|
const results = [];
|
|
2785
3004
|
const batchWarnings = [];
|
|
3005
|
+
// Per-batch cache for the MSAG transport-vs-task guard. The bug is universal so the
|
|
3006
|
+
// guard fires for every MSAG entry, but a batch typically shares one transport — cache
|
|
3007
|
+
// the lookup result to avoid one HTTP roundtrip per object.
|
|
3008
|
+
const transportLookupCache = new Map();
|
|
2786
3009
|
for (const obj of objects) {
|
|
2787
3010
|
const objType = normalizeObjectType(String(obj.type ?? ''));
|
|
2788
3011
|
const objName = String(obj.name ?? '');
|
|
2789
3012
|
const metadataObject = isMetadataWriteType(objType);
|
|
2790
3013
|
const objSource = obj.source ? String(obj.source) : undefined;
|
|
2791
3014
|
const objDescription = String(obj.description ?? objName);
|
|
3015
|
+
// Mixed-case object name rejection (matches the create-path check above).
|
|
3016
|
+
// Universal SAP convention — TADIR is uppercase on every release.
|
|
3017
|
+
// Cheap check first: no HTTP call, fail fast on bad names.
|
|
3018
|
+
if (objName && objName !== objName.toUpperCase()) {
|
|
3019
|
+
results.push({
|
|
3020
|
+
type: objType,
|
|
3021
|
+
name: objName,
|
|
3022
|
+
status: 'failed',
|
|
3023
|
+
error: `Object name "${objName}" contains lowercase characters. SAP object names must be uppercase (e.g. "${objName.toUpperCase()}"). Source code inside the object can use mixed case.`,
|
|
3024
|
+
});
|
|
3025
|
+
break;
|
|
3026
|
+
}
|
|
3027
|
+
// MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
|
|
3028
|
+
if (objType === 'MSAG' && batchTransport) {
|
|
3029
|
+
let tr = transportLookupCache.get(batchTransport);
|
|
3030
|
+
if (tr === undefined) {
|
|
3031
|
+
tr = await getTransport(client.http, client.safety, batchTransport);
|
|
3032
|
+
transportLookupCache.set(batchTransport, tr);
|
|
3033
|
+
}
|
|
3034
|
+
if (!tr) {
|
|
3035
|
+
results.push({
|
|
3036
|
+
type: objType,
|
|
3037
|
+
name: objName,
|
|
3038
|
+
status: 'failed',
|
|
3039
|
+
error: `Transport "${batchTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
|
|
3040
|
+
});
|
|
3041
|
+
break;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2792
3044
|
// AFF header validation per object (if schema available)
|
|
2793
3045
|
const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
|
|
2794
3046
|
if (!affResult.valid) {
|
|
@@ -2836,7 +3088,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2836
3088
|
const contentType = createContentTypeForType(objType);
|
|
2837
3089
|
const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
|
|
2838
3090
|
try {
|
|
2839
|
-
await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport, needsPackageParam ? pkg : undefined);
|
|
3091
|
+
await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
2840
3092
|
}
|
|
2841
3093
|
catch (createErr) {
|
|
2842
3094
|
if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
|
|
@@ -2850,7 +3102,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2850
3102
|
// Step 1b: DTEL POST ignores labels — follow up with PUT on main session
|
|
2851
3103
|
if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
|
|
2852
3104
|
await client.http.withStatefulSession(async (session) => {
|
|
2853
|
-
const lock = await lockObject(session, client.safety, objUrl);
|
|
3105
|
+
const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2854
3106
|
const lockTransport = batchTransport ?? (lock.corrNr || undefined);
|
|
2855
3107
|
try {
|
|
2856
3108
|
await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
|
|
@@ -2863,7 +3115,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2863
3115
|
// Step 2: Write source if provided
|
|
2864
3116
|
if (!metadataObject && objSource) {
|
|
2865
3117
|
const srcUrl = sourceUrlForType(objType, objName);
|
|
2866
|
-
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport);
|
|
3118
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport, cachedFeatures?.abapRelease);
|
|
2867
3119
|
}
|
|
2868
3120
|
// Step 3: Activate the object
|
|
2869
3121
|
const activationResult = await activate(client.http, client.safety, objUrl);
|
|
@@ -3019,7 +3271,7 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
|
|
|
3019
3271
|
}
|
|
3020
3272
|
}
|
|
3021
3273
|
/** Types that carry source code that SAP's /checkruns endpoint can meaningfully compile.
|
|
3022
|
-
* Metadata-write types (DOMA/DTEL/TABL/
|
|
3274
|
+
* Metadata-write types (DOMA/DTEL/TABL/MSAG/DEVC/SKTD) have no /source/main artifact. */
|
|
3023
3275
|
const SYNTAX_CHECKABLE_TYPES = new Set([
|
|
3024
3276
|
'PROG',
|
|
3025
3277
|
'CLAS',
|
|
@@ -3165,11 +3417,15 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
3165
3417
|
const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
|
|
3166
3418
|
if (args.objects && Array.isArray(args.objects)) {
|
|
3167
3419
|
const rawObjects = args.objects;
|
|
3168
|
-
|
|
3420
|
+
// Resolve URLs sequentially. For TABL we await the URL resolver so DDIC
|
|
3421
|
+
// structures (which live at /sap/bc/adt/ddic/structures/) are addressed
|
|
3422
|
+
// correctly; the resolver short-circuits on its in-memory cache.
|
|
3423
|
+
const objects = await Promise.all(rawObjects.map(async (o) => {
|
|
3169
3424
|
const objType = normalizeObjectType(String(o.type ?? type));
|
|
3170
3425
|
const objName = String(o.name ?? '');
|
|
3171
|
-
|
|
3172
|
-
|
|
3426
|
+
const url = objType === 'TABL' ? await client.resolveTablObjectUrl(objName) : objectUrlForType(objType, objName);
|
|
3427
|
+
return { type: objType, name: objName, url };
|
|
3428
|
+
}));
|
|
3173
3429
|
const result = await activateBatch(client.http, client.safety, objects, activateOpts);
|
|
3174
3430
|
const names = objects.map((o) => o.name).join(', ');
|
|
3175
3431
|
const batchStatuses = buildBatchActivationStatuses(objects, result);
|
|
@@ -3191,8 +3447,10 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
3191
3447
|
.join('');
|
|
3192
3448
|
return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
|
|
3193
3449
|
}
|
|
3194
|
-
// Single activation (existing behavior)
|
|
3195
|
-
|
|
3450
|
+
// Single activation (existing behavior). For TABL we resolve the URL because
|
|
3451
|
+
// the existing object may live at /tables/ (transparent) or /structures/
|
|
3452
|
+
// (DDIC structure); using the wrong one would produce a confusing 404.
|
|
3453
|
+
const objectUrl = type === 'TABL' ? await client.resolveTablObjectUrl(name) : objectUrlForType(type, name);
|
|
3196
3454
|
const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
|
|
3197
3455
|
if (result.success) {
|
|
3198
3456
|
cachingLayer?.invalidate(type, name, 'all');
|
|
@@ -3320,6 +3578,15 @@ async function handleSAPNavigate(client, args) {
|
|
|
3320
3578
|
return errorResult(`Cannot resolve function group for "${symName}". Provide the full uri parameter, or use SAPSearch("${symName}") to find the ADT URI.`);
|
|
3321
3579
|
}
|
|
3322
3580
|
}
|
|
3581
|
+
else if (symType === 'TABL') {
|
|
3582
|
+
// DDIC TABL: where-used and other navigate paths must use the canonical
|
|
3583
|
+
// object URL — `/sap/bc/adt/ddic/tables/{name}` for transparent tables,
|
|
3584
|
+
// `/sap/bc/adt/ddic/structures/{name}` for DDIC structures. NW 7.50
|
|
3585
|
+
// returns 500 from usageReferences for /tables/ URLs even for transparent
|
|
3586
|
+
// tables, so we always resolve before building. resolveTablObjectUrl
|
|
3587
|
+
// caches on the AdtClient, so this is one HTTP probe per cold name.
|
|
3588
|
+
uri = await client.resolveTablObjectUrl(symName);
|
|
3589
|
+
}
|
|
3323
3590
|
else {
|
|
3324
3591
|
uri = objectUrlForType(symType, symName);
|
|
3325
3592
|
}
|
|
@@ -3366,6 +3633,39 @@ async function handleSAPNavigate(client, args) {
|
|
|
3366
3633
|
throw err;
|
|
3367
3634
|
}
|
|
3368
3635
|
}
|
|
3636
|
+
// Augment interface where-used with implementing classes from SEOMETAREL.
|
|
3637
|
+
// SAP's scope-based usageReferences endpoint sometimes does NOT surface
|
|
3638
|
+
// interface→implementing-class links — the implementations sit inside a
|
|
3639
|
+
// `canHaveChildren="true"` Interface Section node, and the snippet
|
|
3640
|
+
// expansion endpoint returns 404 on every release we've probed (NW 7.50,
|
|
3641
|
+
// S/4HANA 2023). SEOMETAREL is the canonical OO-relation table and is
|
|
3642
|
+
// always populated, so this augmentation makes references reliable for
|
|
3643
|
+
// interfaces. Silently skipped when SQL/data access isn't available.
|
|
3644
|
+
const intfMatch = uri.match(/\/sap\/bc\/adt\/oo\/interfaces\/([^/?]+)/i);
|
|
3645
|
+
if (intfMatch && (!objectType || /^CLAS/i.test(objectType))) {
|
|
3646
|
+
const interfaceName = decodeURIComponent(intfMatch[1]).toUpperCase();
|
|
3647
|
+
const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
|
|
3648
|
+
const canQuery = isOperationAllowed(client.safety, OperationType.Query);
|
|
3649
|
+
try {
|
|
3650
|
+
let implementers = [];
|
|
3651
|
+
if (canFreeSQL) {
|
|
3652
|
+
implementers = await findInterfaceImplementersViaSeoMetaRel((sql, max) => client.runQuery(sql, max), interfaceName);
|
|
3653
|
+
}
|
|
3654
|
+
else if (canQuery) {
|
|
3655
|
+
implementers = await findInterfaceImplementersViaSeoMetaRel((_sql, max) => client.getTableContents('SEOMETAREL', max, `REFCLSNAME = '${interfaceName}' AND RELTYPE = '1'`), interfaceName);
|
|
3656
|
+
}
|
|
3657
|
+
// Dedupe: don't add an implementer if SAP already returned it
|
|
3658
|
+
const existingNames = new Set(results.map((r) => r.name?.toUpperCase()).filter(Boolean));
|
|
3659
|
+
const augmented = implementers.filter((r) => !existingNames.has(r.name.toUpperCase()));
|
|
3660
|
+
if (augmented.length > 0) {
|
|
3661
|
+
results.push(...augmented);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
catch {
|
|
3665
|
+
// SEOMETAREL augmentation is best-effort; if SQL fails, fall back to
|
|
3666
|
+
// whatever the where-used HTTP endpoint returned. Don't block the response.
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3369
3669
|
if (results.length === 0) {
|
|
3370
3670
|
return textResult('No references found.');
|
|
3371
3671
|
}
|
|
@@ -3890,8 +4190,8 @@ async function handleSAPTransport(client, args) {
|
|
|
3890
4190
|
const description = String(args.description ?? '');
|
|
3891
4191
|
if (!description)
|
|
3892
4192
|
return errorResult('Description is required for "create" action.');
|
|
3893
|
-
const
|
|
3894
|
-
const id = await createTransport(client.http, client.safety, description,
|
|
4193
|
+
const targetPackage = args.package ? String(args.package) : undefined;
|
|
4194
|
+
const id = await createTransport(client.http, client.safety, description, targetPackage);
|
|
3895
4195
|
if (!id)
|
|
3896
4196
|
return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
|
|
3897
4197
|
return textResult(`Created transport request: ${id}`);
|
|
@@ -4389,7 +4689,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
4389
4689
|
transportLayer: transportLayer || undefined,
|
|
4390
4690
|
packageType,
|
|
4391
4691
|
});
|
|
4392
|
-
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport);
|
|
4692
|
+
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
4393
4693
|
return textResult(`Created package ${name}.`);
|
|
4394
4694
|
}
|
|
4395
4695
|
case 'delete_package': {
|
|
@@ -4400,7 +4700,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
4400
4700
|
checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
|
|
4401
4701
|
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
4402
4702
|
await client.http.withStatefulSession(async (session) => {
|
|
4403
|
-
const lock = await lockObject(session, client.safety, packageUrl);
|
|
4703
|
+
const lock = await lockObject(session, client.safety, packageUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
4404
4704
|
const effectiveTransport = transport || lock.corrNr || undefined;
|
|
4405
4705
|
try {
|
|
4406
4706
|
await deleteObject(session, client.safety, packageUrl, lock.lockHandle, effectiveTransport);
|