arc-1 0.9.3 → 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 (59) 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.map +1 -1
  42. package/dist/handlers/intent.js +562 -68
  43. package/dist/handlers/intent.js.map +1 -1
  44. package/dist/handlers/schemas.d.ts +106 -2
  45. package/dist/handlers/schemas.d.ts.map +1 -1
  46. package/dist/handlers/schemas.js +157 -11
  47. package/dist/handlers/schemas.js.map +1 -1
  48. package/dist/handlers/tools.d.ts.map +1 -1
  49. package/dist/handlers/tools.js +144 -32
  50. package/dist/handlers/tools.js.map +1 -1
  51. package/dist/server/config.d.ts.map +1 -1
  52. package/dist/server/config.js +1 -0
  53. package/dist/server/config.js.map +1 -1
  54. package/dist/server/server.d.ts +1 -1
  55. package/dist/server/server.js +1 -1
  56. package/dist/server/types.d.ts +2 -0
  57. package/dist/server/types.d.ts.map +1 -1
  58. package/dist/server/types.js.map +1 -1
  59. package/package.json +1 -1
@@ -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
  };
@@ -2482,16 +2684,60 @@ function objectUrlForTypeRaw(type, name) {
2482
2684
  function sourceUrlForType(type, name) {
2483
2685
  return `${objectUrlForType(type, name)}/source/main`;
2484
2686
  }
2687
+ const CLASS_WRITE_INCLUDES = ['definitions', 'implementations', 'macros', 'testclasses'];
2485
2688
  /** Get a CLAS include URL (definitions/implementations/macros/testclasses) */
2486
2689
  function classIncludeUrl(name, include) {
2487
2690
  return `/sap/bc/adt/oo/classes/${encodeURIComponent(name)}/includes/${include}`;
2488
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
+ }
2489
2733
  // ─── SAPWrite Handler ────────────────────────────────────────────────
2490
2734
  async function handleSAPWrite(client, args, config, cachingLayer) {
2491
2735
  const action = String(args.action ?? '');
2492
2736
  const type = normalizeObjectType(String(args.type ?? ''));
2493
2737
  const name = String(args.name ?? '');
2494
2738
  const source = String(args.source ?? '');
2739
+ const hasSource = typeof args.source === 'string';
2740
+ const include = normalizeClassWriteInclude(args.include);
2495
2741
  const transport = args.transport;
2496
2742
  const lintOverride = args.lintBeforeWrite;
2497
2743
  const preflightOverride = args.preflightBeforeWrite;
@@ -2572,6 +2818,23 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2572
2818
  switch (action) {
2573
2819
  case 'update': {
2574
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
+ }
2575
2838
  if (type === 'SKTD') {
2576
2839
  // KTD update requires the full <sktd:docu> XML envelope with the Markdown
2577
2840
  // body base64-encoded inside <sktd:text>, PUT with
@@ -2611,14 +2874,47 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2611
2874
  // "Parameter comment blocks are not allowed" — verified live a4h S/4HANA 2023,
2612
2875
  // issue #250). LLMs frequently emit them out of muscle memory because every
2613
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.
2614
2882
  let effectiveSource = source;
2615
2883
  let fmParamStripWarning;
2884
+ let fmParamMergeWarning;
2616
2885
  if (type === 'FUNC') {
2617
- const stripped = stripFmParamCommentBlock(source);
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);
2618
2914
  effectiveSource = stripped.source;
2619
2915
  if (stripped.wasStripped) {
2620
2916
  fmParamStripWarning =
2621
- 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (SAP rejects them on PUT — manage FM parameters via SAPGUI/Eclipse).';
2917
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (SAP rejects them on PUT — pass `parameters` as a structured array instead).';
2622
2918
  }
2623
2919
  }
2624
2920
  // Pre-write lint validation (uses sanitized source for FUNC)
@@ -2633,7 +2929,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2633
2929
  invalidateWrittenObject(type, name);
2634
2930
  const msg = `Successfully updated ${type} ${name}.`;
2635
2931
  const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
2636
- const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint, fmParamStripWarning);
2932
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint, fmParamStripWarning, fmParamMergeWarning);
2637
2933
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
2638
2934
  }
2639
2935
  case 'create': {
@@ -2802,17 +3098,46 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2802
3098
  : '';
2803
3099
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
2804
3100
  }
2805
- // Step 2: Write source code if provided
2806
- if (source) {
2807
- // FUNC: strip SAPGUI parameter comment blocks (see update path for rationale).
2808
- let createSource = 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 ?? '';
2809
3112
  let fmParamStripWarning;
3113
+ let fmParamMergeWarning;
2810
3114
  if (type === 'FUNC') {
2811
- const stripped = stripFmParamCommentBlock(source);
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);
2812
3137
  createSource = stripped.source;
2813
3138
  if (stripped.wasStripped) {
2814
3139
  fmParamStripWarning =
2815
- 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (manage FM parameters via SAPGUI/Eclipse).';
3140
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (pass `parameters` as a structured array instead).';
2816
3141
  }
2817
3142
  }
2818
3143
  // Pre-write lint validation
@@ -2823,7 +3148,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2823
3148
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, createSource, effectiveTransport, cachedFeatures?.abapRelease);
2824
3149
  invalidateWrittenObject(type, name);
2825
3150
  const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
2826
- const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning);
3151
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning, fmParamMergeWarning);
2827
3152
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
2828
3153
  }
2829
3154
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
@@ -2837,10 +3162,50 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2837
3162
  if (type !== 'CLAS')
2838
3163
  return errorResult('edit_method is only supported for type=CLAS.');
2839
3164
  await enforcePackageForExistingObject();
2840
- // Fetch current full source (use cache if available)
2841
- const currentSource = cachingLayer
2842
- ? (await cachingLayer.getSource('CLAS', name, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch }))).source
2843
- : (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
+ }
2844
3209
  // Use detected ABAP version from probe if available
2845
3210
  const abaplintVer = cachedFeatures?.abapRelease
2846
3211
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
@@ -2848,18 +3213,40 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2848
3213
  // Splice in the new method body
2849
3214
  const spliced = spliceMethod(currentSource, name, method, source, abaplintVer);
2850
3215
  if (!spliced.success) {
2851
- 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}`);
2852
3224
  }
2853
- // Pre-write lint validation on the full spliced source
2854
- const lintWarnings = runPreWriteLint(spliced.newSource, type, name, config, lintOverride);
2855
- if (lintWarnings.blocked)
2856
- return lintWarnings.result;
2857
- // Pre-write server-side syntax check on the full spliced source (opt-in; warnings only).
2858
- const checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
2859
- // Write the full source back (existing lock/modify/unlock flow)
2860
- 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);
2861
3247
  invalidateWrittenObject(type, name);
2862
- 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}.`;
2863
3250
  const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
2864
3251
  return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
2865
3252
  }
@@ -2965,6 +3352,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2965
3352
  applied: false,
2966
3353
  hint: unresolvedHint,
2967
3354
  applyResult: {
3355
+ skeletons: scaffoldPlan.skeletons,
2968
3356
  main: scaffoldPlan.signatures.main,
2969
3357
  definitions: scaffoldPlan.signatures.definitions,
2970
3358
  implementations: scaffoldPlan.signatures.implementations,
@@ -3029,12 +3417,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3029
3417
  });
3030
3418
  invalidateWrittenObject();
3031
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). ` +
3032
3421
  `Updated section(s): ${scaffoldPlan.changedSections.join(', ')}.`;
3033
3422
  const warnings = mergePreWriteWarnings(lintWarningsMain?.warnings, lintWarningsDefinitions?.warnings, lintWarningsImplementations?.warnings);
3034
3423
  const details = JSON.stringify({
3035
3424
  ...summary,
3036
3425
  applied: true,
3037
3426
  applyResult: {
3427
+ skeletons: scaffoldPlan.skeletons,
3038
3428
  main: scaffoldPlan.signatures.main,
3039
3429
  definitions: scaffoldPlan.signatures.definitions,
3040
3430
  implementations: scaffoldPlan.signatures.implementations,
@@ -3044,6 +3434,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3044
3434
  }, null, 2);
3045
3435
  return warnings ? textResult(`${msg}\n\n${warnings}\n\n${details}`) : textResult(`${msg}\n\n${details}`);
3046
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
+ }
3047
3475
  case 'delete': {
3048
3476
  await enforcePackageForExistingObject();
3049
3477
  // Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
@@ -3085,20 +3513,35 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3085
3513
  if (!objects || !Array.isArray(objects) || objects.length === 0) {
3086
3514
  return errorResult('"objects" array is required and must be non-empty for batch_create action.');
3087
3515
  }
3088
- const pkg = String(args.package ?? '$TMP');
3089
- // Check package is allowed before starting any creates
3090
- checkPackage(client.safety, pkg);
3091
- // Pre-flight transport check for batch_create (same logic as single create)
3092
- let batchTransport = transport;
3093
- 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) {
3094
3540
  try {
3095
- // Use first object's URL for the transport check
3096
- const firstObj = objects[0];
3097
- const firstType = normalizeObjectType(String(firstObj?.type ?? ''));
3098
- const firstUrl = objectUrlForType(firstType, String(firstObj?.name ?? ''));
3541
+ const firstUrl = objectUrlForType(plan.type, plan.name);
3099
3542
  const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
3100
3543
  if (transportInfo.lockedTransport) {
3101
- batchTransport = transportInfo.lockedTransport;
3544
+ autoTransportByPackage.set(pkg, transportInfo.lockedTransport);
3102
3545
  }
3103
3546
  else if (!transportInfo.isLocal && transportInfo.recording) {
3104
3547
  const existingList = transportInfo.existingTransports.length > 0
@@ -3115,7 +3558,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3115
3558
  existingList);
3116
3559
  }
3117
3560
  }
3118
- 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
+ });
3119
3568
  // If transportInfo check fails, proceed — SAP will return its own error if needed.
3120
3569
  }
3121
3570
  }
@@ -3125,9 +3574,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3125
3574
  // guard fires for every MSAG entry, but a batch typically shares one transport — cache
3126
3575
  // the lookup result to avoid one HTTP roundtrip per object.
3127
3576
  const transportLookupCache = new Map();
3128
- for (const obj of objects) {
3129
- const objType = normalizeObjectType(String(obj.type ?? ''));
3130
- 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);
3131
3580
  const metadataObject = isMetadataWriteType(objType);
3132
3581
  const objSource = obj.source ? String(obj.source) : undefined;
3133
3582
  const objDescription = String(obj.description ?? objName);
@@ -3138,24 +3587,26 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3138
3587
  results.push({
3139
3588
  type: objType,
3140
3589
  name: objName,
3590
+ packageName: objPackage,
3141
3591
  status: 'failed',
3142
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.`,
3143
3593
  });
3144
3594
  break;
3145
3595
  }
3146
3596
  // MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
3147
- if (objType === 'MSAG' && batchTransport) {
3148
- let tr = transportLookupCache.get(batchTransport);
3597
+ if (objType === 'MSAG' && objTransport) {
3598
+ let tr = transportLookupCache.get(objTransport);
3149
3599
  if (tr === undefined) {
3150
- tr = await getTransport(client.http, client.safety, batchTransport);
3151
- transportLookupCache.set(batchTransport, tr);
3600
+ tr = await getTransport(client.http, client.safety, objTransport);
3601
+ transportLookupCache.set(objTransport, tr);
3152
3602
  }
3153
3603
  if (!tr) {
3154
3604
  results.push({
3155
3605
  type: objType,
3156
3606
  name: objName,
3607
+ packageName: objPackage,
3157
3608
  status: 'failed',
3158
- 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.`,
3159
3610
  });
3160
3611
  break;
3161
3612
  }
@@ -3166,6 +3617,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3166
3617
  results.push({
3167
3618
  type: objType,
3168
3619
  name: objName,
3620
+ packageName: objPackage,
3169
3621
  status: 'failed',
3170
3622
  error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
3171
3623
  });
@@ -3180,6 +3632,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3180
3632
  results.push({
3181
3633
  type: objType,
3182
3634
  name: objName,
3635
+ packageName: objPackage,
3183
3636
  status: 'failed',
3184
3637
  error: preflightWarnings.result.content[0].text,
3185
3638
  });
@@ -3193,6 +3646,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3193
3646
  results.push({
3194
3647
  type: objType,
3195
3648
  name: objName,
3649
+ packageName: objPackage,
3196
3650
  status: 'failed',
3197
3651
  error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
3198
3652
  });
@@ -3203,11 +3657,11 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3203
3657
  const objUrl = objectUrlForType(objType, objName);
3204
3658
  const createUrl = objUrl.replace(/\/[^/]+$/, '');
3205
3659
  const objMetadataProps = getMetadataWriteProperties(obj);
3206
- const body = buildCreateXml(objType, objName, pkg, objDescription, objMetadataProps);
3660
+ const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
3207
3661
  const contentType = createContentTypeForType(objType);
3208
3662
  const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
3209
3663
  try {
3210
- 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);
3211
3665
  }
3212
3666
  catch (createErr) {
3213
3667
  if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
@@ -3222,7 +3676,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3222
3676
  if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
3223
3677
  await client.http.withStatefulSession(async (session) => {
3224
3678
  const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
3225
- const lockTransport = batchTransport ?? (lock.corrNr || undefined);
3679
+ const lockTransport = objTransport ?? (lock.corrNr || undefined);
3226
3680
  try {
3227
3681
  await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
3228
3682
  }
@@ -3234,7 +3688,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3234
3688
  // Step 2: Write source if provided
3235
3689
  if (!metadataObject && objSource) {
3236
3690
  const srcUrl = sourceUrlForType(objType, objName);
3237
- 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);
3238
3692
  }
3239
3693
  // Step 3: Activate the object
3240
3694
  const activationResult = await activate(client.http, client.safety, objUrl);
@@ -3242,18 +3696,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3242
3696
  results.push({
3243
3697
  type: objType,
3244
3698
  name: objName,
3699
+ packageName: objPackage,
3245
3700
  status: 'failed',
3246
3701
  error: `activation failed: ${activationResult.messages.join('; ')}`,
3247
3702
  });
3248
3703
  break;
3249
3704
  }
3250
3705
  invalidateWrittenObject(objType, objName);
3251
- results.push({ type: objType, name: objName, status: 'success' });
3706
+ results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
3252
3707
  }
3253
3708
  catch (err) {
3254
3709
  results.push({
3255
3710
  type: objType,
3256
3711
  name: objName,
3712
+ packageName: objPackage,
3257
3713
  status: 'failed',
3258
3714
  error: err instanceof Error ? err.message : String(err),
3259
3715
  });
@@ -3262,30 +3718,40 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3262
3718
  }
3263
3719
  // Add 'skipped' entries for objects that were never attempted due to early break
3264
3720
  for (let i = results.length; i < objects.length; i++) {
3265
- const skipped = objects[i];
3721
+ const skippedPlan = batchPlan[i];
3722
+ const skipped = skippedPlan?.obj ?? objects[i];
3266
3723
  results.push({
3267
- type: normalizeObjectType(String(skipped?.type ?? '')),
3268
- 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),
3269
3727
  status: 'failed',
3270
3728
  error: 'skipped — stopped after previous failure',
3271
3729
  });
3272
3730
  }
3273
3731
  const summary = results
3274
- .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}`)
3275
3735
  .join(', ');
3276
3736
  const successCount = results.filter((r) => r.status === 'success').length;
3277
3737
  const hasFailure = results.some((r) => r.status === 'failed');
3278
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`;
3279
3745
  if (hasFailure) {
3280
3746
  const cleanupHint = successCount > 0
3281
3747
  ? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
3282
3748
  : '';
3283
- 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}`);
3284
3750
  }
3285
- return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}${warningSuffix}`);
3751
+ return textResult(`Batch created ${successCount} objects ${packageSummary}: ${summary}${warningSuffix}`);
3286
3752
  }
3287
3753
  default:
3288
- 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`);
3289
3755
  }
3290
3756
  }
3291
3757
  /**
@@ -3348,13 +3814,22 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
3348
3814
  if (!enabled || !source) {
3349
3815
  return { blocked: false };
3350
3816
  }
3351
- // 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
3352
3818
  // its CDS parser. DDLS lint catches syntax errors (cds_parser_error) like missing commas,
3353
3819
  // wrong keywords, and invalid DDL constructs. BDEF/SRVD/SRVB/DDLX are silently ignored
3354
3820
  // by abaplint (no parser for those types — garbage passes without errors). TABL (define
3355
3821
  // table syntax) is not supported by the CDS parser and produces false cds_parser_error.
3356
3822
  // For unsupported types, SAP server-side compilation handles validation.
3357
- 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']);
3358
3833
  if (!LINTABLE_TYPES.has(type)) {
3359
3834
  return { blocked: false };
3360
3835
  }
@@ -3363,7 +3838,7 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
3363
3838
  const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
3364
3839
  const configOptions = {
3365
3840
  systemType,
3366
- abapRelease: cachedFeatures?.abapRelease,
3841
+ abapRelease: cachedFeatures?.abapRelease ?? config.abapRelease,
3367
3842
  configFile: config.abaplintConfig,
3368
3843
  };
3369
3844
  const result = validateBeforeWrite(source, filename, configOptions);
@@ -3929,8 +4404,24 @@ async function handleSAPDiagnose(client, args) {
3929
4404
  const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
3930
4405
  return textResult(JSON.stringify(result, null, 2));
3931
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
+ }
3932
4422
  case 'quickfix': {
3933
4423
  const source = args.source;
4424
+ const sourceUri = args.sourceUri;
3934
4425
  if (!name || !type)
3935
4426
  return errorResult('"name" and "type" are required for "quickfix" action.');
3936
4427
  if (!source)
@@ -3943,13 +4434,15 @@ async function handleSAPDiagnose(client, args) {
3943
4434
  return errorResult('"line" must be a number for "quickfix" action.');
3944
4435
  if (!Number.isFinite(column))
3945
4436
  return errorResult('"column" must be a number for "quickfix" action.');
3946
- 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);
3947
4438
  return textResult(JSON.stringify(proposals, null, 2));
3948
4439
  }
3949
4440
  case 'apply_quickfix': {
3950
4441
  const source = args.source;
4442
+ const sourceUri = args.sourceUri;
3951
4443
  const proposalUri = args.proposalUri;
3952
4444
  const proposalUserContent = args.proposalUserContent;
4445
+ const proposalAffectedObjects = args.proposalAffectedObjects;
3953
4446
  if (!name || !type)
3954
4447
  return errorResult('"name" and "type" are required for "apply_quickfix" action.');
3955
4448
  if (!source)
@@ -3958,7 +4451,7 @@ async function handleSAPDiagnose(client, args) {
3958
4451
  return errorResult('"line" is required for "apply_quickfix" action.');
3959
4452
  if (!proposalUri)
3960
4453
  return errorResult('"proposalUri" is required for "apply_quickfix" action.');
3961
- if (!proposalUserContent)
4454
+ if (proposalUserContent === undefined)
3962
4455
  return errorResult('"proposalUserContent" is required for "apply_quickfix" action.');
3963
4456
  const line = Number(args.line);
3964
4457
  const column = Number(args.column ?? 0);
@@ -3972,7 +4465,8 @@ async function handleSAPDiagnose(client, args) {
3972
4465
  name: '',
3973
4466
  description: '',
3974
4467
  userContent: proposalUserContent,
3975
- }, sourceUrlForType(type, name), source, line, column);
4468
+ ...(proposalAffectedObjects ? { affectedObjects: proposalAffectedObjects } : {}),
4469
+ }, sourceUri ?? sourceUrlForType(type, name), source, line, column);
3976
4470
  return textResult(JSON.stringify(deltas, null, 2));
3977
4471
  }
3978
4472
  case 'dumps': {
@@ -4061,7 +4555,7 @@ async function handleSAPDiagnose(client, args) {
4061
4555
  return textResult(JSON.stringify(errors, null, 2));
4062
4556
  }
4063
4557
  default:
4064
- 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`);
4065
4559
  }
4066
4560
  }
4067
4561
  function selectDumpSections(detail, requestedSections) {