arc-1 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/adt/client.d.ts +57 -3
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +108 -7
- 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 +2 -0
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +18 -6
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +1 -0
- 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 +12 -0
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +15 -3
- 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 -77
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +7 -2
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +18 -4
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +46 -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/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/package.json +3 -3
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,7 +219,7 @@ 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);
|
|
@@ -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) {
|
|
@@ -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
|
}
|
|
@@ -1293,7 +1314,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1293
1314
|
return textResult(JSON.stringify(methods, null, 2));
|
|
1294
1315
|
}
|
|
1295
1316
|
case 'DEVC': {
|
|
1296
|
-
const
|
|
1317
|
+
const maxResults = args.maxResults != null ? Number(args.maxResults) : undefined;
|
|
1318
|
+
const contents = await client.getPackageContents(name, maxResults);
|
|
1297
1319
|
return textResult(JSON.stringify(contents, null, 2));
|
|
1298
1320
|
}
|
|
1299
1321
|
case 'SYSTEM':
|
|
@@ -1302,7 +1324,17 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1302
1324
|
const components = await client.getInstalledComponents();
|
|
1303
1325
|
return textResult(JSON.stringify(components, null, 2));
|
|
1304
1326
|
}
|
|
1305
|
-
case 'MESSAGES':
|
|
1327
|
+
case 'MESSAGES':
|
|
1328
|
+
case 'MSAG': {
|
|
1329
|
+
// MSAG is the canonical TADIR R3TR type for message classes; 'MESSAGES' is a
|
|
1330
|
+
// deprecated read alias kept for one minor release. See
|
|
1331
|
+
// research/abap-types/types/msag.md.
|
|
1332
|
+
if (type === 'MESSAGES') {
|
|
1333
|
+
logger.warn('SAPRead type "MESSAGES" is deprecated — use "MSAG" instead', {
|
|
1334
|
+
type: 'MESSAGES',
|
|
1335
|
+
replacement: 'MSAG',
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1306
1338
|
try {
|
|
1307
1339
|
const mcInfo = await client.getMessageClassInfo(name);
|
|
1308
1340
|
return textResult(JSON.stringify(mcInfo, null, 2));
|
|
@@ -1356,7 +1388,7 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1356
1388
|
return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
|
|
1357
1389
|
}
|
|
1358
1390
|
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,
|
|
1391
|
+
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
1392
|
'Tip: Type aliases are auto-normalized (e.g., DDLS/DF → DDLS, DCLS/DL → DCLS, CLAS/OC → CLAS, PROG/P → PROG). ' +
|
|
1361
1393
|
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
1362
1394
|
}
|
|
@@ -2068,31 +2100,116 @@ function escapeXml(s) {
|
|
|
2068
2100
|
.replace(/>/g, '>');
|
|
2069
2101
|
}
|
|
2070
2102
|
// ─── 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
|
-
'
|
|
2103
|
+
// Every entry verified against either Eclipse ADT apidoc 3.58.1, live a4h S/4HANA
|
|
2104
|
+
// 2023 + npl NW 7.50 ADT responses (captured 2026-05-08 — both systems agree), or
|
|
2105
|
+
// abap-file-formats schemas. Per-entry evidence in research/abap-types/types/<x>.md.
|
|
2106
|
+
// SLASH_TYPE_EVIDENCE below MUST stay key-equal (anti-cargo-cult guard, enforced by
|
|
2107
|
+
// tests/unit/handlers/slash-type-map.test.ts — see issue #218 follow-up).
|
|
2108
|
+
// Exported for tests only — the citation guard
|
|
2109
|
+
// (tests/unit/handlers/slash-type-map.test.ts) needs to assert key-equality
|
|
2110
|
+
// against SLASH_TYPE_EVIDENCE so a new entry without evidence fails CI.
|
|
2111
|
+
// Production callers should keep using normalizeObjectType().
|
|
2112
|
+
export const SLASH_TYPE_MAP = {
|
|
2113
|
+
'PROG/P': 'PROG', // research/abap-types/types/prog.md
|
|
2114
|
+
'PROG/I': 'INCL', // research/abap-types/types/incl.md
|
|
2115
|
+
'CLAS/OC': 'CLAS', // research/abap-types/types/clas.md
|
|
2116
|
+
// 'CLAS/LI' removed — invented; absent from Eclipse apidoc; no live ADT response
|
|
2117
|
+
// emits it. Pass-through means schema validation rejects it loudly.
|
|
2118
|
+
'INTF/OI': 'INTF', // research/abap-types/types/intf.md
|
|
2119
|
+
// 'FUNC/FM' removed — invented; ADT emits FUGR/FF for function modules, not
|
|
2120
|
+
// FUNC/FM. Function modules are LIMU FUNC under R3TR FUGR.
|
|
2121
|
+
'FUGR/F': 'FUGR', // function group container — research/abap-types/types/fugr.md
|
|
2122
|
+
// FUGR/FF is a function module (LIMU FUNC under FUGR), not the function group.
|
|
2123
|
+
// Live a4h: GET .../groups/su_user/fmodules/bapi_user_getlist returns
|
|
2124
|
+
// adtcore:type="FUGR/FF" with <adtcore:containerRef adtcore:type="FUGR/F"/>.
|
|
2125
|
+
'FUGR/FF': 'FUNC', // research/abap-types/types/fugr.md + func.md
|
|
2126
|
+
'DDLS/DF': 'DDLS', // research/abap-types/types/ddls.md
|
|
2127
|
+
'DCLS/DL': 'DCLS', // research/abap-types/types/dcls.md
|
|
2128
|
+
'BDEF/BDO': 'BDEF', // research/abap-types/types/bdef.md
|
|
2129
|
+
'SRVD/SRV': 'SRVD', // research/abap-types/types/srvd.md
|
|
2130
|
+
'SRVB/SVB': 'SRVB', // research/abap-types/types/srvb.md
|
|
2131
|
+
'DDLX/EX': 'DDLX', // research/abap-types/types/ddlx.md (live a4h + npl 2026-05-08)
|
|
2132
|
+
// DDIC TABL: ADT exposes /DT (transparent table) and /DS (DDIC structure)
|
|
2133
|
+
// subtypes. Both share TADIR R3TR TABL (DD02L-TABCLASS = TRANSP vs INTTAB).
|
|
2134
|
+
// ARC-1 collapses both into the canonical short type 'TABL' (Model B — see
|
|
2135
|
+
// docs/plans/completed/collapse-stru-into-tabl.md).
|
|
2136
|
+
'TABL/DT': 'TABL', // research/abap-types/types/tabl.md
|
|
2137
|
+
'TABL/DS': 'TABL', // research/abap-types/types/tabl.md
|
|
2138
|
+
// Legacy slash-form alias — ADT never actually returns this, but pre-Model-B
|
|
2139
|
+
// ARC-1 prompts learned it from older docs. Kept so they normalize to TABL
|
|
2140
|
+
// instead of producing a schema error. Bare 'STRU' is NOT aliased.
|
|
2141
|
+
'STRU/DS': 'TABL', // research/abap-types/types/tabl.md (legacy alias)
|
|
2142
|
+
'DOMA/DD': 'DOMA', // research/abap-types/types/doma.md
|
|
2143
|
+
'DTEL/DE': 'DTEL', // research/abap-types/types/dtel.md
|
|
2144
|
+
'MSAG/N': 'MSAG', // research/abap-types/types/msag.md
|
|
2145
|
+
'DEVC/K': 'DEVC', // research/abap-types/types/devc.md
|
|
2146
|
+
// TRAN/T (was TRAN/O — invented). Live a4h + npl 2026-05-08 both return
|
|
2147
|
+
// adtcore:type="TRAN/T" for SE38, SU01, etc.
|
|
2148
|
+
'TRAN/T': 'TRAN', // research/abap-types/types/tran.md
|
|
2149
|
+
// VIEW/DV (was VIEW/V — invented). Live a4h + npl 2026-05-08 both return
|
|
2150
|
+
// adtcore:type="VIEW/DV" for V_USR_NAME.
|
|
2151
|
+
'VIEW/DV': 'VIEW', // research/abap-types/types/view.md
|
|
2152
|
+
'SKTD/TYP': 'SKTD', // research/abap-types/types/sktd.md
|
|
2095
2153
|
};
|
|
2154
|
+
/**
|
|
2155
|
+
* Citation guard companion for SLASH_TYPE_MAP. Keys MUST stay key-equal to
|
|
2156
|
+
* SLASH_TYPE_MAP (enforced by tests/unit/handlers/slash-type-map.test.ts). Each
|
|
2157
|
+
* value points at a research evidence file or a fixture that backs the slash code.
|
|
2158
|
+
* Adding an entry without evidence is the anti-cargo-cult guard.
|
|
2159
|
+
*/
|
|
2160
|
+
export const SLASH_TYPE_EVIDENCE = {
|
|
2161
|
+
'PROG/P': 'research/abap-types/types/prog.md',
|
|
2162
|
+
'PROG/I': 'research/abap-types/types/incl.md',
|
|
2163
|
+
'CLAS/OC': 'research/abap-types/types/clas.md',
|
|
2164
|
+
'INTF/OI': 'research/abap-types/types/intf.md',
|
|
2165
|
+
'FUGR/F': 'research/abap-types/types/fugr.md',
|
|
2166
|
+
'FUGR/FF': 'research/abap-types/types/fugr.md',
|
|
2167
|
+
'DDLS/DF': 'research/abap-types/types/ddls.md',
|
|
2168
|
+
'DCLS/DL': 'research/abap-types/types/dcls.md',
|
|
2169
|
+
'BDEF/BDO': 'research/abap-types/types/bdef.md',
|
|
2170
|
+
'SRVD/SRV': 'research/abap-types/types/srvd.md',
|
|
2171
|
+
'SRVB/SVB': 'research/abap-types/types/srvb.md',
|
|
2172
|
+
'DDLX/EX': 'research/abap-types/types/ddlx.md',
|
|
2173
|
+
'TABL/DT': 'research/abap-types/types/tabl.md',
|
|
2174
|
+
'TABL/DS': 'research/abap-types/types/tabl.md',
|
|
2175
|
+
'STRU/DS': 'research/abap-types/types/tabl.md',
|
|
2176
|
+
'DOMA/DD': 'research/abap-types/types/doma.md',
|
|
2177
|
+
'DTEL/DE': 'research/abap-types/types/dtel.md',
|
|
2178
|
+
'MSAG/N': 'research/abap-types/types/msag.md',
|
|
2179
|
+
'DEVC/K': 'research/abap-types/types/devc.md',
|
|
2180
|
+
'TRAN/T': 'research/abap-types/types/tran.md',
|
|
2181
|
+
'VIEW/DV': 'research/abap-types/types/view.md',
|
|
2182
|
+
'SKTD/TYP': 'research/abap-types/types/sktd.md',
|
|
2183
|
+
};
|
|
2184
|
+
/**
|
|
2185
|
+
* Set of canonical short types that MUST have a working `objectBasePath` case.
|
|
2186
|
+
* Drives the exhaustiveness guard inside `objectBasePath` so a new canonical type
|
|
2187
|
+
* added to SAPRead/SAPWrite enums without an URL builder fails loudly. The VIEW
|
|
2188
|
+
* silent-fallthrough bug (research/abap-types/types/view.md) is exactly what this
|
|
2189
|
+
* guard prevents from reoccurring.
|
|
2190
|
+
*/
|
|
2191
|
+
export const KNOWN_BASE_TYPES = new Set([
|
|
2192
|
+
'PROG',
|
|
2193
|
+
'CLAS',
|
|
2194
|
+
'INTF',
|
|
2195
|
+
'INCL',
|
|
2196
|
+
'FUGR',
|
|
2197
|
+
'FUNC',
|
|
2198
|
+
'DDLS',
|
|
2199
|
+
'DCLS',
|
|
2200
|
+
'BDEF',
|
|
2201
|
+
'SRVD',
|
|
2202
|
+
'SRVB',
|
|
2203
|
+
'DDLX',
|
|
2204
|
+
'TABL',
|
|
2205
|
+
'DOMA',
|
|
2206
|
+
'DTEL',
|
|
2207
|
+
'MSAG',
|
|
2208
|
+
'DEVC',
|
|
2209
|
+
'TRAN',
|
|
2210
|
+
'VIEW',
|
|
2211
|
+
'SKTD',
|
|
2212
|
+
]);
|
|
2096
2213
|
/** Normalize ADT type codes and aliases to ARC-1 canonical short types. */
|
|
2097
2214
|
export function normalizeObjectType(type) {
|
|
2098
2215
|
const normalized = String(type).trim().toUpperCase();
|
|
@@ -2157,12 +2274,26 @@ function normalizeTypeArgsForValidation(toolName, args) {
|
|
|
2157
2274
|
...args,
|
|
2158
2275
|
type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
|
|
2159
2276
|
};
|
|
2277
|
+
case 'SAPTransport':
|
|
2278
|
+
// Normalize `type` for SAPTransport actions that route through
|
|
2279
|
+
// objectBasePath (e.g. when a future action accepts a slash-form
|
|
2280
|
+
// workbench type). Codex review of PR #223 flagged this gap: without
|
|
2281
|
+
// normalization, a caller passing `type: 'FUNC/FM'` would slip past the
|
|
2282
|
+
// string-typed schema and hit the slash-form throw inside objectBasePath,
|
|
2283
|
+
// which is correct as a last-resort fence but not as a friendly error.
|
|
2284
|
+
return {
|
|
2285
|
+
...args,
|
|
2286
|
+
type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
|
|
2287
|
+
};
|
|
2160
2288
|
default:
|
|
2161
2289
|
return args;
|
|
2162
2290
|
}
|
|
2163
2291
|
}
|
|
2164
|
-
/**
|
|
2165
|
-
|
|
2292
|
+
/**
|
|
2293
|
+
* Base path for an object type. Returns path prefix without trailing name segment.
|
|
2294
|
+
* Exported for tests (Plan A Task 4 — exhaustiveness guard regression test).
|
|
2295
|
+
*/
|
|
2296
|
+
export function objectBasePath(type) {
|
|
2166
2297
|
switch (type) {
|
|
2167
2298
|
case 'PROG':
|
|
2168
2299
|
return '/sap/bc/adt/programs/programs/';
|
|
@@ -2171,7 +2302,23 @@ function objectBasePath(type) {
|
|
|
2171
2302
|
case 'INTF':
|
|
2172
2303
|
return '/sap/bc/adt/oo/interfaces/';
|
|
2173
2304
|
case 'FUNC':
|
|
2174
|
-
|
|
2305
|
+
// Codex review of PR #223 follow-up: function modules cannot be
|
|
2306
|
+
// addressed with a single base path — they live at
|
|
2307
|
+
// /sap/bc/adt/functions/groups/{group}/fmodules/{fm} and require the
|
|
2308
|
+
// parent function group. Returning the group prefix for FUNC was the
|
|
2309
|
+
// pre-PR behaviour and silently mis-routed a real ADT search result
|
|
2310
|
+
// `{ type: "FUGR/FF", name: "BAPI_USER_GETLIST" }` (which now
|
|
2311
|
+
// canonicalises to FUNC) to /functions/groups/BAPI_USER_GETLIST. Throw
|
|
2312
|
+
// so generic URL builders (SAPActivate / SAPDiagnose / SAPTransport via
|
|
2313
|
+
// objectUrlForType) fail loudly. SAPRead and SAPNavigate handle FUNC
|
|
2314
|
+
// through dedicated `case 'FUNC'` branches that take a `group` arg and
|
|
2315
|
+
// build the correct URL via client.getFunction(group, name) — those
|
|
2316
|
+
// paths do not call objectBasePath and remain unaffected.
|
|
2317
|
+
throw new Error(`objectBasePath: type 'FUNC' (function module) cannot be resolved to a ` +
|
|
2318
|
+
`single base path — it requires the parent function group via ` +
|
|
2319
|
+
`client.getFunction(group, name) or an explicit /sap/bc/adt/functions/` +
|
|
2320
|
+
`groups/{group}/fmodules/{name} URI. Caller must take the FUNC-aware ` +
|
|
2321
|
+
`path or pass 'uri' directly. See PR #223 codex follow-up.`);
|
|
2175
2322
|
case 'INCL':
|
|
2176
2323
|
return '/sap/bc/adt/programs/includes/';
|
|
2177
2324
|
case 'FUGR':
|
|
@@ -2189,9 +2336,10 @@ function objectBasePath(type) {
|
|
|
2189
2336
|
case 'SRVB':
|
|
2190
2337
|
return '/sap/bc/adt/businessservices/bindings/';
|
|
2191
2338
|
case 'TABL':
|
|
2339
|
+
// Default URL prefix for TABL: /tables/ (transparent tables). DDIC structures
|
|
2340
|
+
// live at /sap/bc/adt/ddic/structures/<name>; for those, callers must use
|
|
2341
|
+
// AdtClient.resolveTablObjectUrl(name) which falls back on 404.
|
|
2192
2342
|
return '/sap/bc/adt/ddic/tables/';
|
|
2193
|
-
case 'STRU':
|
|
2194
|
-
return '/sap/bc/adt/ddic/structures/';
|
|
2195
2343
|
case 'DOMA':
|
|
2196
2344
|
return '/sap/bc/adt/ddic/domains/';
|
|
2197
2345
|
case 'DTEL':
|
|
@@ -2201,10 +2349,45 @@ function objectBasePath(type) {
|
|
|
2201
2349
|
case 'DEVC':
|
|
2202
2350
|
return '/sap/bc/adt/packages/';
|
|
2203
2351
|
case 'TRAN':
|
|
2352
|
+
// VIT generic-object endpoint. The 'trant' infix is the ADT workbench type
|
|
2353
|
+
// for transactions; live a4h + npl 2026-05-08 confirm GET with this prefix
|
|
2354
|
+
// returns 200 for SE38/SU01.
|
|
2204
2355
|
return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
|
|
2356
|
+
case 'VIEW':
|
|
2357
|
+
// VIT generic-object endpoint for DDIC views. /sap/bc/adt/ddic/views/
|
|
2358
|
+
// returns HTTP 500 on a4h + npl (verified 2026-05-08); only the VIT URL
|
|
2359
|
+
// works. Without this case, VIEW reads silently fell through to
|
|
2360
|
+
// /programs/programs/ — see research/abap-types/types/view.md.
|
|
2361
|
+
return '/sap/bc/adt/vit/wb/object_type/viewdv/object_name/';
|
|
2205
2362
|
case 'SKTD':
|
|
2206
2363
|
return '/sap/bc/adt/documentation/ktd/documents/';
|
|
2207
2364
|
default:
|
|
2365
|
+
// Exhaustiveness guard: canonical types in KNOWN_BASE_TYPES MUST have a
|
|
2366
|
+
// switch case — that catches the silent-fallthrough bug class (VIEW pre-PR).
|
|
2367
|
+
if (KNOWN_BASE_TYPES.has(type)) {
|
|
2368
|
+
throw new Error(`objectBasePath: canonical type '${type}' is in KNOWN_BASE_TYPES but ` +
|
|
2369
|
+
`has no switch case. Add a case here or remove it from KNOWN_BASE_TYPES. ` +
|
|
2370
|
+
`See docs/plans/completed/audit-purge-invented-adt-types.md.`);
|
|
2371
|
+
}
|
|
2372
|
+
// Slash-form guard: a normalized slash code (e.g. 'FUNC/FM', 'CLAS/LI',
|
|
2373
|
+
// 'VIEW/V', 'TRAN/O') must NEVER reach here. If it did, normalizeObjectType
|
|
2374
|
+
// failed to map it and we'd silently route the request to the program
|
|
2375
|
+
// endpoint. Tools like SAPNavigate/SAPActivate/SAPDiagnose/SAPTransport
|
|
2376
|
+
// accept `type: string` (no enum), so the schema layer can't catch this
|
|
2377
|
+
// for them — only this guard can. Throw with a hint pointing at the
|
|
2378
|
+
// citation guard so the contributor adds the alias correctly. Codex
|
|
2379
|
+
// review of PR #223 caught that the previous default-fallback could
|
|
2380
|
+
// still silently route removed aliases via these non-enum tools.
|
|
2381
|
+
if (type.includes('/')) {
|
|
2382
|
+
throw new Error(`objectBasePath: refusing to build URL for slash-form type '${type}' — ` +
|
|
2383
|
+
`this normally indicates an invented or unmapped ADT slash code. Add ` +
|
|
2384
|
+
`it to SLASH_TYPE_MAP + SLASH_TYPE_EVIDENCE (with a research entry) ` +
|
|
2385
|
+
`if it is real, or correct the caller. See ` +
|
|
2386
|
+
`docs/plans/completed/audit-purge-invented-adt-types.md and ` +
|
|
2387
|
+
`tests/unit/handlers/slash-type-map.test.ts.`);
|
|
2388
|
+
}
|
|
2389
|
+
// Unknown raw inputs (no slash, not canonical) fall through to the
|
|
2390
|
+
// program path so legacy callers like inferObjectType keep working.
|
|
2208
2391
|
return '/sap/bc/adt/programs/programs/';
|
|
2209
2392
|
}
|
|
2210
2393
|
}
|
|
@@ -2255,8 +2438,31 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2255
2438
|
if (action !== 'batch_create' && (!type || !name)) {
|
|
2256
2439
|
return errorResult('"type" and "name" are required for this action.');
|
|
2257
2440
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2441
|
+
// SAP TADIR stores object names uppercase. Mixed-case names cause silent corruption
|
|
2442
|
+
// (e.g. DDLS created as "Zc_MyView" registers as "ZC_MYVIEW" in TADIR but the source body
|
|
2443
|
+
// still contains "Zc_MyView", confusing every downstream tool). Reject pre-flight on create —
|
|
2444
|
+
// applies on every SAP release; this is universal SAP convention, not a 7.50 quirk.
|
|
2445
|
+
// Note: source code INSIDE the object can use mixed case (e.g. for DDLS: name="ZC_MYVIEW"
|
|
2446
|
+
// but `define view entity Zc_MyView` is fine inside the source body).
|
|
2447
|
+
if (action === 'create' && name && name !== name.toUpperCase()) {
|
|
2448
|
+
return errorResult(`Object name "${name}" contains lowercase characters. SAP object names must be uppercase (e.g. "${name.toUpperCase()}").\n\n` +
|
|
2449
|
+
`Note: the object NAME in TADIR must be uppercase, but the source code inside the object can use mixed case ` +
|
|
2450
|
+
`(e.g. for DDLS: name="${name.toUpperCase()}" but source can contain "define view entity ${name}").`);
|
|
2451
|
+
}
|
|
2452
|
+
// For TABL update/delete/edit_method, the existing object may live at /tables/
|
|
2453
|
+
// (transparent) or /structures/ (DDIC structure). Resolve once via the client's
|
|
2454
|
+
// cached URL probe. For 'create' the default /tables/ URL is correct (we only
|
|
2455
|
+
// create transparent tables today; structure creation is out of scope).
|
|
2456
|
+
let objectUrl;
|
|
2457
|
+
let srcUrl;
|
|
2458
|
+
if (type === 'TABL' && action !== 'create' && action !== 'batch_create') {
|
|
2459
|
+
objectUrl = await client.resolveTablObjectUrl(name);
|
|
2460
|
+
srcUrl = `${objectUrl}/source/main`;
|
|
2461
|
+
}
|
|
2462
|
+
else {
|
|
2463
|
+
objectUrl = objectUrlForType(type, name);
|
|
2464
|
+
srcUrl = sourceUrlForType(type, name);
|
|
2465
|
+
}
|
|
2260
2466
|
const invalidateWrittenObject = (objType = type, objName = name) => {
|
|
2261
2467
|
cachingLayer?.invalidate(objType, objName, 'all');
|
|
2262
2468
|
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
@@ -2283,8 +2489,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2283
2489
|
// responsible/masterLanguage/packageRef/refObject metadata.
|
|
2284
2490
|
const { source: currentEnvelope } = await client.getKtd(name);
|
|
2285
2491
|
const body = rewriteKtdText(currentEnvelope, source);
|
|
2286
|
-
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, transport);
|
|
2287
|
-
invalidateWrittenObject();
|
|
2492
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, transport, cachedFeatures?.abapRelease);
|
|
2493
|
+
invalidateWrittenObject(type, name);
|
|
2288
2494
|
return textResult(`Successfully updated ${type} ${name}.`);
|
|
2289
2495
|
}
|
|
2290
2496
|
if (isMetadataWriteType(type)) {
|
|
@@ -2296,8 +2502,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2296
2502
|
const description = String(args.description ?? mergedProps._description ?? name);
|
|
2297
2503
|
const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
|
|
2298
2504
|
const body = buildCreateXml(type, name, pkg, description, mergedProps);
|
|
2299
|
-
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport);
|
|
2300
|
-
invalidateWrittenObject();
|
|
2505
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport, cachedFeatures?.abapRelease);
|
|
2506
|
+
invalidateWrittenObject(type, name);
|
|
2301
2507
|
return textResult(`Successfully updated ${type} ${name}.`);
|
|
2302
2508
|
}
|
|
2303
2509
|
// RAP deterministic preflight validation
|
|
@@ -2316,8 +2522,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2316
2522
|
const checkNotes = await runPreWriteSyntaxCheck(client, type, source, objectUrl, config, checkOverride);
|
|
2317
2523
|
// If safeUpdateSource throws (lock conflict, network error, etc.), checkNotes
|
|
2318
2524
|
// 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();
|
|
2525
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport, cachedFeatures?.abapRelease);
|
|
2526
|
+
invalidateWrittenObject(type, name);
|
|
2321
2527
|
const msg = `Successfully updated ${type} ${name}.`;
|
|
2322
2528
|
const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
|
|
2323
2529
|
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint);
|
|
@@ -2361,6 +2567,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2361
2567
|
// SAP will return its own error if a transport is actually needed.
|
|
2362
2568
|
}
|
|
2363
2569
|
}
|
|
2570
|
+
// MSAG transport-vs-task guard. Some SAP releases silently drop message inserts when
|
|
2571
|
+
// given a task number as corrNr — CL_ADT_MESSAGE_CLASS_API=>create() passes corrNr to
|
|
2572
|
+
// CTS_WBO_API_INSERT_OBJECTS which only accepts request numbers. The TADIR entry is
|
|
2573
|
+
// created but T100/T100A are never written, leaving a phantom MSAG. Confirmed on NW 7.50;
|
|
2574
|
+
// unclear whether later releases fixed it, so validate everywhere.
|
|
2575
|
+
// Cost: one extra HTTP roundtrip per MSAG create (negligible vs. the data loss risk).
|
|
2576
|
+
if (type === 'MSAG' && effectiveTransport) {
|
|
2577
|
+
const tr = await getTransport(client.http, client.safety, effectiveTransport);
|
|
2578
|
+
if (!tr) {
|
|
2579
|
+
return errorResult(`Transport "${effectiveTransport}" is not a valid transport request. ` +
|
|
2580
|
+
`MSAG creation requires a transport request number, not a task number. ` +
|
|
2581
|
+
`Use SAPTransport(action="get", id="<request>") to verify, or SAPTransport(action="list") to find modifiable requests.`);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2364
2584
|
// CDS pre-write validation: reject unsupported syntax early
|
|
2365
2585
|
const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
|
|
2366
2586
|
if (cdsGuard)
|
|
@@ -2398,7 +2618,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2398
2618
|
<sktd:refObject adtcore:description="${escapeXml(refDescription)}" adtcore:name="${escapeXml(refName)}" adtcore:type="${escapeXml(refType)}" adtcore:uri="${escapeXml(refUri)}"/>
|
|
2399
2619
|
</sktd:docu>`;
|
|
2400
2620
|
const ktdCreateUrl = '/sap/bc/adt/documentation/ktd/documents';
|
|
2401
|
-
const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport);
|
|
2621
|
+
const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
2402
2622
|
// If initial Markdown was provided, follow up with an update PUT to write it.
|
|
2403
2623
|
// Same envelope contract as the update path: fetch-then-rewrite ensures we
|
|
2404
2624
|
// PUT back exactly the shape SAP gave us (with all the server-assigned
|
|
@@ -2406,8 +2626,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2406
2626
|
if (source) {
|
|
2407
2627
|
const { source: currentEnvelope } = await client.getKtd(name);
|
|
2408
2628
|
const body = rewriteKtdText(currentEnvelope, source);
|
|
2409
|
-
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport);
|
|
2410
|
-
invalidateWrittenObject();
|
|
2629
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport, cachedFeatures?.abapRelease);
|
|
2630
|
+
invalidateWrittenObject(type, name);
|
|
2411
2631
|
return textResult(`Created SKTD ${name} in package ${pkg} and wrote Markdown content.\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
|
|
2412
2632
|
}
|
|
2413
2633
|
invalidateWrittenObject();
|
|
@@ -2427,7 +2647,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2427
2647
|
const needsPackageParam = type === 'BDEF' || type === 'TABL';
|
|
2428
2648
|
let result;
|
|
2429
2649
|
try {
|
|
2430
|
-
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined);
|
|
2650
|
+
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
2431
2651
|
}
|
|
2432
2652
|
catch (createErr) {
|
|
2433
2653
|
if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
|
|
@@ -2445,7 +2665,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2445
2665
|
if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
|
|
2446
2666
|
const ct = vendorContentTypeForType(type);
|
|
2447
2667
|
await client.http.withStatefulSession(async (session) => {
|
|
2448
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2668
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2449
2669
|
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
2450
2670
|
try {
|
|
2451
2671
|
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
@@ -2459,7 +2679,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2459
2679
|
if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
|
|
2460
2680
|
const ct = vendorContentTypeForType(type);
|
|
2461
2681
|
await client.http.withStatefulSession(async (session) => {
|
|
2462
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2682
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2463
2683
|
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
2464
2684
|
try {
|
|
2465
2685
|
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
@@ -2482,8 +2702,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2482
2702
|
if (lintWarnings.blocked) {
|
|
2483
2703
|
return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
|
|
2484
2704
|
}
|
|
2485
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
|
|
2486
|
-
invalidateWrittenObject();
|
|
2705
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport, cachedFeatures?.abapRelease);
|
|
2706
|
+
invalidateWrittenObject(type, name);
|
|
2487
2707
|
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
2488
2708
|
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings);
|
|
2489
2709
|
return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
|
|
@@ -2519,8 +2739,8 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2519
2739
|
// Pre-write server-side syntax check on the full spliced source (opt-in; warnings only).
|
|
2520
2740
|
const checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
|
|
2521
2741
|
// Write the full source back (existing lock/modify/unlock flow)
|
|
2522
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport);
|
|
2523
|
-
invalidateWrittenObject();
|
|
2742
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport, cachedFeatures?.abapRelease);
|
|
2743
|
+
invalidateWrittenObject(type, name);
|
|
2524
2744
|
const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
|
|
2525
2745
|
const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
|
|
2526
2746
|
return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
|
|
@@ -2664,7 +2884,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2664
2884
|
// at the class object URL, and every include PUT carries the same lockHandle.
|
|
2665
2885
|
// This mirrors how ADT-in-Eclipse saves a multi-include class in one commit.
|
|
2666
2886
|
await client.http.withStatefulSession(async (session) => {
|
|
2667
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2887
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2668
2888
|
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
2669
2889
|
try {
|
|
2670
2890
|
if (changed.main) {
|
|
@@ -2711,7 +2931,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2711
2931
|
// Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
|
|
2712
2932
|
try {
|
|
2713
2933
|
await client.http.withStatefulSession(async (session) => {
|
|
2714
|
-
const lock = await lockObject(session, client.safety, objectUrl);
|
|
2934
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2715
2935
|
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
2716
2936
|
try {
|
|
2717
2937
|
await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
|
|
@@ -2783,12 +3003,45 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2783
3003
|
}
|
|
2784
3004
|
const results = [];
|
|
2785
3005
|
const batchWarnings = [];
|
|
3006
|
+
// Per-batch cache for the MSAG transport-vs-task guard. The bug is universal so the
|
|
3007
|
+
// guard fires for every MSAG entry, but a batch typically shares one transport — cache
|
|
3008
|
+
// the lookup result to avoid one HTTP roundtrip per object.
|
|
3009
|
+
const transportLookupCache = new Map();
|
|
2786
3010
|
for (const obj of objects) {
|
|
2787
3011
|
const objType = normalizeObjectType(String(obj.type ?? ''));
|
|
2788
3012
|
const objName = String(obj.name ?? '');
|
|
2789
3013
|
const metadataObject = isMetadataWriteType(objType);
|
|
2790
3014
|
const objSource = obj.source ? String(obj.source) : undefined;
|
|
2791
3015
|
const objDescription = String(obj.description ?? objName);
|
|
3016
|
+
// Mixed-case object name rejection (matches the create-path check above).
|
|
3017
|
+
// Universal SAP convention — TADIR is uppercase on every release.
|
|
3018
|
+
// Cheap check first: no HTTP call, fail fast on bad names.
|
|
3019
|
+
if (objName && objName !== objName.toUpperCase()) {
|
|
3020
|
+
results.push({
|
|
3021
|
+
type: objType,
|
|
3022
|
+
name: objName,
|
|
3023
|
+
status: 'failed',
|
|
3024
|
+
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.`,
|
|
3025
|
+
});
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
// MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
|
|
3029
|
+
if (objType === 'MSAG' && batchTransport) {
|
|
3030
|
+
let tr = transportLookupCache.get(batchTransport);
|
|
3031
|
+
if (tr === undefined) {
|
|
3032
|
+
tr = await getTransport(client.http, client.safety, batchTransport);
|
|
3033
|
+
transportLookupCache.set(batchTransport, tr);
|
|
3034
|
+
}
|
|
3035
|
+
if (!tr) {
|
|
3036
|
+
results.push({
|
|
3037
|
+
type: objType,
|
|
3038
|
+
name: objName,
|
|
3039
|
+
status: 'failed',
|
|
3040
|
+
error: `Transport "${batchTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
|
|
3041
|
+
});
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
2792
3045
|
// AFF header validation per object (if schema available)
|
|
2793
3046
|
const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
|
|
2794
3047
|
if (!affResult.valid) {
|
|
@@ -2836,7 +3089,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2836
3089
|
const contentType = createContentTypeForType(objType);
|
|
2837
3090
|
const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
|
|
2838
3091
|
try {
|
|
2839
|
-
await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport, needsPackageParam ? pkg : undefined);
|
|
3092
|
+
await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
2840
3093
|
}
|
|
2841
3094
|
catch (createErr) {
|
|
2842
3095
|
if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
|
|
@@ -2850,7 +3103,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2850
3103
|
// Step 1b: DTEL POST ignores labels — follow up with PUT on main session
|
|
2851
3104
|
if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
|
|
2852
3105
|
await client.http.withStatefulSession(async (session) => {
|
|
2853
|
-
const lock = await lockObject(session, client.safety, objUrl);
|
|
3106
|
+
const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
2854
3107
|
const lockTransport = batchTransport ?? (lock.corrNr || undefined);
|
|
2855
3108
|
try {
|
|
2856
3109
|
await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
|
|
@@ -2863,7 +3116,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2863
3116
|
// Step 2: Write source if provided
|
|
2864
3117
|
if (!metadataObject && objSource) {
|
|
2865
3118
|
const srcUrl = sourceUrlForType(objType, objName);
|
|
2866
|
-
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport);
|
|
3119
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport, cachedFeatures?.abapRelease);
|
|
2867
3120
|
}
|
|
2868
3121
|
// Step 3: Activate the object
|
|
2869
3122
|
const activationResult = await activate(client.http, client.safety, objUrl);
|
|
@@ -3019,7 +3272,7 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
|
|
|
3019
3272
|
}
|
|
3020
3273
|
}
|
|
3021
3274
|
/** Types that carry source code that SAP's /checkruns endpoint can meaningfully compile.
|
|
3022
|
-
* Metadata-write types (DOMA/DTEL/TABL/
|
|
3275
|
+
* Metadata-write types (DOMA/DTEL/TABL/MSAG/DEVC/SKTD) have no /source/main artifact. */
|
|
3023
3276
|
const SYNTAX_CHECKABLE_TYPES = new Set([
|
|
3024
3277
|
'PROG',
|
|
3025
3278
|
'CLAS',
|
|
@@ -3165,11 +3418,15 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
3165
3418
|
const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
|
|
3166
3419
|
if (args.objects && Array.isArray(args.objects)) {
|
|
3167
3420
|
const rawObjects = args.objects;
|
|
3168
|
-
|
|
3421
|
+
// Resolve URLs sequentially. For TABL we await the URL resolver so DDIC
|
|
3422
|
+
// structures (which live at /sap/bc/adt/ddic/structures/) are addressed
|
|
3423
|
+
// correctly; the resolver short-circuits on its in-memory cache.
|
|
3424
|
+
const objects = await Promise.all(rawObjects.map(async (o) => {
|
|
3169
3425
|
const objType = normalizeObjectType(String(o.type ?? type));
|
|
3170
3426
|
const objName = String(o.name ?? '');
|
|
3171
|
-
|
|
3172
|
-
|
|
3427
|
+
const url = objType === 'TABL' ? await client.resolveTablObjectUrl(objName) : objectUrlForType(objType, objName);
|
|
3428
|
+
return { type: objType, name: objName, url };
|
|
3429
|
+
}));
|
|
3173
3430
|
const result = await activateBatch(client.http, client.safety, objects, activateOpts);
|
|
3174
3431
|
const names = objects.map((o) => o.name).join(', ');
|
|
3175
3432
|
const batchStatuses = buildBatchActivationStatuses(objects, result);
|
|
@@ -3191,8 +3448,10 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
3191
3448
|
.join('');
|
|
3192
3449
|
return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
|
|
3193
3450
|
}
|
|
3194
|
-
// Single activation (existing behavior)
|
|
3195
|
-
|
|
3451
|
+
// Single activation (existing behavior). For TABL we resolve the URL because
|
|
3452
|
+
// the existing object may live at /tables/ (transparent) or /structures/
|
|
3453
|
+
// (DDIC structure); using the wrong one would produce a confusing 404.
|
|
3454
|
+
const objectUrl = type === 'TABL' ? await client.resolveTablObjectUrl(name) : objectUrlForType(type, name);
|
|
3196
3455
|
const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
|
|
3197
3456
|
if (result.success) {
|
|
3198
3457
|
cachingLayer?.invalidate(type, name, 'all');
|
|
@@ -3320,6 +3579,15 @@ async function handleSAPNavigate(client, args) {
|
|
|
3320
3579
|
return errorResult(`Cannot resolve function group for "${symName}". Provide the full uri parameter, or use SAPSearch("${symName}") to find the ADT URI.`);
|
|
3321
3580
|
}
|
|
3322
3581
|
}
|
|
3582
|
+
else if (symType === 'TABL') {
|
|
3583
|
+
// DDIC TABL: where-used and other navigate paths must use the canonical
|
|
3584
|
+
// object URL — `/sap/bc/adt/ddic/tables/{name}` for transparent tables,
|
|
3585
|
+
// `/sap/bc/adt/ddic/structures/{name}` for DDIC structures. NW 7.50
|
|
3586
|
+
// returns 500 from usageReferences for /tables/ URLs even for transparent
|
|
3587
|
+
// tables, so we always resolve before building. resolveTablObjectUrl
|
|
3588
|
+
// caches on the AdtClient, so this is one HTTP probe per cold name.
|
|
3589
|
+
uri = await client.resolveTablObjectUrl(symName);
|
|
3590
|
+
}
|
|
3323
3591
|
else {
|
|
3324
3592
|
uri = objectUrlForType(symType, symName);
|
|
3325
3593
|
}
|
|
@@ -3366,6 +3634,39 @@ async function handleSAPNavigate(client, args) {
|
|
|
3366
3634
|
throw err;
|
|
3367
3635
|
}
|
|
3368
3636
|
}
|
|
3637
|
+
// Augment interface where-used with implementing classes from SEOMETAREL.
|
|
3638
|
+
// SAP's scope-based usageReferences endpoint sometimes does NOT surface
|
|
3639
|
+
// interface→implementing-class links — the implementations sit inside a
|
|
3640
|
+
// `canHaveChildren="true"` Interface Section node, and the snippet
|
|
3641
|
+
// expansion endpoint returns 404 on every release we've probed (NW 7.50,
|
|
3642
|
+
// S/4HANA 2023). SEOMETAREL is the canonical OO-relation table and is
|
|
3643
|
+
// always populated, so this augmentation makes references reliable for
|
|
3644
|
+
// interfaces. Silently skipped when SQL/data access isn't available.
|
|
3645
|
+
const intfMatch = uri.match(/\/sap\/bc\/adt\/oo\/interfaces\/([^/?]+)/i);
|
|
3646
|
+
if (intfMatch && (!objectType || /^CLAS/i.test(objectType))) {
|
|
3647
|
+
const interfaceName = decodeURIComponent(intfMatch[1]).toUpperCase();
|
|
3648
|
+
const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
|
|
3649
|
+
const canQuery = isOperationAllowed(client.safety, OperationType.Query);
|
|
3650
|
+
try {
|
|
3651
|
+
let implementers = [];
|
|
3652
|
+
if (canFreeSQL) {
|
|
3653
|
+
implementers = await findInterfaceImplementersViaSeoMetaRel((sql, max) => client.runQuery(sql, max), interfaceName);
|
|
3654
|
+
}
|
|
3655
|
+
else if (canQuery) {
|
|
3656
|
+
implementers = await findInterfaceImplementersViaSeoMetaRel((_sql, max) => client.getTableContents('SEOMETAREL', max, `REFCLSNAME = '${interfaceName}' AND RELTYPE = '1'`), interfaceName);
|
|
3657
|
+
}
|
|
3658
|
+
// Dedupe: don't add an implementer if SAP already returned it
|
|
3659
|
+
const existingNames = new Set(results.map((r) => r.name?.toUpperCase()).filter(Boolean));
|
|
3660
|
+
const augmented = implementers.filter((r) => !existingNames.has(r.name.toUpperCase()));
|
|
3661
|
+
if (augmented.length > 0) {
|
|
3662
|
+
results.push(...augmented);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
catch {
|
|
3666
|
+
// SEOMETAREL augmentation is best-effort; if SQL fails, fall back to
|
|
3667
|
+
// whatever the where-used HTTP endpoint returned. Don't block the response.
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3369
3670
|
if (results.length === 0) {
|
|
3370
3671
|
return textResult('No references found.');
|
|
3371
3672
|
}
|
|
@@ -3890,8 +4191,8 @@ async function handleSAPTransport(client, args) {
|
|
|
3890
4191
|
const description = String(args.description ?? '');
|
|
3891
4192
|
if (!description)
|
|
3892
4193
|
return errorResult('Description is required for "create" action.');
|
|
3893
|
-
const
|
|
3894
|
-
const id = await createTransport(client.http, client.safety, description,
|
|
4194
|
+
const targetPackage = args.package ? String(args.package) : undefined;
|
|
4195
|
+
const id = await createTransport(client.http, client.safety, description, targetPackage);
|
|
3895
4196
|
if (!id)
|
|
3896
4197
|
return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
|
|
3897
4198
|
return textResult(`Created transport request: ${id}`);
|
|
@@ -4389,7 +4690,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
4389
4690
|
transportLayer: transportLayer || undefined,
|
|
4390
4691
|
packageType,
|
|
4391
4692
|
});
|
|
4392
|
-
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport);
|
|
4693
|
+
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
4393
4694
|
return textResult(`Created package ${name}.`);
|
|
4394
4695
|
}
|
|
4395
4696
|
case 'delete_package': {
|
|
@@ -4400,7 +4701,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
4400
4701
|
checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
|
|
4401
4702
|
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
4402
4703
|
await client.http.withStatefulSession(async (session) => {
|
|
4403
|
-
const lock = await lockObject(session, client.safety, packageUrl);
|
|
4704
|
+
const lock = await lockObject(session, client.safety, packageUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
4404
4705
|
const effectiveTransport = transport || lock.corrNr || undefined;
|
|
4405
4706
|
try {
|
|
4406
4707
|
await deleteObject(session, client.safety, packageUrl, lock.lockHandle, effectiveTransport);
|