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.
Files changed (86) hide show
  1. package/README.md +32 -6
  2. package/dist/adt/btp.d.ts +14 -3
  3. package/dist/adt/btp.d.ts.map +1 -1
  4. package/dist/adt/btp.js +18 -3
  5. package/dist/adt/btp.js.map +1 -1
  6. package/dist/adt/client.d.ts +30 -1
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +72 -2
  9. package/dist/adt/client.js.map +1 -1
  10. package/dist/adt/codeintel.d.ts +23 -0
  11. package/dist/adt/codeintel.d.ts.map +1 -1
  12. package/dist/adt/codeintel.js +39 -0
  13. package/dist/adt/codeintel.js.map +1 -1
  14. package/dist/adt/config.d.ts +4 -0
  15. package/dist/adt/config.d.ts.map +1 -1
  16. package/dist/adt/config.js.map +1 -1
  17. package/dist/adt/crud.d.ts +28 -4
  18. package/dist/adt/crud.d.ts.map +1 -1
  19. package/dist/adt/crud.js +74 -28
  20. package/dist/adt/crud.js.map +1 -1
  21. package/dist/adt/diagnostics.d.ts +7 -1
  22. package/dist/adt/diagnostics.d.ts.map +1 -1
  23. package/dist/adt/diagnostics.js +38 -9
  24. package/dist/adt/diagnostics.js.map +1 -1
  25. package/dist/adt/errors.d.ts +1 -1
  26. package/dist/adt/errors.d.ts.map +1 -1
  27. package/dist/adt/errors.js +47 -2
  28. package/dist/adt/errors.js.map +1 -1
  29. package/dist/adt/http.d.ts +43 -0
  30. package/dist/adt/http.d.ts.map +1 -1
  31. package/dist/adt/http.js +93 -0
  32. package/dist/adt/http.js.map +1 -1
  33. package/dist/adt/transport.d.ts +22 -2
  34. package/dist/adt/transport.d.ts.map +1 -1
  35. package/dist/adt/transport.js +41 -13
  36. package/dist/adt/transport.js.map +1 -1
  37. package/dist/adt/xml-parser.d.ts +4 -0
  38. package/dist/adt/xml-parser.d.ts.map +1 -1
  39. package/dist/adt/xml-parser.js +6 -2
  40. package/dist/adt/xml-parser.js.map +1 -1
  41. package/dist/cli.js +13 -0
  42. package/dist/cli.js.map +1 -1
  43. package/dist/context/compressor.d.ts +1 -1
  44. package/dist/context/compressor.js +7 -11
  45. package/dist/context/compressor.js.map +1 -1
  46. package/dist/handlers/intent.d.ts +21 -0
  47. package/dist/handlers/intent.d.ts.map +1 -1
  48. package/dist/handlers/intent.js +378 -78
  49. package/dist/handlers/intent.js.map +1 -1
  50. package/dist/handlers/schemas.d.ts +5 -2
  51. package/dist/handlers/schemas.d.ts.map +1 -1
  52. package/dist/handlers/schemas.js +14 -4
  53. package/dist/handlers/schemas.js.map +1 -1
  54. package/dist/handlers/tools.d.ts.map +1 -1
  55. package/dist/handlers/tools.js +42 -10
  56. package/dist/handlers/tools.js.map +1 -1
  57. package/dist/probe/catalog.d.ts.map +1 -1
  58. package/dist/probe/catalog.js +19 -11
  59. package/dist/probe/catalog.js.map +1 -1
  60. package/dist/server/audit.d.ts +41 -1
  61. package/dist/server/audit.d.ts.map +1 -1
  62. package/dist/server/audit.js.map +1 -1
  63. package/dist/server/config.d.ts.map +1 -1
  64. package/dist/server/config.js +32 -0
  65. package/dist/server/config.js.map +1 -1
  66. package/dist/server/http.d.ts +25 -0
  67. package/dist/server/http.d.ts.map +1 -1
  68. package/dist/server/http.js +149 -7
  69. package/dist/server/http.js.map +1 -1
  70. package/dist/server/server.d.ts +1 -1
  71. package/dist/server/server.d.ts.map +1 -1
  72. package/dist/server/server.js +40 -3
  73. package/dist/server/server.js.map +1 -1
  74. package/dist/server/stateless-client-store.d.ts +97 -0
  75. package/dist/server/stateless-client-store.d.ts.map +1 -0
  76. package/dist/server/stateless-client-store.js +334 -0
  77. package/dist/server/stateless-client-store.js.map +1 -0
  78. package/dist/server/types.d.ts +12 -0
  79. package/dist/server/types.d.ts.map +1 -1
  80. package/dist/server/types.js +2 -0
  81. package/dist/server/types.js.map +1 -1
  82. package/dist/server/xsuaa.d.ts +11 -43
  83. package/dist/server/xsuaa.d.ts.map +1 -1
  84. package/dist/server/xsuaa.js +12 -177
  85. package/dist/server/xsuaa.js.map +1 -1
  86. package/package.json +10 -4
@@ -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
- const { source, cacheHit, revalidated } = await cachedGet('TABL', name, effectiveVersion, (ifNoneMatch) => client.getTable(name, { ifNoneMatch, version: effectiveVersion }));
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, STRU, DOMA, DTEL, AUTH, FTG2, ENHO, VERSIONS, VERSION_SOURCE, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. ` +
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
- const SLASH_TYPE_MAP = {
2072
- 'PROG/P': 'PROG',
2073
- 'PROG/I': 'INCL',
2074
- 'CLAS/OC': 'CLAS',
2075
- 'CLAS/LI': 'CLAS',
2076
- 'INTF/OI': 'INTF',
2077
- 'FUNC/FM': 'FUNC',
2078
- 'FUGR/F': 'FUGR',
2079
- 'FUGR/FF': 'FUGR',
2080
- 'DDLS/DF': 'DDLS',
2081
- 'DCLS/DL': 'DCLS',
2082
- 'BDEF/BDO': 'BDEF',
2083
- 'SRVD/SRV': 'SRVD',
2084
- 'SRVB/SVB': 'SRVB',
2085
- 'DDLX/EX': 'DDLX',
2086
- 'TABL/DT': 'TABL',
2087
- 'STRU/DS': 'STRU',
2088
- 'DOMA/DD': 'DOMA',
2089
- 'DTEL/DE': 'DTEL',
2090
- 'MSAG/N': 'MSAG',
2091
- 'DEVC/K': 'DEVC',
2092
- 'TRAN/O': 'TRAN',
2093
- 'VIEW/V': 'VIEW',
2094
- 'SKTD/TYP': 'SKTD',
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
- /** Base path for an object type. Returns path prefix without trailing name segment. */
2165
- function objectBasePath(type) {
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
- return '/sap/bc/adt/functions/groups/';
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
- const objectUrl = objectUrlForType(type, name);
2259
- const srcUrl = sourceUrlForType(type, name);
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/STRU/MSAG/DEVC/SKTD) have no /source/main artifact. */
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
- const objects = rawObjects.map((o) => {
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
- return { type: objType, name: objName, url: objectUrlForType(objType, objName) };
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
- const objectUrl = objectUrlForType(type, name);
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 transportType = String(args.type ?? 'K');
3894
- const id = await createTransport(client.http, client.safety, description, undefined, transportType);
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);