arc-1 1.0.2 → 1.1.0

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 (166) hide show
  1. package/bin/arc1-cli.js +3 -7
  2. package/bin/arc1.js +4 -9
  3. package/dist/adt/abapgit.d.ts +10 -5
  4. package/dist/adt/abapgit.d.ts.map +1 -1
  5. package/dist/adt/abapgit.js +357 -76
  6. package/dist/adt/abapgit.js.map +1 -1
  7. package/dist/adt/atc.d.ts +49 -0
  8. package/dist/adt/atc.d.ts.map +1 -0
  9. package/dist/adt/atc.js +294 -0
  10. package/dist/adt/atc.js.map +1 -0
  11. package/dist/adt/aunit-package.d.ts +22 -0
  12. package/dist/adt/aunit-package.d.ts.map +1 -0
  13. package/dist/adt/aunit-package.js +78 -0
  14. package/dist/adt/aunit-package.js.map +1 -0
  15. package/dist/adt/aunit.d.ts +143 -0
  16. package/dist/adt/aunit.d.ts.map +1 -0
  17. package/dist/adt/aunit.js +987 -0
  18. package/dist/adt/aunit.js.map +1 -0
  19. package/dist/adt/client.d.ts +9 -7
  20. package/dist/adt/client.d.ts.map +1 -1
  21. package/dist/adt/client.js +55 -64
  22. package/dist/adt/client.js.map +1 -1
  23. package/dist/adt/config.d.ts +2 -0
  24. package/dist/adt/config.d.ts.map +1 -1
  25. package/dist/adt/config.js +1 -0
  26. package/dist/adt/config.js.map +1 -1
  27. package/dist/adt/devtools.d.ts +8 -30
  28. package/dist/adt/devtools.d.ts.map +1 -1
  29. package/dist/adt/devtools.js +41 -218
  30. package/dist/adt/devtools.js.map +1 -1
  31. package/dist/adt/diagnostics.d.ts +1 -1
  32. package/dist/adt/diagnostics.d.ts.map +1 -1
  33. package/dist/adt/diagnostics.js +12 -12
  34. package/dist/adt/diagnostics.js.map +1 -1
  35. package/dist/adt/errors.d.ts +7 -0
  36. package/dist/adt/errors.d.ts.map +1 -1
  37. package/dist/adt/errors.js +7 -0
  38. package/dist/adt/errors.js.map +1 -1
  39. package/dist/adt/features.d.ts +9 -3
  40. package/dist/adt/features.d.ts.map +1 -1
  41. package/dist/adt/features.js +40 -8
  42. package/dist/adt/features.js.map +1 -1
  43. package/dist/adt/gcts.d.ts +12 -45
  44. package/dist/adt/gcts.d.ts.map +1 -1
  45. package/dist/adt/gcts.js +299 -171
  46. package/dist/adt/gcts.js.map +1 -1
  47. package/dist/adt/http-deadline.d.ts +23 -0
  48. package/dist/adt/http-deadline.d.ts.map +1 -0
  49. package/dist/adt/http-deadline.js +106 -0
  50. package/dist/adt/http-deadline.js.map +1 -0
  51. package/dist/adt/http-wire-body.d.ts +2 -0
  52. package/dist/adt/http-wire-body.d.ts.map +1 -0
  53. package/dist/adt/http-wire-body.js +14 -0
  54. package/dist/adt/http-wire-body.js.map +1 -0
  55. package/dist/adt/http.d.ts +10 -24
  56. package/dist/adt/http.d.ts.map +1 -1
  57. package/dist/adt/http.js +89 -53
  58. package/dist/adt/http.js.map +1 -1
  59. package/dist/adt/path-safety.d.ts +16 -0
  60. package/dist/adt/path-safety.d.ts.map +1 -0
  61. package/dist/adt/path-safety.js +101 -0
  62. package/dist/adt/path-safety.js.map +1 -0
  63. package/dist/adt/semaphore.d.ts +2 -2
  64. package/dist/adt/semaphore.d.ts.map +1 -1
  65. package/dist/adt/semaphore.js +26 -9
  66. package/dist/adt/semaphore.js.map +1 -1
  67. package/dist/adt/source-diff.d.ts +10 -1
  68. package/dist/adt/source-diff.d.ts.map +1 -1
  69. package/dist/adt/source-diff.js +33 -7
  70. package/dist/adt/source-diff.js.map +1 -1
  71. package/dist/adt/text-search.d.ts +25 -0
  72. package/dist/adt/text-search.d.ts.map +1 -0
  73. package/dist/adt/text-search.js +152 -0
  74. package/dist/adt/text-search.js.map +1 -0
  75. package/dist/adt/transport-diff.d.ts +128 -0
  76. package/dist/adt/transport-diff.d.ts.map +1 -0
  77. package/dist/adt/transport-diff.js +524 -0
  78. package/dist/adt/transport-diff.js.map +1 -0
  79. package/dist/adt/transport.d.ts +78 -13
  80. package/dist/adt/transport.d.ts.map +1 -1
  81. package/dist/adt/transport.js +443 -41
  82. package/dist/adt/transport.js.map +1 -1
  83. package/dist/adt/types.d.ts +23 -12
  84. package/dist/adt/types.d.ts.map +1 -1
  85. package/dist/adt/xml-parser.d.ts +1 -8
  86. package/dist/adt/xml-parser.d.ts.map +1 -1
  87. package/dist/adt/xml-parser.js +54 -60
  88. package/dist/adt/xml-parser.js.map +1 -1
  89. package/dist/authz/policy.d.ts.map +1 -1
  90. package/dist/authz/policy.js +5 -2
  91. package/dist/authz/policy.js.map +1 -1
  92. package/dist/cli-args.d.ts +2 -2
  93. package/dist/cli-args.js +2 -2
  94. package/dist/cli-checks.d.ts +56 -0
  95. package/dist/cli-checks.d.ts.map +1 -0
  96. package/dist/cli-checks.js +368 -0
  97. package/dist/cli-checks.js.map +1 -0
  98. package/dist/cli.d.ts +68 -15
  99. package/dist/cli.d.ts.map +1 -1
  100. package/dist/cli.js +677 -297
  101. package/dist/cli.js.map +1 -1
  102. package/dist/handlers/cds-hints.d.ts.map +1 -1
  103. package/dist/handlers/cds-hints.js +1 -10
  104. package/dist/handlers/cds-hints.js.map +1 -1
  105. package/dist/handlers/diagnose.d.ts +4 -0
  106. package/dist/handlers/diagnose.d.ts.map +1 -1
  107. package/dist/handlers/diagnose.js +616 -9
  108. package/dist/handlers/diagnose.js.map +1 -1
  109. package/dist/handlers/dispatch.d.ts.map +1 -1
  110. package/dist/handlers/dispatch.js +40 -1
  111. package/dist/handlers/dispatch.js.map +1 -1
  112. package/dist/handlers/git.d.ts.map +1 -1
  113. package/dist/handlers/git.js +165 -84
  114. package/dist/handlers/git.js.map +1 -1
  115. package/dist/handlers/read.d.ts.map +1 -1
  116. package/dist/handlers/read.js +15 -0
  117. package/dist/handlers/read.js.map +1 -1
  118. package/dist/handlers/schemas.d.ts +15 -2
  119. package/dist/handlers/schemas.d.ts.map +1 -1
  120. package/dist/handlers/schemas.js +75 -6
  121. package/dist/handlers/schemas.js.map +1 -1
  122. package/dist/handlers/search.js +1 -1
  123. package/dist/handlers/search.js.map +1 -1
  124. package/dist/handlers/tools.d.ts.map +1 -1
  125. package/dist/handlers/tools.js +51 -42
  126. package/dist/handlers/tools.js.map +1 -1
  127. package/dist/handlers/transport.d.ts +2 -1
  128. package/dist/handlers/transport.d.ts.map +1 -1
  129. package/dist/handlers/transport.js +128 -27
  130. package/dist/handlers/transport.js.map +1 -1
  131. package/dist/handlers/write/create.d.ts.map +1 -1
  132. package/dist/handlers/write/create.js +11 -0
  133. package/dist/handlers/write/create.js.map +1 -1
  134. package/dist/handlers/write/update-delete.d.ts.map +1 -1
  135. package/dist/handlers/write/update-delete.js +33 -10
  136. package/dist/handlers/write/update-delete.js.map +1 -1
  137. package/dist/handlers/write-helpers.d.ts.map +1 -1
  138. package/dist/handlers/write-helpers.js +3 -2
  139. package/dist/handlers/write-helpers.js.map +1 -1
  140. package/dist/server/audit.d.ts +1 -1
  141. package/dist/server/audit.d.ts.map +1 -1
  142. package/dist/server/audit.js +228 -21
  143. package/dist/server/audit.js.map +1 -1
  144. package/dist/server/config.d.ts +16 -0
  145. package/dist/server/config.d.ts.map +1 -1
  146. package/dist/server/config.js +145 -17
  147. package/dist/server/config.js.map +1 -1
  148. package/dist/server/effective-policy-log.d.ts +2 -1
  149. package/dist/server/effective-policy-log.d.ts.map +1 -1
  150. package/dist/server/effective-policy-log.js +8 -2
  151. package/dist/server/effective-policy-log.js.map +1 -1
  152. package/dist/server/multi-target-runtime.d.ts.map +1 -1
  153. package/dist/server/multi-target-runtime.js +3 -0
  154. package/dist/server/multi-target-runtime.js.map +1 -1
  155. package/dist/server/multi-target-tools.d.ts.map +1 -1
  156. package/dist/server/multi-target-tools.js +5 -0
  157. package/dist/server/multi-target-tools.js.map +1 -1
  158. package/dist/server/server.d.ts +15 -13
  159. package/dist/server/server.d.ts.map +1 -1
  160. package/dist/server/server.js +36 -26
  161. package/dist/server/server.js.map +1 -1
  162. package/dist/server/types.d.ts +2 -0
  163. package/dist/server/types.d.ts.map +1 -1
  164. package/dist/server/types.js +1 -0
  165. package/dist/server/types.js.map +1 -1
  166. package/package.json +13 -13
@@ -2,12 +2,444 @@
2
2
  * SAPDiagnose handler — runtime diagnostics: short dumps (ST22), traces, gateway errors, object
3
3
  * state, ATC, unit tests, CDS test cases.
4
4
  */
5
+ import { AunitIncompleteError, appendAunitJunitDiagnostic, aunitIncompleteToJunit, aunitResultToJunit, DEFAULT_PUBLIC_AUNIT_TIMEOUT_MS, findStaticAbapIncludes, probePublicAunit, reconcileAunitProgramSources, reconcileAunitSourceDeclarations, runPublicAunit, } from '../adt/aunit.js';
6
+ import { resolveAunitPackageSelection, } from '../adt/aunit-package.js';
5
7
  import { applyFixProposal, getAtcSystemDefaultVariant, getCdsTestCases, getFixProposals, listAtcVariants, runAtcCheck, runUnitTests, supportsCdsTestCases, syntaxCheck, } from '../adt/devtools.js';
6
8
  import { createTraceRequest, deleteTraceRequest, getAuthorizationTrace, getCdsCreateStatements, getDump, getGatewayErrorDetail, getObjectState, getSqlTraceDirectory, getSqlTraceState, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraceRequests, listTraces, probeODataPerformance, setSqlTraceState, } from '../adt/diagnostics.js';
7
- import { AdtApiError } from '../adt/errors.js';
9
+ import { AdtApiError, AdtNetworkError } from '../adt/errors.js';
8
10
  import { isBtpSystem } from './feature-cache.js';
9
11
  import { classIncludeUrl, normalizeObjectType, objectUrlForType, sourceUrlForType } from './object-types.js';
10
12
  import { errorResult, textResult, toolJson } from './shared.js';
13
+ const AUNIT_SOURCE_MAX_BLOCKS = 80;
14
+ const AUNIT_SOURCE_MAX_DEPTH = 5;
15
+ const AUNIT_PACKAGE_SOURCE_CONCURRENCY = 8;
16
+ const AUNIT_SOURCE_REQUEST_LIMIT = 500;
17
+ class AunitSourceBudgetError extends Error {
18
+ constructor() {
19
+ super(`The ABAP Unit source audit exceeded its ${AUNIT_SOURCE_REQUEST_LIMIT}-request limit.`);
20
+ this.name = 'AunitSourceBudgetError';
21
+ }
22
+ }
23
+ function reserveAunitSourceRequests(budget, count = 1) {
24
+ if (budget.remainingSourceRequests < count)
25
+ throw new AunitSourceBudgetError();
26
+ budget.remainingSourceRequests -= count;
27
+ return { deadline: budget.deadline };
28
+ }
29
+ function isAunitDeadlineFailure(error) {
30
+ return (error instanceof AdtNetworkError &&
31
+ (error.cause?.name === 'TimeoutError' || /deadline was exceeded|timed out|\btimeout\b/i.test(error.message)));
32
+ }
33
+ function presentAunitSourceBlock(id, result) {
34
+ return {
35
+ id,
36
+ status: 'present',
37
+ source: result.source,
38
+ ...(result.etag ? { etag: result.etag } : {}),
39
+ };
40
+ }
41
+ async function readOptionalAunitClassInclude(client, name, include, budget) {
42
+ try {
43
+ return presentAunitSourceBlock(`CLAS:${name.toUpperCase()}:${include}`, await client.getClassInclude(name, include, {
44
+ ...reserveAunitSourceRequests(budget),
45
+ version: 'active',
46
+ }));
47
+ }
48
+ catch (error) {
49
+ if (!(error instanceof AdtApiError) || error.statusCode !== 404)
50
+ throw error;
51
+ return { id: `CLAS:${name.toUpperCase()}:${include}`, status: 'absent', source: '' };
52
+ }
53
+ }
54
+ async function readAunitSourceTree(client, type, name, budget) {
55
+ let initialBlocks;
56
+ try {
57
+ if (type === 'CLAS') {
58
+ const main = await client.getClass(name, undefined, {
59
+ ...reserveAunitSourceRequests(budget),
60
+ version: 'active',
61
+ });
62
+ const testBlock = await readOptionalAunitClassInclude(client, name, 'testclasses', budget);
63
+ const macrosBlock = await readOptionalAunitClassInclude(client, name, 'macros', budget);
64
+ // Global test classes declare FOR TESTING in main. Production classes normally keep local
65
+ // test declarations and macros in dedicated optional includes; a 404 is a verified absence.
66
+ initialBlocks = [presentAunitSourceBlock(`CLAS:${name.toUpperCase()}:main`, main), testBlock, macrosBlock];
67
+ }
68
+ else if (type === 'PROG') {
69
+ initialBlocks = [
70
+ presentAunitSourceBlock(`PROG:${name.toUpperCase()}:main`, await client.getProgram(name, { ...reserveAunitSourceRequests(budget), version: 'active' })),
71
+ ];
72
+ }
73
+ else if (type === 'FUGR') {
74
+ initialBlocks = [
75
+ presentAunitSourceBlock(`FUGR:${name.toUpperCase()}:main`, await client.getFunctionGroupSource(name, {
76
+ ...reserveAunitSourceRequests(budget),
77
+ version: 'active',
78
+ })),
79
+ ];
80
+ }
81
+ else {
82
+ return {
83
+ source: '',
84
+ complete: false,
85
+ incompleteReason: 'Source-selection verification is supported only for CLAS, PROG, and FUGR test runs.',
86
+ blocks: [],
87
+ };
88
+ }
89
+ }
90
+ catch (error) {
91
+ return {
92
+ source: '',
93
+ complete: false,
94
+ incompleteReason: error instanceof AunitSourceBudgetError
95
+ ? error.message
96
+ : isAunitDeadlineFailure(error)
97
+ ? 'The ABAP Unit evidence deadline expired while reading active source.'
98
+ : 'The source containing ABAP Unit class declarations could not be read from SAP.',
99
+ blocks: [],
100
+ };
101
+ }
102
+ const blocks = [...initialBlocks];
103
+ const seen = new Set();
104
+ let frontier = initialBlocks
105
+ .filter((block) => block.status === 'present')
106
+ .map((block) => ({ source: block.source, depth: 0 }));
107
+ let complete = true;
108
+ let incompleteReason;
109
+ while (frontier.length > 0) {
110
+ const pending = [];
111
+ for (const block of frontier) {
112
+ const includes = findStaticAbapIncludes(block.source);
113
+ if (block.depth >= AUNIT_SOURCE_MAX_DEPTH && includes.some((include) => !seen.has(include))) {
114
+ complete = false;
115
+ incompleteReason = 'The ABAP INCLUDE graph exceeded the source-audit depth limit.';
116
+ continue;
117
+ }
118
+ for (const include of includes) {
119
+ if (seen.has(include))
120
+ continue;
121
+ seen.add(include);
122
+ if (blocks.length + pending.length >= AUNIT_SOURCE_MAX_BLOCKS) {
123
+ complete = false;
124
+ incompleteReason = 'The ABAP INCLUDE graph exceeded the source-audit block limit.';
125
+ continue;
126
+ }
127
+ pending.push({ name: include, depth: block.depth + 1 });
128
+ }
129
+ }
130
+ if (pending.length === 0)
131
+ break;
132
+ const fetched = await Promise.allSettled(pending.map(async ({ name: include }) => client.getInclude(include, { ...reserveAunitSourceRequests(budget), version: 'active' })));
133
+ const next = [];
134
+ for (let index = 0; index < fetched.length; index += 1) {
135
+ const response = fetched[index];
136
+ if (response.status === 'rejected') {
137
+ complete = false;
138
+ incompleteReason =
139
+ response.reason instanceof AunitSourceBudgetError
140
+ ? response.reason.message
141
+ : isAunitDeadlineFailure(response.reason)
142
+ ? 'The ABAP Unit evidence deadline expired while reading static INCLUDE source.'
143
+ : 'One or more static ABAP INCLUDE sources could not be read from SAP.';
144
+ continue;
145
+ }
146
+ blocks.push(presentAunitSourceBlock(`INCL:${pending[index].name}`, response.value));
147
+ next.push({ source: response.value.source, depth: pending[index].depth });
148
+ }
149
+ frontier = next;
150
+ }
151
+ return {
152
+ source: blocks
153
+ .filter((block) => block.status === 'present')
154
+ .map((block) => block.source)
155
+ .join('\n'),
156
+ complete,
157
+ ...(incompleteReason ? { incompleteReason } : {}),
158
+ blocks,
159
+ };
160
+ }
161
+ async function readAunitPackageSourceSnapshot(client, packageName, includeSubpackages, budget, snapshotsRemaining) {
162
+ let selection;
163
+ try {
164
+ selection = await resolveAunitPackageSelection(client.http, client.safety, packageName, includeSubpackages, reserveAunitSourceRequests(budget, 2));
165
+ }
166
+ catch (error) {
167
+ if (!(error instanceof AunitSourceBudgetError) && !isAunitDeadlineFailure(error))
168
+ throw error;
169
+ const incompleteReason = error instanceof AunitSourceBudgetError
170
+ ? error.message
171
+ : 'The ABAP Unit evidence deadline expired while resolving package membership.';
172
+ return {
173
+ selection: {
174
+ packageName: packageName.trim().toUpperCase(),
175
+ includeSubpackages,
176
+ objects: [],
177
+ membership: [],
178
+ complete: false,
179
+ incompleteReason,
180
+ },
181
+ entries: [],
182
+ complete: false,
183
+ incompleteReason,
184
+ };
185
+ }
186
+ const minimumSourceRequests = selection.objects.reduce((total, object) => total + (object.type === 'CLAS' ? 3 : 1), 0);
187
+ const futureSelectionRequests = snapshotsRemaining === 2 ? 2 : 0;
188
+ const minimumRemainingRequests = minimumSourceRequests * snapshotsRemaining + futureSelectionRequests;
189
+ if (!selection.complete || minimumRemainingRequests > budget.remainingSourceRequests) {
190
+ const incompleteReason = selection.incompleteReason ??
191
+ `The package requires at least ${minimumRemainingRequests} additional source-audit requests, exceeding the ${AUNIT_SOURCE_REQUEST_LIMIT}-request limit.`;
192
+ return { selection, entries: [], complete: false, incompleteReason };
193
+ }
194
+ const entries = [];
195
+ for (let offset = 0; offset < selection.objects.length; offset += AUNIT_PACKAGE_SOURCE_CONCURRENCY) {
196
+ const batch = selection.objects.slice(offset, offset + AUNIT_PACKAGE_SOURCE_CONCURRENCY);
197
+ const trees = await Promise.all(batch.map((object) => readAunitSourceTree(client, object.type, object.name, budget)));
198
+ entries.push(...batch.map((object, index) => ({ ...object, tree: trees[index] })));
199
+ }
200
+ const incompleteEntry = entries.find((entry) => !entry.tree.complete);
201
+ const incompleteReason = selection.incompleteReason ??
202
+ (incompleteEntry
203
+ ? `${incompleteEntry.type} ${incompleteEntry.name}: ${incompleteEntry.tree.incompleteReason ?? 'active source was incomplete'}`
204
+ : undefined);
205
+ return {
206
+ selection,
207
+ entries,
208
+ complete: selection.complete && incompleteEntry === undefined,
209
+ ...(incompleteReason ? { incompleteReason } : {}),
210
+ };
211
+ }
212
+ function aunitProgramName(object) {
213
+ if (object.type !== 'FUGR')
214
+ return object.name.toUpperCase();
215
+ const separator = object.name.lastIndexOf('/');
216
+ return separator > 0
217
+ ? `${object.name.slice(0, separator + 1)}SAPL${object.name.slice(separator + 1)}`.toUpperCase()
218
+ : `SAPL${object.name}`.toUpperCase();
219
+ }
220
+ function packageProgramSources(snapshot) {
221
+ return snapshot.entries.map((entry) => ({ program: aunitProgramName(entry), source: entry.tree.source }));
222
+ }
223
+ async function readAunitSelectionSnapshot(client, type, name, includeSubpackages, budget) {
224
+ return type === 'DEVC'
225
+ ? {
226
+ kind: 'package',
227
+ package: await readAunitPackageSourceSnapshot(client, name, includeSubpackages, budget, 2),
228
+ }
229
+ : { kind: 'object', tree: await readAunitSourceTree(client, type, name, budget) };
230
+ }
231
+ function sameAunitSourceSnapshot(before, after) {
232
+ if (before.blocks.length !== after.blocks.length)
233
+ return false;
234
+ return before.blocks.every((block, index) => {
235
+ const later = after.blocks[index];
236
+ return (later !== undefined &&
237
+ later.id === block.id &&
238
+ later.status === block.status &&
239
+ later.source === block.source &&
240
+ later.etag === block.etag);
241
+ });
242
+ }
243
+ function sameAunitPackageSnapshot(before, after) {
244
+ if (before.selection.packageName !== after.selection.packageName ||
245
+ before.selection.includeSubpackages !== after.selection.includeSubpackages ||
246
+ before.selection.membership.length !== after.selection.membership.length ||
247
+ !before.selection.membership.every((row, index) => after.selection.membership[index] === row) ||
248
+ before.entries.length !== after.entries.length) {
249
+ return false;
250
+ }
251
+ return before.entries.every((entry, index) => {
252
+ const later = after.entries[index];
253
+ return (later !== undefined &&
254
+ later.type === entry.type &&
255
+ later.name === entry.name &&
256
+ later.packageName === entry.packageName &&
257
+ later.uri === entry.uri &&
258
+ sameAunitSourceSnapshot(entry.tree, later.tree));
259
+ });
260
+ }
261
+ async function verifyAunitSourceSnapshot(client, type, name, before, budget) {
262
+ if (!before.complete)
263
+ return before;
264
+ const after = await readAunitSourceTree(client, type, name, budget);
265
+ if (!after.complete) {
266
+ return {
267
+ ...before,
268
+ complete: false,
269
+ incompleteReason: after.incompleteReason ?? 'The active ABAP source tree could not be re-read after the ABAP Unit run.',
270
+ };
271
+ }
272
+ if (!sameAunitSourceSnapshot(before, after)) {
273
+ return {
274
+ ...before,
275
+ complete: false,
276
+ incompleteReason: 'The active ABAP source tree changed while ABAP Unit evidence was being collected.',
277
+ };
278
+ }
279
+ return before;
280
+ }
281
+ async function verifyAunitSelectionSnapshot(client, type, name, includeSubpackages, before, budget) {
282
+ if (before.kind === 'object') {
283
+ return {
284
+ kind: 'object',
285
+ tree: await verifyAunitSourceSnapshot(client, type, name, before.tree, budget),
286
+ };
287
+ }
288
+ if (!before.package.complete)
289
+ return before;
290
+ const after = await readAunitPackageSourceSnapshot(client, name, includeSubpackages, budget, 1);
291
+ if (!after.complete) {
292
+ return {
293
+ kind: 'package',
294
+ package: {
295
+ ...before.package,
296
+ complete: false,
297
+ incompleteReason: after.incompleteReason ?? 'The package selection could not be re-read after the ABAP Unit run.',
298
+ },
299
+ };
300
+ }
301
+ if (!sameAunitPackageSnapshot(before.package, after)) {
302
+ return {
303
+ kind: 'package',
304
+ package: {
305
+ ...before.package,
306
+ complete: false,
307
+ incompleteReason: 'Package membership or active ABAP source changed while ABAP Unit evidence was collected.',
308
+ },
309
+ };
310
+ }
311
+ return before;
312
+ }
313
+ function reconcileAunitWithSource(result, evidence) {
314
+ return reconcileAunitSourceDeclarations(result, evidence.source, {
315
+ complete: evidence.complete,
316
+ ...(evidence.incompleteReason ? { incompleteReason: evidence.incompleteReason } : {}),
317
+ });
318
+ }
319
+ function reconcileAunitWithSelection(result, evidence) {
320
+ if (evidence.kind === 'object')
321
+ return reconcileAunitWithSource(result, evidence.tree);
322
+ return reconcileAunitProgramSources(result, packageProgramSources(evidence.package), {
323
+ complete: evidence.package.complete,
324
+ ...(evidence.package.incompleteReason ? { incompleteReason: evidence.package.incompleteReason } : {}),
325
+ });
326
+ }
327
+ function emptyAunitRunResult(coverage) {
328
+ return {
329
+ outcome: 'no_tests',
330
+ summary: { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, warnings: 0 },
331
+ selection: { maxRisk: 'harmless', durations: ['short', 'medium', 'long'] },
332
+ tests: [],
333
+ alerts: [],
334
+ coverageEvidence: coverage ? 'unavailable' : 'not_requested',
335
+ ...(coverage ? { coverageUnavailableReason: 'measurement_not_reported' } : {}),
336
+ };
337
+ }
338
+ function incompleteAunitRunResult(coverage, message) {
339
+ return {
340
+ ...emptyAunitRunResult(coverage),
341
+ outcome: 'incomplete',
342
+ summary: { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, warnings: 1 },
343
+ alerts: [
344
+ {
345
+ scope: 'run',
346
+ kind: 'deadline',
347
+ severity: 'tolerable',
348
+ title: 'ABAP Unit evidence collection did not complete',
349
+ details: [message],
350
+ message,
351
+ stack: [],
352
+ },
353
+ ],
354
+ };
355
+ }
356
+ function aunitSelectionRunnable(evidence) {
357
+ return evidence.kind === 'object' || evidence.package.complete;
358
+ }
359
+ async function runLegacyAunitForSelection(client, type, name, evidence, coverage, budget) {
360
+ if (!aunitSelectionRunnable(evidence))
361
+ return emptyAunitRunResult(coverage);
362
+ const objectUrls = evidence.kind === 'package'
363
+ ? evidence.package.selection.objects.map((object) => object.uri)
364
+ : [objectUrlForType(type, name)];
365
+ if (objectUrls.length === 0)
366
+ return emptyAunitRunResult(coverage);
367
+ try {
368
+ return await runUnitTests(client.http, client.safety, objectUrls, {
369
+ coverage,
370
+ requestOptions: { deadline: budget.deadline },
371
+ });
372
+ }
373
+ catch (error) {
374
+ if (isAunitDeadlineFailure(error)) {
375
+ return incompleteAunitRunResult(coverage, 'The ABAP Unit evidence deadline expired while executing the harmless legacy run.');
376
+ }
377
+ throw error;
378
+ }
379
+ }
380
+ function legacyAlertStatus(alerts) {
381
+ return alerts.some((alert) => alert.severity !== 'tolerable') ? 'failed' : 'skipped';
382
+ }
383
+ function legacyAlertRow(alert, testClass, testMethod) {
384
+ return {
385
+ program: alert.program ?? '',
386
+ testClass,
387
+ testMethod,
388
+ status: legacyAlertStatus([alert]),
389
+ ...(alert.message ? { message: alert.message } : {}),
390
+ };
391
+ }
392
+ /** Compatibility adapter kept at the SAPDiagnose legacy-output boundary. */
393
+ export function toLegacyAunitResults(result) {
394
+ const rows = result.alerts
395
+ .filter((alert) => alert.scope === 'run')
396
+ .map((alert) => legacyAlertRow(alert, '(run)', '(alert)'));
397
+ const programs = new Set();
398
+ for (const test of result.tests)
399
+ programs.add(test.program);
400
+ for (const alert of result.alerts) {
401
+ if (alert.scope !== 'run')
402
+ programs.add(alert.program ?? '');
403
+ }
404
+ for (const program of programs) {
405
+ rows.push(...result.alerts
406
+ .filter((alert) => alert.scope === 'program' && (alert.program ?? '') === program)
407
+ .map((alert) => legacyAlertRow(alert, '(program)', '(alert)')));
408
+ const classes = new Set();
409
+ for (const test of result.tests) {
410
+ if (test.program === program)
411
+ classes.add(test.testClass);
412
+ }
413
+ for (const alert of result.alerts) {
414
+ if (alert.scope === 'class' && (alert.program ?? '') === program)
415
+ classes.add(alert.testClass ?? '');
416
+ }
417
+ for (const testClass of classes) {
418
+ const classAlerts = result.alerts.filter((alert) => alert.scope === 'class' && (alert.program ?? '') === program && (alert.testClass ?? '') === testClass);
419
+ const hasReportedClassAlert = classAlerts.some((alert) => alert.kind !== 'emptyClass');
420
+ for (const alert of classAlerts) {
421
+ if (alert.kind === 'emptyClass' && hasReportedClassAlert)
422
+ continue;
423
+ rows.push(legacyAlertRow(alert.kind === 'emptyClass'
424
+ ? { ...alert, message: 'test class reported no test methods and no alert' }
425
+ : alert, testClass, '(class-level alert)'));
426
+ }
427
+ for (const test of result.tests) {
428
+ if (test.program !== program || test.testClass !== testClass)
429
+ continue;
430
+ rows.push({
431
+ program,
432
+ testClass,
433
+ testMethod: test.testMethod,
434
+ status: test.alerts.length > 0 ? legacyAlertStatus(test.alerts) : 'passed',
435
+ ...(test.alerts[0]?.message ? { message: test.alerts[0].message } : {}),
436
+ ...(test.durationMs !== undefined ? { duration: test.durationMs / 1000 } : {}),
437
+ });
438
+ }
439
+ }
440
+ }
441
+ return rows;
442
+ }
11
443
  export async function handleSAPDiagnose(client, args) {
12
444
  const action = String(args.action ?? '');
13
445
  const name = String(args.name ?? '');
@@ -23,18 +455,179 @@ export async function handleSAPDiagnose(client, args) {
23
455
  if (content !== undefined)
24
456
  opts.content = content;
25
457
  const result = await syntaxCheck(client.http, client.safety, objectUrl, Object.keys(opts).length > 0 ? opts : undefined);
458
+ // Fail closed: SAP checked nothing (object does not exist yet) → never report "clean", or
459
+ // callers read hasErrors:false as "SAP will accept this source".
460
+ if (!result.checked) {
461
+ return textResult(toolJson({
462
+ ...result,
463
+ hasErrors: true,
464
+ messages: [
465
+ {
466
+ severity: 'error',
467
+ text: `Not checked — ${(result.statusText || 'SAP did not process this check').replace(/\.$/, '')}. The source was NOT validated; create the object first (SAPWrite action="create"), then re-run the syntax check.`,
468
+ line: 0,
469
+ column: 0,
470
+ },
471
+ ],
472
+ }));
473
+ }
26
474
  return textResult(toolJson(result));
27
475
  }
28
476
  case 'unittest': {
29
- const objectUrl = objectUrlForType(type, name);
30
477
  const coverage = args.coverage === true;
31
- const result = await runUnitTests(client.http, client.safety, objectUrl, { coverage });
478
+ const resultFormat = String(args.resultFormat ?? 'legacy');
479
+ const includeSubpackages = type === 'DEVC' && args.includeSubpackages === true;
480
+ const timeoutMs = args.timeoutSeconds === undefined ? DEFAULT_PUBLIC_AUNIT_TIMEOUT_MS : Number(args.timeoutSeconds) * 1000;
481
+ const evidenceBudget = {
482
+ deadline: Date.now() + timeoutMs,
483
+ remainingSourceRequests: AUNIT_SOURCE_REQUEST_LIMIT,
484
+ };
485
+ if (resultFormat === 'junit' && !coverage) {
486
+ let publicAvailable;
487
+ try {
488
+ publicAvailable = await probePublicAunit(client.http, client.safety, {
489
+ deadline: evidenceBudget.deadline,
490
+ });
491
+ }
492
+ catch (error) {
493
+ if (!isAunitDeadlineFailure(error))
494
+ throw error;
495
+ const message = 'The ABAP Unit evidence deadline expired while probing the public run API.';
496
+ return textResult(toolJson({
497
+ protocol: 'public-api',
498
+ outcome: 'incomplete',
499
+ summary: { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, warnings: 0 },
500
+ coverageEvidence: 'not_requested',
501
+ incompleteReason: 'timeout',
502
+ junit: aunitIncompleteToJunit(message, `${type} ${name}`),
503
+ polls: 0,
504
+ elapsedMs: Date.now() - (evidenceBudget.deadline - timeoutMs),
505
+ }));
506
+ }
507
+ if (publicAvailable) {
508
+ const sourceSnapshot = await readAunitSelectionSnapshot(client, type, name, includeSubpackages, evidenceBudget);
509
+ let native;
510
+ try {
511
+ native = await runPublicAunit(client.http, client.safety, type, name, {
512
+ includeSubpackages,
513
+ timeoutMs,
514
+ deadline: evidenceBudget.deadline,
515
+ });
516
+ }
517
+ catch (error) {
518
+ if (!(error instanceof AunitIncompleteError))
519
+ throw error;
520
+ const message = error.message;
521
+ return textResult(toolJson({
522
+ protocol: 'public-api',
523
+ outcome: 'incomplete',
524
+ summary: { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, warnings: 0 },
525
+ coverageEvidence: 'not_requested',
526
+ incompleteReason: 'timeout',
527
+ junit: aunitIncompleteToJunit(message, `${type} ${name}`),
528
+ ...error.evidence,
529
+ }));
530
+ }
531
+ const legacyEvidence = await runLegacyAunitForSelection(client, type, name, sourceSnapshot, false, evidenceBudget);
532
+ const stableSourceSnapshot = await verifyAunitSelectionSnapshot(client, type, name, includeSubpackages, sourceSnapshot, evidenceBudget);
533
+ const legacy = reconcileAunitWithSelection(legacyEvidence, stableSourceSnapshot);
534
+ let outcome = native.summary.outcome;
535
+ let incompleteReason;
536
+ // Native JUnit omits run/class alerts, including harmless-selection refusals. Reconcile
537
+ // every public run with one harmless legacy run; disagreement is evidence, never green.
538
+ if (native.summary.failures > 0 || native.summary.errors > 0 || legacy.outcome === 'failed') {
539
+ outcome = 'failed';
540
+ }
541
+ else if (native.summary.tests !== legacy.summary.tests) {
542
+ outcome = 'incomplete';
543
+ incompleteReason = 'native_legacy_result_mismatch';
544
+ }
545
+ else if (legacy.outcome === 'incomplete' ||
546
+ (native.summary.tests > 0 && native.summary.skipped === native.summary.tests)) {
547
+ outcome = 'incomplete';
548
+ incompleteReason =
549
+ legacy.sourceSelectionEvidence?.status === 'unavailable'
550
+ ? 'source_selection_unavailable'
551
+ : (legacy.sourceSelectionEvidence?.omittedNonHarmlessTestClasses.length ?? 0) > 0
552
+ ? 'source_declared_non_harmless_omitted'
553
+ : legacy.outcome === 'incomplete'
554
+ ? 'harmless_selection_incomplete'
555
+ : 'all_tests_skipped';
556
+ }
557
+ else if (native.summary.tests === 0 && legacy.outcome === 'no_tests') {
558
+ outcome = 'no_tests';
559
+ }
560
+ else {
561
+ outcome = 'passed';
562
+ }
563
+ const summary = {
564
+ tests: native.summary.tests,
565
+ passed: Math.max(0, native.summary.tests - native.summary.failures - native.summary.errors - native.summary.skipped),
566
+ failures: native.summary.failures,
567
+ errors: native.summary.errors,
568
+ skipped: native.summary.skipped,
569
+ warnings: legacy.summary.warnings,
570
+ };
571
+ let junit = native.junit;
572
+ if ((outcome === 'failed' || outcome === 'incomplete') &&
573
+ (outcome !== native.summary.outcome || incompleteReason !== undefined)) {
574
+ const diagnosticMessage = outcome === 'failed'
575
+ ? 'ARC-1 reconciliation found failure or error evidence in the harmless legacy ABAP Unit result that SAP native JUnit did not represent.'
576
+ : `ARC-1 reconciliation could not verify a complete harmless ABAP Unit run (${incompleteReason ?? 'incomplete legacy evidence'}).`;
577
+ junit = appendAunitJunitDiagnostic(native.junit, native.summary, outcome, diagnosticMessage);
578
+ }
579
+ return textResult(toolJson({
580
+ protocol: native.protocol,
581
+ outcome,
582
+ summary,
583
+ coverageEvidence: 'not_requested',
584
+ ...(incompleteReason ? { incompleteReason } : {}),
585
+ selectionEvidence: {
586
+ outcome: legacy.outcome,
587
+ summary: legacy.summary,
588
+ alerts: legacy.alerts,
589
+ },
590
+ sourceSelectionEvidence: legacy.sourceSelectionEvidence,
591
+ junit,
592
+ runPath: native.runPath,
593
+ resultPath: native.resultPath,
594
+ polls: native.polls,
595
+ elapsedMs: native.elapsedMs,
596
+ }));
597
+ }
598
+ }
599
+ // SAP 7.58 can silently omit dangerous/critical classes from harmless-only legacy results.
600
+ // Audit a stable active-source snapshot for every format, including the compatibility array
601
+ // and coverage object, so the generic tool path cannot report a partial run as green.
602
+ const sourceSnapshot = await readAunitSelectionSnapshot(client, type, name, includeSubpackages, evidenceBudget);
603
+ const result = await runLegacyAunitForSelection(client, type, name, sourceSnapshot, coverage, evidenceBudget);
604
+ const stableSourceSnapshot = await verifyAunitSelectionSnapshot(client, type, name, includeSubpackages, sourceSnapshot, evidenceBudget);
605
+ const structured = reconcileAunitWithSelection(result, stableSourceSnapshot);
606
+ if (resultFormat === 'structured')
607
+ return textResult(toolJson(structured));
608
+ if (resultFormat === 'junit') {
609
+ return textResult(toolJson({
610
+ protocol: 'legacy-generated',
611
+ outcome: structured.outcome,
612
+ summary: structured.summary,
613
+ coverage: structured.coverage,
614
+ coverageEvidence: structured.coverageEvidence,
615
+ sourceSelectionEvidence: structured.sourceSelectionEvidence,
616
+ junit: aunitResultToJunit(structured, `${type} ${name}`),
617
+ }));
618
+ }
619
+ const sourceSelectionUnsafe = structured.sourceSelectionEvidence?.status !== 'verified' ||
620
+ (structured.sourceSelectionEvidence?.omittedTestClasses.length ?? 0) > 0;
621
+ if (structured.outcome === 'incomplete' || sourceSelectionUnsafe) {
622
+ return errorResult(toolJson(structured));
623
+ }
32
624
  // Default (no coverage) keeps the historical array output; coverage requested → {tests, coverage}.
625
+ const tests = toLegacyAunitResults(structured);
33
626
  if (!coverage)
34
- return textResult(toolJson(result.tests));
35
- const out = { tests: result.tests };
36
- if (result.coverage)
37
- out.coverage = result.coverage;
627
+ return textResult(toolJson(tests));
628
+ const out = { tests };
629
+ if (structured.coverage)
630
+ out.coverage = structured.coverage;
38
631
  else
39
632
  out.coverageNote =
40
633
  'Coverage unavailable on this system (the coverage-measurement endpoint or measurement result was not available).';
@@ -43,8 +636,22 @@ export async function handleSAPDiagnose(client, args) {
43
636
  case 'atc': {
44
637
  const objectUrl = objectUrlForType(type, name);
45
638
  const variant = args.variant;
46
- const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
47
- return textResult(toolJson(result));
639
+ const result = await runAtcCheck(client.http, client.safety, objectUrl, variant, {
640
+ ...(args.timeoutSeconds === undefined ? {} : { timeoutMs: Number(args.timeoutSeconds) * 1000 }),
641
+ });
642
+ const response = result.processedObjectCount === 0
643
+ ? {
644
+ ...result,
645
+ hint: 'SAP reported no processed ATC objects. Verify the object, package, and selected check variant, then rerun ATC.',
646
+ }
647
+ : result;
648
+ if (args.resultFormat === 'structured')
649
+ return textResult(toolJson(response));
650
+ // Keep the successful legacy `{findings}` shape, but never discard completeness failures:
651
+ // a generic tool/CLI call must not turn malformed or missing worklist evidence into a clean run.
652
+ if (!result.complete)
653
+ return errorResult(toolJson(response));
654
+ return textResult(toolJson({ findings: result.findings }));
48
655
  }
49
656
  case 'atc_variants': {
50
657
  // Discover which check variant to pass to action="atc": the system default (used when none is