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.
- package/README.md +4 -4
- package/dist/adt/client.d.ts +12 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +56 -1
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +191 -51
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +21 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +72 -0
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/fm-signature.d.ts +77 -0
- package/dist/adt/fm-signature.d.ts.map +1 -0
- package/dist/adt/fm-signature.js +343 -0
- package/dist/adt/fm-signature.js.map +1 -0
- package/dist/adt/http.d.ts +9 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +8 -7
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/rap-generate.d.ts +110 -0
- package/dist/adt/rap-generate.d.ts.map +1 -0
- package/dist/adt/rap-generate.js +262 -0
- package/dist/adt/rap-generate.js.map +1 -0
- package/dist/adt/rap-handlers.d.ts +14 -0
- package/dist/adt/rap-handlers.d.ts.map +1 -1
- package/dist/adt/rap-handlers.js +96 -9
- package/dist/adt/rap-handlers.js.map +1 -1
- package/dist/adt/types.d.ts +73 -1
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +14 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +9 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/context/method-surgery.d.ts +27 -0
- package/dist/context/method-surgery.d.ts.map +1 -1
- package/dist/context/method-surgery.js +104 -7
- package/dist/context/method-surgery.js.map +1 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +562 -68
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +106 -2
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +157 -11
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +144 -32
- package/dist/handlers/tools.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +1 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +1 -1
package/dist/handlers/intent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 —
|
|
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
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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
|
-
|
|
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
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
//
|
|
2860
|
-
|
|
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
|
|
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
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3129
|
-
const
|
|
3130
|
-
const
|
|
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' &&
|
|
3148
|
-
let tr = transportLookupCache.get(
|
|
3597
|
+
if (objType === 'MSAG' && objTransport) {
|
|
3598
|
+
let tr = transportLookupCache.get(objTransport);
|
|
3149
3599
|
if (tr === undefined) {
|
|
3150
|
-
tr = await getTransport(client.http, client.safety,
|
|
3151
|
-
transportLookupCache.set(
|
|
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 "${
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
3749
|
+
return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}: ${summary}${cleanupHint}${warningSuffix}`);
|
|
3284
3750
|
}
|
|
3285
|
-
return textResult(`Batch created ${successCount} objects
|
|
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/
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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) {
|