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