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.
Files changed (59) hide show
  1. package/README.md +5 -2
  2. package/dist/adt/client.d.ts +57 -3
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +108 -7
  5. package/dist/adt/client.js.map +1 -1
  6. package/dist/adt/codeintel.d.ts +23 -0
  7. package/dist/adt/codeintel.d.ts.map +1 -1
  8. package/dist/adt/codeintel.js +39 -0
  9. package/dist/adt/codeintel.js.map +1 -1
  10. package/dist/adt/config.d.ts +4 -0
  11. package/dist/adt/config.d.ts.map +1 -1
  12. package/dist/adt/config.js.map +1 -1
  13. package/dist/adt/crud.d.ts +28 -4
  14. package/dist/adt/crud.d.ts.map +1 -1
  15. package/dist/adt/crud.js +74 -28
  16. package/dist/adt/crud.js.map +1 -1
  17. package/dist/adt/diagnostics.d.ts +2 -0
  18. package/dist/adt/diagnostics.d.ts.map +1 -1
  19. package/dist/adt/diagnostics.js +18 -6
  20. package/dist/adt/diagnostics.js.map +1 -1
  21. package/dist/adt/errors.d.ts.map +1 -1
  22. package/dist/adt/errors.js +1 -0
  23. package/dist/adt/errors.js.map +1 -1
  24. package/dist/adt/http.d.ts +43 -0
  25. package/dist/adt/http.d.ts.map +1 -1
  26. package/dist/adt/http.js +93 -0
  27. package/dist/adt/http.js.map +1 -1
  28. package/dist/adt/transport.d.ts +22 -2
  29. package/dist/adt/transport.d.ts.map +1 -1
  30. package/dist/adt/transport.js +41 -13
  31. package/dist/adt/transport.js.map +1 -1
  32. package/dist/adt/xml-parser.d.ts +12 -0
  33. package/dist/adt/xml-parser.d.ts.map +1 -1
  34. package/dist/adt/xml-parser.js +15 -3
  35. package/dist/adt/xml-parser.js.map +1 -1
  36. package/dist/cli.js +13 -0
  37. package/dist/cli.js.map +1 -1
  38. package/dist/context/compressor.d.ts +1 -1
  39. package/dist/context/compressor.js +7 -11
  40. package/dist/context/compressor.js.map +1 -1
  41. package/dist/handlers/intent.d.ts +21 -0
  42. package/dist/handlers/intent.d.ts.map +1 -1
  43. package/dist/handlers/intent.js +378 -77
  44. package/dist/handlers/intent.js.map +1 -1
  45. package/dist/handlers/schemas.d.ts +7 -2
  46. package/dist/handlers/schemas.d.ts.map +1 -1
  47. package/dist/handlers/schemas.js +18 -4
  48. package/dist/handlers/schemas.js.map +1 -1
  49. package/dist/handlers/tools.d.ts.map +1 -1
  50. package/dist/handlers/tools.js +46 -10
  51. package/dist/handlers/tools.js.map +1 -1
  52. package/dist/probe/catalog.d.ts.map +1 -1
  53. package/dist/probe/catalog.js +19 -11
  54. package/dist/probe/catalog.js.map +1 -1
  55. package/dist/server/server.d.ts +1 -1
  56. package/dist/server/server.d.ts.map +1 -1
  57. package/dist/server/server.js +40 -3
  58. package/dist/server/server.js.map +1 -1
  59. package/package.json +3 -3
@@ -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
- 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
  }
@@ -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 contents = await client.getPackageContents(name);
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, 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. ` +
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
- 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',
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
- /** Base path for an object type. Returns path prefix without trailing name segment. */
2165
- function objectBasePath(type) {
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
- return '/sap/bc/adt/functions/groups/';
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
- const objectUrl = objectUrlForType(type, name);
2259
- const srcUrl = sourceUrlForType(type, name);
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/STRU/MSAG/DEVC/SKTD) have no /source/main artifact. */
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
- const objects = rawObjects.map((o) => {
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
- return { type: objType, name: objName, url: objectUrlForType(objType, objName) };
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
- const objectUrl = objectUrlForType(type, name);
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 transportType = String(args.type ?? 'K');
3894
- const id = await createTransport(client.http, client.safety, description, undefined, transportType);
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);