arc-1 0.9.3 → 0.9.5

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 (71) hide show
  1. package/README.md +4 -4
  2. package/dist/adt/client.d.ts +35 -1
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +194 -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 +55 -0
  26. package/dist/adt/rap-handlers.d.ts.map +1 -1
  27. package/dist/adt/rap-handlers.js +119 -9
  28. package/dist/adt/rap-handlers.js.map +1 -1
  29. package/dist/adt/types.d.ts +81 -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 +17 -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 +748 -80
  43. package/dist/handlers/intent.js.map +1 -1
  44. package/dist/handlers/schemas.d.ts +144 -28
  45. package/dist/handlers/schemas.d.ts.map +1 -1
  46. package/dist/handlers/schemas.js +182 -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 +155 -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 +25 -8
  53. package/dist/server/config.js.map +1 -1
  54. package/dist/server/http.d.ts.map +1 -1
  55. package/dist/server/http.js +1 -0
  56. package/dist/server/http.js.map +1 -1
  57. package/dist/server/server.d.ts +1 -1
  58. package/dist/server/server.js +1 -1
  59. package/dist/server/stateless-client-store.d.ts +11 -3
  60. package/dist/server/stateless-client-store.d.ts.map +1 -1
  61. package/dist/server/stateless-client-store.js +39 -9
  62. package/dist/server/stateless-client-store.js.map +1 -1
  63. package/dist/server/types.d.ts +19 -5
  64. package/dist/server/types.d.ts.map +1 -1
  65. package/dist/server/types.js +1 -1
  66. package/dist/server/types.js.map +1 -1
  67. package/dist/server/xsuaa.d.ts +10 -1
  68. package/dist/server/xsuaa.d.ts.map +1 -1
  69. package/dist/server/xsuaa.js +38 -5
  70. package/dist/server/xsuaa.js.map +1 -1
  71. package/package.json +3 -3
@@ -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';
@@ -764,14 +766,26 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
764
766
  // Unified scope enforcement via ACTION_POLICY — routes through action/type-aware lookup.
765
767
  // For SAPRead, the policy key is Tool.{type}; for other action-bearing tools, Tool.{action};
766
768
  // for tools without an action/type enum (SAPSearch, SAPQuery), the tool-level default applies.
769
+ // For SAPSearch.tadir_lookup with source='db'|'both', synthesize a sub-action key so the
770
+ // sql-scoped policy entry kicks in (otherwise viewer-only profiles could piggyback on the
771
+ // ADT info-system route to issue freestyle SQL).
767
772
  // Runs BEFORE Zod validation so scope errors don't leak schema details to unauthorized callers.
768
- const actionOrType = toolName === 'SAPRead'
773
+ let actionOrType = toolName === 'SAPRead'
769
774
  ? typeof args.type === 'string'
770
775
  ? args.type
771
776
  : undefined
772
777
  : typeof args.action === 'string'
773
778
  ? args.action
774
779
  : undefined;
780
+ if (toolName === 'SAPSearch' &&
781
+ typeof args.searchType === 'string' &&
782
+ args.searchType === 'tadir_lookup' &&
783
+ typeof args.source === 'string') {
784
+ const src = args.source.toLowerCase();
785
+ if (src === 'db' || src === 'both') {
786
+ actionOrType = `tadir_lookup_${src}`;
787
+ }
788
+ }
775
789
  const policy = getActionPolicy(toolName, actionOrType);
776
790
  if (authInfo && policy) {
777
791
  if (!hasRequiredScope(authInfo, policy.scope)) {
@@ -1086,6 +1100,26 @@ async function handleSAPRead(client, args, cachingLayer) {
1086
1100
  group = resolved;
1087
1101
  }
1088
1102
  const { source, cacheHit, revalidated } = await cachedGet('FUNC', name, effectiveVersion, (ifNoneMatch) => client.getFunction(group, name, { ifNoneMatch, version: effectiveVersion }));
1103
+ // Issue #252: when caller asks for includeSignature, return JSON with the
1104
+ // source body and the parsed structured signature.
1105
+ if (args.includeSignature === true) {
1106
+ const parsed = parseFmSignature(source);
1107
+ const grouped = {
1108
+ importing: [],
1109
+ exporting: [],
1110
+ changing: [],
1111
+ tables: [],
1112
+ exceptions: [],
1113
+ raising: [],
1114
+ };
1115
+ for (const p of parsed.params)
1116
+ grouped[p.kind].push(p);
1117
+ const payload = {
1118
+ source,
1119
+ signature: grouped,
1120
+ };
1121
+ return textResult(JSON.stringify(payload, null, 2));
1122
+ }
1089
1123
  return cachedTextResult(source, cacheHit, revalidated, versionWarning);
1090
1124
  }
1091
1125
  case 'FUGR': {
@@ -1397,6 +1431,99 @@ async function handleSAPSearch(client, args) {
1397
1431
  const rawQuery = String(args.query ?? '');
1398
1432
  const maxResults = Number(args.maxResults ?? 100);
1399
1433
  const searchType = String(args.searchType ?? 'object');
1434
+ if (searchType === 'tadir_lookup') {
1435
+ const names = extractLookupNames(rawQuery, args.names);
1436
+ if (names.length === 0) {
1437
+ return errorResult('SAPSearch(searchType="tadir_lookup") requires names[] or query with at least one name.');
1438
+ }
1439
+ const objectTypes = extractLookupObjectTypes(args.objectType, args.objectTypes);
1440
+ const rawSource = typeof args.source === 'string' ? args.source.toLowerCase() : 'adt';
1441
+ const source = rawSource === 'db' || rawSource === 'both' ? rawSource : 'adt';
1442
+ // Stamp each match with provenance so a merged 'both' result is unambiguous and
1443
+ // viewer tooling can colour-code ghost rows. The DB path already stamps `_origin:'db'`
1444
+ // (see `lookupObjectsViaDb`); we stamp ADT matches here.
1445
+ const tagOrigin = (lookups, origin) => lookups.map((l) => ({
1446
+ ...l,
1447
+ matches: l.matches.map((m) => ({ ...m, _origin: m._origin ?? origin })),
1448
+ }));
1449
+ let finalLookups;
1450
+ const wildcardNames = names.filter((name) => name.includes('*'));
1451
+ const warnings = [];
1452
+ let splitBrain = [];
1453
+ if (source === 'adt') {
1454
+ finalLookups = tagOrigin(await client.lookupObjects(names, { maxResults, objectTypes }), 'adt');
1455
+ }
1456
+ else if (source === 'db') {
1457
+ // The 'db' path bypasses ADT info-system entirely; `lookupObjectsViaDb` already
1458
+ // tags matches with `_origin:'db'`. Safety/scope gating runs at handleToolCall
1459
+ // and in client.runQuery (FreeSQL operation), so unauthorized callers never reach here.
1460
+ finalLookups = await client.lookupObjectsViaDb(names, { maxResults, objectTypes });
1461
+ }
1462
+ else {
1463
+ // 'both' — parallel ADT + DB, merge per name with dedupe.
1464
+ const [adtLookups, dbLookups] = await Promise.all([
1465
+ client.lookupObjects(names, { maxResults, objectTypes }).then((r) => tagOrigin(r, 'adt')),
1466
+ client.lookupObjectsViaDb(names, { maxResults, objectTypes }),
1467
+ ]);
1468
+ const dbByName = new Map(dbLookups.map((l) => [l.name.toUpperCase(), l]));
1469
+ const adtByName = new Map(adtLookups.map((l) => [l.name.toUpperCase(), l]));
1470
+ finalLookups = names.map((rawName) => {
1471
+ const upper = rawName.toUpperCase();
1472
+ const adt = adtByName.get(upper);
1473
+ const db = dbByName.get(upper);
1474
+ const adtMatches = adt?.matches ?? [];
1475
+ const dbMatches = db?.matches ?? [];
1476
+ // Dedupe by (baseObjectType, objectName) — TADIR stores bare types ('DDLS')
1477
+ // while ADT info-system returns slash-form ('DDLS/DF'). Stripping the suffix
1478
+ // keeps the same logical object from appearing twice in the merged matches.
1479
+ // Preserve the more-specific slash form when both originate from ADT+DB.
1480
+ const seen = new Map();
1481
+ const baseKey = (m) => `${(m.objectType.split('/')[0] || m.objectType).toUpperCase()}${m.objectName.toUpperCase()}`;
1482
+ for (const m of adtMatches)
1483
+ seen.set(baseKey(m), m);
1484
+ for (const m of dbMatches) {
1485
+ const k = baseKey(m);
1486
+ if (!seen.has(k))
1487
+ seen.set(k, m);
1488
+ }
1489
+ const mergedMatches = [...seen.values()];
1490
+ // Split-brain detection: an object is divergent if exactly one source has matches.
1491
+ // (Zero matches on both sides = consistent absence; matches on both = consistent presence.)
1492
+ if (adtMatches.length > 0 !== dbMatches.length > 0) {
1493
+ splitBrain.push(rawName);
1494
+ }
1495
+ return { name: rawName, found: mergedMatches.length > 0, matches: mergedMatches };
1496
+ });
1497
+ // Compose human-friendly warnings per split-brain name. Keep them grounded in
1498
+ // the most common cause (TADIR ghost from aborted create/delete) so LLM clients
1499
+ // can suggest the right cleanup path without inventing a new pointer.
1500
+ for (const name of splitBrain) {
1501
+ const adt = adtByName.get(name.toUpperCase());
1502
+ const db = dbByName.get(name.toUpperCase());
1503
+ const adtHas = (adt?.matches.length ?? 0) > 0;
1504
+ const dbHas = (db?.matches.length ?? 0) > 0;
1505
+ if (dbHas && !adtHas) {
1506
+ warnings.push(`${name} exists in TADIR (DB) but ADT cannot resolve it — likely a TADIR ghost from an aborted create/delete cycle. Consider RS_DD_TADIR_CLEANUP or manual SE03 cleanup.`);
1507
+ }
1508
+ else if (adtHas && !dbHas) {
1509
+ warnings.push(`${name} resolves via ADT but is not present in the TADIR row scan — likely a release-time mismatch or a type filter excluding the row. Re-run with broader objectTypes or no filter to confirm.`);
1510
+ }
1511
+ }
1512
+ }
1513
+ // Dedupe split-brain names (defensive; merge loop should already avoid duplicates).
1514
+ splitBrain = [...new Set(splitBrain)];
1515
+ if (wildcardNames.length > 0) {
1516
+ warnings.push(`tadir_lookup performs exact-name lookup; wildcard characters are treated literally for: ${wildcardNames.join(', ')}`);
1517
+ }
1518
+ const missing = finalLookups.filter((l) => !l.found).map((l) => l.name);
1519
+ const matchCount = finalLookups.reduce((count, lookup) => count + lookup.matches.length, 0);
1520
+ const payload = { count: matchCount, lookups: finalLookups, missing };
1521
+ if (splitBrain.length > 0)
1522
+ payload.splitBrain = splitBrain;
1523
+ if (warnings.length > 0)
1524
+ payload.warnings = warnings;
1525
+ return textResult(JSON.stringify(payload, null, 2));
1526
+ }
1400
1527
  if (searchType === 'source_code') {
1401
1528
  // Source code search: do NOT transliterate — source can contain umlauts in strings/comments
1402
1529
  if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
@@ -1441,6 +1568,37 @@ async function handleSAPSearch(client, args) {
1441
1568
  }
1442
1569
  return textResult(transliterationNote + JSON.stringify(results, null, 2));
1443
1570
  }
1571
+ function extractLookupNames(query, rawNames) {
1572
+ const fromNames = Array.isArray(rawNames) ? rawNames.map((n) => String(n).trim()).filter(Boolean) : [];
1573
+ const fromQuery = query
1574
+ .split(/[,\s]+/)
1575
+ .map((n) => n.trim())
1576
+ .filter(Boolean);
1577
+ return [...new Set([...fromNames, ...fromQuery].map((n) => n.toUpperCase()))];
1578
+ }
1579
+ function extractLookupObjectTypes(rawObjectType, rawObjectTypes) {
1580
+ const types = Array.isArray(rawObjectTypes)
1581
+ ? rawObjectTypes.map((t) => normalizeObjectType(String(t))).filter(Boolean)
1582
+ : [];
1583
+ if (typeof rawObjectType === 'string' && rawObjectType.trim()) {
1584
+ types.push(normalizeObjectType(rawObjectType));
1585
+ }
1586
+ return [...new Set(types)];
1587
+ }
1588
+ function normalizePackageOverride(rawPackage, fallback) {
1589
+ if (rawPackage === undefined || rawPackage === null) {
1590
+ return fallback;
1591
+ }
1592
+ const value = String(rawPackage).trim();
1593
+ return value || fallback;
1594
+ }
1595
+ function normalizeTransportOverride(rawTransport) {
1596
+ if (rawTransport === undefined || rawTransport === null) {
1597
+ return undefined;
1598
+ }
1599
+ const value = String(rawTransport).trim();
1600
+ return value || undefined;
1601
+ }
1444
1602
  function classifySapQueryParserError(err, sql) {
1445
1603
  if (err.statusCode !== 400)
1446
1604
  return undefined;
@@ -1460,11 +1618,136 @@ function classifySapQueryParserError(err, sql) {
1460
1618
  }
1461
1619
  return `${err.message}\n\nHint: ${hints.join(' ')}`;
1462
1620
  }
1621
+ const SAPQUERY_IN_LIST_CHUNK_SIZE = 8;
1622
+ function planSimpleInListChunking(sql, chunkSize = SAPQUERY_IN_LIST_CHUNK_SIZE) {
1623
+ const maskedSql = maskSqlStringLiterals(sql);
1624
+ if (maskedSql.includes(';'))
1625
+ return undefined;
1626
+ if (countSelectKeywords(maskedSql) !== 1)
1627
+ return undefined;
1628
+ const matches = [...maskedSql.matchAll(/\b[A-Za-z_][A-Za-z0-9_~.]*\s+IN\s*\(/gi)];
1629
+ if (matches.length !== 1)
1630
+ return undefined;
1631
+ const match = matches[0];
1632
+ const matchText = match[0];
1633
+ const fieldName = matchText.match(/^([A-Za-z_][A-Za-z0-9_~.]*)\s+IN\s*\(/i)?.[1];
1634
+ if (!fieldName || fieldName.toUpperCase() === 'NOT')
1635
+ return undefined;
1636
+ const matchStart = match.index ?? 0;
1637
+ const openParen = matchStart + matchText.lastIndexOf('(');
1638
+ const closeParen = findMatchingParen(maskedSql, openParen);
1639
+ if (closeParen < 0)
1640
+ return undefined;
1641
+ const literals = parseSingleQuotedLiteralList(sql.slice(openParen + 1, closeParen));
1642
+ if (!literals || literals.length <= chunkSize)
1643
+ return undefined;
1644
+ const prefix = sql.slice(0, openParen + 1);
1645
+ const suffix = sql.slice(closeParen);
1646
+ const statements = [];
1647
+ for (let i = 0; i < literals.length; i += chunkSize) {
1648
+ statements.push(`${prefix}${literals.slice(i, i + chunkSize).join(', ')}${suffix}`);
1649
+ }
1650
+ return { statements };
1651
+ }
1652
+ function maskSqlStringLiterals(sql) {
1653
+ let masked = '';
1654
+ let inString = false;
1655
+ for (let i = 0; i < sql.length; i++) {
1656
+ const ch = sql[i];
1657
+ if (ch === "'") {
1658
+ if (inString && sql[i + 1] === "'") {
1659
+ masked += ' ';
1660
+ i++;
1661
+ continue;
1662
+ }
1663
+ inString = !inString;
1664
+ masked += ' ';
1665
+ continue;
1666
+ }
1667
+ masked += inString ? ' ' : ch;
1668
+ }
1669
+ return masked;
1670
+ }
1671
+ function countSelectKeywords(maskedSql) {
1672
+ return [...maskedSql.matchAll(/\bSELECT\b/gi)].length;
1673
+ }
1674
+ function findMatchingParen(text, openParen) {
1675
+ let depth = 0;
1676
+ for (let i = openParen; i < text.length; i++) {
1677
+ if (text[i] === '(')
1678
+ depth++;
1679
+ if (text[i] === ')') {
1680
+ depth--;
1681
+ if (depth === 0)
1682
+ return i;
1683
+ }
1684
+ }
1685
+ return -1;
1686
+ }
1687
+ function parseSingleQuotedLiteralList(listText) {
1688
+ const literals = [];
1689
+ let i = 0;
1690
+ let expectingValue = true;
1691
+ while (i < listText.length) {
1692
+ while (i < listText.length && /\s/.test(listText[i]))
1693
+ i++;
1694
+ if (i >= listText.length)
1695
+ return expectingValue && literals.length > 0 ? undefined : literals;
1696
+ if (!expectingValue || listText[i] !== "'")
1697
+ return undefined;
1698
+ const start = i;
1699
+ i++;
1700
+ let closed = false;
1701
+ while (i < listText.length) {
1702
+ if (listText[i] === "'") {
1703
+ if (listText[i + 1] === "'") {
1704
+ i += 2;
1705
+ continue;
1706
+ }
1707
+ i++;
1708
+ closed = true;
1709
+ break;
1710
+ }
1711
+ i++;
1712
+ }
1713
+ if (!closed)
1714
+ return undefined;
1715
+ literals.push(listText.slice(start, i));
1716
+ expectingValue = false;
1717
+ while (i < listText.length && /\s/.test(listText[i]))
1718
+ i++;
1719
+ if (i >= listText.length)
1720
+ return literals;
1721
+ if (listText[i] !== ',')
1722
+ return undefined;
1723
+ i++;
1724
+ expectingValue = true;
1725
+ }
1726
+ return expectingValue && literals.length > 0 ? undefined : literals;
1727
+ }
1728
+ async function runChunkedSapQuery(client, plan, maxRows) {
1729
+ const rowLimit = Number.isFinite(maxRows) && maxRows > 0 ? Math.floor(maxRows) : 100;
1730
+ const rows = [];
1731
+ let columns = [];
1732
+ for (const statement of plan.statements) {
1733
+ const remaining = Math.max(0, rowLimit - rows.length);
1734
+ if (remaining === 0)
1735
+ break;
1736
+ const chunk = await client.runQuery(statement, remaining);
1737
+ if (columns.length === 0)
1738
+ columns = chunk.columns;
1739
+ rows.push(...chunk.rows);
1740
+ }
1741
+ return { columns, rows: rows.slice(0, rowLimit) };
1742
+ }
1463
1743
  async function handleSAPQuery(client, args) {
1464
1744
  const sql = String(args.sql ?? '');
1465
1745
  const maxRows = Number(args.maxRows ?? 100);
1746
+ const chunkPlan = planSimpleInListChunking(sql);
1747
+ let chunkingAttempted = false;
1466
1748
  try {
1467
- const data = await client.runQuery(sql, maxRows);
1749
+ chunkingAttempted = chunkPlan != null;
1750
+ const data = chunkPlan ? await runChunkedSapQuery(client, chunkPlan, maxRows) : await client.runQuery(sql, maxRows);
1468
1751
  return textResult(JSON.stringify(data, null, 2));
1469
1752
  }
1470
1753
  catch (err) {
@@ -1489,7 +1772,11 @@ async function handleSAPQuery(client, args) {
1489
1772
  }
1490
1773
  }
1491
1774
  if (err instanceof AdtApiError) {
1492
- const parserHint = classifySapQueryParserError(err, sql);
1775
+ let parserHint = classifySapQueryParserError(err, sql);
1776
+ if (parserHint && chunkingAttempted) {
1777
+ parserHint +=
1778
+ '\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.';
1779
+ }
1493
1780
  if (parserHint)
1494
1781
  return errorResult(parserHint);
1495
1782
  }
@@ -1527,9 +1814,12 @@ async function handleSAPLint(client, args, config) {
1527
1814
  const rules = listRulesFromConfig(lintConfig);
1528
1815
  const enabled = rules.filter((r) => r.enabled);
1529
1816
  const disabled = rules.filter((r) => !r.enabled);
1817
+ const effectiveAbapRelease = configOptions.abapRelease ?? 'unknown';
1818
+ const syntax = lintConfig.get().syntax;
1530
1819
  return textResult(JSON.stringify({
1531
1820
  preset: configOptions.systemType === 'btp' ? 'cloud' : 'onprem',
1532
- abapVersion: cachedFeatures?.abapRelease ?? 'unknown',
1821
+ abapVersion: effectiveAbapRelease,
1822
+ syntaxVersion: syntax?.version ?? 'unknown',
1533
1823
  enabledRules: enabled.length,
1534
1824
  disabledRules: disabled.length,
1535
1825
  rules: enabled,
@@ -1578,7 +1868,7 @@ function buildLintConfigOptions(config, ruleOverrides) {
1578
1868
  const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
1579
1869
  return {
1580
1870
  systemType,
1581
- abapRelease: cachedFeatures?.abapRelease,
1871
+ abapRelease: cachedFeatures?.abapRelease ?? config.abapRelease,
1582
1872
  configFile: config.abaplintConfig,
1583
1873
  ruleOverrides,
1584
1874
  };
@@ -2482,16 +2772,60 @@ function objectUrlForTypeRaw(type, name) {
2482
2772
  function sourceUrlForType(type, name) {
2483
2773
  return `${objectUrlForType(type, name)}/source/main`;
2484
2774
  }
2775
+ const CLASS_WRITE_INCLUDES = ['definitions', 'implementations', 'macros', 'testclasses'];
2485
2776
  /** Get a CLAS include URL (definitions/implementations/macros/testclasses) */
2486
2777
  function classIncludeUrl(name, include) {
2487
2778
  return `/sap/bc/adt/oo/classes/${encodeURIComponent(name)}/includes/${include}`;
2488
2779
  }
2780
+ function normalizeClassWriteInclude(include) {
2781
+ if (typeof include !== 'string')
2782
+ return undefined;
2783
+ const normalized = include.toLowerCase();
2784
+ return CLASS_WRITE_INCLUDES.includes(normalized) ? normalized : undefined;
2785
+ }
2786
+ /**
2787
+ * Auto-detect which class include a method specifier targets, based on the
2788
+ * local-class prefix on the LHS of `<localclass>~<method>`. Used by
2789
+ * `edit_method` so callers can pass `lhc_project~approve_project` and have
2790
+ * ARC-1 transparently route the read+write to `/includes/implementations`
2791
+ * instead of `/source/main`.
2792
+ *
2793
+ * Prefix → include mapping (intentionally narrow; extend via explicit
2794
+ * `include` parameter when a code-base uses other conventions):
2795
+ * - `lhc_*` → implementations (RAP behavior pool handler classes)
2796
+ * - `lcl_*` → implementations (local helper classes)
2797
+ * - `ltc_*` → testclasses (ABAP Unit local test classes)
2798
+ *
2799
+ * Returns `undefined` for:
2800
+ * - Specifiers with no `~` (route to MAIN)
2801
+ * - Global-interface methods like `zif_order~create`, `if_oo_adt_classrun~main`
2802
+ * (route to MAIN — the impl lives in a global class)
2803
+ * - `lif_*` local interfaces (interfaces only declare methods — there's no
2804
+ * impl in CCDEF; an `lhc_*`/`lcl_*` class implements them and the call
2805
+ * site uses that class's prefix instead)
2806
+ */
2807
+ function detectLocalHandlerInclude(method) {
2808
+ if (!method.includes('~'))
2809
+ return undefined;
2810
+ const lhs = method.slice(0, method.indexOf('~')).trim().toLowerCase();
2811
+ if (/^(lhc|lcl)_/.test(lhs))
2812
+ return 'implementations';
2813
+ if (/^ltc_/.test(lhs))
2814
+ return 'testclasses';
2815
+ return undefined;
2816
+ }
2817
+ /** Strip the leading "=== <include> ===\n" header that `client.getClass(name, include)` prepends. */
2818
+ function stripIncludeHeader(source) {
2819
+ return source.replace(/^=== \w+ ===\n/, '');
2820
+ }
2489
2821
  // ─── SAPWrite Handler ────────────────────────────────────────────────
2490
2822
  async function handleSAPWrite(client, args, config, cachingLayer) {
2491
2823
  const action = String(args.action ?? '');
2492
2824
  const type = normalizeObjectType(String(args.type ?? ''));
2493
2825
  const name = String(args.name ?? '');
2494
2826
  const source = String(args.source ?? '');
2827
+ const hasSource = typeof args.source === 'string';
2828
+ const include = normalizeClassWriteInclude(args.include);
2495
2829
  const transport = args.transport;
2496
2830
  const lintOverride = args.lintBeforeWrite;
2497
2831
  const preflightOverride = args.preflightBeforeWrite;
@@ -2572,6 +2906,23 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2572
2906
  switch (action) {
2573
2907
  case 'update': {
2574
2908
  const existingPackage = await enforcePackageForExistingObject();
2909
+ // Keep CLAS local include writes ahead of the generic /source/main fallthrough.
2910
+ // If CLAS ever gains separate metadata-update handling, this branch must still
2911
+ // win whenever callers pass include=definitions|implementations|macros|testclasses.
2912
+ if (args.include !== undefined) {
2913
+ if (!include) {
2914
+ return errorResult(`Invalid CLAS include "${String(args.include)}". Valid values: ${CLASS_WRITE_INCLUDES.join(', ')}.`);
2915
+ }
2916
+ if (type !== 'CLAS') {
2917
+ return errorResult('SAPWrite include is only supported for action="update" with type="CLAS".');
2918
+ }
2919
+ if (!hasSource) {
2920
+ return errorResult('"source" is required when updating a CLAS include.');
2921
+ }
2922
+ await safeUpdateSource(client.http, client.safety, objectUrl, classIncludeUrl(name, include), source, transport, cachedFeatures?.abapRelease);
2923
+ invalidateWrittenObject(type, name);
2924
+ return textResult(`Successfully updated ${type} ${name} include ${include}. Active version remains unchanged until activation; read with SAPRead(version="inactive") to verify the draft.`);
2925
+ }
2575
2926
  if (type === 'SKTD') {
2576
2927
  // KTD update requires the full <sktd:docu> XML envelope with the Markdown
2577
2928
  // body base64-encoded inside <sktd:text>, PUT with
@@ -2611,14 +2962,47 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2611
2962
  // "Parameter comment blocks are not allowed" — verified live a4h S/4HANA 2023,
2612
2963
  // issue #250). LLMs frequently emit them out of muscle memory because every
2613
2964
  // released FM has one. Strip and warn rather than fail.
2965
+ //
2966
+ // Issue #252: when `parameters` is supplied as a structured array, splice
2967
+ // it into the FM source as ABAP-source-based signature syntax. If `source`
2968
+ // is omitted entirely, fetch the existing source first to preserve the
2969
+ // body. The structured clause replaces any existing signature region.
2614
2970
  let effectiveSource = source;
2615
2971
  let fmParamStripWarning;
2972
+ let fmParamMergeWarning;
2616
2973
  if (type === 'FUNC') {
2617
- const stripped = stripFmParamCommentBlock(source);
2974
+ const parameters = args.parameters;
2975
+ if (parameters !== undefined) {
2976
+ // If caller passed parameters but no source, fetch the current source so
2977
+ // the body is preserved (the parameters array re-emits only the signature).
2978
+ let baseSource = source;
2979
+ if (!baseSource || baseSource.trim() === '') {
2980
+ const groupName = String(args.group ?? '');
2981
+ const fetched = await client.getFunction(groupName, name).catch(() => null);
2982
+ baseSource = fetched?.source ?? `FUNCTION ${name}.\nENDFUNCTION.\n`;
2983
+ }
2984
+ else if (!/^\s*FUNCTION\s+/i.test(baseSource)) {
2985
+ // Body-only source: wrap in FUNCTION/ENDFUNCTION so the splicer has
2986
+ // something to work with. Common shape from LLMs: just the body.
2987
+ baseSource = `FUNCTION ${name}.\n${baseSource}\nENDFUNCTION.\n`;
2988
+ }
2989
+ try {
2990
+ effectiveSource = spliceFmSignature(baseSource, name, parameters);
2991
+ }
2992
+ catch {
2993
+ // No FUNCTION token in the supplied source — fall back to user's source.
2994
+ effectiveSource = baseSource;
2995
+ fmParamMergeWarning =
2996
+ 'Could not splice structured parameters: source did not start with FUNCTION keyword. Used the supplied source verbatim.';
2997
+ }
2998
+ }
2999
+ // Defense-in-depth: strip *" comment blocks even after splicing — the
3000
+ // user's body may contain them (e.g. pasted from SAPGUI).
3001
+ const stripped = stripFmParamCommentBlock(effectiveSource);
2618
3002
  effectiveSource = stripped.source;
2619
3003
  if (stripped.wasStripped) {
2620
3004
  fmParamStripWarning =
2621
- 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (SAP rejects them on PUT — manage FM parameters via SAPGUI/Eclipse).';
3005
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (SAP rejects them on PUT — pass `parameters` as a structured array instead).';
2622
3006
  }
2623
3007
  }
2624
3008
  // Pre-write lint validation (uses sanitized source for FUNC)
@@ -2633,7 +3017,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2633
3017
  invalidateWrittenObject(type, name);
2634
3018
  const msg = `Successfully updated ${type} ${name}.`;
2635
3019
  const cdsUpdateHint = type === 'DDLS' ? await buildCdsUpdateCrudHint(client, name, objectUrl) : undefined;
2636
- const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint, fmParamStripWarning);
3020
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, checkNotes, cdsUpdateHint, fmParamStripWarning, fmParamMergeWarning);
2637
3021
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
2638
3022
  }
2639
3023
  case 'create': {
@@ -2802,17 +3186,46 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2802
3186
  : '';
2803
3187
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
2804
3188
  }
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;
3189
+ // Step 2: Write source code if provided.
3190
+ // Issue #252: FUNC create accepts a structured `parameters` array; if
3191
+ // provided we must follow up with a source PUT even when `source` is
3192
+ // omitted (the array alone synthesizes a minimal FUNCTION/ENDFUNCTION
3193
+ // body containing the signature clause).
3194
+ const funcParameters = type === 'FUNC' ? args.parameters : undefined;
3195
+ const shouldWriteSource = !!source || (funcParameters !== undefined && funcParameters.length > 0);
3196
+ if (shouldWriteSource) {
3197
+ // FUNC: build/splice the signature, then strip SAPGUI parameter comment
3198
+ // blocks as defense-in-depth (see update path for rationale).
3199
+ let createSource = source ?? '';
2809
3200
  let fmParamStripWarning;
3201
+ let fmParamMergeWarning;
2810
3202
  if (type === 'FUNC') {
2811
- const stripped = stripFmParamCommentBlock(source);
3203
+ if (funcParameters !== undefined) {
3204
+ let baseSource;
3205
+ if (!createSource || createSource.trim() === '') {
3206
+ baseSource = `FUNCTION ${name}.\nENDFUNCTION.\n`;
3207
+ }
3208
+ else if (!/^\s*FUNCTION\s+/i.test(createSource)) {
3209
+ // Body-only source — wrap so the splicer has a signature region.
3210
+ baseSource = `FUNCTION ${name}.\n${createSource}\nENDFUNCTION.\n`;
3211
+ }
3212
+ else {
3213
+ baseSource = createSource;
3214
+ }
3215
+ try {
3216
+ createSource = spliceFmSignature(baseSource, name, funcParameters);
3217
+ }
3218
+ catch {
3219
+ createSource = baseSource;
3220
+ fmParamMergeWarning =
3221
+ 'Could not splice structured parameters: source did not start with FUNCTION keyword. Used the supplied source verbatim.';
3222
+ }
3223
+ }
3224
+ const stripped = stripFmParamCommentBlock(createSource);
2812
3225
  createSource = stripped.source;
2813
3226
  if (stripped.wasStripped) {
2814
3227
  fmParamStripWarning =
2815
- 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (manage FM parameters via SAPGUI/Eclipse).';
3228
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (pass `parameters` as a structured array instead).';
2816
3229
  }
2817
3230
  }
2818
3231
  // Pre-write lint validation
@@ -2823,7 +3236,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2823
3236
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, createSource, effectiveTransport, cachedFeatures?.abapRelease);
2824
3237
  invalidateWrittenObject(type, name);
2825
3238
  const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
2826
- const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning);
3239
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning, fmParamMergeWarning);
2827
3240
  return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
2828
3241
  }
2829
3242
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
@@ -2837,10 +3250,50 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2837
3250
  if (type !== 'CLAS')
2838
3251
  return errorResult('edit_method is only supported for type=CLAS.');
2839
3252
  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;
3253
+ // ── Resolve which class section the method body lives in ──
3254
+ // Order:
3255
+ // 1. Explicit `include` parameter wins (must be a valid CLAS include).
3256
+ // If the user passed something but normalization rejected it,
3257
+ // report it the same way `case 'update'` does.
3258
+ // 2. Auto-detect from local-class prefix in `method` specifier
3259
+ // (lhc_*/lcl_* → implementations, ltc_* → testclasses). This is
3260
+ // transparent to RAP-skill callers passing `lhc_project~approve_project`.
3261
+ // 3. Fall through to MAIN (existing behavior — covers global classes
3262
+ // and `zif_order~create` style interface methods).
3263
+ if (args.include !== undefined && !include) {
3264
+ return errorResult(`Invalid CLAS include "${String(args.include)}". Valid values: ${CLASS_WRITE_INCLUDES.join(', ')}.`);
3265
+ }
3266
+ const detectedInclude = include ? undefined : detectLocalHandlerInclude(method);
3267
+ const resolvedInclude = include ?? detectedInclude;
3268
+ // Fetch the source that contains the method.
3269
+ // Note: include reads bypass the source cache because the cache key is
3270
+ // `(type, name, active|inactive)` and does not differentiate by include.
3271
+ // Mixing MAIN and CCIMP bytes under the same key would silently corrupt
3272
+ // subsequent reads. Future enhancement: extend cache key with include.
3273
+ let currentSource;
3274
+ if (resolvedInclude) {
3275
+ // **Draft-aware include reads (PR-D review fix, P1).**
3276
+ // After `SAPWrite update include=...` or `scaffold_rap_handlers`, the
3277
+ // edited CCDEF/CCIMP lives as an inactive draft; the active include
3278
+ // is often still the empty placeholder. Reading "active" here would
3279
+ // splice against stale content (and frequently "method not found").
3280
+ // Use the standard inactive-list lookup to pick the right version —
3281
+ // same auto-resolution semantics SAPRead exposes via `version='auto'`.
3282
+ const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', name, 'auto');
3283
+ const fetched = await client.getClass(name, resolvedInclude, { version: effectiveVersion });
3284
+ currentSource = stripIncludeHeader(fetched.source);
3285
+ // If the include itself has no draft (only MAIN does), SAP returns the
3286
+ // active include body for `?version=inactive`. That's correct — we
3287
+ // splice whatever the editor would see. If the include source isn't
3288
+ // available at all (response contains the "not available" placeholder
3289
+ // injected by client.getClass on 404), splice will surface a clean
3290
+ // "method not found" with the include name.
3291
+ }
3292
+ else {
3293
+ currentSource = cachingLayer
3294
+ ? (await cachingLayer.getSource('CLAS', name, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch }))).source
3295
+ : (await client.getClass(name)).source;
3296
+ }
2844
3297
  // Use detected ABAP version from probe if available
2845
3298
  const abaplintVer = cachedFeatures?.abapRelease
2846
3299
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
@@ -2848,18 +3301,40 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2848
3301
  // Splice in the new method body
2849
3302
  const spliced = spliceMethod(currentSource, name, method, source, abaplintVer);
2850
3303
  if (!spliced.success) {
2851
- return errorResult(spliced.error ?? `Failed to splice method "${method}" in ${name}.`);
3304
+ // Augment the error with which include was searched, so the LLM can
3305
+ // either correct the method specifier or override include= explicitly.
3306
+ const where = resolvedInclude ? `include "${resolvedInclude}"` : 'main source';
3307
+ const baseError = spliced.error ?? `Failed to splice method "${method}" in ${name}.`;
3308
+ const hint = detectedInclude
3309
+ ? ` (auto-routed via "${method}" prefix; pass include= explicitly to override).`
3310
+ : '';
3311
+ return errorResult(`${baseError} Searched ${where} of ${name}.${hint}`);
2852
3312
  }
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);
3313
+ // Pre-write lint + server-side syntax check on the spliced source.
3314
+ //
3315
+ // Skip BOTH for include= writes. abaplint cannot parse a CCIMP/CCDEF
3316
+ // fragment as a complete class (the DEFINITION/IMPLEMENTATION halves
3317
+ // live in different files), so it would block legitimate writes with
3318
+ // "Expected CLASSDEFINITION" errors. The existing `case 'update'` include=
3319
+ // path also bypasses these checks for the same reason keep parity.
3320
+ // The full-class activation pass after the write is the authoritative
3321
+ // syntax check.
3322
+ let lintWarnings = { blocked: false };
3323
+ let checkNotes = '';
3324
+ if (!resolvedInclude) {
3325
+ lintWarnings = runPreWriteLint(spliced.newSource, type, name, config, lintOverride);
3326
+ if (lintWarnings.blocked)
3327
+ return lintWarnings.result;
3328
+ checkNotes = await runPreWriteSyntaxCheck(client, type, spliced.newSource, objectUrl, config, checkOverride);
3329
+ }
3330
+ // Write the full source back (existing lock/modify/unlock flow).
3331
+ // For include writes, the parent class lock auto-applies; the include URL
3332
+ // takes the body. See `compare/eclipse-adt/api/05-lock-create-update-transport.md`.
3333
+ const writeUrl = resolvedInclude ? classIncludeUrl(name, resolvedInclude) : srcUrl;
3334
+ await safeUpdateSource(client.http, client.safety, objectUrl, writeUrl, spliced.newSource, transport, cachedFeatures?.abapRelease);
2861
3335
  invalidateWrittenObject(type, name);
2862
- const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
3336
+ const where = resolvedInclude ? ` (include: ${resolvedInclude})` : '';
3337
+ const msg = `Successfully updated method "${method}" in ${type} ${name}${where}.`;
2863
3338
  const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
2864
3339
  return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
2865
3340
  }
@@ -2965,6 +3440,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2965
3440
  applied: false,
2966
3441
  hint: unresolvedHint,
2967
3442
  applyResult: {
3443
+ skeletons: scaffoldPlan.skeletons,
2968
3444
  main: scaffoldPlan.signatures.main,
2969
3445
  definitions: scaffoldPlan.signatures.definitions,
2970
3446
  implementations: scaffoldPlan.signatures.implementations,
@@ -3029,12 +3505,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3029
3505
  });
3030
3506
  invalidateWrittenObject();
3031
3507
  const msg = `Scaffolded ${scaffoldPlan.insertedSignatureCount} RAP handler signature(s) and ${scaffoldPlan.insertedImplementationStubCount} implementation stub(s) in ${type} ${name} from BDEF ${bdefName}. ` +
3508
+ `Auto-created ${scaffoldPlan.skeletons.createdDefinitions.length + scaffoldPlan.skeletons.createdImplementations.length} handler skeleton section(s). ` +
3032
3509
  `Updated section(s): ${scaffoldPlan.changedSections.join(', ')}.`;
3033
3510
  const warnings = mergePreWriteWarnings(lintWarningsMain?.warnings, lintWarningsDefinitions?.warnings, lintWarningsImplementations?.warnings);
3034
3511
  const details = JSON.stringify({
3035
3512
  ...summary,
3036
3513
  applied: true,
3037
3514
  applyResult: {
3515
+ skeletons: scaffoldPlan.skeletons,
3038
3516
  main: scaffoldPlan.signatures.main,
3039
3517
  definitions: scaffoldPlan.signatures.definitions,
3040
3518
  implementations: scaffoldPlan.signatures.implementations,
@@ -3044,6 +3522,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3044
3522
  }, null, 2);
3045
3523
  return warnings ? textResult(`${msg}\n\n${warnings}\n\n${details}`) : textResult(`${msg}\n\n${details}`);
3046
3524
  }
3525
+ case 'generate_behavior_implementation': {
3526
+ // PR-C: high-level RAP one-shot — auto-discover BDEF via class metadata's
3527
+ // rootEntityRef, scaffold every required handler (creating lhc_<alias>
3528
+ // skeletons when missing), write under one lock, and (by default) activate.
3529
+ // Reliable equivalent of Eclipse ADT's "Generate Behavior Implementation"
3530
+ // Cmd+1 quickfix; avoids the broken /sap/bc/adt/quickfixes/proposals/
3531
+ // create_class_implementation server endpoint (HTTP 500 on a4h, verified
3532
+ // live during PR-C research). See docs/plans/add-generate-behavior-implementation.md.
3533
+ if (type !== 'CLAS') {
3534
+ return errorResult('generate_behavior_implementation is only supported for type=CLAS behavior pool classes.');
3535
+ }
3536
+ if (!name) {
3537
+ return errorResult('"name" is required for generate_behavior_implementation.');
3538
+ }
3539
+ const dryRun = args.dryRun === true || String(args.dryRun ?? '') === 'true';
3540
+ const activate = args.activate === undefined ? true : args.activate === true || String(args.activate) === 'true';
3541
+ const explicitBdef = args.bdefName?.trim() || undefined;
3542
+ const targetAlias = args.targetAlias?.trim() || undefined;
3543
+ // Package gate only when we'll actually mutate. dryRun=true is read-only;
3544
+ // bypassing the gate matches the scaffold_rap_handlers preview pattern.
3545
+ if (!dryRun) {
3546
+ await enforcePackageForExistingObject();
3547
+ }
3548
+ const result = await generateBehaviorImplementation(client, name, {
3549
+ bdefName: explicitBdef,
3550
+ targetAlias,
3551
+ activate,
3552
+ dryRun,
3553
+ transport,
3554
+ });
3555
+ invalidateWrittenObject();
3556
+ // MCP result-code mapping via the exported helper — see
3557
+ // `isRapGenerateResultSuccess` for the success/error contract (Codex review on PR #260, P1).
3558
+ // The structured JSON is preserved in both branches so the caller can still see what
3559
+ // was discovered, written, and what activation reported.
3560
+ const json = JSON.stringify(result, null, 2);
3561
+ return isRapGenerateResultSuccess(result) ? textResult(json) : errorResult(json);
3562
+ }
3047
3563
  case 'delete': {
3048
3564
  await enforcePackageForExistingObject();
3049
3565
  // Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
@@ -3085,20 +3601,40 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3085
3601
  if (!objects || !Array.isArray(objects) || objects.length === 0) {
3086
3602
  return errorResult('"objects" array is required and must be non-empty for batch_create action.');
3087
3603
  }
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') {
3604
+ // Opt-in deferred-activation: writes every object as an inactive draft first,
3605
+ // then issues a single terminal activateBatch over the written subset. Use case:
3606
+ // composition-linked DDLS / interdependent RAP graphs where per-object inline
3607
+ // activate() can't resolve cross-references to not-yet-active siblings.
3608
+ const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
3609
+ const defaultPackage = normalizePackageOverride(args.package, '$TMP');
3610
+ const batchPlan = objects.map((obj) => {
3611
+ const objType = normalizeObjectType(String(obj.type ?? ''));
3612
+ const objName = String(obj.name ?? '');
3613
+ const objPackage = normalizePackageOverride(obj.package, defaultPackage);
3614
+ const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
3615
+ return { obj, type: objType, name: objName, packageName: objPackage, explicitTransport };
3616
+ });
3617
+ // Check every target package before starting any creates.
3618
+ for (const pkg of new Set(batchPlan.map((item) => item.packageName))) {
3619
+ checkPackage(client.safety, pkg);
3620
+ }
3621
+ // Pre-flight transport check for batch_create (same logic as single create),
3622
+ // but keyed by each effective package because objects can override package.
3623
+ const autoTransportByPackage = new Map();
3624
+ const firstPlanNeedingTransportByPackage = new Map();
3625
+ for (const plan of batchPlan) {
3626
+ if (!plan.explicitTransport &&
3627
+ plan.packageName.toUpperCase() !== '$TMP' &&
3628
+ !firstPlanNeedingTransportByPackage.has(plan.packageName)) {
3629
+ firstPlanNeedingTransportByPackage.set(plan.packageName, plan);
3630
+ }
3631
+ }
3632
+ for (const [pkg, plan] of firstPlanNeedingTransportByPackage) {
3094
3633
  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 ?? ''));
3634
+ const firstUrl = objectUrlForType(plan.type, plan.name);
3099
3635
  const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
3100
3636
  if (transportInfo.lockedTransport) {
3101
- batchTransport = transportInfo.lockedTransport;
3637
+ autoTransportByPackage.set(pkg, transportInfo.lockedTransport);
3102
3638
  }
3103
3639
  else if (!transportInfo.isLocal && transportInfo.recording) {
3104
3640
  const existingList = transportInfo.existingTransports.length > 0
@@ -3115,7 +3651,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3115
3651
  existingList);
3116
3652
  }
3117
3653
  }
3118
- catch {
3654
+ catch (err) {
3655
+ logger.warn('SAPWrite batch_create transport preflight failed; continuing without auto transport', {
3656
+ package: pkg,
3657
+ type: plan.type,
3658
+ name: plan.name,
3659
+ error: err instanceof Error ? err.message : String(err),
3660
+ });
3119
3661
  // If transportInfo check fails, proceed — SAP will return its own error if needed.
3120
3662
  }
3121
3663
  }
@@ -3125,9 +3667,12 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3125
3667
  // guard fires for every MSAG entry, but a batch typically shares one transport — cache
3126
3668
  // the lookup result to avoid one HTTP roundtrip per object.
3127
3669
  const transportLookupCache = new Map();
3128
- for (const obj of objects) {
3129
- const objType = normalizeObjectType(String(obj.type ?? ''));
3130
- const objName = String(obj.name ?? '');
3670
+ // Accumulated objects whose create + source-write phase succeeded — used by the
3671
+ // terminal activateBatch when activateAtEnd=true. Order matches the input order.
3672
+ const writtenObjects = [];
3673
+ for (const plan of batchPlan) {
3674
+ const { obj, type: objType, name: objName, packageName: objPackage } = plan;
3675
+ const objTransport = plan.explicitTransport ?? autoTransportByPackage.get(objPackage);
3131
3676
  const metadataObject = isMetadataWriteType(objType);
3132
3677
  const objSource = obj.source ? String(obj.source) : undefined;
3133
3678
  const objDescription = String(obj.description ?? objName);
@@ -3138,24 +3683,26 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3138
3683
  results.push({
3139
3684
  type: objType,
3140
3685
  name: objName,
3686
+ packageName: objPackage,
3141
3687
  status: 'failed',
3142
3688
  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
3689
  });
3144
3690
  break;
3145
3691
  }
3146
3692
  // MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
3147
- if (objType === 'MSAG' && batchTransport) {
3148
- let tr = transportLookupCache.get(batchTransport);
3693
+ if (objType === 'MSAG' && objTransport) {
3694
+ let tr = transportLookupCache.get(objTransport);
3149
3695
  if (tr === undefined) {
3150
- tr = await getTransport(client.http, client.safety, batchTransport);
3151
- transportLookupCache.set(batchTransport, tr);
3696
+ tr = await getTransport(client.http, client.safety, objTransport);
3697
+ transportLookupCache.set(objTransport, tr);
3152
3698
  }
3153
3699
  if (!tr) {
3154
3700
  results.push({
3155
3701
  type: objType,
3156
3702
  name: objName,
3703
+ packageName: objPackage,
3157
3704
  status: 'failed',
3158
- error: `Transport "${batchTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
3705
+ error: `Transport "${objTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
3159
3706
  });
3160
3707
  break;
3161
3708
  }
@@ -3166,6 +3713,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3166
3713
  results.push({
3167
3714
  type: objType,
3168
3715
  name: objName,
3716
+ packageName: objPackage,
3169
3717
  status: 'failed',
3170
3718
  error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
3171
3719
  });
@@ -3180,6 +3728,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3180
3728
  results.push({
3181
3729
  type: objType,
3182
3730
  name: objName,
3731
+ packageName: objPackage,
3183
3732
  status: 'failed',
3184
3733
  error: preflightWarnings.result.content[0].text,
3185
3734
  });
@@ -3193,6 +3742,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3193
3742
  results.push({
3194
3743
  type: objType,
3195
3744
  name: objName,
3745
+ packageName: objPackage,
3196
3746
  status: 'failed',
3197
3747
  error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
3198
3748
  });
@@ -3203,11 +3753,11 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3203
3753
  const objUrl = objectUrlForType(objType, objName);
3204
3754
  const createUrl = objUrl.replace(/\/[^/]+$/, '');
3205
3755
  const objMetadataProps = getMetadataWriteProperties(obj);
3206
- const body = buildCreateXml(objType, objName, pkg, objDescription, objMetadataProps);
3756
+ const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
3207
3757
  const contentType = createContentTypeForType(objType);
3208
3758
  const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
3209
3759
  try {
3210
- await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
3760
+ await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
3211
3761
  }
3212
3762
  catch (createErr) {
3213
3763
  if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
@@ -3222,7 +3772,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3222
3772
  if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
3223
3773
  await client.http.withStatefulSession(async (session) => {
3224
3774
  const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
3225
- const lockTransport = batchTransport ?? (lock.corrNr || undefined);
3775
+ const lockTransport = objTransport ?? (lock.corrNr || undefined);
3226
3776
  try {
3227
3777
  await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
3228
3778
  }
@@ -3234,26 +3784,58 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3234
3784
  // Step 2: Write source if provided
3235
3785
  if (!metadataObject && objSource) {
3236
3786
  const srcUrl = sourceUrlForType(objType, objName);
3237
- await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport, cachedFeatures?.abapRelease);
3787
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, objTransport, cachedFeatures?.abapRelease);
3238
3788
  }
3239
- // Step 3: Activate the object
3240
- const activationResult = await activate(client.http, client.safety, objUrl);
3241
- if (!activationResult.success) {
3242
- results.push({
3243
- type: objType,
3244
- name: objName,
3245
- status: 'failed',
3246
- error: `activation failed: ${activationResult.messages.join('; ')}`,
3247
- });
3248
- break;
3789
+ // Resolve the activation URL up front so both the inline path and the
3790
+ // deferred terminal-activate path use the same URL. FUNC needs the parent
3791
+ // function-group baked into the path (issue #250); objectUrlForType throws
3792
+ // for FUNC so we mirror the FUNC-aware resolver from handleSAPActivate. For
3793
+ // TABL we keep objUrl (already resolved to /tables/) — DDIC-structure FMs
3794
+ // aren't a real concept and the create path doesn't expose one.
3795
+ let activationUrl = objUrl;
3796
+ if (objType === 'FUNC') {
3797
+ let group = String(obj.group ?? args.group ?? '').trim();
3798
+ if (!group) {
3799
+ const resolved = cachingLayer
3800
+ ? await cachingLayer.resolveFuncGroup(client, objName)
3801
+ : await client.resolveFunctionGroup(objName);
3802
+ if (!resolved) {
3803
+ throw new Error(`Cannot resolve function group for FM "${objName}" in batch_create activation step. Provide "group" on the FUNC entry.`);
3804
+ }
3805
+ group = resolved;
3806
+ }
3807
+ const groupLc = encodeURIComponent(group.toLowerCase());
3808
+ activationUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(objName.toLowerCase())}`;
3809
+ }
3810
+ if (activateAtEnd) {
3811
+ // Step 3 deferred: track this object for the terminal activateBatch call.
3812
+ // Cache invalidation also moves to AFTER the terminal activate succeeds —
3813
+ // invalidating now would let the next read see a draft we couldn't activate.
3814
+ writtenObjects.push({ type: objType, name: objName, url: activationUrl });
3815
+ results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
3816
+ }
3817
+ else {
3818
+ // Step 3: Activate the object (inline, default behavior).
3819
+ const activationResult = await activate(client.http, client.safety, activationUrl);
3820
+ if (!activationResult.success) {
3821
+ results.push({
3822
+ type: objType,
3823
+ name: objName,
3824
+ packageName: objPackage,
3825
+ status: 'failed',
3826
+ error: `activation failed: ${activationResult.messages.join('; ')}`,
3827
+ });
3828
+ break;
3829
+ }
3830
+ invalidateWrittenObject(objType, objName);
3831
+ results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
3249
3832
  }
3250
- invalidateWrittenObject(objType, objName);
3251
- results.push({ type: objType, name: objName, status: 'success' });
3252
3833
  }
3253
3834
  catch (err) {
3254
3835
  results.push({
3255
3836
  type: objType,
3256
3837
  name: objName,
3838
+ packageName: objPackage,
3257
3839
  status: 'failed',
3258
3840
  error: err instanceof Error ? err.message : String(err),
3259
3841
  });
@@ -3262,30 +3844,88 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3262
3844
  }
3263
3845
  // Add 'skipped' entries for objects that were never attempted due to early break
3264
3846
  for (let i = results.length; i < objects.length; i++) {
3265
- const skipped = objects[i];
3847
+ const skippedPlan = batchPlan[i];
3848
+ const skipped = skippedPlan?.obj ?? objects[i];
3266
3849
  results.push({
3267
- type: normalizeObjectType(String(skipped?.type ?? '')),
3268
- name: String(skipped.name ?? ''),
3850
+ type: skippedPlan?.type ?? normalizeObjectType(String(skipped?.type ?? '')),
3851
+ name: skippedPlan?.name ?? String(skipped?.name ?? ''),
3852
+ packageName: skippedPlan?.packageName ?? normalizePackageOverride(skipped?.package, defaultPackage),
3269
3853
  status: 'failed',
3270
3854
  error: 'skipped — stopped after previous failure',
3271
3855
  });
3272
3856
  }
3857
+ // ── Terminal activateBatch (activateAtEnd=true) ─────────────────────
3858
+ // After every write-phase succeeded (or broke off early), issue ONE batch
3859
+ // activate over the already-written subset. This is the killer feature
3860
+ // for composition-linked DDLS and RAP behavior stacks — SAP's activator
3861
+ // sees the whole graph in a single POST and resolves cross-references
3862
+ // internally, so parent → child siblings activate cleanly.
3863
+ let terminalActivationFailure;
3864
+ if (activateAtEnd && writtenObjects.length > 0) {
3865
+ const activationOutcome = await activateBatch(client.http, client.safety, writtenObjects);
3866
+ if (activationOutcome.success) {
3867
+ // Defensive: per-object status was already 'success' from the write phase.
3868
+ // Cache invalidation moves here so a failed terminal activate doesn't strand
3869
+ // a stale 'active' cache entry. Invalidate inactive-lists once for the user.
3870
+ for (const o of writtenObjects) {
3871
+ cachingLayer?.invalidate(o.type, o.name, 'all');
3872
+ }
3873
+ cachingLayer?.inactiveLists.invalidate(client.username);
3874
+ }
3875
+ else {
3876
+ // Flip every written-but-not-yet-activated entry to 'failed', preserving the
3877
+ // "create + source-write succeeded" context. Reuse the existing per-object
3878
+ // diagnostic mapper so callers see the activation messages keyed by object name.
3879
+ const batchStatuses = buildBatchActivationStatuses(writtenObjects, activationOutcome);
3880
+ const statusDetails = formatBatchActivationStatuses(batchStatuses);
3881
+ terminalActivationFailure = statusDetails;
3882
+ const statusByName = new Map(batchStatuses.map((s) => [`${s.type}${s.name}`, s]));
3883
+ for (const result of results) {
3884
+ if (result.status !== 'success')
3885
+ continue;
3886
+ const key = `${result.type}${result.name}`;
3887
+ const matched = statusByName.get(key);
3888
+ if (!matched)
3889
+ continue;
3890
+ // Some entries may still report status 'active' if the activator returned
3891
+ // success: false but had no per-object error details — keep them as 'success'.
3892
+ if (matched.status === 'active')
3893
+ continue;
3894
+ result.status = 'failed';
3895
+ const detail = matched.messages.length > 0 ? ` — ${matched.messages.join('; ')}` : '';
3896
+ // Preserve the "create + source-write succeeded" context so the user sees that
3897
+ // the failure was specifically the activation step, not the write step.
3898
+ result.error = `${writtenObjects.length}/${writtenObjects.length} written, batch activation failed${detail}`;
3899
+ }
3900
+ }
3901
+ }
3902
+ // ────────────────────────────────────────────────────────────────────
3273
3903
  const summary = results
3274
- .map((r) => `${r.name} (${r.type}) ${r.status === 'success' ? '✓' : `✗ — ${r.error}`}`)
3904
+ .map((r) => r.status === 'success'
3905
+ ? `${r.name} (${r.type}) ✓ [${r.packageName}]`
3906
+ : `${r.name} (${r.type}) ✗ [${r.packageName}] — ${r.error}`)
3275
3907
  .join(', ');
3276
3908
  const successCount = results.filter((r) => r.status === 'success').length;
3277
3909
  const hasFailure = results.some((r) => r.status === 'failed');
3278
3910
  const warningSuffix = batchWarnings.length > 0 ? `\n\nRAP preflight warnings:\n- ${batchWarnings.join('\n- ')}` : '';
3911
+ const activateAtEndSuffix = terminalActivationFailure !== undefined ? `\n\nBatch activation diagnostics:${terminalActivationFailure}` : '';
3912
+ const packageNames = [...new Set(batchPlan.map((item) => item.packageName))];
3913
+ const packageSummary = packageNames.length === 1
3914
+ ? `in package ${packageNames[0]}`
3915
+ : packageNames.length <= 3
3916
+ ? `across packages [${packageNames.join(', ')}]`
3917
+ : `across ${packageNames.length} packages`;
3918
+ const activateAtEndPrefix = activateAtEnd ? '; activated as a single batch' : '';
3279
3919
  if (hasFailure) {
3280
3920
  const cleanupHint = successCount > 0
3281
3921
  ? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
3282
3922
  : '';
3283
- return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}${warningSuffix}`);
3923
+ return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${cleanupHint}${warningSuffix}${activateAtEndSuffix}`);
3284
3924
  }
3285
- return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}${warningSuffix}`);
3925
+ return textResult(`Batch created ${successCount} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${warningSuffix}${activateAtEndSuffix}`);
3286
3926
  }
3287
3927
  default:
3288
- return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create, scaffold_rap_handlers`);
3928
+ return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create, scaffold_rap_handlers, generate_behavior_implementation`);
3289
3929
  }
3290
3930
  }
3291
3931
  /**
@@ -3348,13 +3988,22 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
3348
3988
  if (!enabled || !source) {
3349
3989
  return { blocked: false };
3350
3990
  }
3351
- // abaplint supports ABAP source (PROG/CLAS/INTF/FUNC/INCL) and CDS views (DDLS) via
3991
+ // abaplint supports ABAP source (PROG/CLAS/INTF/INCL) and CDS views (DDLS) via
3352
3992
  // its CDS parser. DDLS lint catches syntax errors (cds_parser_error) like missing commas,
3353
3993
  // wrong keywords, and invalid DDL constructs. BDEF/SRVD/SRVB/DDLX are silently ignored
3354
3994
  // by abaplint (no parser for those types — garbage passes without errors). TABL (define
3355
3995
  // table syntax) is not supported by the CDS parser and produces false cds_parser_error.
3356
3996
  // For unsupported types, SAP server-side compilation handles validation.
3357
- const LINTABLE_TYPES = new Set(['PROG', 'CLAS', 'INTF', 'FUNC', 'INCL', 'DDLS']);
3997
+ //
3998
+ // FUNC is intentionally excluded: abaplint's FM-source parser does not understand
3999
+ // source-based signatures (`FUNCTION X\n IMPORTING …\n.`) and emits a structural
4000
+ // parser_error that blocks the write. Issue #252 made this visible — once we
4001
+ // started emitting real signatures from structured `parameters`, every FUNC PUT
4002
+ // hit the lint gate. Pre-#252 lint coverage was effectively trivial (only
4003
+ // signature-less FUNCTION/ENDFUNCTION stubs passed). Validation falls back to
4004
+ // SAP's server-side syntax check (opt-in via `SAP_CHECK_BEFORE_WRITE`) and the
4005
+ // activate step.
4006
+ const LINTABLE_TYPES = new Set(['PROG', 'CLAS', 'INTF', 'INCL', 'DDLS']);
3358
4007
  if (!LINTABLE_TYPES.has(type)) {
3359
4008
  return { blocked: false };
3360
4009
  }
@@ -3363,7 +4012,7 @@ function runPreWriteLint(source, type, name, config, perCallOverride) {
3363
4012
  const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
3364
4013
  const configOptions = {
3365
4014
  systemType,
3366
- abapRelease: cachedFeatures?.abapRelease,
4015
+ abapRelease: cachedFeatures?.abapRelease ?? config.abapRelease,
3367
4016
  configFile: config.abaplintConfig,
3368
4017
  };
3369
4018
  const result = validateBeforeWrite(source, filename, configOptions);
@@ -3929,8 +4578,24 @@ async function handleSAPDiagnose(client, args) {
3929
4578
  const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
3930
4579
  return textResult(JSON.stringify(result, null, 2));
3931
4580
  }
4581
+ case 'object_state': {
4582
+ if (!name || !type)
4583
+ return errorResult('"name" and "type" are required for "object_state" action.');
4584
+ const sections = type === 'CLAS'
4585
+ ? [
4586
+ { section: 'main', uri: sourceUrlForType(type, name) },
4587
+ { section: 'definitions', uri: classIncludeUrl(name, 'definitions'), optional: true },
4588
+ { section: 'implementations', uri: classIncludeUrl(name, 'implementations'), optional: true },
4589
+ { section: 'macros', uri: classIncludeUrl(name, 'macros'), optional: true },
4590
+ { section: 'testclasses', uri: classIncludeUrl(name, 'testclasses'), optional: true },
4591
+ ]
4592
+ : [{ section: 'main', uri: sourceUrlForType(type, name) }];
4593
+ const result = await getObjectState(client.http, client.safety, { type, name, sections });
4594
+ return textResult(JSON.stringify(result, null, 2));
4595
+ }
3932
4596
  case 'quickfix': {
3933
4597
  const source = args.source;
4598
+ const sourceUri = args.sourceUri;
3934
4599
  if (!name || !type)
3935
4600
  return errorResult('"name" and "type" are required for "quickfix" action.');
3936
4601
  if (!source)
@@ -3943,13 +4608,15 @@ async function handleSAPDiagnose(client, args) {
3943
4608
  return errorResult('"line" must be a number for "quickfix" action.');
3944
4609
  if (!Number.isFinite(column))
3945
4610
  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);
4611
+ const proposals = await getFixProposals(client.http, client.safety, sourceUri ?? sourceUrlForType(type, name), source, line, column);
3947
4612
  return textResult(JSON.stringify(proposals, null, 2));
3948
4613
  }
3949
4614
  case 'apply_quickfix': {
3950
4615
  const source = args.source;
4616
+ const sourceUri = args.sourceUri;
3951
4617
  const proposalUri = args.proposalUri;
3952
4618
  const proposalUserContent = args.proposalUserContent;
4619
+ const proposalAffectedObjects = args.proposalAffectedObjects;
3953
4620
  if (!name || !type)
3954
4621
  return errorResult('"name" and "type" are required for "apply_quickfix" action.');
3955
4622
  if (!source)
@@ -3958,7 +4625,7 @@ async function handleSAPDiagnose(client, args) {
3958
4625
  return errorResult('"line" is required for "apply_quickfix" action.');
3959
4626
  if (!proposalUri)
3960
4627
  return errorResult('"proposalUri" is required for "apply_quickfix" action.');
3961
- if (!proposalUserContent)
4628
+ if (proposalUserContent === undefined)
3962
4629
  return errorResult('"proposalUserContent" is required for "apply_quickfix" action.');
3963
4630
  const line = Number(args.line);
3964
4631
  const column = Number(args.column ?? 0);
@@ -3972,7 +4639,8 @@ async function handleSAPDiagnose(client, args) {
3972
4639
  name: '',
3973
4640
  description: '',
3974
4641
  userContent: proposalUserContent,
3975
- }, sourceUrlForType(type, name), source, line, column);
4642
+ ...(proposalAffectedObjects ? { affectedObjects: proposalAffectedObjects } : {}),
4643
+ }, sourceUri ?? sourceUrlForType(type, name), source, line, column);
3976
4644
  return textResult(JSON.stringify(deltas, null, 2));
3977
4645
  }
3978
4646
  case 'dumps': {
@@ -4061,7 +4729,7 @@ async function handleSAPDiagnose(client, args) {
4061
4729
  return textResult(JSON.stringify(errors, null, 2));
4062
4730
  }
4063
4731
  default:
4064
- return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, quickfix, apply_quickfix, dumps, traces, system_messages, gateway_errors`);
4732
+ return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, object_state, quickfix, apply_quickfix, dumps, traces, system_messages, gateway_errors`);
4065
4733
  }
4066
4734
  }
4067
4735
  function selectDumpSections(detail, requestedSections) {