arc-1 0.9.2 → 0.9.4

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 (60) hide show
  1. package/README.md +4 -4
  2. package/dist/adt/client.d.ts +12 -1
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +56 -1
  5. package/dist/adt/client.js.map +1 -1
  6. package/dist/adt/devtools.d.ts.map +1 -1
  7. package/dist/adt/devtools.js +191 -51
  8. package/dist/adt/devtools.js.map +1 -1
  9. package/dist/adt/diagnostics.d.ts +21 -1
  10. package/dist/adt/diagnostics.d.ts.map +1 -1
  11. package/dist/adt/diagnostics.js +72 -0
  12. package/dist/adt/diagnostics.js.map +1 -1
  13. package/dist/adt/fm-signature.d.ts +77 -0
  14. package/dist/adt/fm-signature.d.ts.map +1 -0
  15. package/dist/adt/fm-signature.js +343 -0
  16. package/dist/adt/fm-signature.js.map +1 -0
  17. package/dist/adt/http.d.ts +9 -1
  18. package/dist/adt/http.d.ts.map +1 -1
  19. package/dist/adt/http.js +8 -7
  20. package/dist/adt/http.js.map +1 -1
  21. package/dist/adt/rap-generate.d.ts +110 -0
  22. package/dist/adt/rap-generate.d.ts.map +1 -0
  23. package/dist/adt/rap-generate.js +262 -0
  24. package/dist/adt/rap-generate.js.map +1 -0
  25. package/dist/adt/rap-handlers.d.ts +14 -0
  26. package/dist/adt/rap-handlers.d.ts.map +1 -1
  27. package/dist/adt/rap-handlers.js +96 -9
  28. package/dist/adt/rap-handlers.js.map +1 -1
  29. package/dist/adt/types.d.ts +73 -1
  30. package/dist/adt/types.d.ts.map +1 -1
  31. package/dist/adt/xml-parser.d.ts.map +1 -1
  32. package/dist/adt/xml-parser.js +14 -0
  33. package/dist/adt/xml-parser.js.map +1 -1
  34. package/dist/authz/policy.d.ts.map +1 -1
  35. package/dist/authz/policy.js +9 -0
  36. package/dist/authz/policy.js.map +1 -1
  37. package/dist/context/method-surgery.d.ts +27 -0
  38. package/dist/context/method-surgery.d.ts.map +1 -1
  39. package/dist/context/method-surgery.js +104 -7
  40. package/dist/context/method-surgery.js.map +1 -1
  41. package/dist/handlers/intent.d.ts +20 -0
  42. package/dist/handlers/intent.d.ts.map +1 -1
  43. package/dist/handlers/intent.js +729 -71
  44. package/dist/handlers/intent.js.map +1 -1
  45. package/dist/handlers/schemas.d.ts +111 -3
  46. package/dist/handlers/schemas.d.ts.map +1 -1
  47. package/dist/handlers/schemas.js +163 -11
  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 +152 -33
  51. package/dist/handlers/tools.js.map +1 -1
  52. package/dist/server/config.d.ts.map +1 -1
  53. package/dist/server/config.js +1 -0
  54. package/dist/server/config.js.map +1 -1
  55. package/dist/server/server.d.ts +1 -1
  56. package/dist/server/server.js +1 -1
  57. package/dist/server/types.d.ts +2 -0
  58. package/dist/server/types.d.ts.map +1 -1
  59. package/dist/server/types.js.map +1 -1
  60. package/package.json +1 -2
@@ -15,11 +15,13 @@ import { findDefinition, findInterfaceImplementersViaSeoMetaRel, findReferences,
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';
18
- import { getDump, getGatewayErrorDetail, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraces, } from '../adt/diagnostics.js';
18
+ import { getDump, getGatewayErrorDetail, getObjectState, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraces, } from '../adt/diagnostics.js';
19
19
  import { AdtApiError, AdtNetworkError, AdtSafetyError, classifySapDomainError, isNotFoundError, } from '../adt/errors.js';
20
20
  import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
21
21
  import { addTileToGroup, createCatalog, createGroup, createTile, deleteCatalog, listCatalogs, listGroups, listTiles, } from '../adt/flp.js';
22
+ import { parseFmSignature, spliceFmSignature } from '../adt/fm-signature.js';
22
23
  import { cloneRepo as gctsCloneRepo, commitRepo as gctsCommitRepo, createBranch as gctsCreateBranch, deleteRepo as gctsDeleteRepo, getCommitHistory as gctsGetCommitHistory, getConfig as gctsGetConfig, getUserInfo as gctsGetUserInfo, listBranches as gctsListBranches, listRepoObjects as gctsListRepoObjects, listRepos as gctsListRepos, pullRepo as gctsPullRepo, switchBranch as gctsSwitchBranch, } from '../adt/gcts.js';
24
+ import { generateBehaviorImplementation, isRapGenerateResultSuccess } from '../adt/rap-generate.js';
23
25
  import { applyRapHandlerScaffold, extractRapHandlerRequirements, findMissingRapHandlerImplementationStubs, findMissingRapHandlerRequirements, } from '../adt/rap-handlers.js';
24
26
  import { formatRapPreflightFindings, validateRapSource } from '../adt/rap-preflight.js';
25
27
  import { changePackage } from '../adt/refactoring.js';
@@ -1086,6 +1088,26 @@ async function handleSAPRead(client, args, cachingLayer) {
1086
1088
  group = resolved;
1087
1089
  }
1088
1090
  const { source, cacheHit, revalidated } = await cachedGet('FUNC', name, effectiveVersion, (ifNoneMatch) => client.getFunction(group, name, { ifNoneMatch, version: effectiveVersion }));
1091
+ // Issue #252: when caller asks for includeSignature, return JSON with the
1092
+ // source body and the parsed structured signature.
1093
+ if (args.includeSignature === true) {
1094
+ const parsed = parseFmSignature(source);
1095
+ const grouped = {
1096
+ importing: [],
1097
+ exporting: [],
1098
+ changing: [],
1099
+ tables: [],
1100
+ exceptions: [],
1101
+ raising: [],
1102
+ };
1103
+ for (const p of parsed.params)
1104
+ grouped[p.kind].push(p);
1105
+ const payload = {
1106
+ source,
1107
+ signature: grouped,
1108
+ };
1109
+ return textResult(JSON.stringify(payload, null, 2));
1110
+ }
1089
1111
  return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1090
1112
  }
1091
1113
  case 'FUGR': {
@@ -1397,6 +1419,23 @@ async function handleSAPSearch(client, args) {
1397
1419
  const rawQuery = String(args.query ?? '');
1398
1420
  const maxResults = Number(args.maxResults ?? 100);
1399
1421
  const searchType = String(args.searchType ?? 'object');
1422
+ if (searchType === 'tadir_lookup') {
1423
+ const names = extractLookupNames(rawQuery, args.names);
1424
+ if (names.length === 0) {
1425
+ return errorResult('SAPSearch(searchType="tadir_lookup") requires names[] or query with at least one name.');
1426
+ }
1427
+ const objectTypes = extractLookupObjectTypes(args.objectType, args.objectTypes);
1428
+ const lookups = await client.lookupObjects(names, { maxResults, objectTypes });
1429
+ const missing = lookups.filter((l) => !l.found).map((l) => l.name);
1430
+ const matchCount = lookups.reduce((count, lookup) => count + lookup.matches.length, 0);
1431
+ const wildcardNames = names.filter((name) => name.includes('*'));
1432
+ const warnings = wildcardNames.length > 0
1433
+ ? [
1434
+ `tadir_lookup performs exact-name lookup; wildcard characters are treated literally for: ${wildcardNames.join(', ')}`,
1435
+ ]
1436
+ : undefined;
1437
+ return textResult(JSON.stringify({ count: matchCount, lookups, missing, ...(warnings ? { warnings } : {}) }, null, 2));
1438
+ }
1400
1439
  if (searchType === 'source_code') {
1401
1440
  // Source code search: do NOT transliterate — source can contain umlauts in strings/comments
1402
1441
  if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
@@ -1441,6 +1480,37 @@ async function handleSAPSearch(client, args) {
1441
1480
  }
1442
1481
  return textResult(transliterationNote + JSON.stringify(results, null, 2));
1443
1482
  }
1483
+ function extractLookupNames(query, rawNames) {
1484
+ const fromNames = Array.isArray(rawNames) ? rawNames.map((n) => String(n).trim()).filter(Boolean) : [];
1485
+ const fromQuery = query
1486
+ .split(/[,\s]+/)
1487
+ .map((n) => n.trim())
1488
+ .filter(Boolean);
1489
+ return [...new Set([...fromNames, ...fromQuery].map((n) => n.toUpperCase()))];
1490
+ }
1491
+ function extractLookupObjectTypes(rawObjectType, rawObjectTypes) {
1492
+ const types = Array.isArray(rawObjectTypes)
1493
+ ? rawObjectTypes.map((t) => normalizeObjectType(String(t))).filter(Boolean)
1494
+ : [];
1495
+ if (typeof rawObjectType === 'string' && rawObjectType.trim()) {
1496
+ types.push(normalizeObjectType(rawObjectType));
1497
+ }
1498
+ return [...new Set(types)];
1499
+ }
1500
+ function normalizePackageOverride(rawPackage, fallback) {
1501
+ if (rawPackage === undefined || rawPackage === null) {
1502
+ return fallback;
1503
+ }
1504
+ const value = String(rawPackage).trim();
1505
+ return value || fallback;
1506
+ }
1507
+ function normalizeTransportOverride(rawTransport) {
1508
+ if (rawTransport === undefined || rawTransport === null) {
1509
+ return undefined;
1510
+ }
1511
+ const value = String(rawTransport).trim();
1512
+ return value || undefined;
1513
+ }
1444
1514
  function classifySapQueryParserError(err, sql) {
1445
1515
  if (err.statusCode !== 400)
1446
1516
  return undefined;
@@ -1460,11 +1530,136 @@ function classifySapQueryParserError(err, sql) {
1460
1530
  }
1461
1531
  return `${err.message}\n\nHint: ${hints.join(' ')}`;
1462
1532
  }
1533
+ const SAPQUERY_IN_LIST_CHUNK_SIZE = 8;
1534
+ function planSimpleInListChunking(sql, chunkSize = SAPQUERY_IN_LIST_CHUNK_SIZE) {
1535
+ const maskedSql = maskSqlStringLiterals(sql);
1536
+ if (maskedSql.includes(';'))
1537
+ return undefined;
1538
+ if (countSelectKeywords(maskedSql) !== 1)
1539
+ return undefined;
1540
+ const matches = [...maskedSql.matchAll(/\b[A-Za-z_][A-Za-z0-9_~.]*\s+IN\s*\(/gi)];
1541
+ if (matches.length !== 1)
1542
+ return undefined;
1543
+ const match = matches[0];
1544
+ const matchText = match[0];
1545
+ const fieldName = matchText.match(/^([A-Za-z_][A-Za-z0-9_~.]*)\s+IN\s*\(/i)?.[1];
1546
+ if (!fieldName || fieldName.toUpperCase() === 'NOT')
1547
+ return undefined;
1548
+ const matchStart = match.index ?? 0;
1549
+ const openParen = matchStart + matchText.lastIndexOf('(');
1550
+ const closeParen = findMatchingParen(maskedSql, openParen);
1551
+ if (closeParen < 0)
1552
+ return undefined;
1553
+ const literals = parseSingleQuotedLiteralList(sql.slice(openParen + 1, closeParen));
1554
+ if (!literals || literals.length <= chunkSize)
1555
+ return undefined;
1556
+ const prefix = sql.slice(0, openParen + 1);
1557
+ const suffix = sql.slice(closeParen);
1558
+ const statements = [];
1559
+ for (let i = 0; i < literals.length; i += chunkSize) {
1560
+ statements.push(`${prefix}${literals.slice(i, i + chunkSize).join(', ')}${suffix}`);
1561
+ }
1562
+ return { statements };
1563
+ }
1564
+ function maskSqlStringLiterals(sql) {
1565
+ let masked = '';
1566
+ let inString = false;
1567
+ for (let i = 0; i < sql.length; i++) {
1568
+ const ch = sql[i];
1569
+ if (ch === "'") {
1570
+ if (inString && sql[i + 1] === "'") {
1571
+ masked += ' ';
1572
+ i++;
1573
+ continue;
1574
+ }
1575
+ inString = !inString;
1576
+ masked += ' ';
1577
+ continue;
1578
+ }
1579
+ masked += inString ? ' ' : ch;
1580
+ }
1581
+ return masked;
1582
+ }
1583
+ function countSelectKeywords(maskedSql) {
1584
+ return [...maskedSql.matchAll(/\bSELECT\b/gi)].length;
1585
+ }
1586
+ function findMatchingParen(text, openParen) {
1587
+ let depth = 0;
1588
+ for (let i = openParen; i < text.length; i++) {
1589
+ if (text[i] === '(')
1590
+ depth++;
1591
+ if (text[i] === ')') {
1592
+ depth--;
1593
+ if (depth === 0)
1594
+ return i;
1595
+ }
1596
+ }
1597
+ return -1;
1598
+ }
1599
+ function parseSingleQuotedLiteralList(listText) {
1600
+ const literals = [];
1601
+ let i = 0;
1602
+ let expectingValue = true;
1603
+ while (i < listText.length) {
1604
+ while (i < listText.length && /\s/.test(listText[i]))
1605
+ i++;
1606
+ if (i >= listText.length)
1607
+ return expectingValue && literals.length > 0 ? undefined : literals;
1608
+ if (!expectingValue || listText[i] !== "'")
1609
+ return undefined;
1610
+ const start = i;
1611
+ i++;
1612
+ let closed = false;
1613
+ while (i < listText.length) {
1614
+ if (listText[i] === "'") {
1615
+ if (listText[i + 1] === "'") {
1616
+ i += 2;
1617
+ continue;
1618
+ }
1619
+ i++;
1620
+ closed = true;
1621
+ break;
1622
+ }
1623
+ i++;
1624
+ }
1625
+ if (!closed)
1626
+ return undefined;
1627
+ literals.push(listText.slice(start, i));
1628
+ expectingValue = false;
1629
+ while (i < listText.length && /\s/.test(listText[i]))
1630
+ i++;
1631
+ if (i >= listText.length)
1632
+ return literals;
1633
+ if (listText[i] !== ',')
1634
+ return undefined;
1635
+ i++;
1636
+ expectingValue = true;
1637
+ }
1638
+ return expectingValue && literals.length > 0 ? undefined : literals;
1639
+ }
1640
+ async function runChunkedSapQuery(client, plan, maxRows) {
1641
+ const rowLimit = Number.isFinite(maxRows) && maxRows > 0 ? Math.floor(maxRows) : 100;
1642
+ const rows = [];
1643
+ let columns = [];
1644
+ for (const statement of plan.statements) {
1645
+ const remaining = Math.max(0, rowLimit - rows.length);
1646
+ if (remaining === 0)
1647
+ break;
1648
+ const chunk = await client.runQuery(statement, remaining);
1649
+ if (columns.length === 0)
1650
+ columns = chunk.columns;
1651
+ rows.push(...chunk.rows);
1652
+ }
1653
+ return { columns, rows: rows.slice(0, rowLimit) };
1654
+ }
1463
1655
  async function handleSAPQuery(client, args) {
1464
1656
  const sql = String(args.sql ?? '');
1465
1657
  const maxRows = Number(args.maxRows ?? 100);
1658
+ const chunkPlan = planSimpleInListChunking(sql);
1659
+ let chunkingAttempted = false;
1466
1660
  try {
1467
- const data = await client.runQuery(sql, maxRows);
1661
+ chunkingAttempted = chunkPlan != null;
1662
+ const data = chunkPlan ? await runChunkedSapQuery(client, chunkPlan, maxRows) : await client.runQuery(sql, maxRows);
1468
1663
  return textResult(JSON.stringify(data, null, 2));
1469
1664
  }
1470
1665
  catch (err) {
@@ -1489,7 +1684,11 @@ async function handleSAPQuery(client, args) {
1489
1684
  }
1490
1685
  }
1491
1686
  if (err instanceof AdtApiError) {
1492
- const parserHint = classifySapQueryParserError(err, sql);
1687
+ let parserHint = classifySapQueryParserError(err, sql);
1688
+ if (parserHint && chunkingAttempted) {
1689
+ parserHint +=
1690
+ '\nARC-1 already split this simple long IN list into smaller ADT freestyle queries; this backend still rejected one chunk. Reduce the query further or use staged named-table previews.';
1691
+ }
1493
1692
  if (parserHint)
1494
1693
  return errorResult(parserHint);
1495
1694
  }
@@ -1527,9 +1726,12 @@ async function handleSAPLint(client, args, config) {
1527
1726
  const rules = listRulesFromConfig(lintConfig);
1528
1727
  const enabled = rules.filter((r) => r.enabled);
1529
1728
  const disabled = rules.filter((r) => !r.enabled);
1729
+ const effectiveAbapRelease = configOptions.abapRelease ?? 'unknown';
1730
+ const syntax = lintConfig.get().syntax;
1530
1731
  return textResult(JSON.stringify({
1531
1732
  preset: configOptions.systemType === 'btp' ? 'cloud' : 'onprem',
1532
- abapVersion: cachedFeatures?.abapRelease ?? 'unknown',
1733
+ abapVersion: effectiveAbapRelease,
1734
+ syntaxVersion: syntax?.version ?? 'unknown',
1533
1735
  enabledRules: enabled.length,
1534
1736
  disabledRules: disabled.length,
1535
1737
  rules: enabled,
@@ -1578,7 +1780,7 @@ function buildLintConfigOptions(config, ruleOverrides) {
1578
1780
  const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
1579
1781
  return {
1580
1782
  systemType,
1581
- abapRelease: cachedFeatures?.abapRelease,
1783
+ abapRelease: cachedFeatures?.abapRelease ?? config.abapRelease,
1582
1784
  configFile: config.abaplintConfig,
1583
1785
  ruleOverrides,
1584
1786
  };
@@ -1590,12 +1792,22 @@ const SERVICEBINDING_V2_CONTENT_TYPE = 'application/vnd.sap.adt.businessservices
1590
1792
  const BDEF_CONTENT_TYPE = 'application/vnd.sap.adt.blues.v1+xml';
1591
1793
  const MESSAGECLASS_CONTENT_TYPE = 'application/vnd.sap.adt.mc.messageclass+xml';
1592
1794
  const SKTD_V2_CONTENT_TYPE = 'application/vnd.sap.adt.sktdv2+xml';
1795
+ // Function group + function module content types — verified live on a4h S/4HANA 2023
1796
+ // (issue #250). FUGR uses the v3 group envelope; FUNC uses the unversioned fmodule envelope.
1797
+ const FUNCTION_GROUP_CONTENT_TYPE = 'application/vnd.sap.adt.functions.groups.v3+xml';
1798
+ const FUNCTION_MODULE_CONTENT_TYPE = 'application/vnd.sap.adt.functions.fmodules+xml';
1593
1799
  function isMetadataWriteType(type) {
1594
1800
  return type === 'DOMA' || type === 'DTEL' || type === 'MSAG' || type === 'SRVB';
1595
1801
  }
1596
1802
  /** Types that require a specific vendor content type for creation (not application/*) */
1597
1803
  function needsVendorContentType(type) {
1598
- return type === 'DOMA' || type === 'DTEL' || type === 'BDEF' || type === 'MSAG' || type === 'SKTD';
1804
+ return (type === 'DOMA' ||
1805
+ type === 'DTEL' ||
1806
+ type === 'BDEF' ||
1807
+ type === 'MSAG' ||
1808
+ type === 'SKTD' ||
1809
+ type === 'FUGR' ||
1810
+ type === 'FUNC');
1599
1811
  }
1600
1812
  /** Content type used for create POST */
1601
1813
  function createContentTypeForType(type) {
@@ -1634,6 +1846,10 @@ function vendorContentTypeForType(type) {
1634
1846
  return MESSAGECLASS_CONTENT_TYPE;
1635
1847
  case 'SKTD':
1636
1848
  return SKTD_V2_CONTENT_TYPE;
1849
+ case 'FUGR':
1850
+ return FUNCTION_GROUP_CONTENT_TYPE;
1851
+ case 'FUNC':
1852
+ return FUNCTION_MODULE_CONTENT_TYPE;
1637
1853
  default:
1638
1854
  // Wildcard lets the SAP server resolve the correct handler.
1639
1855
  // Sending 'application/xml' causes 415 on DDL-based endpoints
@@ -1684,6 +1900,9 @@ function getMetadataWriteProperties(input) {
1684
1900
  category: input.category,
1685
1901
  version: input.version,
1686
1902
  odataVersion: input.odataVersion,
1903
+ // Function-module create needs the parent function-group name for the
1904
+ // <adtcore:containerRef> in the create payload (issue #250).
1905
+ group: input.group,
1687
1906
  };
1688
1907
  return props;
1689
1908
  }
@@ -2082,6 +2301,30 @@ export function buildCreateXml(type, name, pkg, description, properties) {
2082
2301
  };
2083
2302
  return buildMessageClassXml(params);
2084
2303
  }
2304
+ case 'FUGR':
2305
+ // Function group create envelope. POSTed to /sap/bc/adt/functions/groups
2306
+ // with Content-Type: application/vnd.sap.adt.functions.groups.v3+xml.
2307
+ // Verified live on a4h S/4HANA 2023 (issue #250).
2308
+ return `<?xml version="1.0" encoding="UTF-8"?>
2309
+ <group:abapFunctionGroup xmlns:group="http://www.sap.com/adt/functions/groups" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${escapeXml(description)}" adtcore:language="EN" adtcore:name="${escapeXml(name)}" adtcore:type="FUGR/F" adtcore:masterLanguage="EN">
2310
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
2311
+ </group:abapFunctionGroup>`;
2312
+ case 'FUNC': {
2313
+ // Function module create envelope. POSTed to
2314
+ // /sap/bc/adt/functions/groups/{group_lc}/fmodules with
2315
+ // Content-Type: application/vnd.sap.adt.functions.fmodules+xml.
2316
+ // No <adtcore:packageRef> — FM inherits package from the parent FUGR.
2317
+ // adtcore:uri must be lowercase (verified live on a4h).
2318
+ const group = String(properties?.group ?? '').trim();
2319
+ if (!group) {
2320
+ throw new Error('FUNC create requires "group" property — pass it via SAPWrite args (the parent function group must already exist).');
2321
+ }
2322
+ const groupLc = encodeURIComponent(group.toLowerCase());
2323
+ return `<?xml version="1.0" encoding="UTF-8"?>
2324
+ <fmodule:abapFunctionModule xmlns:fmodule="http://www.sap.com/adt/functions/fmodules" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${escapeXml(description)}" adtcore:name="${escapeXml(name)}" adtcore:type="FUGR/FF">
2325
+ <adtcore:containerRef adtcore:name="${escapeXml(group)}" adtcore:type="FUGR/F" adtcore:uri="/sap/bc/adt/functions/groups/${groupLc}"/>
2326
+ </fmodule:abapFunctionModule>`;
2327
+ }
2085
2328
  default:
2086
2329
  // Fallback — generic objectReferences using the correct URL for the type
2087
2330
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -2090,6 +2333,27 @@ export function buildCreateXml(type, name, pkg, description, properties) {
2090
2333
  </adtcore:objectReferences>`;
2091
2334
  }
2092
2335
  }
2336
+ /**
2337
+ * Strip SAPGUI-style function-module parameter comment blocks from an FM source body.
2338
+ *
2339
+ * SAP rejects PUT-to-source/main with parameter comment blocks (verified live on a4h
2340
+ * S/4HANA 2023 — issue #250):
2341
+ * HTTP 400 / com.sap.adt.sedi / ExceptionResourceScanDuringSaveFailure
2342
+ * "Parameter comment blocks are not allowed" (T100KEY FUNC_ADT028)
2343
+ *
2344
+ * The signature is metadata, not source. LLMs frequently emit the SAPGUI block out
2345
+ * of muscle memory (every released FM ships with one). This helper strips lines whose
2346
+ * first non-whitespace tokens are `*"` so the PUT succeeds, and reports back whether
2347
+ * stripping occurred so the caller can append a warning to the response.
2348
+ *
2349
+ * Only `*"…` lines are stripped — single `*` ABAP comments and inline `"` comments
2350
+ * are preserved. Exported for unit tests.
2351
+ */
2352
+ export function stripFmParamCommentBlock(source) {
2353
+ const lines = source.split('\n');
2354
+ const kept = lines.filter((line) => !/^\s*\*"/.test(line));
2355
+ return { source: kept.join('\n'), wasStripped: kept.length !== lines.length };
2356
+ }
2093
2357
  /** Escape special characters for XML attribute values */
2094
2358
  function escapeXml(s) {
2095
2359
  return s
@@ -2420,16 +2684,60 @@ function objectUrlForTypeRaw(type, name) {
2420
2684
  function sourceUrlForType(type, name) {
2421
2685
  return `${objectUrlForType(type, name)}/source/main`;
2422
2686
  }
2687
+ const CLASS_WRITE_INCLUDES = ['definitions', 'implementations', 'macros', 'testclasses'];
2423
2688
  /** Get a CLAS include URL (definitions/implementations/macros/testclasses) */
2424
2689
  function classIncludeUrl(name, include) {
2425
2690
  return `/sap/bc/adt/oo/classes/${encodeURIComponent(name)}/includes/${include}`;
2426
2691
  }
2692
+ function normalizeClassWriteInclude(include) {
2693
+ if (typeof include !== 'string')
2694
+ return undefined;
2695
+ const normalized = include.toLowerCase();
2696
+ return CLASS_WRITE_INCLUDES.includes(normalized) ? normalized : undefined;
2697
+ }
2698
+ /**
2699
+ * Auto-detect which class include a method specifier targets, based on the
2700
+ * local-class prefix on the LHS of `<localclass>~<method>`. Used by
2701
+ * `edit_method` so callers can pass `lhc_project~approve_project` and have
2702
+ * ARC-1 transparently route the read+write to `/includes/implementations`
2703
+ * instead of `/source/main`.
2704
+ *
2705
+ * Prefix → include mapping (intentionally narrow; extend via explicit
2706
+ * `include` parameter when a code-base uses other conventions):
2707
+ * - `lhc_*` → implementations (RAP behavior pool handler classes)
2708
+ * - `lcl_*` → implementations (local helper classes)
2709
+ * - `ltc_*` → testclasses (ABAP Unit local test classes)
2710
+ *
2711
+ * Returns `undefined` for:
2712
+ * - Specifiers with no `~` (route to MAIN)
2713
+ * - Global-interface methods like `zif_order~create`, `if_oo_adt_classrun~main`
2714
+ * (route to MAIN — the impl lives in a global class)
2715
+ * - `lif_*` local interfaces (interfaces only declare methods — there's no
2716
+ * impl in CCDEF; an `lhc_*`/`lcl_*` class implements them and the call
2717
+ * site uses that class's prefix instead)
2718
+ */
2719
+ function detectLocalHandlerInclude(method) {
2720
+ if (!method.includes('~'))
2721
+ return undefined;
2722
+ const lhs = method.slice(0, method.indexOf('~')).trim().toLowerCase();
2723
+ if (/^(lhc|lcl)_/.test(lhs))
2724
+ return 'implementations';
2725
+ if (/^ltc_/.test(lhs))
2726
+ return 'testclasses';
2727
+ return undefined;
2728
+ }
2729
+ /** Strip the leading "=== <include> ===\n" header that `client.getClass(name, include)` prepends. */
2730
+ function stripIncludeHeader(source) {
2731
+ return source.replace(/^=== \w+ ===\n/, '');
2732
+ }
2427
2733
  // ─── SAPWrite Handler ────────────────────────────────────────────────
2428
2734
  async function handleSAPWrite(client, args, config, cachingLayer) {
2429
2735
  const action = String(args.action ?? '');
2430
2736
  const type = normalizeObjectType(String(args.type ?? ''));
2431
2737
  const name = String(args.name ?? '');
2432
2738
  const source = String(args.source ?? '');
2739
+ const hasSource = typeof args.source === 'string';
2740
+ const include = normalizeClassWriteInclude(args.include);
2433
2741
  const transport = args.transport;
2434
2742
  const lintOverride = args.lintBeforeWrite;
2435
2743
  const preflightOverride = args.preflightBeforeWrite;
@@ -2453,12 +2761,42 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2453
2761
  // (transparent) or /structures/ (DDIC structure). Resolve once via the client's
2454
2762
  // cached URL probe. For 'create' the default /tables/ URL is correct (we only
2455
2763
  // create transparent tables today; structure creation is out of scope).
2764
+ //
2765
+ // For FUNC, the URL has the parent function group baked into the path:
2766
+ // /sap/bc/adt/functions/groups/{group_lc}/fmodules/{name_lc}
2767
+ // `objectBasePath('FUNC')` deliberately throws (PR #223 — generic URL builders
2768
+ // must fail loudly for FM since they can't know the parent group). Issue #250:
2769
+ // we pre-resolve the URL here from `args.group` (required for create; auto-
2770
+ // resolved via search for update/delete) so the action switch downstream uses
2771
+ // the correct URL. We also mirror the resolved group back onto args so
2772
+ // `buildCreateXml('FUNC', …, properties)` finds it.
2456
2773
  let objectUrl;
2457
2774
  let srcUrl;
2458
2775
  if (type === 'TABL' && action !== 'create' && action !== 'batch_create') {
2459
2776
  objectUrl = await client.resolveTablObjectUrl(name);
2460
2777
  srcUrl = `${objectUrl}/source/main`;
2461
2778
  }
2779
+ else if (type === 'FUNC') {
2780
+ let group = String(args.group ?? '').trim();
2781
+ if (!group) {
2782
+ if (action === 'create') {
2783
+ return errorResult('"group" is required to create a FUNC. Create the parent function group first (SAPWrite type=FUGR) or pass group explicitly.');
2784
+ }
2785
+ // For update/delete try to auto-resolve the group via search
2786
+ const resolved = cachingLayer
2787
+ ? await cachingLayer.resolveFuncGroup(client, name)
2788
+ : await client.resolveFunctionGroup(name);
2789
+ if (!resolved) {
2790
+ return errorResult(`Cannot resolve function group for FM "${name}". Provide the "group" parameter explicitly, or use SAPSearch to find the parent group.`);
2791
+ }
2792
+ group = resolved;
2793
+ }
2794
+ const groupLc = encodeURIComponent(group.toLowerCase());
2795
+ objectUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(name.toLowerCase())}`;
2796
+ srcUrl = `${objectUrl}/source/main`;
2797
+ // Pass the resolved group through to buildCreateXml via args.group
2798
+ args.group = group;
2799
+ }
2462
2800
  else {
2463
2801
  objectUrl = objectUrlForType(type, name);
2464
2802
  srcUrl = sourceUrlForType(type, name);
@@ -2480,6 +2818,23 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2480
2818
  switch (action) {
2481
2819
  case 'update': {
2482
2820
  const existingPackage = await enforcePackageForExistingObject();
2821
+ // Keep CLAS local include writes ahead of the generic /source/main fallthrough.
2822
+ // If CLAS ever gains separate metadata-update handling, this branch must still
2823
+ // win whenever callers pass include=definitions|implementations|macros|testclasses.
2824
+ if (args.include !== undefined) {
2825
+ if (!include) {
2826
+ return errorResult(`Invalid CLAS include "${String(args.include)}". Valid values: ${CLASS_WRITE_INCLUDES.join(', ')}.`);
2827
+ }
2828
+ if (type !== 'CLAS') {
2829
+ return errorResult('SAPWrite include is only supported for action="update" with type="CLAS".');
2830
+ }
2831
+ if (!hasSource) {
2832
+ return errorResult('"source" is required when updating a CLAS include.');
2833
+ }
2834
+ await safeUpdateSource(client.http, client.safety, objectUrl, classIncludeUrl(name, include), source, transport, cachedFeatures?.abapRelease);
2835
+ invalidateWrittenObject(type, name);
2836
+ return textResult(`Successfully updated ${type} ${name} include ${include}. Active version remains unchanged until activation; read with SAPRead(version="inactive") to verify the draft.`);
2837
+ }
2483
2838
  if (type === 'SKTD') {
2484
2839
  // KTD update requires the full <sktd:docu> XML envelope with the Markdown
2485
2840
  // body base64-encoded inside <sktd:text>, PUT with
@@ -2514,19 +2869,67 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2514
2869
  const cdsGuardUpdate = guardCdsSyntax(type, source, cachedFeatures);
2515
2870
  if (cdsGuardUpdate)
2516
2871
  return cdsGuardUpdate;
2517
- // Pre-write lint validation
2518
- const lintWarnings = runPreWriteLint(source, type, name, config, lintOverride);
2872
+ // FUNC-source sanitization: strip SAPGUI-style parameter comment blocks.
2873
+ // SAP rejects PUT-to-source/main with these blocks (HTTP 400 / FUNC_ADT028
2874
+ // "Parameter comment blocks are not allowed" — verified live a4h S/4HANA 2023,
2875
+ // issue #250). LLMs frequently emit them out of muscle memory because every
2876
+ // released FM has one. Strip and warn rather than fail.
2877
+ //
2878
+ // Issue #252: when `parameters` is supplied as a structured array, splice
2879
+ // it into the FM source as ABAP-source-based signature syntax. If `source`
2880
+ // is omitted entirely, fetch the existing source first to preserve the
2881
+ // body. The structured clause replaces any existing signature region.
2882
+ let effectiveSource = source;
2883
+ let fmParamStripWarning;
2884
+ let fmParamMergeWarning;
2885
+ if (type === 'FUNC') {
2886
+ const parameters = args.parameters;
2887
+ if (parameters !== undefined) {
2888
+ // If caller passed parameters but no source, fetch the current source so
2889
+ // the body is preserved (the parameters array re-emits only the signature).
2890
+ let baseSource = source;
2891
+ if (!baseSource || baseSource.trim() === '') {
2892
+ const groupName = String(args.group ?? '');
2893
+ const fetched = await client.getFunction(groupName, name).catch(() => null);
2894
+ baseSource = fetched?.source ?? `FUNCTION ${name}.\nENDFUNCTION.\n`;
2895
+ }
2896
+ else if (!/^\s*FUNCTION\s+/i.test(baseSource)) {
2897
+ // Body-only source: wrap in FUNCTION/ENDFUNCTION so the splicer has
2898
+ // something to work with. Common shape from LLMs: just the body.
2899
+ baseSource = `FUNCTION ${name}.\n${baseSource}\nENDFUNCTION.\n`;
2900
+ }
2901
+ try {
2902
+ effectiveSource = spliceFmSignature(baseSource, name, parameters);
2903
+ }
2904
+ catch {
2905
+ // No FUNCTION token in the supplied source — fall back to user's source.
2906
+ effectiveSource = baseSource;
2907
+ fmParamMergeWarning =
2908
+ 'Could not splice structured parameters: source did not start with FUNCTION keyword. Used the supplied source verbatim.';
2909
+ }
2910
+ }
2911
+ // Defense-in-depth: strip *" comment blocks even after splicing — the
2912
+ // user's body may contain them (e.g. pasted from SAPGUI).
2913
+ const stripped = stripFmParamCommentBlock(effectiveSource);
2914
+ effectiveSource = stripped.source;
2915
+ if (stripped.wasStripped) {
2916
+ fmParamStripWarning =
2917
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (SAP rejects them on PUT — pass `parameters` as a structured array instead).';
2918
+ }
2919
+ }
2920
+ // Pre-write lint validation (uses sanitized source for FUNC)
2921
+ const lintWarnings = runPreWriteLint(effectiveSource, type, name, config, lintOverride);
2519
2922
  if (lintWarnings.blocked)
2520
2923
  return lintWarnings.result;
2521
2924
  // Pre-write server-side syntax check (opt-in; never blocks — warnings only).
2522
- const checkNotes = await runPreWriteSyntaxCheck(client, type, source, objectUrl, config, checkOverride);
2925
+ const checkNotes = await runPreWriteSyntaxCheck(client, type, effectiveSource, objectUrl, config, checkOverride);
2523
2926
  // If safeUpdateSource throws (lock conflict, network error, etc.), checkNotes
2524
2927
  // is intentionally discarded — pre-check warnings only matter when the write succeeded.
2525
- await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport, cachedFeatures?.abapRelease);
2928
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, effectiveSource, transport, cachedFeatures?.abapRelease);
2526
2929
  invalidateWrittenObject(type, name);
2527
2930
  const msg = `Successfully updated ${type} ${name}.`;
2528
2931
  const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
2529
- const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint);
2932
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint, fmParamStripWarning, fmParamMergeWarning);
2530
2933
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
2531
2934
  }
2532
2935
  case 'create': {
@@ -2695,17 +3098,57 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2695
3098
  : '';
2696
3099
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
2697
3100
  }
2698
- // Step 2: Write source code if provided
2699
- if (source) {
3101
+ // Step 2: Write source code if provided.
3102
+ // Issue #252: FUNC create accepts a structured `parameters` array; if
3103
+ // provided we must follow up with a source PUT even when `source` is
3104
+ // omitted (the array alone synthesizes a minimal FUNCTION/ENDFUNCTION
3105
+ // body containing the signature clause).
3106
+ const funcParameters = type === 'FUNC' ? args.parameters : undefined;
3107
+ const shouldWriteSource = !!source || (funcParameters !== undefined && funcParameters.length > 0);
3108
+ if (shouldWriteSource) {
3109
+ // FUNC: build/splice the signature, then strip SAPGUI parameter comment
3110
+ // blocks as defense-in-depth (see update path for rationale).
3111
+ let createSource = source ?? '';
3112
+ let fmParamStripWarning;
3113
+ let fmParamMergeWarning;
3114
+ if (type === 'FUNC') {
3115
+ if (funcParameters !== undefined) {
3116
+ let baseSource;
3117
+ if (!createSource || createSource.trim() === '') {
3118
+ baseSource = `FUNCTION ${name}.\nENDFUNCTION.\n`;
3119
+ }
3120
+ else if (!/^\s*FUNCTION\s+/i.test(createSource)) {
3121
+ // Body-only source — wrap so the splicer has a signature region.
3122
+ baseSource = `FUNCTION ${name}.\n${createSource}\nENDFUNCTION.\n`;
3123
+ }
3124
+ else {
3125
+ baseSource = createSource;
3126
+ }
3127
+ try {
3128
+ createSource = spliceFmSignature(baseSource, name, funcParameters);
3129
+ }
3130
+ catch {
3131
+ createSource = baseSource;
3132
+ fmParamMergeWarning =
3133
+ 'Could not splice structured parameters: source did not start with FUNCTION keyword. Used the supplied source verbatim.';
3134
+ }
3135
+ }
3136
+ const stripped = stripFmParamCommentBlock(createSource);
3137
+ createSource = stripped.source;
3138
+ if (stripped.wasStripped) {
3139
+ fmParamStripWarning =
3140
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (pass `parameters` as a structured array instead).';
3141
+ }
3142
+ }
2700
3143
  // Pre-write lint validation
2701
- const lintWarnings = runPreWriteLint(source, type, name, config, lintOverride);
3144
+ const lintWarnings = runPreWriteLint(createSource, type, name, config, lintOverride);
2702
3145
  if (lintWarnings.blocked) {
2703
3146
  return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
2704
3147
  }
2705
- await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport, cachedFeatures?.abapRelease);
3148
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, createSource, effectiveTransport, cachedFeatures?.abapRelease);
2706
3149
  invalidateWrittenObject(type, name);
2707
3150
  const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
2708
- const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings);
3151
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning, fmParamMergeWarning);
2709
3152
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
2710
3153
  }
2711
3154
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
@@ -2719,10 +3162,50 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2719
3162
  if (type !== 'CLAS')
2720
3163
  return errorResult('edit_method is only supported for type=CLAS.');
2721
3164
  await enforcePackageForExistingObject();
2722
- // Fetch current full source (use cache if available)
2723
- const currentSource = cachingLayer
2724
- ? (await cachingLayer.getSource('CLAS', name, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch }))).source
2725
- : (await client.getClass(name)).source;
3165
+ // ── Resolve which class section the method body lives in ──
3166
+ // Order:
3167
+ // 1. Explicit `include` parameter wins (must be a valid CLAS include).
3168
+ // If the user passed something but normalization rejected it,
3169
+ // report it the same way `case 'update'` does.
3170
+ // 2. Auto-detect from local-class prefix in `method` specifier
3171
+ // (lhc_*/lcl_* → implementations, ltc_* → testclasses). This is
3172
+ // transparent to RAP-skill callers passing `lhc_project~approve_project`.
3173
+ // 3. Fall through to MAIN (existing behavior — covers global classes
3174
+ // and `zif_order~create` style interface methods).
3175
+ if (args.include !== undefined && !include) {
3176
+ return errorResult(`Invalid CLAS include "${String(args.include)}". Valid values: ${CLASS_WRITE_INCLUDES.join(', ')}.`);
3177
+ }
3178
+ const detectedInclude = include ? undefined : detectLocalHandlerInclude(method);
3179
+ const resolvedInclude = include ?? detectedInclude;
3180
+ // Fetch the source that contains the method.
3181
+ // Note: include reads bypass the source cache because the cache key is
3182
+ // `(type, name, active|inactive)` and does not differentiate by include.
3183
+ // Mixing MAIN and CCIMP bytes under the same key would silently corrupt
3184
+ // subsequent reads. Future enhancement: extend cache key with include.
3185
+ let currentSource;
3186
+ if (resolvedInclude) {
3187
+ // **Draft-aware include reads (PR-D review fix, P1).**
3188
+ // After `SAPWrite update include=...` or `scaffold_rap_handlers`, the
3189
+ // edited CCDEF/CCIMP lives as an inactive draft; the active include
3190
+ // is often still the empty placeholder. Reading "active" here would
3191
+ // splice against stale content (and frequently "method not found").
3192
+ // Use the standard inactive-list lookup to pick the right version —
3193
+ // same auto-resolution semantics SAPRead exposes via `version='auto'`.
3194
+ const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', name, 'auto');
3195
+ const fetched = await client.getClass(name, resolvedInclude, { version: effectiveVersion });
3196
+ currentSource = stripIncludeHeader(fetched.source);
3197
+ // If the include itself has no draft (only MAIN does), SAP returns the
3198
+ // active include body for `?version=inactive`. That's correct — we
3199
+ // splice whatever the editor would see. If the include source isn't
3200
+ // available at all (response contains the "not available" placeholder
3201
+ // injected by client.getClass on 404), splice will surface a clean
3202
+ // "method not found" with the include name.
3203
+ }
3204
+ else {
3205
+ currentSource = cachingLayer
3206
+ ? (await cachingLayer.getSource('CLAS', name, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch }))).source
3207
+ : (await client.getClass(name)).source;
3208
+ }
2726
3209
  // Use detected ABAP version from probe if available
2727
3210
  const abaplintVer = cachedFeatures?.abapRelease
2728
3211
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
@@ -2730,18 +3213,40 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2730
3213
  // Splice in the new method body
2731
3214
  const spliced = spliceMethod(currentSource, name, method, source, abaplintVer);
2732
3215
  if (!spliced.success) {
2733
- return errorResult(spliced.error ?? `Failed to splice method "${method}" in ${name}.`);
3216
+ // Augment the error with which include was searched, so the LLM can
3217
+ // either correct the method specifier or override include= explicitly.
3218
+ const where = resolvedInclude ? `include "${resolvedInclude}"` : 'main source';
3219
+ const baseError = spliced.error ?? `Failed to splice method "${method}" in ${name}.`;
3220
+ const hint = detectedInclude
3221
+ ? ` (auto-routed via "${method}" prefix; pass include= explicitly to override).`
3222
+ : '';
3223
+ return errorResult(`${baseError} Searched ${where} of ${name}.${hint}`);
2734
3224
  }
2735
- // Pre-write lint validation on the full spliced source
2736
- const lintWarnings = runPreWriteLint(spliced.newSource, type, name, config, lintOverride);
2737
- if (lintWarnings.blocked)
2738
- return lintWarnings.result;
2739
- // Pre-write server-side syntax check on the full spliced source (opt-in; warnings only).
2740
- const checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
2741
- // Write the full source back (existing lock/modify/unlock flow)
2742
- await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport, cachedFeatures?.abapRelease);
3225
+ // Pre-write lint + server-side syntax check on the spliced source.
3226
+ //
3227
+ // Skip BOTH for include= writes. abaplint cannot parse a CCIMP/CCDEF
3228
+ // fragment as a complete class (the DEFINITION/IMPLEMENTATION halves
3229
+ // live in different files), so it would block legitimate writes with
3230
+ // "Expected CLASSDEFINITION" errors. The existing `case 'update'` include=
3231
+ // path also bypasses these checks for the same reason keep parity.
3232
+ // The full-class activation pass after the write is the authoritative
3233
+ // syntax check.
3234
+ let lintWarnings = { blocked: false };
3235
+ let checkNotes = '';
3236
+ if (!resolvedInclude) {
3237
+ lintWarnings = runPreWriteLint(spliced.newSource, type, name, config, lintOverride);
3238
+ if (lintWarnings.blocked)
3239
+ return lintWarnings.result;
3240
+ checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
3241
+ }
3242
+ // Write the full source back (existing lock/modify/unlock flow).
3243
+ // For include writes, the parent class lock auto-applies; the include URL
3244
+ // takes the body. See `compare/eclipse-adt/api/05-lock-create-update-transport.md`.
3245
+ const writeUrl = resolvedInclude ? classIncludeUrl(name, resolvedInclude) : srcUrl;
3246
+ await safeUpdateSource(client.http, client.safety, objectUrl, writeUrl, spliced.newSource, transport, cachedFeatures?.abapRelease);
2743
3247
  invalidateWrittenObject(type, name);
2744
- const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
3248
+ const where = resolvedInclude ? ` (include: ${resolvedInclude})` : '';
3249
+ const msg = `Successfully updated method "${method}" in ${type} ${name}${where}.`;
2745
3250
  const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
2746
3251
  return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
2747
3252
  }
@@ -2847,6 +3352,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2847
3352
  applied: false,
2848
3353
  hint: unresolvedHint,
2849
3354
  applyResult: {
3355
+ skeletons: scaffoldPlan.skeletons,
2850
3356
  main: scaffoldPlan.signatures.main,
2851
3357
  definitions: scaffoldPlan.signatures.definitions,
2852
3358
  implementations: scaffoldPlan.signatures.implementations,
@@ -2911,12 +3417,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2911
3417
  });
2912
3418
  invalidateWrittenObject();
2913
3419
  const msg = `Scaffolded ${scaffoldPlan.insertedSignatureCount} RAP handler signature(s) and ${scaffoldPlan.insertedImplementationStubCount} implementation stub(s) in ${type} ${name} from BDEF ${bdefName}. ` +
3420
+ `Auto-created ${scaffoldPlan.skeletons.createdDefinitions.length + scaffoldPlan.skeletons.createdImplementations.length} handler skeleton section(s). ` +
2914
3421
  `Updated section(s): ${scaffoldPlan.changedSections.join(', ')}.`;
2915
3422
  const warnings = mergePreWriteWarnings(lintWarningsMain?.warnings, lintWarningsDefinitions?.warnings, lintWarningsImplementations?.warnings);
2916
3423
  const details = JSON.stringify({
2917
3424
  ...summary,
2918
3425
  applied: true,
2919
3426
  applyResult: {
3427
+ skeletons: scaffoldPlan.skeletons,
2920
3428
  main: scaffoldPlan.signatures.main,
2921
3429
  definitions: scaffoldPlan.signatures.definitions,
2922
3430
  implementations: scaffoldPlan.signatures.implementations,
@@ -2926,6 +3434,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2926
3434
  }, null, 2);
2927
3435
  return warnings ? textResult(`${msg}\n\n${warnings}\n\n${details}`) : textResult(`${msg}\n\n${details}`);
2928
3436
  }
3437
+ case 'generate_behavior_implementation': {
3438
+ // PR-C: high-level RAP one-shot — auto-discover BDEF via class metadata's
3439
+ // rootEntityRef, scaffold every required handler (creating lhc_<alias>
3440
+ // skeletons when missing), write under one lock, and (by default) activate.
3441
+ // Reliable equivalent of Eclipse ADT's "Generate Behavior Implementation"
3442
+ // Cmd+1 quickfix; avoids the broken /sap/bc/adt/quickfixes/proposals/
3443
+ // create_class_implementation server endpoint (HTTP 500 on a4h, verified
3444
+ // live during PR-C research). See docs/plans/add-generate-behavior-implementation.md.
3445
+ if (type !== 'CLAS') {
3446
+ return errorResult('generate_behavior_implementation is only supported for type=CLAS behavior pool classes.');
3447
+ }
3448
+ if (!name) {
3449
+ return errorResult('"name" is required for generate_behavior_implementation.');
3450
+ }
3451
+ const dryRun = args.dryRun === true || String(args.dryRun ?? '') === 'true';
3452
+ const activate = args.activate === undefined ? true : args.activate === true || String(args.activate) === 'true';
3453
+ const explicitBdef = args.bdefName?.trim() || undefined;
3454
+ const targetAlias = args.targetAlias?.trim() || undefined;
3455
+ // Package gate only when we'll actually mutate. dryRun=true is read-only;
3456
+ // bypassing the gate matches the scaffold_rap_handlers preview pattern.
3457
+ if (!dryRun) {
3458
+ await enforcePackageForExistingObject();
3459
+ }
3460
+ const result = await generateBehaviorImplementation(client, name, {
3461
+ bdefName: explicitBdef,
3462
+ targetAlias,
3463
+ activate,
3464
+ dryRun,
3465
+ transport,
3466
+ });
3467
+ invalidateWrittenObject();
3468
+ // MCP result-code mapping via the exported helper — see
3469
+ // `isRapGenerateResultSuccess` for the success/error contract (Codex review on PR #260, P1).
3470
+ // The structured JSON is preserved in both branches so the caller can still see what
3471
+ // was discovered, written, and what activation reported.
3472
+ const json = JSON.stringify(result, null, 2);
3473
+ return isRapGenerateResultSuccess(result) ? textResult(json) : errorResult(json);
3474
+ }
2929
3475
  case 'delete': {
2930
3476
  await enforcePackageForExistingObject();
2931
3477
  // Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
@@ -2967,20 +3513,35 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2967
3513
  if (!objects || !Array.isArray(objects) || objects.length === 0) {
2968
3514
  return errorResult('"objects" array is required and must be non-empty for batch_create action.');
2969
3515
  }
2970
- const pkg = String(args.package ?? '$TMP');
2971
- // Check package is allowed before starting any creates
2972
- checkPackage(client.safety, pkg);
2973
- // Pre-flight transport check for batch_create (same logic as single create)
2974
- let batchTransport = transport;
2975
- if (!transport && pkg.toUpperCase() !== '$TMP') {
3516
+ const defaultPackage = normalizePackageOverride(args.package, '$TMP');
3517
+ const batchPlan = objects.map((obj) => {
3518
+ const objType = normalizeObjectType(String(obj.type ?? ''));
3519
+ const objName = String(obj.name ?? '');
3520
+ const objPackage = normalizePackageOverride(obj.package, defaultPackage);
3521
+ const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
3522
+ return { obj, type: objType, name: objName, packageName: objPackage, explicitTransport };
3523
+ });
3524
+ // Check every target package before starting any creates.
3525
+ for (const pkg of new Set(batchPlan.map((item) => item.packageName))) {
3526
+ checkPackage(client.safety, pkg);
3527
+ }
3528
+ // Pre-flight transport check for batch_create (same logic as single create),
3529
+ // but keyed by each effective package because objects can override package.
3530
+ const autoTransportByPackage = new Map();
3531
+ const firstPlanNeedingTransportByPackage = new Map();
3532
+ for (const plan of batchPlan) {
3533
+ if (!plan.explicitTransport &&
3534
+ plan.packageName.toUpperCase() !== '$TMP' &&
3535
+ !firstPlanNeedingTransportByPackage.has(plan.packageName)) {
3536
+ firstPlanNeedingTransportByPackage.set(plan.packageName, plan);
3537
+ }
3538
+ }
3539
+ for (const [pkg, plan] of firstPlanNeedingTransportByPackage) {
2976
3540
  try {
2977
- // Use first object's URL for the transport check
2978
- const firstObj = objects[0];
2979
- const firstType = normalizeObjectType(String(firstObj?.type ?? ''));
2980
- const firstUrl = objectUrlForType(firstType, String(firstObj?.name ?? ''));
3541
+ const firstUrl = objectUrlForType(plan.type, plan.name);
2981
3542
  const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
2982
3543
  if (transportInfo.lockedTransport) {
2983
- batchTransport = transportInfo.lockedTransport;
3544
+ autoTransportByPackage.set(pkg, transportInfo.lockedTransport);
2984
3545
  }
2985
3546
  else if (!transportInfo.isLocal && transportInfo.recording) {
2986
3547
  const existingList = transportInfo.existingTransports.length > 0
@@ -2997,7 +3558,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2997
3558
  existingList);
2998
3559
  }
2999
3560
  }
3000
- catch {
3561
+ catch (err) {
3562
+ logger.warn('SAPWrite batch_create transport preflight failed; continuing without auto transport', {
3563
+ package: pkg,
3564
+ type: plan.type,
3565
+ name: plan.name,
3566
+ error: err instanceof Error ? err.message : String(err),
3567
+ });
3001
3568
  // If transportInfo check fails, proceed — SAP will return its own error if needed.
3002
3569
  }
3003
3570
  }
@@ -3007,9 +3574,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3007
3574
  // guard fires for every MSAG entry, but a batch typically shares one transport — cache
3008
3575
  // the lookup result to avoid one HTTP roundtrip per object.
3009
3576
  const transportLookupCache = new Map();
3010
- for (const obj of objects) {
3011
- const objType = normalizeObjectType(String(obj.type ?? ''));
3012
- const objName = String(obj.name ?? '');
3577
+ for (const plan of batchPlan) {
3578
+ const { obj, type: objType, name: objName, packageName: objPackage } = plan;
3579
+ const objTransport = plan.explicitTransport ?? autoTransportByPackage.get(objPackage);
3013
3580
  const metadataObject = isMetadataWriteType(objType);
3014
3581
  const objSource = obj.source ? String(obj.source) : undefined;
3015
3582
  const objDescription = String(obj.description ?? objName);
@@ -3020,24 +3587,26 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3020
3587
  results.push({
3021
3588
  type: objType,
3022
3589
  name: objName,
3590
+ packageName: objPackage,
3023
3591
  status: 'failed',
3024
3592
  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
3593
  });
3026
3594
  break;
3027
3595
  }
3028
3596
  // MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
3029
- if (objType === 'MSAG' && batchTransport) {
3030
- let tr = transportLookupCache.get(batchTransport);
3597
+ if (objType === 'MSAG' && objTransport) {
3598
+ let tr = transportLookupCache.get(objTransport);
3031
3599
  if (tr === undefined) {
3032
- tr = await getTransport(client.http, client.safety, batchTransport);
3033
- transportLookupCache.set(batchTransport, tr);
3600
+ tr = await getTransport(client.http, client.safety, objTransport);
3601
+ transportLookupCache.set(objTransport, tr);
3034
3602
  }
3035
3603
  if (!tr) {
3036
3604
  results.push({
3037
3605
  type: objType,
3038
3606
  name: objName,
3607
+ packageName: objPackage,
3039
3608
  status: 'failed',
3040
- error: `Transport "${batchTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
3609
+ error: `Transport "${objTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
3041
3610
  });
3042
3611
  break;
3043
3612
  }
@@ -3048,6 +3617,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3048
3617
  results.push({
3049
3618
  type: objType,
3050
3619
  name: objName,
3620
+ packageName: objPackage,
3051
3621
  status: 'failed',
3052
3622
  error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
3053
3623
  });
@@ -3062,6 +3632,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3062
3632
  results.push({
3063
3633
  type: objType,
3064
3634
  name: objName,
3635
+ packageName: objPackage,
3065
3636
  status: 'failed',
3066
3637
  error: preflightWarnings.result.content[0].text,
3067
3638
  });
@@ -3075,6 +3646,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3075
3646
  results.push({
3076
3647
  type: objType,
3077
3648
  name: objName,
3649
+ packageName: objPackage,
3078
3650
  status: 'failed',
3079
3651
  error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
3080
3652
  });
@@ -3085,11 +3657,11 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3085
3657
  const objUrl = objectUrlForType(objType, objName);
3086
3658
  const createUrl = objUrl.replace(/\/[^/]+$/, '');
3087
3659
  const objMetadataProps = getMetadataWriteProperties(obj);
3088
- const body = buildCreateXml(objType, objName, pkg, objDescription, objMetadataProps);
3660
+ const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
3089
3661
  const contentType = createContentTypeForType(objType);
3090
3662
  const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
3091
3663
  try {
3092
- await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
3664
+ await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
3093
3665
  }
3094
3666
  catch (createErr) {
3095
3667
  if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
@@ -3104,7 +3676,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3104
3676
  if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
3105
3677
  await client.http.withStatefulSession(async (session) => {
3106
3678
  const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
3107
- const lockTransport = batchTransport ?? (lock.corrNr || undefined);
3679
+ const lockTransport = objTransport ?? (lock.corrNr || undefined);
3108
3680
  try {
3109
3681
  await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
3110
3682
  }
@@ -3116,7 +3688,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3116
3688
  // Step 2: Write source if provided
3117
3689
  if (!metadataObject && objSource) {
3118
3690
  const srcUrl = sourceUrlForType(objType, objName);
3119
- await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport, cachedFeatures?.abapRelease);
3691
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, objTransport, cachedFeatures?.abapRelease);
3120
3692
  }
3121
3693
  // Step 3: Activate the object
3122
3694
  const activationResult = await activate(client.http, client.safety, objUrl);
@@ -3124,18 +3696,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3124
3696
  results.push({
3125
3697
  type: objType,
3126
3698
  name: objName,
3699
+ packageName: objPackage,
3127
3700
  status: 'failed',
3128
3701
  error: `activation failed: ${activationResult.messages.join('; ')}`,
3129
3702
  });
3130
3703
  break;
3131
3704
  }
3132
3705
  invalidateWrittenObject(objType, objName);
3133
- results.push({ type: objType, name: objName, status: 'success' });
3706
+ results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
3134
3707
  }
3135
3708
  catch (err) {
3136
3709
  results.push({
3137
3710
  type: objType,
3138
3711
  name: objName,
3712
+ packageName: objPackage,
3139
3713
  status: 'failed',
3140
3714
  error: err instanceof Error ? err.message : String(err),
3141
3715
  });
@@ -3144,30 +3718,40 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3144
3718
  }
3145
3719
  // Add 'skipped' entries for objects that were never attempted due to early break
3146
3720
  for (let i = results.length; i < objects.length; i++) {
3147
- const skipped = objects[i];
3721
+ const skippedPlan = batchPlan[i];
3722
+ const skipped = skippedPlan?.obj ?? objects[i];
3148
3723
  results.push({
3149
- type: normalizeObjectType(String(skipped?.type ?? '')),
3150
- name: String(skipped.name ?? ''),
3724
+ type: skippedPlan?.type ?? normalizeObjectType(String(skipped?.type ?? '')),
3725
+ name: skippedPlan?.name ?? String(skipped?.name ?? ''),
3726
+ packageName: skippedPlan?.packageName ?? normalizePackageOverride(skipped?.package, defaultPackage),
3151
3727
  status: 'failed',
3152
3728
  error: 'skipped — stopped after previous failure',
3153
3729
  });
3154
3730
  }
3155
3731
  const summary = results
3156
- .map((r) => `${r.name} (${r.type}) ${r.status === 'success' ? '✓' : `✗ — ${r.error}`}`)
3732
+ .map((r) => r.status === 'success'
3733
+ ? `${r.name} (${r.type}) ✓ [${r.packageName}]`
3734
+ : `${r.name} (${r.type}) ✗ [${r.packageName}] — ${r.error}`)
3157
3735
  .join(', ');
3158
3736
  const successCount = results.filter((r) => r.status === 'success').length;
3159
3737
  const hasFailure = results.some((r) => r.status === 'failed');
3160
3738
  const warningSuffix = batchWarnings.length > 0 ? `\n\nRAP preflight warnings:\n- ${batchWarnings.join('\n- ')}` : '';
3739
+ const packageNames = [...new Set(batchPlan.map((item) => item.packageName))];
3740
+ const packageSummary = packageNames.length === 1
3741
+ ? `in package ${packageNames[0]}`
3742
+ : packageNames.length <= 3
3743
+ ? `across packages [${packageNames.join(', ')}]`
3744
+ : `across ${packageNames.length} packages`;
3161
3745
  if (hasFailure) {
3162
3746
  const cleanupHint = successCount > 0
3163
3747
  ? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
3164
3748
  : '';
3165
- return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}${warningSuffix}`);
3749
+ return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}: ${summary}${cleanupHint}${warningSuffix}`);
3166
3750
  }
3167
- return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}${warningSuffix}`);
3751
+ return textResult(`Batch created ${successCount} objects ${packageSummary}: ${summary}${warningSuffix}`);
3168
3752
  }
3169
3753
  default:
3170
- return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create, scaffold_rap_handlers`);
3754
+ return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create, scaffold_rap_handlers, generate_behavior_implementation`);
3171
3755
  }
3172
3756
  }
3173
3757
  /**
@@ -3230,13 +3814,22 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
3230
3814
  if (!enabled || !source) {
3231
3815
  return { blocked: false };
3232
3816
  }
3233
- // abaplint supports ABAP source (PROG/CLAS/INTF/FUNC/INCL) and CDS views (DDLS) via
3817
+ // abaplint supports ABAP source (PROG/CLAS/INTF/INCL) and CDS views (DDLS) via
3234
3818
  // its CDS parser. DDLS lint catches syntax errors (cds_parser_error) like missing commas,
3235
3819
  // wrong keywords, and invalid DDL constructs. BDEF/SRVD/SRVB/DDLX are silently ignored
3236
3820
  // by abaplint (no parser for those types — garbage passes without errors). TABL (define
3237
3821
  // table syntax) is not supported by the CDS parser and produces false cds_parser_error.
3238
3822
  // For unsupported types, SAP server-side compilation handles validation.
3239
- const LINTABLE_TYPES = new Set(['PROG', 'CLAS', 'INTF', 'FUNC', 'INCL', 'DDLS']);
3823
+ //
3824
+ // FUNC is intentionally excluded: abaplint's FM-source parser does not understand
3825
+ // source-based signatures (`FUNCTION X\n IMPORTING …\n.`) and emits a structural
3826
+ // parser_error that blocks the write. Issue #252 made this visible — once we
3827
+ // started emitting real signatures from structured `parameters`, every FUNC PUT
3828
+ // hit the lint gate. Pre-#252 lint coverage was effectively trivial (only
3829
+ // signature-less FUNCTION/ENDFUNCTION stubs passed). Validation falls back to
3830
+ // SAP's server-side syntax check (opt-in via `SAP_CHECK_BEFORE_WRITE`) and the
3831
+ // activate step.
3832
+ const LINTABLE_TYPES = new Set(['PROG', 'CLAS', 'INTF', 'INCL', 'DDLS']);
3240
3833
  if (!LINTABLE_TYPES.has(type)) {
3241
3834
  return { blocked: false };
3242
3835
  }
@@ -3245,7 +3838,7 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
3245
3838
  const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
3246
3839
  const configOptions = {
3247
3840
  systemType,
3248
- abapRelease: cachedFeatures?.abapRelease,
3841
+ abapRelease: cachedFeatures?.abapRelease ?? config.abapRelease,
3249
3842
  configFile: config.abaplintConfig,
3250
3843
  };
3251
3844
  const result = validateBeforeWrite(source, filename, configOptions);
@@ -3421,10 +4014,33 @@ async function handleSAPActivate(client, args, cachingLayer) {
3421
4014
  // Resolve URLs sequentially. For TABL we await the URL resolver so DDIC
3422
4015
  // structures (which live at /sap/bc/adt/ddic/structures/) are addressed
3423
4016
  // correctly; the resolver short-circuits on its in-memory cache.
4017
+ // For FUNC the URL needs the parent function-group baked into the path
4018
+ // (issue #250); each batch entry must carry `group` or be auto-resolvable
4019
+ // by name.
3424
4020
  const objects = await Promise.all(rawObjects.map(async (o) => {
3425
4021
  const objType = normalizeObjectType(String(o.type ?? type));
3426
4022
  const objName = String(o.name ?? '');
3427
- const url = objType === 'TABL' ? await client.resolveTablObjectUrl(objName) : objectUrlForType(objType, objName);
4023
+ let url;
4024
+ if (objType === 'TABL') {
4025
+ url = await client.resolveTablObjectUrl(objName);
4026
+ }
4027
+ else if (objType === 'FUNC') {
4028
+ let group = String(o.group ?? args.group ?? '').trim();
4029
+ if (!group) {
4030
+ const resolved = cachingLayer
4031
+ ? await cachingLayer.resolveFuncGroup(client, objName)
4032
+ : await client.resolveFunctionGroup(objName);
4033
+ if (!resolved) {
4034
+ throw new Error(`Cannot resolve function group for FM "${objName}" in batch activate. Provide "group" on each FUNC entry.`);
4035
+ }
4036
+ group = resolved;
4037
+ }
4038
+ const groupLc = encodeURIComponent(group.toLowerCase());
4039
+ url = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(objName.toLowerCase())}`;
4040
+ }
4041
+ else {
4042
+ url = objectUrlForType(objType, objName);
4043
+ }
3428
4044
  return { type: objType, name: objName, url };
3429
4045
  }));
3430
4046
  const result = await activateBatch(client.http, client.safety, objects, activateOpts);
@@ -3451,7 +4067,30 @@ async function handleSAPActivate(client, args, cachingLayer) {
3451
4067
  // Single activation (existing behavior). For TABL we resolve the URL because
3452
4068
  // the existing object may live at /tables/ (transparent) or /structures/
3453
4069
  // (DDIC structure); using the wrong one would produce a confusing 404.
3454
- const objectUrl = type === 'TABL' ? await client.resolveTablObjectUrl(name) : objectUrlForType(type, name);
4070
+ // For FUNC the URL needs the parent function group baked into the path
4071
+ // (issue #250) — `objectBasePath('FUNC')` deliberately throws so generic
4072
+ // builders fail loudly. Auto-resolve the group when omitted.
4073
+ let objectUrl;
4074
+ if (type === 'TABL') {
4075
+ objectUrl = await client.resolveTablObjectUrl(name);
4076
+ }
4077
+ else if (type === 'FUNC') {
4078
+ let group = String(args.group ?? '').trim();
4079
+ if (!group) {
4080
+ const resolved = cachingLayer
4081
+ ? await cachingLayer.resolveFuncGroup(client, name)
4082
+ : await client.resolveFunctionGroup(name);
4083
+ if (!resolved) {
4084
+ return errorResult(`Cannot resolve function group for FM "${name}". Provide the "group" parameter explicitly.`);
4085
+ }
4086
+ group = resolved;
4087
+ }
4088
+ const groupLc = encodeURIComponent(group.toLowerCase());
4089
+ objectUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(name.toLowerCase())}`;
4090
+ }
4091
+ else {
4092
+ objectUrl = objectUrlForType(type, name);
4093
+ }
3455
4094
  const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
3456
4095
  if (result.success) {
3457
4096
  cachingLayer?.invalidate(type, name, 'all');
@@ -3765,8 +4404,24 @@ async function handleSAPDiagnose(client, args) {
3765
4404
  const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
3766
4405
  return textResult(JSON.stringify(result, null, 2));
3767
4406
  }
4407
+ case 'object_state': {
4408
+ if (!name || !type)
4409
+ return errorResult('"name" and "type" are required for "object_state" action.');
4410
+ const sections = type === 'CLAS'
4411
+ ? [
4412
+ { section: 'main', uri: sourceUrlForType(type, name) },
4413
+ { section: 'definitions', uri: classIncludeUrl(name, 'definitions'), optional: true },
4414
+ { section: 'implementations', uri: classIncludeUrl(name, 'implementations'), optional: true },
4415
+ { section: 'macros', uri: classIncludeUrl(name, 'macros'), optional: true },
4416
+ { section: 'testclasses', uri: classIncludeUrl(name, 'testclasses'), optional: true },
4417
+ ]
4418
+ : [{ section: 'main', uri: sourceUrlForType(type, name) }];
4419
+ const result = await getObjectState(client.http, client.safety, { type, name, sections });
4420
+ return textResult(JSON.stringify(result, null, 2));
4421
+ }
3768
4422
  case 'quickfix': {
3769
4423
  const source = args.source;
4424
+ const sourceUri = args.sourceUri;
3770
4425
  if (!name || !type)
3771
4426
  return errorResult('"name" and "type" are required for "quickfix" action.');
3772
4427
  if (!source)
@@ -3779,13 +4434,15 @@ async function handleSAPDiagnose(client, args) {
3779
4434
  return errorResult('"line" must be a number for "quickfix" action.');
3780
4435
  if (!Number.isFinite(column))
3781
4436
  return errorResult('"column" must be a number for "quickfix" action.');
3782
- const proposals = await getFixProposals(client.http, client.safety, sourceUrlForType(type, name), source, line, column);
4437
+ const proposals = await getFixProposals(client.http, client.safety, sourceUri ?? sourceUrlForType(type, name), source, line, column);
3783
4438
  return textResult(JSON.stringify(proposals, null, 2));
3784
4439
  }
3785
4440
  case 'apply_quickfix': {
3786
4441
  const source = args.source;
4442
+ const sourceUri = args.sourceUri;
3787
4443
  const proposalUri = args.proposalUri;
3788
4444
  const proposalUserContent = args.proposalUserContent;
4445
+ const proposalAffectedObjects = args.proposalAffectedObjects;
3789
4446
  if (!name || !type)
3790
4447
  return errorResult('"name" and "type" are required for "apply_quickfix" action.');
3791
4448
  if (!source)
@@ -3794,7 +4451,7 @@ async function handleSAPDiagnose(client, args) {
3794
4451
  return errorResult('"line" is required for "apply_quickfix" action.');
3795
4452
  if (!proposalUri)
3796
4453
  return errorResult('"proposalUri" is required for "apply_quickfix" action.');
3797
- if (!proposalUserContent)
4454
+ if (proposalUserContent === undefined)
3798
4455
  return errorResult('"proposalUserContent" is required for "apply_quickfix" action.');
3799
4456
  const line = Number(args.line);
3800
4457
  const column = Number(args.column ?? 0);
@@ -3808,7 +4465,8 @@ async function handleSAPDiagnose(client, args) {
3808
4465
  name: '',
3809
4466
  description: '',
3810
4467
  userContent: proposalUserContent,
3811
- }, sourceUrlForType(type, name), source, line, column);
4468
+ ...(proposalAffectedObjects ? { affectedObjects: proposalAffectedObjects } : {}),
4469
+ }, sourceUri ?? sourceUrlForType(type, name), source, line, column);
3812
4470
  return textResult(JSON.stringify(deltas, null, 2));
3813
4471
  }
3814
4472
  case 'dumps': {
@@ -3897,7 +4555,7 @@ async function handleSAPDiagnose(client, args) {
3897
4555
  return textResult(JSON.stringify(errors, null, 2));
3898
4556
  }
3899
4557
  default:
3900
- return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, quickfix, apply_quickfix, dumps, traces, system_messages, gateway_errors`);
4558
+ return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, object_state, quickfix, apply_quickfix, dumps, traces, system_messages, gateway_errors`);
3901
4559
  }
3902
4560
  }
3903
4561
  function selectDumpSections(detail, requestedSections) {