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,859 @@
1
+ /**
2
+ * Write + pre-write-validation helpers.
3
+ *
4
+ * Content-type negotiation, DDIC metadata write properties, buildCreateXml, the pre-write
5
+ * lint/syntax/RAP-preflight gates, package enforcement, and the server-driven-object write engine.
6
+ * Shared by the SAPWrite and SAPLint handlers.
7
+ */
8
+ import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildServiceBindingXml, normalizeAdtLanguage, normalizeAdtResponsible, } from '../adt/ddic-xml.js';
9
+ import { syntaxCheck } from '../adt/devtools.js';
10
+ import { AdtSafetyError } from '../adt/errors.js';
11
+ import { formatRapPreflightFindings, validateRapSource } from '../adt/rap-preflight.js';
12
+ import { checkPackage } from '../adt/safety.js';
13
+ import { createServerDrivenObject, deleteServerDrivenObject, serverDrivenBlueContentType, serverDrivenObjectUrl, supportsServerDrivenObject, updateServerDrivenObjectSource, } from '../adt/server-driven.js';
14
+ import { escapeXmlAttr } from '../adt/xml-parser.js';
15
+ import { detectFilename, validateBeforeWrite } from '../lint/lint.js';
16
+ import { invalidateInactiveList } from './cache-security.js';
17
+ import { cachedFeatures } from './feature-cache.js';
18
+ import { canonicalTablType, objectUrlForType } from './object-types.js';
19
+ import { errorResult, textResult } from './shared.js';
20
+ /**
21
+ * Build LintConfigOptions from server config and cached features.
22
+ *
23
+ * Uses cachedFeatures (from SAPManage probe) when available, but falls back
24
+ * to config.systemType so that --system-type btp works even before the first
25
+ * probe. Without this fallback, cloud lint rules wouldn't apply until a probe
26
+ * populates cachedFeatures.
27
+ */
28
+ export function buildLintConfigOptions(config, ruleOverrides) {
29
+ // Probe-detected system type is most accurate; fall back to CLI config
30
+ const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
31
+ return {
32
+ systemType,
33
+ abapRelease: cachedFeatures?.abapRelease ?? config.abapRelease,
34
+ configFile: config.abaplintConfig,
35
+ ruleOverrides,
36
+ };
37
+ }
38
+ // ─── Object Creation XML ─────────────────────────────────────────────
39
+ export const DOMAIN_V2_CONTENT_TYPE = 'application/vnd.sap.adt.domains.v2+xml; charset=utf-8';
40
+ export const DATAELEMENT_V2_CONTENT_TYPE = 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8';
41
+ export const SERVICEBINDING_V2_CONTENT_TYPE = 'application/vnd.sap.adt.businessservices.servicebinding.v2+xml; charset=utf-8';
42
+ // Accept variant WITHOUT media-type parameters. On-prem 758 rejects an Accept that carries
43
+ // "; charset=utf-8" on the bindings resource with 406 SADT_RESOURCE 037 ("The message content
44
+ // is not acceptable", no accepted-types list — so the generic negotiation retry cannot infer a
45
+ // fallback either). Live-verified on S/4HANA 2023: bare type → 200, charset-suffixed → 406.
46
+ // Use this for reads (the publish/unpublish package gate); keep the charset form for PUT/POST
47
+ // Content-Type only.
48
+ export const SERVICEBINDING_V2_ACCEPT = 'application/vnd.sap.adt.businessservices.servicebinding.v2+xml';
49
+ const BDEF_CONTENT_TYPE = 'application/vnd.sap.adt.blues.v1+xml';
50
+ const MESSAGECLASS_CONTENT_TYPE = 'application/vnd.sap.adt.mc.messageclass+xml';
51
+ export const SKTD_V2_CONTENT_TYPE = 'application/vnd.sap.adt.sktdv2+xml';
52
+ // Function group + function module content types — verified live on a4h S/4HANA 2023
53
+ // (issue #250). FUGR uses the v3 group envelope; FUNC uses the unversioned fmodule envelope.
54
+ const FUNCTION_GROUP_CONTENT_TYPE = 'application/vnd.sap.adt.functions.groups.v3+xml';
55
+ const FUNCTION_MODULE_CONTENT_TYPE = 'application/vnd.sap.adt.functions.fmodules+xml';
56
+ export function isMetadataWriteType(type) {
57
+ return type === 'DOMA' || type === 'DTEL' || type === 'MSAG' || type === 'SRVB';
58
+ }
59
+ /** Types that require a specific vendor content type for creation (not application/*) */
60
+ function needsVendorContentType(type) {
61
+ return (type === 'DOMA' ||
62
+ type === 'DTEL' ||
63
+ type === 'BDEF' ||
64
+ type === 'MSAG' ||
65
+ type === 'SKTD' ||
66
+ type === 'FUGR' ||
67
+ type === 'FUNC');
68
+ }
69
+ /** Content type used for create POST */
70
+ export function createContentTypeForType(type) {
71
+ // SRVB creation works with wildcard content type; updates use vendor v2 type.
72
+ if (type === 'SRVB')
73
+ return 'application/*';
74
+ return needsVendorContentType(type) ? vendorContentTypeForType(type) : 'application/*';
75
+ }
76
+ /**
77
+ * Check if a DTEL create has properties that SAP ignores on POST but accepts on PUT.
78
+ * SAP's DTEL POST only stores the shell (name, description, package, typeKind, typeName, dataType, length).
79
+ * Labels, searchHelp, setGetParameter, etc. require a follow-up PUT to take effect.
80
+ */
81
+ export function dtelNeedsPostCreateUpdate(props) {
82
+ return Boolean(props.shortLabel ||
83
+ props.mediumLabel ||
84
+ props.longLabel ||
85
+ props.headingLabel ||
86
+ props.searchHelp ||
87
+ props.searchHelpParameter ||
88
+ props.setGetParameter ||
89
+ props.defaultComponentName ||
90
+ props.changeDocument);
91
+ }
92
+ export function vendorContentTypeForType(type) {
93
+ switch (type) {
94
+ case 'DOMA':
95
+ return DOMAIN_V2_CONTENT_TYPE;
96
+ case 'DTEL':
97
+ return DATAELEMENT_V2_CONTENT_TYPE;
98
+ case 'SRVB':
99
+ return SERVICEBINDING_V2_CONTENT_TYPE;
100
+ case 'BDEF':
101
+ return BDEF_CONTENT_TYPE;
102
+ case 'MSAG':
103
+ return MESSAGECLASS_CONTENT_TYPE;
104
+ case 'SKTD':
105
+ return SKTD_V2_CONTENT_TYPE;
106
+ case 'FUGR':
107
+ return FUNCTION_GROUP_CONTENT_TYPE;
108
+ case 'FUNC':
109
+ return FUNCTION_MODULE_CONTENT_TYPE;
110
+ default:
111
+ // Wildcard lets the SAP server resolve the correct handler.
112
+ // Sending 'application/xml' causes 415 on DDL-based endpoints
113
+ // (DDLS, SRVD, DDLX) whose resource classes reject that literal type.
114
+ return 'application/*';
115
+ }
116
+ }
117
+ function toBoolean(value) {
118
+ if (typeof value === 'boolean')
119
+ return value;
120
+ if (typeof value === 'number')
121
+ return value !== 0;
122
+ if (typeof value === 'string') {
123
+ const normalized = value.trim().toLowerCase();
124
+ if (normalized === 'true')
125
+ return true;
126
+ if (normalized === 'false')
127
+ return false;
128
+ }
129
+ return undefined;
130
+ }
131
+ export function getMetadataWriteProperties(input) {
132
+ const props = {
133
+ dataType: input.dataType,
134
+ length: input.length,
135
+ decimals: input.decimals,
136
+ outputLength: input.outputLength,
137
+ conversionExit: input.conversionExit,
138
+ signExists: input.signExists,
139
+ lowercase: input.lowercase,
140
+ fixedValues: input.fixedValues,
141
+ valueTable: input.valueTable,
142
+ typeKind: input.typeKind,
143
+ typeName: input.typeName,
144
+ domainName: input.domainName,
145
+ shortLabel: input.shortLabel,
146
+ mediumLabel: input.mediumLabel,
147
+ longLabel: input.longLabel,
148
+ headingLabel: input.headingLabel,
149
+ searchHelp: input.searchHelp,
150
+ searchHelpParameter: input.searchHelpParameter,
151
+ setGetParameter: input.setGetParameter,
152
+ defaultComponentName: input.defaultComponentName,
153
+ changeDocument: input.changeDocument,
154
+ messages: input.messages,
155
+ serviceDefinition: input.serviceDefinition,
156
+ bindingType: input.bindingType,
157
+ category: input.category,
158
+ version: input.version,
159
+ odataVersion: input.odataVersion,
160
+ // Function-module create needs the parent function-group name for the
161
+ // <adtcore:containerRef> in the create payload (issue #250).
162
+ group: input.group,
163
+ };
164
+ return props;
165
+ }
166
+ /**
167
+ * Fetch existing DDIC metadata and merge with provided properties.
168
+ * This ensures that updating a single field (e.g., shortLabel) doesn't
169
+ * reset other fields (e.g., dataType, typeKind) to defaults, since
170
+ * DDIC updates are full-XML-replace operations.
171
+ *
172
+ * Internal _description and _package fields carry the existing values
173
+ * for the caller to use as fallbacks.
174
+ */
175
+ function normalizeSrvbCategory(value) {
176
+ if (value === '0' || value === 0 || value === 'UI')
177
+ return '0';
178
+ if (value === '1' || value === 1 || value === 'Web API')
179
+ return '1';
180
+ return undefined;
181
+ }
182
+ export async function mergeMetadataWriteProperties(client, type, name, provided) {
183
+ try {
184
+ if (type === 'MSAG') {
185
+ const existing = await client.getMessageClassInfo(name);
186
+ return {
187
+ _description: existing.description,
188
+ _package: existing.package,
189
+ messages: provided.messages ?? existing.messages,
190
+ };
191
+ }
192
+ if (type === 'DOMA') {
193
+ const existing = await client.getDomain(name);
194
+ return {
195
+ _description: existing.description,
196
+ _package: existing.package,
197
+ dataType: provided.dataType ?? existing.dataType,
198
+ length: provided.length ?? existing.length,
199
+ decimals: provided.decimals ?? existing.decimals,
200
+ outputLength: provided.outputLength ?? existing.outputLength,
201
+ conversionExit: provided.conversionExit ?? existing.conversionExit,
202
+ signExists: provided.signExists ?? existing.signExists,
203
+ lowercase: provided.lowercase ?? existing.lowercase,
204
+ fixedValues: provided.fixedValues ?? existing.fixedValues,
205
+ valueTable: provided.valueTable ?? existing.valueTable,
206
+ };
207
+ }
208
+ if (type === 'DTEL') {
209
+ const existing = await client.getDataElement(name);
210
+ return {
211
+ _description: existing.description,
212
+ _package: existing.package,
213
+ dataType: provided.dataType ?? existing.dataType,
214
+ length: provided.length ?? existing.length,
215
+ decimals: provided.decimals ?? existing.decimals,
216
+ typeKind: provided.typeKind ?? existing.typeKind,
217
+ typeName: provided.typeName ?? existing.typeName,
218
+ domainName: provided.domainName ?? existing.typeName, // DTEL stores domain in typeName
219
+ shortLabel: provided.shortLabel ?? existing.shortLabel,
220
+ mediumLabel: provided.mediumLabel ?? existing.mediumLabel,
221
+ longLabel: provided.longLabel ?? existing.longLabel,
222
+ headingLabel: provided.headingLabel ?? existing.headingLabel,
223
+ searchHelp: provided.searchHelp ?? existing.searchHelp,
224
+ searchHelpParameter: provided.searchHelpParameter,
225
+ setGetParameter: provided.setGetParameter,
226
+ defaultComponentName: provided.defaultComponentName ?? existing.defaultComponentName,
227
+ changeDocument: provided.changeDocument,
228
+ };
229
+ }
230
+ if (type === 'SRVB') {
231
+ const { source: existingRaw } = await client.getSrvb(name);
232
+ const existing = JSON.parse(existingRaw);
233
+ return {
234
+ _description: existing.description,
235
+ _package: existing.package,
236
+ serviceDefinition: provided.serviceDefinition ?? existing.serviceDefinition,
237
+ bindingType: provided.bindingType ?? existing.bindingType,
238
+ category: provided.category ?? normalizeSrvbCategory(existing.bindingCategory),
239
+ version: provided.version ?? existing.serviceVersion,
240
+ odataVersion: provided.odataVersion ?? existing.odataVersion,
241
+ };
242
+ }
243
+ }
244
+ catch {
245
+ // If we can't read existing metadata (e.g., object is new/inactive), fall through
246
+ }
247
+ return provided;
248
+ }
249
+ /**
250
+ * Build the type-specific XML body for ADT object creation.
251
+ *
252
+ * SAP ADT requires each object type to have its own root XML element.
253
+ * Using a generic body (e.g. adtcore:objectReferences) returns 400:
254
+ * "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
255
+ */
256
+ export function buildCreateXml(type, name, pkg, description, properties, language, responsible) {
257
+ // Master/original language for the created object. Derived from the configured
258
+ // SAP_LANGUAGE (passed by callers as config.language) so the create-XML body
259
+ // matches the sap-language URL param ARC-1 already sends. Defaults to "EN" when
260
+ // unset, preserving legacy output. See issue #343.
261
+ const masterLanguage = normalizeAdtLanguage(language);
262
+ // Person responsible for the created object. Derived from the configured logon
263
+ // user (passed by callers as config.username). The legacy hard-coded "DEVELOPER"
264
+ // only exists on SAP demo systems, so on a real system it fails with
265
+ // 400 [?/049] "Enter a valid user, not DEVELOPER, as the person responsible".
266
+ // Defaults to "DEVELOPER" only when no user is configured. Same threading as #343.
267
+ const responsibleUser = normalizeAdtResponsible(responsible);
268
+ switch (type) {
269
+ case 'PROG':
270
+ return `<?xml version="1.0" encoding="UTF-8"?>
271
+ <program:abapProgram xmlns:program="http://www.sap.com/adt/programs/programs"
272
+ xmlns:adtcore="http://www.sap.com/adt/core"
273
+ adtcore:description="${escapeXmlAttr(description)}"
274
+ adtcore:name="${escapeXmlAttr(name)}"
275
+ adtcore:type="PROG/P"
276
+ adtcore:masterLanguage="${masterLanguage}"
277
+ adtcore:masterSystem="H00"
278
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
279
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
280
+ </program:abapProgram>`;
281
+ case 'CLAS':
282
+ return `<?xml version="1.0" encoding="UTF-8"?>
283
+ <class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes"
284
+ xmlns:adtcore="http://www.sap.com/adt/core"
285
+ adtcore:description="${escapeXmlAttr(description)}"
286
+ adtcore:name="${escapeXmlAttr(name)}"
287
+ adtcore:type="CLAS/OC"
288
+ adtcore:masterLanguage="${masterLanguage}"
289
+ adtcore:masterSystem="H00"
290
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
291
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
292
+ </class:abapClass>`;
293
+ case 'INTF':
294
+ return `<?xml version="1.0" encoding="UTF-8"?>
295
+ <intf:abapInterface xmlns:intf="http://www.sap.com/adt/oo/interfaces"
296
+ xmlns:adtcore="http://www.sap.com/adt/core"
297
+ adtcore:description="${escapeXmlAttr(description)}"
298
+ adtcore:name="${escapeXmlAttr(name)}"
299
+ adtcore:type="INTF/OI"
300
+ adtcore:masterLanguage="${masterLanguage}"
301
+ adtcore:masterSystem="H00"
302
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
303
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
304
+ </intf:abapInterface>`;
305
+ case 'INCL':
306
+ return `<?xml version="1.0" encoding="UTF-8"?>
307
+ <include:abapInclude xmlns:include="http://www.sap.com/adt/programs/includes"
308
+ xmlns:adtcore="http://www.sap.com/adt/core"
309
+ adtcore:description="${escapeXmlAttr(description)}"
310
+ adtcore:name="${escapeXmlAttr(name)}"
311
+ adtcore:type="PROG/I"
312
+ adtcore:masterLanguage="${masterLanguage}"
313
+ adtcore:masterSystem="H00"
314
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
315
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
316
+ </include:abapInclude>`;
317
+ case 'DDLS':
318
+ return `<?xml version="1.0" encoding="UTF-8"?>
319
+ <ddl:ddlSource xmlns:ddl="http://www.sap.com/adt/ddic/ddlsources"
320
+ xmlns:adtcore="http://www.sap.com/adt/core"
321
+ adtcore:description="${escapeXmlAttr(description)}"
322
+ adtcore:name="${escapeXmlAttr(name)}"
323
+ adtcore:type="DDLS/DF"
324
+ adtcore:masterLanguage="${masterLanguage}"
325
+ adtcore:masterSystem="H00"
326
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
327
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
328
+ </ddl:ddlSource>`;
329
+ case 'DCLS':
330
+ return `<?xml version="1.0" encoding="UTF-8"?>
331
+ <dcl:dclSource xmlns:dcl="http://www.sap.com/adt/acm/dclsources"
332
+ xmlns:adtcore="http://www.sap.com/adt/core"
333
+ adtcore:description="${escapeXmlAttr(description)}"
334
+ adtcore:name="${escapeXmlAttr(name)}"
335
+ adtcore:type="DCLS/DL"
336
+ adtcore:masterLanguage="${masterLanguage}"
337
+ adtcore:masterSystem="H00"
338
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
339
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
340
+ </dcl:dclSource>`;
341
+ case 'TABL':
342
+ case 'TABL/DT':
343
+ case 'TABL/DS': {
344
+ // Bare TABL is the legacy alias for TABL/DT (transparent table). The same
345
+ // <blue:blueSource> envelope works for both subtypes — only adtcore:type
346
+ // and the POST URL differ. See docs/plans/completed/fix-tabl-ds-create-routing.md.
347
+ const adtType = type === 'TABL/DS' ? 'TABL/DS' : 'TABL/DT';
348
+ return `<?xml version="1.0" encoding="UTF-8"?>
349
+ <blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
350
+ xmlns:adtcore="http://www.sap.com/adt/core"
351
+ adtcore:description="${escapeXmlAttr(description)}"
352
+ adtcore:name="${escapeXmlAttr(name)}"
353
+ adtcore:type="${adtType}"
354
+ adtcore:masterLanguage="${masterLanguage}"
355
+ adtcore:masterSystem="H00"
356
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
357
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
358
+ </blue:blueSource>`;
359
+ }
360
+ case 'BDEF':
361
+ // BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
362
+ // Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
363
+ return `<?xml version="1.0" encoding="UTF-8"?>
364
+ <blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
365
+ xmlns:adtcore="http://www.sap.com/adt/core"
366
+ adtcore:description="${escapeXmlAttr(description)}"
367
+ adtcore:name="${escapeXmlAttr(name)}"
368
+ adtcore:type="BDEF/BDO"
369
+ adtcore:masterLanguage="${masterLanguage}"
370
+ adtcore:masterSystem="H00"
371
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
372
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
373
+ </blue:blueSource>`;
374
+ case 'SRVD':
375
+ return `<?xml version="1.0" encoding="UTF-8"?>
376
+ <srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvdsources"
377
+ xmlns:adtcore="http://www.sap.com/adt/core"
378
+ adtcore:description="${escapeXmlAttr(description)}"
379
+ adtcore:name="${escapeXmlAttr(name)}"
380
+ adtcore:type="SRVD/SRV"
381
+ adtcore:masterLanguage="${masterLanguage}"
382
+ adtcore:masterSystem="H00"
383
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}"
384
+ srvd:srvdSourceType="S">
385
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
386
+ </srvd:srvdSource>`;
387
+ case 'SRVB': {
388
+ const serviceDefinition = String(properties?.serviceDefinition ?? '').trim();
389
+ if (!serviceDefinition) {
390
+ throw new Error('SRVB create/update requires "serviceDefinition" (referenced SRVD name).');
391
+ }
392
+ const categoryRaw = properties?.category;
393
+ const category = categoryRaw === '1' || categoryRaw === 1 ? '1' : categoryRaw === '0' || categoryRaw === 0 ? '0' : undefined;
394
+ const params = {
395
+ name,
396
+ description,
397
+ package: pkg,
398
+ serviceDefinition,
399
+ bindingType: properties?.bindingType ? String(properties.bindingType) : undefined,
400
+ category,
401
+ version: properties?.version ? String(properties.version) : undefined,
402
+ odataVersion: properties?.odataVersion ? String(properties.odataVersion) : undefined,
403
+ language: masterLanguage,
404
+ responsible: responsibleUser,
405
+ };
406
+ return buildServiceBindingXml(params);
407
+ }
408
+ case 'DDLX':
409
+ return `<?xml version="1.0" encoding="UTF-8"?>
410
+ <ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlxsources"
411
+ xmlns:adtcore="http://www.sap.com/adt/core"
412
+ adtcore:description="${escapeXmlAttr(description)}"
413
+ adtcore:name="${escapeXmlAttr(name)}"
414
+ adtcore:type="DDLX/EX"
415
+ adtcore:masterLanguage="${masterLanguage}"
416
+ adtcore:masterSystem="H00"
417
+ adtcore:responsible="${escapeXmlAttr(responsibleUser)}">
418
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
419
+ </ddlx:ddlxSource>`;
420
+ case 'DOMA': {
421
+ const fixedValuesRaw = Array.isArray(properties?.fixedValues) ? properties.fixedValues : [];
422
+ const fixedValues = fixedValuesRaw
423
+ .filter((value) => typeof value === 'object' && value !== null)
424
+ .map((value) => ({
425
+ low: String(value.low ?? ''),
426
+ high: value.high === undefined ? undefined : String(value.high),
427
+ description: value.description === undefined ? undefined : String(value.description),
428
+ }));
429
+ const params = {
430
+ name,
431
+ description,
432
+ package: pkg,
433
+ dataType: String(properties?.dataType ?? 'CHAR'),
434
+ length: properties?.length ?? 0,
435
+ decimals: properties?.decimals,
436
+ outputLength: properties?.outputLength,
437
+ conversionExit: properties?.conversionExit ? String(properties.conversionExit) : undefined,
438
+ signExists: toBoolean(properties?.signExists),
439
+ lowercase: toBoolean(properties?.lowercase),
440
+ fixedValues,
441
+ valueTable: properties?.valueTable ? String(properties.valueTable) : undefined,
442
+ language: masterLanguage,
443
+ responsible: responsibleUser,
444
+ };
445
+ return buildDomainXml(params);
446
+ }
447
+ case 'DTEL': {
448
+ const typeKindRaw = String(properties?.typeKind ?? '');
449
+ const typeKind = typeKindRaw === 'domain' || typeKindRaw === 'predefinedAbapType' ? typeKindRaw : undefined;
450
+ const params = {
451
+ name,
452
+ description,
453
+ package: pkg,
454
+ typeKind,
455
+ typeName: properties?.typeName ? String(properties.typeName) : undefined,
456
+ domainName: properties?.domainName ? String(properties.domainName) : undefined,
457
+ dataType: properties?.dataType ? String(properties.dataType) : undefined,
458
+ length: properties?.length,
459
+ decimals: properties?.decimals,
460
+ shortLabel: properties?.shortLabel ? String(properties.shortLabel) : undefined,
461
+ mediumLabel: properties?.mediumLabel ? String(properties.mediumLabel) : undefined,
462
+ longLabel: properties?.longLabel ? String(properties.longLabel) : undefined,
463
+ headingLabel: properties?.headingLabel ? String(properties.headingLabel) : undefined,
464
+ searchHelp: properties?.searchHelp ? String(properties.searchHelp) : undefined,
465
+ searchHelpParameter: properties?.searchHelpParameter ? String(properties.searchHelpParameter) : undefined,
466
+ setGetParameter: properties?.setGetParameter ? String(properties.setGetParameter) : undefined,
467
+ defaultComponentName: properties?.defaultComponentName ? String(properties.defaultComponentName) : undefined,
468
+ changeDocument: toBoolean(properties?.changeDocument),
469
+ language: masterLanguage,
470
+ responsible: responsibleUser,
471
+ };
472
+ return buildDataElementXml(params);
473
+ }
474
+ case 'MSAG': {
475
+ const messagesRaw = Array.isArray(properties?.messages) ? properties.messages : [];
476
+ const messages = messagesRaw
477
+ .filter((m) => typeof m === 'object' && m !== null)
478
+ .map((m) => ({
479
+ number: String(m.number ?? ''),
480
+ shortText: String(m.shortText ?? ''),
481
+ }));
482
+ const params = {
483
+ name,
484
+ description,
485
+ package: pkg,
486
+ messages: messages.length > 0 ? messages : undefined,
487
+ // Thread the configured language into the body (same spirit as #343).
488
+ // Live-verified on a4h 7.58: the MSAG handler keys T100.SPRSL by the
489
+ // BODY adtcore:language — without it the messages are stored under a
490
+ // BLANK language key (texts never resolve at runtime; ATC/SLIN flags
491
+ // every number as missing). The sap-language URL param alone does NOT
492
+ // prevent this.
493
+ language: masterLanguage,
494
+ };
495
+ return buildMessageClassXml(params);
496
+ }
497
+ case 'FUGR':
498
+ // Function group create envelope. POSTed to /sap/bc/adt/functions/groups
499
+ // with Content-Type: application/vnd.sap.adt.functions.groups.v3+xml.
500
+ // Verified live on a4h S/4HANA 2023 (issue #250).
501
+ return `<?xml version="1.0" encoding="UTF-8"?>
502
+ <group:abapFunctionGroup xmlns:group="http://www.sap.com/adt/functions/groups" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${escapeXmlAttr(description)}" adtcore:language="${masterLanguage}" adtcore:name="${escapeXmlAttr(name)}" adtcore:type="FUGR/F" adtcore:masterLanguage="${masterLanguage}">
503
+ <adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
504
+ </group:abapFunctionGroup>`;
505
+ case 'FUNC': {
506
+ // Function module create envelope. POSTed to
507
+ // /sap/bc/adt/functions/groups/{group_lc}/fmodules with
508
+ // Content-Type: application/vnd.sap.adt.functions.fmodules+xml.
509
+ // No <adtcore:packageRef> — FM inherits package from the parent FUGR.
510
+ // adtcore:uri must be lowercase (verified live on a4h).
511
+ const group = String(properties?.group ?? '').trim();
512
+ if (!group) {
513
+ throw new Error('FUNC create requires "group" property — pass it via SAPWrite args (the parent function group must already exist).');
514
+ }
515
+ const groupLc = encodeURIComponent(group.toLowerCase());
516
+ return `<?xml version="1.0" encoding="UTF-8"?>
517
+ <fmodule:abapFunctionModule xmlns:fmodule="http://www.sap.com/adt/functions/fmodules" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${escapeXmlAttr(description)}" adtcore:name="${escapeXmlAttr(name)}" adtcore:type="FUGR/FF">
518
+ <adtcore:containerRef adtcore:name="${escapeXmlAttr(group)}" adtcore:type="FUGR/F" adtcore:uri="/sap/bc/adt/functions/groups/${groupLc}"/>
519
+ </fmodule:abapFunctionModule>`;
520
+ }
521
+ default:
522
+ // Fallback — generic objectReferences using the correct URL for the type
523
+ return `<?xml version="1.0" encoding="UTF-8"?>
524
+ <adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">
525
+ <adtcore:objectReference adtcore:uri="${escapeXmlAttr(objectUrlForType(type, name))}" adtcore:type="${escapeXmlAttr(type)}" adtcore:name="${escapeXmlAttr(name)}" adtcore:packageName="${escapeXmlAttr(pkg)}"/>
526
+ </adtcore:objectReferences>`;
527
+ }
528
+ }
529
+ /**
530
+ * Strip SAPGUI-style function-module parameter comment blocks from an FM source body.
531
+ *
532
+ * SAP rejects PUT-to-source/main with parameter comment blocks (verified live on a4h
533
+ * S/4HANA 2023 — issue #250):
534
+ * HTTP 400 / com.sap.adt.sedi / ExceptionResourceScanDuringSaveFailure
535
+ * "Parameter comment blocks are not allowed" (T100KEY FUNC_ADT028)
536
+ *
537
+ * The signature is metadata, not source. LLMs frequently emit the SAPGUI block out
538
+ * of muscle memory (every released FM ships with one). This helper strips lines whose
539
+ * first non-whitespace tokens are `*"` so the PUT succeeds, and reports back whether
540
+ * stripping occurred so the caller can append a warning to the response.
541
+ *
542
+ * Only `*"…` lines are stripped — single `*` ABAP comments and inline `"` comments
543
+ * are preserved. Exported for unit tests.
544
+ */
545
+ export function stripFmParamCommentBlock(source) {
546
+ const lines = source.split('\n');
547
+ const kept = lines.filter((line) => !/^\s*\*"/.test(line));
548
+ return { source: kept.join('\n'), wasStripped: kept.length !== lines.length };
549
+ }
550
+ // ─── SAPWrite Handler ────────────────────────────────────────────────
551
+ /**
552
+ * Single-object actions whose top-level `name` is an SAP object name and must be
553
+ * uppercase (TADIR convention). `batch_create` is excluded — its names live in
554
+ * the `objects[]` items and are validated per item in the batch_create branch.
555
+ */
556
+ export const NAME_CASE_GUARD_ACTIONS = new Set(['create', 'update', 'edit_method', 'delete']);
557
+ /**
558
+ * Enforce the `allowedPackages` ceiling for an EXISTING object addressed by its
559
+ * ADT object URL. Resolves the object's REAL package from ADT metadata and gates
560
+ * it via `checkPackage`. Fail-closed: if the package can't be determined, refuse
561
+ * the operation rather than passing the gate. No-op (and no HTTP round-trip) when
562
+ * no package restrictions are configured.
563
+ *
564
+ * Shared by every mutating operation that targets an existing object —
565
+ * update/delete/surgery (via `enforcePackageForExistingObject`), activation, and
566
+ * change_package — so they all honor the same package boundary against the
567
+ * object's true package, never a caller-supplied package string.
568
+ */
569
+ export async function enforceAllowedPackageForObjectUrl(client, objectUrl, label, accept) {
570
+ if (client.safety.allowedPackages.length === 0)
571
+ return undefined;
572
+ const pkg = await client.resolveObjectPackage(objectUrl, accept);
573
+ if (!pkg) {
574
+ throw new AdtSafetyError(`${label} blocked: ARC-1 could not determine the object's package from ADT metadata ` +
575
+ `(no adtcore:packageRef/containerRef). Fail-closed because allowedPackages is restricted.`);
576
+ }
577
+ await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
578
+ return pkg;
579
+ }
580
+ /**
581
+ * SAPWrite for server-driven objects (8.16+): create / update-source / delete via the generic AFF
582
+ * blue:blueSource + JSON-source engine. Discovery-gated (clean 8.16 error otherwise), allowWrites-gated
583
+ * (through the engine's checkOperation), and allowedPackages-gated against the REAL package
584
+ * (create gates the caller-supplied package like every create; update/delete resolve the object's true
585
+ * package under the blues Accept). The `source` param carries the AFF JSON — parse-validated before the
586
+ * PUT; ABAP-specific pre-write steps (lint, RAP preflight, CDS guard) do not apply. Create leaves the
587
+ * object inactive — callers follow with SAPActivate (never auto-activated).
588
+ */
589
+ export async function handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer, cacheSecurity) {
590
+ // Discovery gate — mirror handleSAPRead's server-driven branch.
591
+ if (supportsServerDrivenObject(client.http, type) === false) {
592
+ return errorResult(`SAPWrite type=${type} (server-driven object) requires SAP_BASIS 8.16+ (ABAP Platform 2025 / S/4HANA 2025). ` +
593
+ 'This system does not expose this object type.');
594
+ }
595
+ const transport = args.transport;
596
+ const objUrl = serverDrivenObjectUrl(type, name);
597
+ const blueAccept = serverDrivenBlueContentType(type);
598
+ const invalidate = () => {
599
+ cachingLayer?.invalidate(type, name, 'all');
600
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
601
+ };
602
+ // SDO source is AFF JSON (not ABAP) — validate it parses before any PUT.
603
+ const validateSource = () => {
604
+ const src = String(args.source ?? '');
605
+ try {
606
+ JSON.parse(src);
607
+ }
608
+ catch {
609
+ return {
610
+ ok: false,
611
+ result: errorResult(`SAPWrite ${action} for ${type} ${name}: "source" must be valid AFF JSON ` +
612
+ '(e.g. {"formatVersion":"1","header":{"description":"…","originalLanguage":"en"}}).'),
613
+ };
614
+ }
615
+ return { ok: true, json: src };
616
+ };
617
+ const hasSourceArg = typeof args.source === 'string' && args.source.trim() !== '';
618
+ switch (action) {
619
+ case 'create': {
620
+ const pkg = String(args.package ?? '$TMP');
621
+ await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
622
+ const description = String(args.description ?? name);
623
+ await createServerDrivenObject(client.http, client.safety, type, name, {
624
+ package: pkg,
625
+ description,
626
+ transport,
627
+ });
628
+ let wroteSource = false;
629
+ if (hasSourceArg) {
630
+ const v = validateSource();
631
+ if (!v.ok)
632
+ return v.result;
633
+ await updateServerDrivenObjectSource(client.http, client.safety, type, name, v.json, { transport });
634
+ wroteSource = true;
635
+ }
636
+ invalidate();
637
+ return textResult(`Created ${type} ${name} in package ${pkg}${wroteSource ? ' and wrote AFF JSON source' : ''}.\n` +
638
+ `Next step: SAPActivate(type="${type}", name="${name}").`);
639
+ }
640
+ case 'update': {
641
+ if (!hasSourceArg) {
642
+ return errorResult(`SAPWrite update for ${type} ${name} requires "source" (the AFF JSON body).`);
643
+ }
644
+ const v = validateSource();
645
+ if (!v.ok)
646
+ return v.result;
647
+ await enforceAllowedPackageForObjectUrl(client, objUrl, `Operations on ${type} '${name}'`, blueAccept);
648
+ await updateServerDrivenObjectSource(client.http, client.safety, type, name, v.json, { transport });
649
+ invalidate();
650
+ return textResult(`Updated source of ${type} ${name}.\nNext step: SAPActivate(type="${type}", name="${name}").`);
651
+ }
652
+ case 'delete': {
653
+ await enforceAllowedPackageForObjectUrl(client, objUrl, `Operations on ${type} '${name}'`, blueAccept);
654
+ await deleteServerDrivenObject(client.http, client.safety, type, name, { transport });
655
+ invalidate();
656
+ return textResult(`Deleted ${type} ${name}.`);
657
+ }
658
+ default:
659
+ return errorResult(`Action "${action}" is not supported for server-driven object type ${type}. ` +
660
+ 'Supported: create, update, delete (source is AFF JSON) — then SAPActivate to activate.');
661
+ }
662
+ }
663
+ /**
664
+ * Run deterministic RAP preflight checks for non-ABAP RAP artifact types.
665
+ *
666
+ * Unlike lint, this check is intentionally narrow and rule-based. It focuses on
667
+ * known activation churn patterns (TABL curr/quan semantics, BDEF enum/header
668
+ * misuse, DDLX scope/duplicate annotations) and can cover types that offline
669
+ * abaplint does not parse well.
670
+ */
671
+ export function runRapPreflightValidation(source, type, name, features, configSystemType, perCallOverride) {
672
+ const enabled = perCallOverride ?? true;
673
+ if (!enabled || !source) {
674
+ return { blocked: false };
675
+ }
676
+ const systemType = features?.systemType ?? (configSystemType !== 'auto' ? configSystemType : undefined);
677
+ // Canonicalize so validateRapSource's 'TABL' case matches TABL/DT and TABL/DS.
678
+ const result = validateRapSource(canonicalTablType(type), source, {
679
+ systemType,
680
+ abapRelease: features?.abapRelease,
681
+ });
682
+ if (result.errors.length > 0) {
683
+ const details = formatRapPreflightFindings(result.errors);
684
+ return {
685
+ blocked: true,
686
+ result: errorResult(`RAP preflight validation failed for ${type} ${name}. Fix these issues before writing:\n${details}\n\n` +
687
+ 'Set preflightBeforeWrite=false only when you intentionally need to bypass these checks.'),
688
+ };
689
+ }
690
+ if (result.warnings.length > 0) {
691
+ return {
692
+ blocked: false,
693
+ warnings: `RAP preflight warnings:\n${formatRapPreflightFindings(result.warnings)}`,
694
+ };
695
+ }
696
+ return { blocked: false };
697
+ }
698
+ export function mergePreWriteWarnings(...warnings) {
699
+ const parts = warnings.filter((w) => Boolean(w));
700
+ if (parts.length === 0)
701
+ return undefined;
702
+ return parts.join('\n\n');
703
+ }
704
+ /**
705
+ * Run pre-write lint validation on source code.
706
+ *
707
+ * This is a "lint-before-lock" optimization (pattern from vibing-steampunk):
708
+ * by validating locally before acquiring the SAP object lock, we avoid
709
+ * holding locks on objects that would fail validation anyway.
710
+ *
711
+ * Only runs a strict subset of correctness rules (parser_error, cloud_types, etc.)
712
+ * — not style/formatting rules. This prevents false rejections from opinionated
713
+ * style checks while catching genuine errors that would fail server-side anyway.
714
+ *
715
+ * If lint itself throws (e.g., abaplint bug on unusual syntax), we don't block
716
+ * the write — we let the SAP server-side syntax check handle it instead.
717
+ */
718
+ export function runPreWriteLint(source, type, name, config, perCallOverride) {
719
+ // Per-call override takes precedence over server config
720
+ const enabled = perCallOverride ?? config.lintBeforeWrite;
721
+ if (!enabled || !source) {
722
+ return { blocked: false };
723
+ }
724
+ // abaplint supports ABAP source (PROG/CLAS/INTF/INCL) and CDS views (DDLS) via
725
+ // its CDS parser. DDLS lint catches syntax errors (cds_parser_error) like missing commas,
726
+ // wrong keywords, and invalid DDL constructs. BDEF/SRVD/SRVB/DDLX are silently ignored
727
+ // by abaplint (no parser for those types — garbage passes without errors). TABL (define
728
+ // table syntax) is not supported by the CDS parser and produces false cds_parser_error.
729
+ // For unsupported types, SAP server-side compilation handles validation.
730
+ //
731
+ // FUNC is intentionally excluded: abaplint's FM-source parser does not understand
732
+ // source-based signatures (`FUNCTION X\n IMPORTING …\n.`) and emits a structural
733
+ // parser_error that blocks the write. Issue #252 made this visible — once we
734
+ // started emitting real signatures from structured `parameters`, every FUNC PUT
735
+ // hit the lint gate. Pre-#252 lint coverage was effectively trivial (only
736
+ // signature-less FUNCTION/ENDFUNCTION stubs passed). Validation falls back to
737
+ // SAP's server-side syntax check (opt-in via `SAP_CHECK_BEFORE_WRITE`) and the
738
+ // activate step.
739
+ const LINTABLE_TYPES = new Set(['PROG', 'CLAS', 'INTF', 'INCL', 'DDLS']);
740
+ if (!LINTABLE_TYPES.has(type)) {
741
+ return { blocked: false };
742
+ }
743
+ try {
744
+ const filename = detectFilename(source, name);
745
+ // Reuse the single systemType/abapRelease/configFile resolution (avoids drift with SAPLint's
746
+ // own config — a release-ceiling fix applied to one copy only would split lint behavior).
747
+ const configOptions = buildLintConfigOptions(config);
748
+ const result = validateBeforeWrite(source, filename, configOptions);
749
+ if (!result.pass) {
750
+ const errorLines = result.errors.map((e) => ` Line ${e.line}: [${e.rule}] ${e.message}`).join('\n');
751
+ return {
752
+ blocked: true,
753
+ result: errorResult(`Pre-write lint check failed for ${type} ${name}. Fix these errors before writing:\n${errorLines}\n\n` +
754
+ 'Use SAPLint action="lint_and_fix" to auto-fix, or disable with --lint-before-write=false.'),
755
+ };
756
+ }
757
+ if (result.warnings.length > 0) {
758
+ const warningLines = result.warnings.map((w) => ` Line ${w.line}: [${w.rule}] ${w.message}`).join('\n');
759
+ return {
760
+ blocked: false,
761
+ warnings: `Lint warnings:\n${warningLines}`,
762
+ };
763
+ }
764
+ return { blocked: false };
765
+ }
766
+ catch {
767
+ // If lint itself fails, don't block the write
768
+ return { blocked: false };
769
+ }
770
+ }
771
+ /** Types that carry source code that SAP's /checkruns endpoint can meaningfully compile.
772
+ * Metadata-write types (DOMA/DTEL/TABL/MSAG/DEVC/SKTD) have no /source/main artifact. */
773
+ const SYNTAX_CHECKABLE_TYPES = new Set([
774
+ 'PROG',
775
+ 'CLAS',
776
+ 'INTF',
777
+ 'FUNC',
778
+ 'FUGR',
779
+ 'INCL',
780
+ 'DDLS',
781
+ 'DCLS',
782
+ 'DDLX',
783
+ 'BDEF',
784
+ 'SRVD',
785
+ ]);
786
+ /** Pre-write SAP server-side syntax check via /checkruns with inline <chkrun:content>.
787
+ * Sends the proposed source to SAP's compiler without writing. Surfaces errors AND
788
+ * warnings as informational text appended to the write's success message — never
789
+ * blocks the write. Rationale: multi-file edits have inter-object dependencies, so
790
+ * intermediate writes legitimately trip compile errors that resolve once the whole
791
+ * sequence lands. Real blocking is deferred to SAPActivate, which runs after all
792
+ * dependencies are in place. Best-effort: network/endpoint failures return ''. */
793
+ export async function runPreWriteSyntaxCheck(client, type, source, objectUrl, config, perCallOverride) {
794
+ const enabled = perCallOverride ?? config.checkBeforeWrite;
795
+ if (!enabled || !source)
796
+ return '';
797
+ if (!SYNTAX_CHECKABLE_TYPES.has(type.toUpperCase()))
798
+ return '';
799
+ try {
800
+ const result = await syntaxCheck(client.http, client.safety, objectUrl, { content: source, version: 'active' });
801
+ if (result.messages.length === 0)
802
+ return '';
803
+ const errors = result.messages.filter((m) => m.severity === 'error');
804
+ const warnings = result.messages.filter((m) => m.severity === 'warning');
805
+ const parts = [];
806
+ if (errors.length > 0) {
807
+ const lines = errors.map((m) => ` Line ${m.line || '?'}${m.column ? `:${m.column}` : ''}: ${m.text}`).join('\n');
808
+ parts.push(`Server syntax check errors (source was still written — activate to confirm whether these resolve once dependencies are in place):\n${lines}`);
809
+ }
810
+ if (warnings.length > 0) {
811
+ const lines = warnings.map((m) => ` Line ${m.line || '?'}: ${m.text}`).join('\n');
812
+ parts.push(`Server syntax check warnings:\n${lines}`);
813
+ }
814
+ return parts.join('\n\n');
815
+ }
816
+ catch {
817
+ // Best-effort: never let a failing pre-check fail the write.
818
+ return '';
819
+ }
820
+ }
821
+ // ─── Post-save syntax check ───
822
+ const DDIC_POST_SAVE_CHECK_TYPES = new Set(['TABL', 'DDLS', 'DCLS', 'BDEF', 'SRVD', 'SRVB', 'DDLX']);
823
+ /** Run a syntax check on the inactive version and format the errors for appending to an
824
+ * error message. Returns '' on any failure or when no errors are reported. */
825
+ export async function inactiveSyntaxDiagnostic(client, type, name) {
826
+ try {
827
+ const checkResult = await syntaxCheck(client.http, client.safety, objectUrlForType(type, name), {
828
+ version: 'inactive',
829
+ });
830
+ if (!checkResult.hasErrors)
831
+ return '';
832
+ const errors = checkResult.messages.filter((msg) => msg.severity === 'error');
833
+ if (errors.length === 0)
834
+ return '';
835
+ const lines = errors.map((msg) => {
836
+ const prefix = msg.line ? `[line ${msg.line}] ` : '';
837
+ const suffix = msg.uri ? ` (${msg.uri})` : '';
838
+ return `- ${prefix}${msg.text}${suffix}`;
839
+ });
840
+ return `\nServer syntax check (inactive):\n${lines.join('\n')}`;
841
+ }
842
+ catch {
843
+ return '';
844
+ }
845
+ }
846
+ export async function tryPostSaveSyntaxCheck(client, type, name) {
847
+ if (!DDIC_POST_SAVE_CHECK_TYPES.has(canonicalTablType(type.toUpperCase())))
848
+ return '';
849
+ return inactiveSyntaxDiagnostic(client, type, name);
850
+ }
851
+ // Stable hint surfaced when ARC-1 refuses a TABL/DT write because the connected system does not
852
+ // expose /sap/bc/adt/ddic/tables/. Shared by the discovery gate (write.ts prologue) and batch create.
853
+ export const TABL_DT_WRITE_UNAVAILABLE_HINT = 'Transparent table writes via ADT REST are not available on this system ' +
854
+ '(/sap/bc/adt/ddic/tables/ is not exposed — NW 7.50/7.51 ship the DDIC ' +
855
+ 'structures endpoint only; the table editor was added in NW 7.52). ' +
856
+ 'Use SE11 in SAPGUI, or connect ARC-1 to an SAP_BASIS ≥ 7.52 system. ' +
857
+ 'Writing the source via /sap/bc/adt/ddic/structures/ would silently flip ' +
858
+ 'DD02L-TABCLASS to INTTAB and corrupt the table.';
859
+ //# sourceMappingURL=write-helpers.js.map