arc-1 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +17 -4
  2. package/dist/adt/class-structure.d.ts +1 -1
  3. package/dist/adt/class-structure.js +1 -1
  4. package/dist/adt/client.d.ts +16 -3
  5. package/dist/adt/client.d.ts.map +1 -1
  6. package/dist/adt/client.js +40 -35
  7. package/dist/adt/client.js.map +1 -1
  8. package/dist/adt/ddic-xml.d.ts +9 -0
  9. package/dist/adt/ddic-xml.d.ts.map +1 -1
  10. package/dist/adt/ddic-xml.js +49 -53
  11. package/dist/adt/ddic-xml.js.map +1 -1
  12. package/dist/adt/devtools.d.ts.map +1 -1
  13. package/dist/adt/devtools.js +46 -15
  14. package/dist/adt/devtools.js.map +1 -1
  15. package/dist/adt/fm-signature.d.ts +1 -1
  16. package/dist/adt/fm-signature.js +1 -1
  17. package/dist/adt/rap-generate.js +1 -1
  18. package/dist/adt/rap-generate.js.map +1 -1
  19. package/dist/adt/rap-handlers.d.ts +1 -1
  20. package/dist/adt/rap-handlers.js +1 -1
  21. package/dist/adt/refactoring.d.ts.map +1 -1
  22. package/dist/adt/refactoring.js +11 -14
  23. package/dist/adt/refactoring.js.map +1 -1
  24. package/dist/adt/server-driven.d.ts +46 -2
  25. package/dist/adt/server-driven.d.ts.map +1 -1
  26. package/dist/adt/server-driven.js +13 -3
  27. package/dist/adt/server-driven.js.map +1 -1
  28. package/dist/adt/xml-parser.d.ts +8 -1
  29. package/dist/adt/xml-parser.d.ts.map +1 -1
  30. package/dist/adt/xml-parser.js +57 -49
  31. package/dist/adt/xml-parser.js.map +1 -1
  32. package/dist/authz/policy.d.ts +1 -1
  33. package/dist/authz/policy.js +1 -1
  34. package/dist/cache/cache.d.ts +1 -0
  35. package/dist/cache/cache.d.ts.map +1 -1
  36. package/dist/cache/cache.js.map +1 -1
  37. package/dist/cache/inactive-list-cache.d.ts +4 -4
  38. package/dist/cache/inactive-list-cache.d.ts.map +1 -1
  39. package/dist/cache/inactive-list-cache.js +19 -12
  40. package/dist/cache/inactive-list-cache.js.map +1 -1
  41. package/dist/cache/memory.d.ts +1 -0
  42. package/dist/cache/memory.d.ts.map +1 -1
  43. package/dist/cache/memory.js +3 -0
  44. package/dist/cache/memory.js.map +1 -1
  45. package/dist/cache/sqlite.d.ts +1 -0
  46. package/dist/cache/sqlite.d.ts.map +1 -1
  47. package/dist/cache/sqlite.js +3 -0
  48. package/dist/cache/sqlite.js.map +1 -1
  49. package/dist/cache/warmup.d.ts.map +1 -1
  50. package/dist/cache/warmup.js +85 -38
  51. package/dist/cache/warmup.js.map +1 -1
  52. package/dist/cli.d.ts +1 -1
  53. package/dist/cli.js +2 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/context/compressor.d.ts.map +1 -1
  56. package/dist/context/compressor.js +22 -13
  57. package/dist/context/compressor.js.map +1 -1
  58. package/dist/context/contract.d.ts.map +1 -1
  59. package/dist/context/contract.js +4 -3
  60. package/dist/context/contract.js.map +1 -1
  61. package/dist/context/deps.d.ts.map +1 -1
  62. package/dist/context/deps.js +3 -2
  63. package/dist/context/deps.js.map +1 -1
  64. package/dist/context/grep.d.ts +3 -1
  65. package/dist/context/grep.d.ts.map +1 -1
  66. package/dist/context/grep.js +83 -1
  67. package/dist/context/grep.js.map +1 -1
  68. package/dist/context/method-surgery.d.ts.map +1 -1
  69. package/dist/context/method-surgery.js +3 -2
  70. package/dist/context/method-surgery.js.map +1 -1
  71. package/dist/handlers/activate.d.ts +25 -0
  72. package/dist/handlers/activate.d.ts.map +1 -0
  73. package/dist/handlers/activate.js +334 -0
  74. package/dist/handlers/activate.js.map +1 -0
  75. package/dist/handlers/cache-security.d.ts +22 -0
  76. package/dist/handlers/cache-security.d.ts.map +1 -0
  77. package/dist/handlers/cache-security.js +51 -0
  78. package/dist/handlers/cache-security.js.map +1 -0
  79. package/dist/handlers/cds-hints.d.ts +26 -0
  80. package/dist/handlers/cds-hints.d.ts.map +1 -0
  81. package/dist/handlers/cds-hints.js +380 -0
  82. package/dist/handlers/cds-hints.js.map +1 -0
  83. package/dist/handlers/context.d.ts +10 -0
  84. package/dist/handlers/context.d.ts.map +1 -0
  85. package/dist/handlers/context.js +344 -0
  86. package/dist/handlers/context.js.map +1 -0
  87. package/dist/handlers/diagnose.d.ts +8 -0
  88. package/dist/handlers/diagnose.d.ts.map +1 -0
  89. package/dist/handlers/diagnose.js +274 -0
  90. package/dist/handlers/diagnose.js.map +1 -0
  91. package/dist/handlers/dispatch.d.ts +39 -0
  92. package/dist/handlers/dispatch.d.ts.map +1 -0
  93. package/dist/handlers/dispatch.js +640 -0
  94. package/dist/handlers/dispatch.js.map +1 -0
  95. package/dist/handlers/feature-cache.d.ts +26 -0
  96. package/dist/handlers/feature-cache.d.ts.map +1 -0
  97. package/dist/handlers/feature-cache.js +45 -0
  98. package/dist/handlers/feature-cache.js.map +1 -0
  99. package/dist/handlers/git.d.ts +9 -0
  100. package/dist/handlers/git.d.ts.map +1 -0
  101. package/dist/handlers/git.js +227 -0
  102. package/dist/handlers/git.js.map +1 -0
  103. package/dist/handlers/lint.d.ts +9 -0
  104. package/dist/handlers/lint.d.ts.map +1 -0
  105. package/dist/handlers/lint.js +82 -0
  106. package/dist/handlers/lint.js.map +1 -0
  107. package/dist/handlers/manage.d.ts +10 -0
  108. package/dist/handlers/manage.d.ts.map +1 -0
  109. package/dist/handlers/manage.js +375 -0
  110. package/dist/handlers/manage.js.map +1 -0
  111. package/dist/handlers/navigate.d.ts +8 -0
  112. package/dist/handlers/navigate.d.ts.map +1 -0
  113. package/dist/handlers/navigate.js +188 -0
  114. package/dist/handlers/navigate.js.map +1 -0
  115. package/dist/handlers/object-types.d.ts +103 -0
  116. package/dist/handlers/object-types.d.ts.map +1 -0
  117. package/dist/handlers/object-types.js +476 -0
  118. package/dist/handlers/object-types.js.map +1 -0
  119. package/dist/handlers/query.d.ts +7 -0
  120. package/dist/handlers/query.d.ts.map +1 -0
  121. package/dist/handlers/query.js +190 -0
  122. package/dist/handlers/query.js.map +1 -0
  123. package/dist/handlers/read.d.ts +18 -0
  124. package/dist/handlers/read.d.ts.map +1 -0
  125. package/dist/handlers/read.js +581 -0
  126. package/dist/handlers/read.js.map +1 -0
  127. package/dist/handlers/schemas.d.ts +28 -26
  128. package/dist/handlers/schemas.d.ts.map +1 -1
  129. package/dist/handlers/schemas.js +17 -153
  130. package/dist/handlers/schemas.js.map +1 -1
  131. package/dist/handlers/search.d.ts +24 -0
  132. package/dist/handlers/search.d.ts.map +1 -0
  133. package/dist/handlers/search.js +208 -0
  134. package/dist/handlers/search.js.map +1 -0
  135. package/dist/handlers/shared.d.ts +19 -0
  136. package/dist/handlers/shared.d.ts.map +1 -0
  137. package/dist/handlers/shared.js +23 -0
  138. package/dist/handlers/shared.js.map +1 -0
  139. package/dist/handlers/tool-registry.d.ts +44 -0
  140. package/dist/handlers/tool-registry.d.ts.map +1 -0
  141. package/dist/handlers/tool-registry.js +152 -0
  142. package/dist/handlers/tool-registry.js.map +1 -0
  143. package/dist/handlers/tools.d.ts +9 -0
  144. package/dist/handlers/tools.d.ts.map +1 -1
  145. package/dist/handlers/tools.js +40 -141
  146. package/dist/handlers/tools.js.map +1 -1
  147. package/dist/handlers/transport.d.ts +8 -0
  148. package/dist/handlers/transport.d.ts.map +1 -0
  149. package/dist/handlers/transport.js +281 -0
  150. package/dist/handlers/transport.js.map +1 -0
  151. package/dist/handlers/write/class-surgery.d.ts +18 -0
  152. package/dist/handlers/write/class-surgery.d.ts.map +1 -0
  153. package/dist/handlers/write/class-surgery.js +366 -0
  154. package/dist/handlers/write/class-surgery.js.map +1 -0
  155. package/dist/handlers/write/context.d.ts +43 -0
  156. package/dist/handlers/write/context.d.ts.map +1 -0
  157. package/dist/handlers/write/context.js +5 -0
  158. package/dist/handlers/write/context.js.map +1 -0
  159. package/dist/handlers/write/create.d.ts +8 -0
  160. package/dist/handlers/write/create.d.ts.map +1 -0
  161. package/dist/handlers/write/create.js +603 -0
  162. package/dist/handlers/write/create.js.map +1 -0
  163. package/dist/handlers/write/rap.d.ts +8 -0
  164. package/dist/handlers/write/rap.d.ts.map +1 -0
  165. package/dist/handlers/write/rap.js +235 -0
  166. package/dist/handlers/write/rap.js.map +1 -0
  167. package/dist/handlers/write/update-delete.d.ts +8 -0
  168. package/dist/handlers/write/update-delete.d.ts.map +1 -0
  169. package/dist/handlers/write/update-delete.js +182 -0
  170. package/dist/handlers/write/update-delete.js.map +1 -0
  171. package/dist/handlers/write-helpers.d.ts +155 -0
  172. package/dist/handlers/write-helpers.d.ts.map +1 -0
  173. package/dist/handlers/write-helpers.js +859 -0
  174. package/dist/handlers/write-helpers.js.map +1 -0
  175. package/dist/handlers/write.d.ts +16 -0
  176. package/dist/handlers/write.d.ts.map +1 -0
  177. package/dist/handlers/write.js +210 -0
  178. package/dist/handlers/write.js.map +1 -0
  179. package/dist/handlers/zod-errors.d.ts +1 -1
  180. package/dist/handlers/zod-errors.js +1 -1
  181. package/dist/lint/abaplint-config-cache.d.ts +5 -0
  182. package/dist/lint/abaplint-config-cache.d.ts.map +1 -0
  183. package/dist/lint/abaplint-config-cache.js +29 -0
  184. package/dist/lint/abaplint-config-cache.js.map +1 -0
  185. package/dist/lint/config-builder.d.ts.map +1 -1
  186. package/dist/lint/config-builder.js +3 -4
  187. package/dist/lint/config-builder.js.map +1 -1
  188. package/dist/lint/lint.d.ts +1 -1
  189. package/dist/lint/lint.d.ts.map +1 -1
  190. package/dist/lint/lint.js +3 -2
  191. package/dist/lint/lint.js.map +1 -1
  192. package/dist/server/config.d.ts.map +1 -1
  193. package/dist/server/config.js +22 -8
  194. package/dist/server/config.js.map +1 -1
  195. package/dist/server/mcp-rate-limit.d.ts +1 -1
  196. package/dist/server/mcp-rate-limit.js +1 -1
  197. package/dist/server/server.d.ts +1 -1
  198. package/dist/server/server.js +3 -2
  199. package/dist/server/server.js.map +1 -1
  200. package/package.json +15 -10
  201. package/dist/handlers/intent.d.ts +0 -144
  202. package/dist/handlers/intent.d.ts.map +0 -1
  203. package/dist/handlers/intent.js +0 -6782
  204. package/dist/handlers/intent.js.map +0 -1
@@ -0,0 +1,603 @@
1
+ /**
2
+ * SAPWrite actions — create + batch_create.
3
+ */
4
+ import { createObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../../adt/crud.js';
5
+ import { normalizeAdtLanguage, rewriteKtdText } from '../../adt/ddic-xml.js';
6
+ import { activate, activateBatch } from '../../adt/devtools.js';
7
+ import { AdtApiError } from '../../adt/errors.js';
8
+ import { spliceFmSignature } from '../../adt/fm-signature.js';
9
+ import { checkPackage } from '../../adt/safety.js';
10
+ import { getTransport, getTransportInfo } from '../../adt/transport.js';
11
+ import { escapeXmlAttr } from '../../adt/xml-parser.js';
12
+ import { validateAffHeader } from '../../aff/validator.js';
13
+ import { logger } from '../../server/logger.js';
14
+ import { buildBatchActivationStatuses, formatBatchActivationStatuses, } from '../activate.js';
15
+ import { invalidateInactiveList } from '../cache-security.js';
16
+ import { guardCdsSyntax } from '../cds-hints.js';
17
+ import { cachedFeatures, isTablesEndpointAvailable } from '../feature-cache.js';
18
+ import { normalizeObjectType, normalizeWriteObjectType, objectBasePath, objectUrlForType, sourceUrlForType, } from '../object-types.js';
19
+ import { errorResult, textResult } from '../shared.js';
20
+ import { buildCreateXml, createContentTypeForType, dtelNeedsPostCreateUpdate, getMetadataWriteProperties, isMetadataWriteType, mergePreWriteWarnings, runPreWriteLint, runRapPreflightValidation, SKTD_V2_CONTENT_TYPE, stripFmParamCommentBlock, TABL_DT_WRITE_UNAVAILABLE_HINT, tryPostSaveSyntaxCheck, vendorContentTypeForType, } from '../write-helpers.js';
21
+ function normalizePackageOverride(rawPackage, fallback) {
22
+ if (rawPackage === undefined || rawPackage === null) {
23
+ return fallback;
24
+ }
25
+ const value = String(rawPackage).trim();
26
+ return value || fallback;
27
+ }
28
+ function normalizeTransportOverride(rawTransport) {
29
+ if (rawTransport === undefined || rawTransport === null) {
30
+ return undefined;
31
+ }
32
+ const value = String(rawTransport).trim();
33
+ return value || undefined;
34
+ }
35
+ export async function writeActionCreate(ctx) {
36
+ const { client, args, config, type, name, source, transport, lintOverride, preflightOverride, objectUrl, srcUrl, invalidateWrittenObject, } = ctx;
37
+ const pkg = String(args.package ?? '$TMP');
38
+ await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
39
+ const description = String(args.description ?? name);
40
+ // Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
41
+ // SAP requires a transport number for objects in transportable packages.
42
+ // Instead of letting SAP return a cryptic error, we detect this early and return
43
+ // an actionable error message guiding the LLM to use SAPTransport first.
44
+ let effectiveTransport = transport;
45
+ if (!transport && pkg.toUpperCase() !== '$TMP') {
46
+ try {
47
+ const transportInfo = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
48
+ if (transportInfo.lockedTransport) {
49
+ // Object is already locked in a transport — use it automatically
50
+ effectiveTransport = transportInfo.lockedTransport;
51
+ }
52
+ else if (!transportInfo.isLocal && transportInfo.recording) {
53
+ // Transport IS required but none provided — return guidance
54
+ const existingList = transportInfo.existingTransports.length > 0
55
+ ? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
56
+ .slice(0, 10)
57
+ .map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
58
+ .join('\n')}`
59
+ : '';
60
+ return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
61
+ `To fix this, either:\n` +
62
+ `1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
63
+ `2. Use SAPTransport(action="create", description="...") to create a new one\n` +
64
+ `3. Then retry SAPWrite(action="create", ..., transport="<transport_id>")` +
65
+ existingList);
66
+ }
67
+ // isLocal=true or recording=false → no transport needed, proceed without one
68
+ }
69
+ catch {
70
+ // If transportInfo check fails (older system, permissions, etc.), proceed without it.
71
+ // SAP will return its own error if a transport is actually needed.
72
+ }
73
+ }
74
+ // MSAG transport-vs-task guard. Some SAP releases silently drop message inserts when
75
+ // given a task number as corrNr — CL_ADT_MESSAGE_CLASS_API=>create() passes corrNr to
76
+ // CTS_WBO_API_INSERT_OBJECTS which only accepts request numbers. The TADIR entry is
77
+ // created but T100/T100A are never written, leaving a phantom MSAG. Confirmed on NW 7.50;
78
+ // unclear whether later releases fixed it, so validate everywhere.
79
+ // Cost: one extra HTTP roundtrip per MSAG create (negligible vs. the data loss risk).
80
+ if (type === 'MSAG' && effectiveTransport) {
81
+ const tr = await getTransport(client.http, client.safety, effectiveTransport);
82
+ if (!tr) {
83
+ return errorResult(`Transport "${effectiveTransport}" is not a valid transport request. ` +
84
+ `MSAG creation requires a transport request number, not a task number. ` +
85
+ `Use SAPTransport(action="get", id="<request>") to verify, or SAPTransport(action="list") to find modifiable requests.`);
86
+ }
87
+ }
88
+ // CDS pre-write validation: reject unsupported syntax early
89
+ const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
90
+ if (cdsGuard)
91
+ return cdsGuard;
92
+ // RAP deterministic preflight validation (before object creation to avoid stubs)
93
+ const preflightWarnings = runRapPreflightValidation(source, type, name, cachedFeatures, config.systemType, preflightOverride);
94
+ if (preflightWarnings.blocked)
95
+ return preflightWarnings.result;
96
+ // AFF header validation (if schema available for this type)
97
+ const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
98
+ if (!affResult.valid) {
99
+ return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
100
+ }
101
+ if (type === 'SKTD') {
102
+ // A KTD is not a standalone object — it documents a parent object (e.g., a DDLS view or a CLAS).
103
+ // The create POST goes to the collection URL with a sktd:docu XML body that references the parent.
104
+ const refType = String(args.refObjectType ?? '');
105
+ if (!refType) {
106
+ return errorResult('"refObjectType" is required for SKTD create — the ADT type+subtype of the parent object being documented (e.g., "DDLS/DF", "CLAS/OC", "PROG/P", "INTF/OI", "BDEF/BDO", "SRVD/SRV").');
107
+ }
108
+ const refName = String(args.refObjectName ?? name);
109
+ // SAP rule: a KTD's own name must equal the parent object's name (one KTD per object).
110
+ // Creating a KTD named differently from its parent fails server-side with a cryptic
111
+ // "Check of condition failed" — fail fast with a clear message instead.
112
+ if (refName.toUpperCase() !== name.toUpperCase()) {
113
+ return errorResult(`SKTD name "${name}" must match refObjectName "${refName}" — a Knowledge Transfer Document inherits the name of the ABAP object it documents (one KTD per object). To document "${refName}", call SAPWrite(action="create", type="SKTD", name="${refName}", refObjectType="${refType}", ...).`);
114
+ }
115
+ const refDescription = String(args.refObjectDescription ?? '');
116
+ // Build the parent URI. ADT URIs use lowercase names by convention (matches the Eclipse trace).
117
+ const refParentType = refType.split('/')[0] ?? '';
118
+ const refUri = `${objectBasePath(refParentType)}${encodeURIComponent(refName.toLowerCase())}`;
119
+ const ktdLang = normalizeAdtLanguage(config.language);
120
+ const ktdBody = `<?xml version="1.0" encoding="UTF-8"?>
121
+ <sktd:docu xmlns:sktd="http://www.sap.com/wbobj/texts/sktd" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:language="${ktdLang}" adtcore:name="${escapeXmlAttr(name)}" adtcore:type="SKTD/TYP" adtcore:masterLanguage="${ktdLang}">
122
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
123
+ <sktd:refObject adtcore:description="${escapeXmlAttr(refDescription)}" adtcore:name="${escapeXmlAttr(refName)}" adtcore:type="${escapeXmlAttr(refType)}" adtcore:uri="${escapeXmlAttr(refUri)}"/>
124
+ </sktd:docu>`;
125
+ const ktdCreateUrl = '/sap/bc/adt/documentation/ktd/documents';
126
+ const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport, undefined, cachedFeatures?.abapRelease);
127
+ // If initial Markdown was provided, follow up with an update PUT to write it.
128
+ // Same envelope contract as the update path: fetch-then-rewrite ensures we
129
+ // PUT back exactly the shape SAP gave us (with all the server-assigned
130
+ // metadata), only swapping <sktd:text>.
131
+ if (source) {
132
+ const { source: currentEnvelope } = await client.getKtd(name);
133
+ const body = rewriteKtdText(currentEnvelope, source);
134
+ await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport, cachedFeatures?.abapRelease);
135
+ invalidateWrittenObject(type, name);
136
+ return textResult(`Created SKTD ${name} in package ${pkg} and wrote Markdown content.\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
137
+ }
138
+ invalidateWrittenObject();
139
+ return textResult(`Created SKTD ${name} in package ${pkg} (no Markdown content written — pass "source" to write the body).\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
140
+ }
141
+ // Build type-specific creation XML body.
142
+ // SAP ADT requires the root element to match the object type —
143
+ // a generic objectReferences body returns 400 "System expected the element ...".
144
+ const metadataProperties = getMetadataWriteProperties(args);
145
+ const body = buildCreateXml(type, name, pkg, description, metadataProperties, config.language, config.username);
146
+ // Step 1: Create the object (metadata only)
147
+ const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
148
+ // DOMA/DTEL/BDEF require vendor-specific content types; all other types use
149
+ // 'application/*' — the wildcard lets the SAP server resolve the correct
150
+ // handler (matching how ADT Eclipse and abap-adt-api send requests).
151
+ const contentType = createContentTypeForType(type);
152
+ const needsPackageParam = type === 'BDEF' || type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS';
153
+ let result;
154
+ try {
155
+ result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
156
+ }
157
+ catch (createErr) {
158
+ if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
159
+ const syntaxDetail = await tryPostSaveSyntaxCheck(client, type, name);
160
+ if (syntaxDetail) {
161
+ createErr.message += syntaxDetail;
162
+ }
163
+ }
164
+ throw createErr;
165
+ }
166
+ if (isMetadataWriteType(type)) {
167
+ // SAP's DTEL POST ignores labels, searchHelp, etc. — they require a follow-up PUT.
168
+ // Use withStatefulSession directly (not safeUpdateObject) to keep the lock cycle
169
+ // on the main client's session, avoiding lock contention with subsequent operations.
170
+ if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
171
+ const ct = vendorContentTypeForType(type);
172
+ await client.http.withStatefulSession(async (session) => {
173
+ const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
174
+ const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
175
+ try {
176
+ await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
177
+ }
178
+ finally {
179
+ await unlockObject(session, objectUrl, lock.lockHandle);
180
+ }
181
+ });
182
+ }
183
+ // MSAG: POST creates empty container — follow-up PUT to write messages
184
+ if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
185
+ const ct = vendorContentTypeForType(type);
186
+ await client.http.withStatefulSession(async (session) => {
187
+ const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
188
+ const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
189
+ try {
190
+ await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
191
+ }
192
+ finally {
193
+ await unlockObject(session, objectUrl, lock.lockHandle);
194
+ }
195
+ });
196
+ }
197
+ invalidateWrittenObject();
198
+ const followUpHint = type === 'SRVB'
199
+ ? `\n\nNext steps:\n1. SAPActivate(type="SRVB", name="${name}")\n2. SAPActivate(action="publish_srvb", name="${name}")`
200
+ : '';
201
+ return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
202
+ }
203
+ // Step 2: Write source code if provided.
204
+ // Issue #252: FUNC create accepts a structured `parameters` array; if
205
+ // provided we must follow up with a source PUT even when `source` is
206
+ // omitted (the array alone synthesizes a minimal FUNCTION/ENDFUNCTION
207
+ // body containing the signature clause).
208
+ const funcParameters = type === 'FUNC' ? args.parameters : undefined;
209
+ const shouldWriteSource = !!source || (funcParameters !== undefined && funcParameters.length > 0);
210
+ if (shouldWriteSource) {
211
+ // FUNC: build/splice the signature, then strip SAPGUI parameter comment
212
+ // blocks as defense-in-depth (see update path for rationale).
213
+ let createSource = source ?? '';
214
+ let fmParamStripWarning;
215
+ let fmParamMergeWarning;
216
+ if (type === 'FUNC') {
217
+ if (funcParameters !== undefined) {
218
+ let baseSource;
219
+ if (!createSource || createSource.trim() === '') {
220
+ baseSource = `FUNCTION ${name}.\nENDFUNCTION.\n`;
221
+ }
222
+ else if (!/^\s*FUNCTION\s+/i.test(createSource)) {
223
+ // Body-only source — wrap so the splicer has a signature region.
224
+ baseSource = `FUNCTION ${name}.\n${createSource}\nENDFUNCTION.\n`;
225
+ }
226
+ else {
227
+ baseSource = createSource;
228
+ }
229
+ try {
230
+ createSource = spliceFmSignature(baseSource, name, funcParameters);
231
+ }
232
+ catch {
233
+ createSource = baseSource;
234
+ fmParamMergeWarning =
235
+ 'Could not splice structured parameters: source did not start with FUNCTION keyword. Used the supplied source verbatim.';
236
+ }
237
+ }
238
+ const stripped = stripFmParamCommentBlock(createSource);
239
+ createSource = stripped.source;
240
+ if (stripped.wasStripped) {
241
+ fmParamStripWarning =
242
+ 'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (pass `parameters` as a structured array instead).';
243
+ }
244
+ }
245
+ // Pre-write lint validation
246
+ const lintWarnings = runPreWriteLint(createSource, type, name, config, lintOverride);
247
+ if (lintWarnings.blocked) {
248
+ return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
249
+ }
250
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, createSource, effectiveTransport, cachedFeatures?.abapRelease);
251
+ invalidateWrittenObject(type, name);
252
+ const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
253
+ const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning, fmParamMergeWarning);
254
+ return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
255
+ }
256
+ return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
257
+ }
258
+ export async function writeActionBatchCreate(ctx) {
259
+ const { client, args, config, cachingLayer, cacheSecurity, transport, lintOverride, preflightOverride, invalidateWrittenObject, } = ctx;
260
+ const objects = args.objects;
261
+ if (!objects || !Array.isArray(objects) || objects.length === 0) {
262
+ return errorResult('"objects" array is required and must be non-empty for batch_create action.');
263
+ }
264
+ // Opt-in deferred-activation: writes every object as an inactive draft first,
265
+ // then issues a single terminal activateBatch over the written subset. Use case:
266
+ // composition-linked DDLS / interdependent RAP graphs where per-object inline
267
+ // activate() can't resolve cross-references to not-yet-active siblings.
268
+ const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
269
+ const defaultPackage = normalizePackageOverride(args.package, '$TMP');
270
+ const batchPlan = objects.map((obj) => {
271
+ const objType = normalizeWriteObjectType(String(obj.type ?? ''));
272
+ const objName = String(obj.name ?? '');
273
+ const objPackage = normalizePackageOverride(obj.package, defaultPackage);
274
+ const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
275
+ return { obj, type: objType, name: objName, packageName: objPackage, explicitTransport };
276
+ });
277
+ // Check every target package before starting any creates.
278
+ // Resolver is shared across the loop so subtree BFS happens once even when
279
+ // many objects target descendants of the same `ZFOO/**` root.
280
+ {
281
+ const resolver = client.getPackageHierarchyResolver();
282
+ for (const pkg of new Set(batchPlan.map((item) => item.packageName))) {
283
+ await checkPackage(client.safety, pkg, resolver);
284
+ }
285
+ }
286
+ // Pre-flight transport check for batch_create (same logic as single create),
287
+ // but keyed by each effective package because objects can override package.
288
+ const autoTransportByPackage = new Map();
289
+ const firstPlanNeedingTransportByPackage = new Map();
290
+ for (const plan of batchPlan) {
291
+ if (!plan.explicitTransport &&
292
+ plan.packageName.toUpperCase() !== '$TMP' &&
293
+ !firstPlanNeedingTransportByPackage.has(plan.packageName)) {
294
+ firstPlanNeedingTransportByPackage.set(plan.packageName, plan);
295
+ }
296
+ }
297
+ for (const [pkg, plan] of firstPlanNeedingTransportByPackage) {
298
+ try {
299
+ const firstUrl = objectUrlForType(plan.type, plan.name);
300
+ const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
301
+ if (transportInfo.lockedTransport) {
302
+ autoTransportByPackage.set(pkg, transportInfo.lockedTransport);
303
+ }
304
+ else if (!transportInfo.isLocal && transportInfo.recording) {
305
+ const existingList = transportInfo.existingTransports.length > 0
306
+ ? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
307
+ .slice(0, 10)
308
+ .map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
309
+ .join('\n')}`
310
+ : '';
311
+ return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
312
+ `To fix this, either:\n` +
313
+ `1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
314
+ `2. Use SAPTransport(action="create", description="...") to create a new one\n` +
315
+ `3. Then retry SAPWrite(action="batch_create", ..., transport="<transport_id>")` +
316
+ existingList);
317
+ }
318
+ }
319
+ catch (err) {
320
+ logger.warn('SAPWrite batch_create transport preflight failed; continuing without auto transport', {
321
+ package: pkg,
322
+ type: plan.type,
323
+ name: plan.name,
324
+ error: err instanceof Error ? err.message : String(err),
325
+ });
326
+ // If transportInfo check fails, proceed — SAP will return its own error if needed.
327
+ }
328
+ }
329
+ const results = [];
330
+ const batchWarnings = [];
331
+ // Per-batch cache for the MSAG transport-vs-task guard. The bug is universal so the
332
+ // guard fires for every MSAG entry, but a batch typically shares one transport — cache
333
+ // the lookup result to avoid one HTTP roundtrip per object.
334
+ const transportLookupCache = new Map();
335
+ // Accumulated objects whose create + source-write phase succeeded — used by the
336
+ // terminal activateBatch when activateAtEnd=true. Order matches the input order.
337
+ const writtenObjects = [];
338
+ for (const plan of batchPlan) {
339
+ const { obj, type: objType, name: objName, packageName: objPackage } = plan;
340
+ const objTransport = plan.explicitTransport ?? autoTransportByPackage.get(objPackage);
341
+ const metadataObject = isMetadataWriteType(objType);
342
+ const objSource = obj.source ? String(obj.source) : undefined;
343
+ const objDescription = String(obj.description ?? objName);
344
+ // Mixed-case object name rejection (matches the create-path check above).
345
+ // Universal SAP convention — TADIR is uppercase on every release.
346
+ // Cheap check first: no HTTP call, fail fast on bad names.
347
+ if (objName && objName !== objName.toUpperCase()) {
348
+ results.push({
349
+ type: objType,
350
+ name: objName,
351
+ packageName: objPackage,
352
+ status: 'failed',
353
+ 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.`,
354
+ });
355
+ break;
356
+ }
357
+ // MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
358
+ if (objType === 'MSAG' && objTransport) {
359
+ let tr = transportLookupCache.get(objTransport);
360
+ if (tr === undefined) {
361
+ tr = await getTransport(client.http, client.safety, objTransport);
362
+ transportLookupCache.set(objTransport, tr);
363
+ }
364
+ if (!tr) {
365
+ results.push({
366
+ type: objType,
367
+ name: objName,
368
+ packageName: objPackage,
369
+ status: 'failed',
370
+ error: `Transport "${objTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
371
+ });
372
+ break;
373
+ }
374
+ }
375
+ // AFF header validation per object (if schema available)
376
+ const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
377
+ if (!affResult.valid) {
378
+ results.push({
379
+ type: objType,
380
+ name: objName,
381
+ packageName: objPackage,
382
+ status: 'failed',
383
+ error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
384
+ });
385
+ break;
386
+ }
387
+ try {
388
+ // Pre-validate source with lint BEFORE creating the object to avoid orphaned objects.
389
+ // Metadata objects (DOMA/DTEL) are XML-only and intentionally skip source lint.
390
+ if (!metadataObject && objSource) {
391
+ const preflightWarnings = runRapPreflightValidation(objSource, objType, objName, cachedFeatures, config.systemType, preflightOverride);
392
+ if (preflightWarnings.blocked) {
393
+ results.push({
394
+ type: objType,
395
+ name: objName,
396
+ packageName: objPackage,
397
+ status: 'failed',
398
+ error: preflightWarnings.result.content[0].text,
399
+ });
400
+ break;
401
+ }
402
+ if (preflightWarnings.warnings) {
403
+ batchWarnings.push(`${objType} ${objName}: ${preflightWarnings.warnings}`);
404
+ }
405
+ const lintWarnings = runPreWriteLint(objSource, objType, objName, config, lintOverride);
406
+ if (lintWarnings.blocked) {
407
+ results.push({
408
+ type: objType,
409
+ name: objName,
410
+ packageName: objPackage,
411
+ status: 'failed',
412
+ error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
413
+ });
414
+ break;
415
+ }
416
+ }
417
+ // Step 1: Create the object (per-entry transparent-table discovery gate;
418
+ // mirrors the single-create site above. TABL/DS skips it — /structures/ always exists.)
419
+ if ((objType === 'TABL' || objType === 'TABL/DT') && isTablesEndpointAvailable() === false) {
420
+ results.push({
421
+ type: objType,
422
+ name: objName,
423
+ packageName: objPackage,
424
+ status: 'failed',
425
+ error: TABL_DT_WRITE_UNAVAILABLE_HINT,
426
+ });
427
+ break;
428
+ }
429
+ const objUrl = objectUrlForType(objType, objName);
430
+ const createUrl = objUrl.replace(/\/[^/]+$/, '');
431
+ const objMetadataProps = getMetadataWriteProperties(obj);
432
+ const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps, config.language, config.username);
433
+ const contentType = createContentTypeForType(objType);
434
+ const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
435
+ try {
436
+ await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
437
+ }
438
+ catch (createErr) {
439
+ if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
440
+ const syntaxDetail = await tryPostSaveSyntaxCheck(client, objType, objName);
441
+ if (syntaxDetail) {
442
+ createErr.message += syntaxDetail;
443
+ }
444
+ }
445
+ throw createErr;
446
+ }
447
+ // Step 1b: DTEL POST ignores labels — follow up with PUT on main session
448
+ if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
449
+ await client.http.withStatefulSession(async (session) => {
450
+ const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
451
+ const lockTransport = objTransport ?? (lock.corrNr || undefined);
452
+ try {
453
+ await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
454
+ }
455
+ finally {
456
+ await unlockObject(session, objUrl, lock.lockHandle);
457
+ }
458
+ });
459
+ }
460
+ // Step 2: Write source if provided
461
+ if (!metadataObject && objSource) {
462
+ const srcUrl = sourceUrlForType(objType, objName);
463
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, objTransport, cachedFeatures?.abapRelease);
464
+ }
465
+ // Resolve the activation URL up front so both the inline path and the
466
+ // deferred terminal-activate path use the same URL. FUNC needs the parent
467
+ // function-group baked into the path (issue #250); objectUrlForType throws
468
+ // for FUNC so we mirror the FUNC-aware resolver from handleSAPActivate. For
469
+ // TABL we keep objUrl (already resolved to /tables/) — DDIC-structure FMs
470
+ // aren't a real concept and the create path doesn't expose one.
471
+ let activationUrl = objUrl;
472
+ if (objType === 'FUNC') {
473
+ let group = String(obj.group ?? args.group ?? '').trim();
474
+ if (!group) {
475
+ const resolved = cachingLayer
476
+ ? await cachingLayer.resolveFuncGroup(client, objName)
477
+ : await client.resolveFunctionGroup(objName);
478
+ if (!resolved) {
479
+ throw new Error(`Cannot resolve function group for FM "${objName}" in batch_create activation step. Provide "group" on the FUNC entry.`);
480
+ }
481
+ group = resolved;
482
+ }
483
+ const groupLc = encodeURIComponent(group.toLowerCase());
484
+ activationUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(objName.toLowerCase())}`;
485
+ }
486
+ if (activateAtEnd) {
487
+ // Step 3 deferred: track this object for the terminal activateBatch call.
488
+ // Cache invalidation also moves to AFTER the terminal activate succeeds —
489
+ // invalidating now would let the next read see a draft we couldn't activate.
490
+ writtenObjects.push({ type: objType, name: objName, url: activationUrl });
491
+ results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
492
+ }
493
+ else {
494
+ // Step 3: Activate the object (inline, default behavior).
495
+ const activationResult = await activate(client.http, client.safety, activationUrl);
496
+ if (!activationResult.success) {
497
+ results.push({
498
+ type: objType,
499
+ name: objName,
500
+ packageName: objPackage,
501
+ status: 'failed',
502
+ error: `activation failed: ${activationResult.messages.join('; ')}`,
503
+ });
504
+ break;
505
+ }
506
+ invalidateWrittenObject(objType, objName);
507
+ results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
508
+ }
509
+ }
510
+ catch (err) {
511
+ results.push({
512
+ type: objType,
513
+ name: objName,
514
+ packageName: objPackage,
515
+ status: 'failed',
516
+ error: err instanceof Error ? err.message : String(err),
517
+ });
518
+ break;
519
+ }
520
+ }
521
+ // Add 'skipped' entries for objects that were never attempted due to early break
522
+ for (let i = results.length; i < objects.length; i++) {
523
+ const skippedPlan = batchPlan[i];
524
+ const skipped = skippedPlan?.obj ?? objects[i];
525
+ results.push({
526
+ type: skippedPlan?.type ?? normalizeObjectType(String(skipped?.type ?? '')),
527
+ name: skippedPlan?.name ?? String(skipped?.name ?? ''),
528
+ packageName: skippedPlan?.packageName ?? normalizePackageOverride(skipped?.package, defaultPackage),
529
+ status: 'failed',
530
+ error: 'skipped — stopped after previous failure',
531
+ });
532
+ }
533
+ // ── Terminal activateBatch (activateAtEnd=true) ─────────────────────
534
+ // After every write-phase succeeded (or broke off early), issue ONE batch
535
+ // activate over the already-written subset. This is the killer feature
536
+ // for composition-linked DDLS and RAP behavior stacks — SAP's activator
537
+ // sees the whole graph in a single POST and resolves cross-references
538
+ // internally, so parent → child siblings activate cleanly.
539
+ let terminalActivationFailure;
540
+ if (activateAtEnd && writtenObjects.length > 0) {
541
+ const activationOutcome = await activateBatch(client.http, client.safety, writtenObjects);
542
+ if (activationOutcome.success) {
543
+ // Defensive: per-object status was already 'success' from the write phase.
544
+ // Cache invalidation moves here so a failed terminal activate doesn't strand
545
+ // a stale 'active' cache entry. Invalidate inactive-lists once for the user.
546
+ for (const o of writtenObjects) {
547
+ cachingLayer?.invalidate(o.type, o.name, 'all');
548
+ }
549
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
550
+ }
551
+ else {
552
+ // Flip every written-but-not-yet-activated entry to 'failed', preserving the
553
+ // "create + source-write succeeded" context. Reuse the existing per-object
554
+ // diagnostic mapper so callers see the activation messages keyed by object name.
555
+ const batchStatuses = buildBatchActivationStatuses(writtenObjects, activationOutcome);
556
+ const statusDetails = formatBatchActivationStatuses(batchStatuses);
557
+ terminalActivationFailure = statusDetails;
558
+ const statusByName = new Map(batchStatuses.map((s) => [`${s.type}\x00${s.name}`, s]));
559
+ for (const result of results) {
560
+ if (result.status !== 'success')
561
+ continue;
562
+ const key = `${result.type}\x00${result.name}`;
563
+ const matched = statusByName.get(key);
564
+ if (!matched)
565
+ continue;
566
+ // Some entries may still report status 'active' if the activator returned
567
+ // success: false but had no per-object error details — keep them as 'success'.
568
+ if (matched.status === 'active')
569
+ continue;
570
+ result.status = 'failed';
571
+ const detail = matched.messages.length > 0 ? ` — ${matched.messages.join('; ')}` : '';
572
+ // Preserve the "create + source-write succeeded" context so the user sees that
573
+ // the failure was specifically the activation step, not the write step.
574
+ result.error = `${writtenObjects.length}/${writtenObjects.length} written, batch activation failed${detail}`;
575
+ }
576
+ }
577
+ }
578
+ // ────────────────────────────────────────────────────────────────────
579
+ const summary = results
580
+ .map((r) => r.status === 'success'
581
+ ? `${r.name} (${r.type}) ✓ [${r.packageName}]`
582
+ : `${r.name} (${r.type}) ✗ [${r.packageName}] — ${r.error}`)
583
+ .join(', ');
584
+ const successCount = results.filter((r) => r.status === 'success').length;
585
+ const hasFailure = results.some((r) => r.status === 'failed');
586
+ const warningSuffix = batchWarnings.length > 0 ? `\n\nRAP preflight warnings:\n- ${batchWarnings.join('\n- ')}` : '';
587
+ const activateAtEndSuffix = terminalActivationFailure !== undefined ? `\n\nBatch activation diagnostics:${terminalActivationFailure}` : '';
588
+ const packageNames = [...new Set(batchPlan.map((item) => item.packageName))];
589
+ const packageSummary = packageNames.length === 1
590
+ ? `in package ${packageNames[0]}`
591
+ : packageNames.length <= 3
592
+ ? `across packages [${packageNames.join(', ')}]`
593
+ : `across ${packageNames.length} packages`;
594
+ const activateAtEndPrefix = activateAtEnd ? '; activated as a single batch' : '';
595
+ if (hasFailure) {
596
+ const cleanupHint = successCount > 0
597
+ ? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
598
+ : '';
599
+ return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${cleanupHint}${warningSuffix}${activateAtEndSuffix}`);
600
+ }
601
+ return textResult(`Batch created ${successCount} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${warningSuffix}${activateAtEndSuffix}`);
602
+ }
603
+ //# sourceMappingURL=create.js.map