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