@cspeach/cli 1.1.42 → 1.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspeach/project.json +2 -2
- package/dist/approvals/op-labels.js +9 -0
- package/dist/renderer/tool-labels.js +4 -1
- package/dist/sap/profile-features.js +6 -0
- package/dist/tools/sap-read.js +114 -7
- package/dist/tools/sap-write.js +171 -15
- package/dist/tools/transport-fit-check.js +9 -0
- package/dist/tools/verify.js +11 -1
- package/dist/tools/write-pipeline.js +49 -4
- package/package.json +2 -2
package/.cspeach/project.json
CHANGED
|
@@ -42,6 +42,9 @@ const TYPE_WORDS = {
|
|
|
42
42
|
ENHS: 'BAdI enhancement spot',
|
|
43
43
|
XSLT: 'transformation',
|
|
44
44
|
SHLP: 'search help',
|
|
45
|
+
FUNC: 'function module',
|
|
46
|
+
FINC: 'function group include',
|
|
47
|
+
INCL: 'include',
|
|
45
48
|
};
|
|
46
49
|
export function humanVerb(op) {
|
|
47
50
|
return VERBS[op] ?? op.charAt(0).toUpperCase() + op.slice(1);
|
|
@@ -68,12 +71,18 @@ export function verbLine(change) {
|
|
|
68
71
|
.filter(Boolean)
|
|
69
72
|
.join(' ');
|
|
70
73
|
}
|
|
74
|
+
/** Part 2c D20 — the fixed card line for a function-group delete. */
|
|
75
|
+
const FUGR_DELETE_LINE = 'Deletes every function module and include in the group.';
|
|
71
76
|
/**
|
|
72
77
|
* Part 2b D7 — "Affects: <process>" for a change that names the standard SAP
|
|
73
78
|
* process it runs in (a BAdI implementation); null when it names none. The
|
|
74
79
|
* text comes from the model: control characters are removed.
|
|
75
80
|
*/
|
|
76
81
|
export function affectsLine(change) {
|
|
82
|
+
// Part 2c D20 — a group delete takes every module and include with it. The
|
|
83
|
+
// line comes from the change's op and type, never from a model-supplied field.
|
|
84
|
+
if (change.op === 'delete' && typeCode(change.type) === 'FUGR')
|
|
85
|
+
return FUGR_DELETE_LINE;
|
|
77
86
|
const t = sanitizeControlChars(String(change.affects ?? '')).trim();
|
|
78
87
|
return t ? `Affects: ${t}` : null;
|
|
79
88
|
}
|
|
@@ -170,6 +170,9 @@ const RULES = {
|
|
|
170
170
|
sap_badi_read: (a) => ({ verb: 'Read BAdI', object: shortRight([str(a['kind']), str(a['name'])].filter(Boolean).join(' ')) }),
|
|
171
171
|
sap_enhancement_options: (a) => ({ verb: 'Read enhancement options', object: shortRight([str(a['type']), str(a['name'])].filter(Boolean).join(' ')) }),
|
|
172
172
|
sap_create_badi_implementation: sapObject('Created BAdI implementation'),
|
|
173
|
+
sap_create_table_type: sapObject('Created table type'),
|
|
174
|
+
sap_table_type_read: sapObject('Read table type'),
|
|
175
|
+
sap_function_group_list: (a) => ({ verb: 'Listed function group', object: shortRight(str(a['group']) ?? '') }),
|
|
173
176
|
sap_snapshot_list: (a) => ({ verb: 'Listed snapshots', object: shortRight(str(a['name']) ?? '') }),
|
|
174
177
|
sap_snapshot_read: sapObject('Read snapshot'),
|
|
175
178
|
sap_snapshot_cleanup: () => ({ verb: 'Cleaned snapshots', object: '' }),
|
|
@@ -268,7 +271,7 @@ const PRESENT = {
|
|
|
268
271
|
'Read snapshot': 'Reading snapshot of', 'Cleaned snapshots': 'Cleaning snapshots',
|
|
269
272
|
'Created transport': 'Creating transport', 'Transport for': 'Finding transport for',
|
|
270
273
|
'Listed transports': 'Listing transports', Released: 'Releasing', 'Created domain': 'Creating domain',
|
|
271
|
-
'Created data element': 'Creating data element', 'Read BAdI': 'Reading BAdI', 'Read enhancement options': 'Reading enhancement options of', 'Created BAdI implementation': 'Creating BAdI implementation', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
274
|
+
'Created data element': 'Creating data element', 'Read BAdI': 'Reading BAdI', 'Read enhancement options': 'Reading enhancement options of', 'Created BAdI implementation': 'Creating BAdI implementation', 'Created table type': 'Creating table type', 'Read table type': 'Reading table type', 'Listed function group': 'Listing function group', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
272
275
|
Published: 'Publishing', Unpublished: 'Unpublishing', Messages: 'Maintaining messages in',
|
|
273
276
|
'Number range': 'Maintaining number range', Question: 'Asking', 'Updated todos': 'Updating todos',
|
|
274
277
|
'Ran skill': 'Running skill', 'Read document': 'Reading document', Fetched: 'Fetching',
|
|
@@ -30,6 +30,11 @@ export const FEATURES = Object.freeze({
|
|
|
30
30
|
// 2020" has no cited source (Part 1 I1 rule): listed = yes, else unknown.
|
|
31
31
|
'enh.badiImplementations': { label: 'BAdI implementations', collection: `${A}/enhancements/enhoxhb`, provenOnOurBox: true },
|
|
32
32
|
'enh.badiSpots': { label: 'BAdI enhancement spots', collection: `${A}/enhancements/enhsxsb`, provenOnOurBox: true },
|
|
33
|
+
// Part 2c: no `since`. The gap table says "all on-prem", unverified (Part 1
|
|
34
|
+
// I1 rule): listed = yes, else unknown.
|
|
35
|
+
'fugr.modules': { label: 'Function modules and function-group includes', collection: `${A}/functions/groups`, provenOnOurBox: true },
|
|
36
|
+
'prog.includes': { label: 'Program includes', collection: `${A}/programs/includes`, provenOnOurBox: true },
|
|
37
|
+
'ddic.tabletypes': { label: 'Table types', collection: `${A}/ddic/tabletypes`, provenOnOurBox: true },
|
|
33
38
|
});
|
|
34
39
|
export const FEATURE_IDS = Object.freeze(Object.keys(FEATURES));
|
|
35
40
|
export function isFeatureId(s) {
|
|
@@ -42,6 +47,7 @@ export const OBJECT_TYPE_FEATURE = Object.freeze({
|
|
|
42
47
|
TABL: 'ddic.tables', STRU: 'ddic.structures', DOMA: 'ddic.domains', DTEL: 'ddic.dataelements',
|
|
43
48
|
MSAG: 'msag', NROB: 'nrob',
|
|
44
49
|
ENHO: 'enh.badiImplementations', ENHS: 'enh.badiSpots',
|
|
50
|
+
FUNC: 'fugr.modules', FINC: 'fugr.modules', INCL: 'prog.includes', TTYP: 'ddic.tabletypes',
|
|
45
51
|
});
|
|
46
52
|
/** The RAP/CDS subset: the only types a READ is ever refused for (R6). */
|
|
47
53
|
export const READ_GATED_TYPES = new Set(['BDEF', 'SRVD', 'SRVB', 'DDLS', 'CDS', 'DDLX', 'DCLS']);
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Read-only SAP tools wired to @cspeach/sap-client AdtClient.
|
|
3
3
|
*
|
|
4
4
|
* All method names verified against sap-client/src/adt-client.ts:
|
|
5
5
|
* getSource(type, name)
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
* badiList(query, enhancementSpot?, maxResults?)
|
|
23
23
|
* readBadiSpot(name) / readBadiImplementation(name) (Part 2b)
|
|
24
24
|
* enhancementOptions(type, name) (Part 2b)
|
|
25
|
+
* readTableType(name) / functionGroupMembers(group) / includeMainPrograms(name) (Part 2c)
|
|
25
26
|
*/
|
|
26
27
|
import { registerTool } from './index.js';
|
|
27
28
|
import { byObjectTypeForRead } from '../sap/profile-gate.js';
|
|
28
29
|
import { normaliseSyntaxResult, checkSyntax } from './verify.js';
|
|
29
30
|
import { recordSyntaxCheck } from './syntax-state.js';
|
|
30
31
|
import { spillToolResult } from './result-spill.js';
|
|
31
|
-
import { SapError } from '@cspeach/sap-client';
|
|
32
|
+
import { SapError, parseFunctionModuleDocument, parseContainerDocument } from '@cspeach/sap-client';
|
|
32
33
|
// -----------------------------------------------------------------------
|
|
33
34
|
// Task 17 (spec D12, ruling F18) — model-facing formats.
|
|
34
35
|
// -----------------------------------------------------------------------
|
|
@@ -84,7 +85,9 @@ registerTool({
|
|
|
84
85
|
+ 'caller knows the inactive edits are not visible. For a freshly '
|
|
85
86
|
+ 'created object with NO active version yet, the inactive (working) '
|
|
86
87
|
+ 'source is returned with version="inactive" and an honest note. '
|
|
87
|
-
+ 'The first line is a [version: …] header, not part of the source.'
|
|
88
|
+
+ 'The first line is a [version: …] header, not part of the source. '
|
|
89
|
+
+ 'Types FUNC (function module), FINC (function-group include, e.g. L<GROUP>TOP), INCL (program include) and TTYP (table type) are supported; give the bare name — CSPeach finds the group.' + ' '
|
|
90
|
+
+ 'A table type has no source: use sap_table_type_read.',
|
|
88
91
|
isMutating: false,
|
|
89
92
|
requiresSap: true,
|
|
90
93
|
input_schema: {
|
|
@@ -180,7 +183,8 @@ registerTool({
|
|
|
180
183
|
registerTool({
|
|
181
184
|
name: 'sap_object_structure',
|
|
182
185
|
requires: byObjectTypeForRead,
|
|
183
|
-
description: 'Retrieve metadata and structural information for an ABAP repository object (package, description, lock state, etc.).'
|
|
186
|
+
description: 'Retrieve metadata and structural information for an ABAP repository object (package, description, lock state, etc.). '
|
|
187
|
+
+ 'Types FUNC (function module), FINC (function-group include, e.g. L<GROUP>TOP), INCL (program include) and TTYP (table type) are supported; give the bare name — CSPeach finds the group.',
|
|
184
188
|
isMutating: false,
|
|
185
189
|
requiresSap: true,
|
|
186
190
|
input_schema: {
|
|
@@ -208,10 +212,51 @@ registerTool({
|
|
|
208
212
|
// the excerpt window. Task 17: rawXml is parsed above and never sent to
|
|
209
213
|
// the model (it was the bulk of the result).
|
|
210
214
|
const { name, type, rawXml, ...rest } = result;
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
// Part 2c (D18) — after `version`: the module document's group, processing
|
|
216
|
+
// type and release state; an include's group; a program include's main
|
|
217
|
+
// programs (one extra read). Every other type's JSON is unchanged.
|
|
218
|
+
let extra;
|
|
219
|
+
try {
|
|
220
|
+
extra = await objectStructureExtras(String(args.type ?? ''), String(args.name ?? ''), rawXml ?? '', ctx);
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
if (!(e instanceof UnreadableDocument))
|
|
224
|
+
throw e;
|
|
225
|
+
return { content: JSON.stringify({ error: 'read_failed', detail: e.message }), is_error: true };
|
|
226
|
+
}
|
|
227
|
+
return { content: JSON.stringify({ name, type, version, ...extra, ...rest }, null, 2) };
|
|
213
228
|
},
|
|
214
229
|
});
|
|
230
|
+
/** A module or include document that does not parse: said in plain words, never the parser's text. */
|
|
231
|
+
class UnreadableDocument extends Error {
|
|
232
|
+
}
|
|
233
|
+
/** Part 2c (D18) — the type-specific fields sap_object_structure adds after `version`. */
|
|
234
|
+
async function objectStructureExtras(type, name, rawXml, ctx) {
|
|
235
|
+
switch (type.trim().toUpperCase().split('/')[0]) {
|
|
236
|
+
case 'FUNC': {
|
|
237
|
+
let fm;
|
|
238
|
+
try {
|
|
239
|
+
fm = parseFunctionModuleDocument(rawXml);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw new UnreadableDocument(`SAP's answer for function module ${name.trim().toUpperCase()} could not be read as a function module document.`);
|
|
243
|
+
}
|
|
244
|
+
return { function_group: fm.group, processing_type: fm.processingType, release_state: fm.releaseState };
|
|
245
|
+
}
|
|
246
|
+
case 'FINC': {
|
|
247
|
+
try {
|
|
248
|
+
return { function_group: parseContainerDocument(rawXml).group };
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
throw new UnreadableDocument(`SAP's answer for function-group include ${name.trim().toUpperCase()} could not be read as an include document.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
case 'INCL':
|
|
255
|
+
return { main_programs: (await ctx.adt.includeMainPrograms(name)).map((m) => m.name) };
|
|
256
|
+
default:
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
215
260
|
// -----------------------------------------------------------------------
|
|
216
261
|
// 3. sap_search_object
|
|
217
262
|
// -----------------------------------------------------------------------
|
|
@@ -569,7 +614,8 @@ registerTool({
|
|
|
569
614
|
// -----------------------------------------------------------------------
|
|
570
615
|
registerTool({
|
|
571
616
|
name: 'sap_syntax_check',
|
|
572
|
-
description: 'Run a syntax check on an ABAP object and return any errors or warnings without activating it.'
|
|
617
|
+
description: 'Run a syntax check on an ABAP object and return any errors or warnings without activating it. '
|
|
618
|
+
+ 'Types FUNC (function module), FINC (function-group include, e.g. L<GROUP>TOP), INCL (program include) and TTYP (table type) are supported; give the bare name — CSPeach finds the group.',
|
|
573
619
|
isMutating: false,
|
|
574
620
|
requiresSap: true,
|
|
575
621
|
input_schema: {
|
|
@@ -850,3 +896,64 @@ registerTool({
|
|
|
850
896
|
};
|
|
851
897
|
},
|
|
852
898
|
});
|
|
899
|
+
// -----------------------------------------------------------------------
|
|
900
|
+
// 21. sap_table_type_read (Part 2c, D18) — one table type, read only
|
|
901
|
+
// -----------------------------------------------------------------------
|
|
902
|
+
registerTool({
|
|
903
|
+
name: 'sap_table_type_read',
|
|
904
|
+
description: 'Read one table type (TTYP): its row type, access type and key. A table type has no source, so sap_get_source does not read it.',
|
|
905
|
+
isMutating: false,
|
|
906
|
+
requiresSap: true,
|
|
907
|
+
input_schema: {
|
|
908
|
+
type: 'object',
|
|
909
|
+
properties: {
|
|
910
|
+
name: { type: 'string', description: 'Table type name, e.g. ZCSP_ORDERS_TT' },
|
|
911
|
+
},
|
|
912
|
+
required: ['name'],
|
|
913
|
+
},
|
|
914
|
+
handler: async (args, ctx) => {
|
|
915
|
+
const name = String(args.name ?? '').trim().toUpperCase();
|
|
916
|
+
try {
|
|
917
|
+
const info = await ctx.adt.readTableType(args.name);
|
|
918
|
+
return { content: JSON.stringify(info, null, 2) };
|
|
919
|
+
}
|
|
920
|
+
catch (e) {
|
|
921
|
+
if (!isSapNotFound(e))
|
|
922
|
+
throw e;
|
|
923
|
+
return notFound(`Table type ${name} does not exist.`);
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
// -----------------------------------------------------------------------
|
|
928
|
+
// 22. sap_function_group_list (Part 2c, D18) — a group's modules and includes
|
|
929
|
+
// -----------------------------------------------------------------------
|
|
930
|
+
registerTool({
|
|
931
|
+
name: 'sap_function_group_list',
|
|
932
|
+
description: 'List the function modules and includes of one function group, by name. Read each one with sap_get_source (type FUNC or FINC).',
|
|
933
|
+
isMutating: false,
|
|
934
|
+
requiresSap: true,
|
|
935
|
+
input_schema: {
|
|
936
|
+
type: 'object',
|
|
937
|
+
properties: {
|
|
938
|
+
group: { type: 'string', description: 'Function group name, e.g. ZCSP_ORDERS' },
|
|
939
|
+
},
|
|
940
|
+
required: ['group'],
|
|
941
|
+
},
|
|
942
|
+
handler: async (args, ctx) => {
|
|
943
|
+
const group = String(args.group ?? '').trim().toUpperCase();
|
|
944
|
+
let members;
|
|
945
|
+
try {
|
|
946
|
+
members = await ctx.adt.functionGroupMembers(args.group);
|
|
947
|
+
}
|
|
948
|
+
catch (e) {
|
|
949
|
+
if (!isSapNotFound(e))
|
|
950
|
+
throw e;
|
|
951
|
+
return notFound(`Function group ${group} does not exist.`);
|
|
952
|
+
}
|
|
953
|
+
const function_modules = members.functionModules.map((m) => m.name);
|
|
954
|
+
const includes = members.includes.map((m) => m.name);
|
|
955
|
+
return {
|
|
956
|
+
content: JSON.stringify({ group: members.group || group, function_modules, includes, count: function_modules.length + includes.length }),
|
|
957
|
+
};
|
|
958
|
+
},
|
|
959
|
+
});
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -32,7 +32,8 @@ import { hasRecordedSyntaxErrorsByName, clearSyntaxErrorsAfterActivation } from
|
|
|
32
32
|
import { teamFixOffers, cspeachWritesThisSession } from '../register/offer.js';
|
|
33
33
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
34
34
|
import { checkTransportFit, reusedRequestRefusal } from './transport-fit-check.js';
|
|
35
|
-
import {
|
|
35
|
+
import { typeCode } from '../approvals/risk-floor.js';
|
|
36
|
+
import { auditLog, SapError, hasClassicParameterBlock } from '@cspeach/sap-client';
|
|
36
37
|
import { ERR } from '../errors/codes.js';
|
|
37
38
|
import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
|
|
38
39
|
// F-L2-1: statusline-only wire — records the transport a write ACTUALLY
|
|
@@ -41,9 +42,20 @@ import { interceptUpdateMethod, CancelledByUserError, } from './update-method-in
|
|
|
41
42
|
import { noteWriteTransport, setCurrentTransport } from '../repl/current-transport.js';
|
|
42
43
|
import { markTransportCreatedThisSession } from './transport.js';
|
|
43
44
|
import { hasManualLocks, noteManualLock, noteManualUnlock } from '../sap/session-end.js';
|
|
44
|
-
import { snapshotChainState, writtenWithSyntaxErrors, chainActivation, journeyWording, activatedByWrite, nextStepAfterWrite, snapshotSource, writeToolHandler, } from './write-pipeline.js';
|
|
45
|
+
import { snapshotChainState, writtenWithSyntaxErrors, chainActivation, journeyWording, activatedByWrite, sweepFields, nextStepAfterWrite, snapshotSource, writeToolHandler, } from './write-pipeline.js';
|
|
46
|
+
/**
|
|
47
|
+
* Part 2c D16 — sap-client's own sentence (adt-client.ts FUNC_CLASSIC_BLOCK_TEXT),
|
|
48
|
+
* said here before the approval is spent (Task 5 review I-4).
|
|
49
|
+
*/
|
|
50
|
+
const FUNC_CLASSIC_BLOCK_TEXT = 'A function module\'s signature goes in the FUNCTION statement (IMPORTING / EXPORTING / CHANGING / TABLES / EXCEPTIONS), '
|
|
51
|
+
+ 'not in a *" comment block. Rewrite the header and try again.';
|
|
45
52
|
const SET_SOURCE_SPEC = {
|
|
46
53
|
tool: 'sap_set_source',
|
|
54
|
+
// Part 2c D16 (review I-4): a function module's classic *" block is refused
|
|
55
|
+
// before the approval is spent, the snapshot is read or the WAL is opened.
|
|
56
|
+
beforeApproval: (a) => (typeCode(a.type) === 'FUNC' && hasClassicParameterBlock(String(a.source ?? ''))
|
|
57
|
+
? { error: 'classic_parameter_block', detail: FUNC_CLASSIC_BLOCK_TEXT }
|
|
58
|
+
: undefined),
|
|
47
59
|
approval: {
|
|
48
60
|
object: (a) => a.name,
|
|
49
61
|
op: 'modify',
|
|
@@ -62,6 +74,11 @@ const SET_SOURCE_SPEC = {
|
|
|
62
74
|
}),
|
|
63
75
|
walArgs: (a) => ({ name: a.name, type: a.type }),
|
|
64
76
|
call: (a, ctx, pre) => ctx.adt.setSource(a.type, a.name, a.source, pre.transport),
|
|
77
|
+
// Part 2c: a module's or include's refusal from sap-client in plain words;
|
|
78
|
+
// every other type keeps the default `{ error, detail: String(err) }`.
|
|
79
|
+
onThrow: (err, a) => (isFunctionGroupMember(a.type) && err instanceof SapError
|
|
80
|
+
? { error: 'write_failed', detail: plainSapText(err) }
|
|
81
|
+
: undefined),
|
|
65
82
|
errorCode: 'write_failed',
|
|
66
83
|
landed: (r) => (r.error && !writtenWithSyntaxErrors(r)) || (r.writeStatus !== undefined && r.writeStatus >= 400)
|
|
67
84
|
? { ok: false, detail: r.error ?? `Write returned HTTP ${r.writeStatus} but did not throw`, extra: { write_status: r.writeStatus } }
|
|
@@ -101,7 +118,8 @@ registerTool({
|
|
|
101
118
|
description: 'Overwrite the full source of an ABAP object. Requires approval_id. '
|
|
102
119
|
+ 'Use for class-definition changes, new methods, or any change not confined to '
|
|
103
120
|
+ 'a single method body. For method-body-only edits use sap_update_method (Rule 7a). '
|
|
104
|
-
+ 'The transport auto-resolves to the owning request if the object is already locked.'
|
|
121
|
+
+ 'The transport auto-resolves to the owning request if the object is already locked. '
|
|
122
|
+
+ 'Types FUNC (function module), FINC (function-group include, e.g. L<GROUP>TOP), INCL (program include) and TTYP (table type) are supported; give the bare name — CSPeach finds the group.',
|
|
105
123
|
isMutating: true,
|
|
106
124
|
requiresSap: true,
|
|
107
125
|
input_schema: {
|
|
@@ -231,7 +249,10 @@ const DELETE_OBJECT_SPEC = {
|
|
|
231
249
|
rule: 'RULE_7_SNAPSHOT_BEFORE_WRITE',
|
|
232
250
|
op: 'delete_object',
|
|
233
251
|
object: { type: a.type, name: a.name },
|
|
234
|
-
|
|
252
|
+
// Part 2c D20 — a group delete takes every module and include with it.
|
|
253
|
+
what_will_happen: typeCode(a.type) === 'FUGR'
|
|
254
|
+
? 'Deletes the function group and every function module and include in it from SAP; CSPeach tries to save a copy of each member and of the main program first (a member it cannot read is not saved); recovery via sap_snapshot_read + sap_create_object/sap_set_source (LLM composes the rollback).'
|
|
255
|
+
: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
|
|
235
256
|
rollback_available: true,
|
|
236
257
|
transport,
|
|
237
258
|
args: { ...a },
|
|
@@ -245,12 +266,15 @@ const DELETE_OBJECT_SPEC = {
|
|
|
245
266
|
: { ok: true },
|
|
246
267
|
audit: { method: 'deleteObject', type: (a) => a.type, name: (a) => a.name },
|
|
247
268
|
verify: { syntax: false, activation: false, deletedCheck: true },
|
|
248
|
-
envelope: (
|
|
269
|
+
envelope: (a, _r, pre) => {
|
|
249
270
|
const stillExists = pre.extra['still_exists'];
|
|
250
271
|
return {
|
|
251
272
|
deleted: !stillExists,
|
|
252
273
|
still_exists: stillExists,
|
|
253
274
|
...(pre.owning ? { transport_used: pre.owning, transport_note: pre.note } : {}),
|
|
275
|
+
// Part 2c D10 — deleting a function-group include comments out its
|
|
276
|
+
// INCLUDE line in the group's main program, which is then inactive.
|
|
277
|
+
...(!stillExists && typeCode(a.type) === 'FINC' ? fincDeleteNextStep(a.name) : {}),
|
|
254
278
|
};
|
|
255
279
|
},
|
|
256
280
|
};
|
|
@@ -260,7 +284,8 @@ registerTool({
|
|
|
260
284
|
description: 'Delete an ABAP object. Requires approval_id. '
|
|
261
285
|
+ 'A snapshot is taken automatically before deletion. '
|
|
262
286
|
+ 'Post-check: confirms the object no longer exists. '
|
|
263
|
-
+ 'The transport auto-resolves to the owning request if the object is already locked.'
|
|
287
|
+
+ 'The transport auto-resolves to the owning request if the object is already locked. '
|
|
288
|
+
+ 'Types FUNC (function module), FINC (function-group include, e.g. L<GROUP>TOP), INCL (program include) and TTYP (table type) are supported; give the bare name — CSPeach finds the group.',
|
|
264
289
|
isMutating: true,
|
|
265
290
|
requiresSap: true,
|
|
266
291
|
input_schema: {
|
|
@@ -279,6 +304,16 @@ const isDevcCreate = (a) => String(a.type ?? '').toUpperCase().split('/')[0].tri
|
|
|
279
304
|
const devcTransportCreated = (pre) => pre.extra['devc_transport_created'];
|
|
280
305
|
const CREATE_OBJECT_SPEC = {
|
|
281
306
|
tool: 'sap_create_object',
|
|
307
|
+
// Part 2c D6 (review I-4): a module or include needs its group — refused
|
|
308
|
+
// before the approval is spent. Presence only: whether the group is a
|
|
309
|
+
// customer group (D19) is sap-client's rule.
|
|
310
|
+
beforeApproval: (a) => (isFunctionGroupMember(a.type) && !String(a.functionGroup ?? '').trim()
|
|
311
|
+
? {
|
|
312
|
+
error: 'function_group_required',
|
|
313
|
+
detail: `A ${typeCode(a.type) === 'FUNC' ? 'function module' : 'function-group include'} is created inside a function group. `
|
|
314
|
+
+ 'Give functionGroup: the customer function group it belongs to (Z, Y or a registered namespace).',
|
|
315
|
+
}
|
|
316
|
+
: undefined),
|
|
282
317
|
// 2026-10-04 — a plan-grant id holds the write to its claims (type, package, transport).
|
|
283
318
|
approval: {
|
|
284
319
|
object: (a) => a.name, op: 'create',
|
|
@@ -341,12 +376,20 @@ const CREATE_OBJECT_SPEC = {
|
|
|
341
376
|
serviceDefinition: a.serviceDefinition,
|
|
342
377
|
bindingType: a.bindingType,
|
|
343
378
|
bindingVersion: a.bindingVersion,
|
|
379
|
+
// Part 2c D6 — FUNC/FINC: the group; sap-client refuses a missing or SAP group (D19).
|
|
380
|
+
...(a.functionGroup !== undefined ? { functionGroup: a.functionGroup } : {}),
|
|
344
381
|
...(pre.extra['devc_extras'] ?? {}),
|
|
345
382
|
// Review fix round 1, I-1: a sap_lock handle lives in the stateful
|
|
346
383
|
// session; sap-client ends that session before a create unless told.
|
|
347
384
|
keepSession: hasManualLocks(),
|
|
348
385
|
}),
|
|
349
|
-
|
|
386
|
+
// Part 2c: sap-client's refusal of a module or include (no group, an SAP
|
|
387
|
+
// group, a wrong include name) reads in plain words; every other type as before.
|
|
388
|
+
onThrow: (err, a, pre) => ({
|
|
389
|
+
error: 'create_failed',
|
|
390
|
+
detail: isFunctionGroupMember(a.type) && err instanceof SapError ? plainSapText(err) : String(err),
|
|
391
|
+
...orphanRequest(devcTransportCreated(pre)),
|
|
392
|
+
}),
|
|
350
393
|
errorCode: 'create_failed',
|
|
351
394
|
landed: (r, pre) => r.status === 'created'
|
|
352
395
|
? { ok: true }
|
|
@@ -375,11 +418,7 @@ const CREATE_OBJECT_SPEC = {
|
|
|
375
418
|
object: r.objectName,
|
|
376
419
|
type: r.objectType,
|
|
377
420
|
package: r.packageName,
|
|
378
|
-
|
|
379
|
-
// go in through sap_message_maintain (bug 3, 2026-10-12).
|
|
380
|
-
next_step: String(r.objectType ?? '').toUpperCase() === 'MSAG'
|
|
381
|
-
? 'add_messages_via_sap_message_maintain (a message class has no source and no activation)'
|
|
382
|
-
: 'write_source_via_sap_set_source_then_activate',
|
|
421
|
+
next_step: createNextStep(r.objectType),
|
|
383
422
|
};
|
|
384
423
|
},
|
|
385
424
|
};
|
|
@@ -395,7 +434,8 @@ registerTool({
|
|
|
395
434
|
+ "\"Create package NAME and its transport '<text>'\" (<text> = the description argument, ≤60 ASCII chars) and the approval object is the package name. "
|
|
396
435
|
+ 'Optional DEVC params: description, softwareComponent (default HOME), transportLayer '
|
|
397
436
|
+ '(default: the system\'s layer), applicationComponent, superPackage, responsible '
|
|
398
|
-
+ '(default: you, upper case). A $-local package needs no transport.'
|
|
437
|
+
+ '(default: you, upper case). A $-local package needs no transport. '
|
|
438
|
+
+ 'FUNC and FINC need `functionGroup` (a customer group: Z, Y or a registered namespace — never an SAP group); TTYP is created with sap_create_table_type.',
|
|
399
439
|
isMutating: true,
|
|
400
440
|
requiresSap: true,
|
|
401
441
|
input_schema: {
|
|
@@ -412,6 +452,8 @@ registerTool({
|
|
|
412
452
|
serviceDefinition: { type: 'string', description: 'SRVB ONLY — REQUIRED when type=SRVB: name of the activated service definition (SRVD) to bind, e.g. ZUI_MYSERVICE.' },
|
|
413
453
|
bindingType: { type: 'string', description: 'SRVB ONLY (optional): binding protocol. Default ODATA.' },
|
|
414
454
|
bindingVersion: { type: 'string', description: 'SRVB ONLY (optional): binding version. Default V4 (OData V4 UI). Use V2 for OData V2.' },
|
|
455
|
+
// Part 2c D6 — FUNC/FINC only.
|
|
456
|
+
functionGroup: { type: 'string', description: 'For type FUNC or FINC: the customer function group (Z, Y or a registered namespace) the module or include belongs to. Required for those two types.' },
|
|
415
457
|
// DEVC-only (§10a): package attributes; ignored for other types.
|
|
416
458
|
softwareComponent: { type: 'string', description: 'DEVC ONLY (optional): software component. Default HOME.' },
|
|
417
459
|
transportLayer: { type: 'string', description: 'DEVC ONLY (optional): transport layer. When omitted: used if the SAP transport check names one; otherwise the create is refused and the layer must be named.' },
|
|
@@ -423,6 +465,39 @@ registerTool({
|
|
|
423
465
|
},
|
|
424
466
|
handler: writeToolHandler(CREATE_OBJECT_SPEC),
|
|
425
467
|
});
|
|
468
|
+
/**
|
|
469
|
+
* Part 2c D10 — after a function-group include is deleted, the group's main
|
|
470
|
+
* program is inactive. SAP names such an include L<GROUP><three characters>
|
|
471
|
+
* (after an optional /NAMESPACE/), so the group is read from the name; a name
|
|
472
|
+
* that is not of that shape names no group (none is guessed).
|
|
473
|
+
*/
|
|
474
|
+
function fincDeleteNextStep(name) {
|
|
475
|
+
const m = /^(\/[A-Z0-9_]+\/)?L([A-Z0-9_]+)[A-Z0-9_$]{3}$/.exec(String(name ?? '').trim().toUpperCase());
|
|
476
|
+
if (!m)
|
|
477
|
+
return { next_step: 'the group main program is now inactive: run sap_activate with type FUGR and the group name' };
|
|
478
|
+
const group = `${m[1] ?? ''}${m[2]}`;
|
|
479
|
+
return { function_group: group, next_step: `the group main program is now inactive: run sap_activate with type FUGR and name ${group}` };
|
|
480
|
+
}
|
|
481
|
+
/** Part 2c — a module or an include created inside a function group. */
|
|
482
|
+
const isFunctionGroupMember = (type) => ['FUNC', 'FINC'].includes(typeCode(type));
|
|
483
|
+
/**
|
|
484
|
+
* The next step after a create. A message class has no source and is never
|
|
485
|
+
* activated: its messages go in through sap_message_maintain (bug 3,
|
|
486
|
+
* 2026-10-12). Part 2c: a function group is active at once (probe trap 2); a
|
|
487
|
+
* module or include is activated by its write together with the group's
|
|
488
|
+
* inactive includes (D5); a program include is checked and activated only
|
|
489
|
+
* once a program includes it (D7).
|
|
490
|
+
*/
|
|
491
|
+
function createNextStep(objectType) {
|
|
492
|
+
switch (String(objectType ?? '').toUpperCase()) {
|
|
493
|
+
case 'MSAG': return 'add_messages_via_sap_message_maintain (a message class has no source and no activation)';
|
|
494
|
+
case 'FUGR': return 'create_modules_with_sap_create_object_FUNC (the group is active; no activation needed)';
|
|
495
|
+
case 'FUNC': return "write_source_via_sap_set_source (signature in the FUNCTION statement) — the write activates it with the group's inactive includes";
|
|
496
|
+
case 'FINC': return "write_source_via_sap_set_source — the write activates it with the group's other inactive includes";
|
|
497
|
+
case 'INCL': return 'write_source_via_sap_set_source — it is checked and activated once a program includes it; then sap_activate INCL';
|
|
498
|
+
default: return 'write_source_via_sap_set_source_then_activate';
|
|
499
|
+
}
|
|
500
|
+
}
|
|
426
501
|
/**
|
|
427
502
|
* §10a — when the package create fails after the flow already made a request,
|
|
428
503
|
* say so: the request exists, empty, and can be used on the next try.
|
|
@@ -760,7 +835,9 @@ registerTool({
|
|
|
760
835
|
name: 'sap_activate',
|
|
761
836
|
description: 'Activate one or more ABAP objects. Required after sap_set_source / '
|
|
762
837
|
+ 'sap_update_method / sap_create_object (Rule 10). '
|
|
763
|
-
+ 'Post-check: objects must no longer appear in the inactive-objects list.'
|
|
838
|
+
+ 'Post-check: objects must no longer appear in the inactive-objects list. '
|
|
839
|
+
+ "A function module or function-group include is activated together with its group's inactive includes; `also_activated` lists them. "
|
|
840
|
+
+ 'Type FUGR activates every inactive member of the group.',
|
|
764
841
|
isMutating: true,
|
|
765
842
|
requiresSap: true,
|
|
766
843
|
input_schema: {
|
|
@@ -849,7 +926,14 @@ registerTool({
|
|
|
849
926
|
try {
|
|
850
927
|
const res = await ctx.adt.activate(obj.type, obj.name);
|
|
851
928
|
await auditLog('activate', obj.type, obj.name, res.success ? 'success' : 'error', Date.now() - startedAt);
|
|
852
|
-
|
|
929
|
+
// Part 2c D2/D5 — the function-group sweep: `also_activated` only when
|
|
930
|
+
// it succeeded; a failed one says what was SENT with the target
|
|
931
|
+
// (`sent_with`) and, when an error names no part, that it may be a
|
|
932
|
+
// member's (review I-2). Warnings are already in `messages`.
|
|
933
|
+
activationResults.push({
|
|
934
|
+
name: obj.name, type: obj.type, success: res.success, messages: res.messages,
|
|
935
|
+
...sweepFields(res, obj.name, { warnings: false }),
|
|
936
|
+
});
|
|
853
937
|
if (!res.success)
|
|
854
938
|
anyError = true;
|
|
855
939
|
}
|
|
@@ -1411,3 +1495,75 @@ registerTool({
|
|
|
1411
1495
|
},
|
|
1412
1496
|
handler: writeToolHandler(CREATE_BADI_IMPL_SPEC),
|
|
1413
1497
|
});
|
|
1498
|
+
const CREATE_TABLE_TYPE_SPEC = {
|
|
1499
|
+
tool: 'sap_create_table_type',
|
|
1500
|
+
approval: {
|
|
1501
|
+
object: (a) => a.name, op: 'create',
|
|
1502
|
+
// Review I-3: only an id from a table-type card creates one (an id from
|
|
1503
|
+
// "Create table ZX" or "Create structure ZX" never does), as for ENHO.
|
|
1504
|
+
boundType: 'TTYP',
|
|
1505
|
+
claims: (a, ctx) => ({ type: 'TTYP', package: a.package, transport: a.transport ?? ctx.currentTransport?.get() ?? null }),
|
|
1506
|
+
},
|
|
1507
|
+
transport: { mode: 'three-tier' },
|
|
1508
|
+
snapshot: { kind: 'none', reason: 'a new table type has no prior document' },
|
|
1509
|
+
preGate: async (a, ctx, pre) => {
|
|
1510
|
+
const fit = await checkTransportFit(ctx.adt, { type: 'TTYP', name: a.name, pkg: a.package, transport: pre.transport });
|
|
1511
|
+
return fit.ok ? { ok: true } : { error: fit.refusal };
|
|
1512
|
+
},
|
|
1513
|
+
walArgs: (a) => ({ name: a.name, type: 'TTYP', package: a.package, row_type: a.row_type }),
|
|
1514
|
+
call: (a, ctx, pre) => ctx.adt.createTableType({ name: a.name, description: a.description, packageName: a.package, rowType: a.row_type }, pre.transport),
|
|
1515
|
+
errorCode: 'create_failed',
|
|
1516
|
+
landed: (r) => (r.status === 'created' && r.written && r.activated
|
|
1517
|
+
? { ok: true }
|
|
1518
|
+
: {
|
|
1519
|
+
ok: false,
|
|
1520
|
+
// Activated, but the read-back is not active: sap-client's text names an
|
|
1521
|
+
// address, so it is said plainly. Otherwise SAP's own words, as for a BAdI.
|
|
1522
|
+
detail: r.created && r.written && r.activated
|
|
1523
|
+
? 'SAP reported the activation, but the table type does not read back as active. Check sap_inactive_objects.'
|
|
1524
|
+
: r.error ?? (!r.created ? 'SAP did not create the table type'
|
|
1525
|
+
: !r.written ? 'the table type document was not written'
|
|
1526
|
+
: `activation failed: ${firstError(r.activationResult)}`),
|
|
1527
|
+
extra: {
|
|
1528
|
+
created: r.created, written: r.written, activated: r.activated,
|
|
1529
|
+
...(r.activationResult?.messages?.length ? { activation_messages: r.activationResult.messages.map((m) => m.shortText) } : {}),
|
|
1530
|
+
},
|
|
1531
|
+
audit: { status: 'error', reason: r.error ?? 'not activated' },
|
|
1532
|
+
}),
|
|
1533
|
+
audit: { method: 'createTableType', type: () => 'TTYP', name: (a) => a.name, detail: (a) => `row=${upperOf(a.row_type)}` },
|
|
1534
|
+
verify: { syntax: false, activation: true },
|
|
1535
|
+
walResult: (_a, r) => ({ created: true, activated: r.activated }),
|
|
1536
|
+
chain: (a, pre, _syntax, act) => ({
|
|
1537
|
+
snapshot: 'new_object', written: true, syntax: 'not_checked', activation: chainActivation(act),
|
|
1538
|
+
card: { object: upperOf(a.name), what: 'table type', transport: pre.transport },
|
|
1539
|
+
}),
|
|
1540
|
+
envelope: (a, r, pre, _syntax, act) => ({
|
|
1541
|
+
created: true, object: r.objectName, type: 'TTYP', package: r.packageName, row_type: upperOf(a.row_type),
|
|
1542
|
+
...activatedByWrite(act), ...(pre.transport ? { transport: pre.transport } : {}),
|
|
1543
|
+
next_step: act?.success && act.verified
|
|
1544
|
+
? 'the table type is active: use it in signatures and DATA statements'
|
|
1545
|
+
: 'activation is not confirmed: check sap_inactive_objects; a row type that is not active or does not exist stops the activation',
|
|
1546
|
+
}),
|
|
1547
|
+
};
|
|
1548
|
+
registerTool({
|
|
1549
|
+
name: 'sap_create_table_type',
|
|
1550
|
+
requires: 'ddic.tabletypes',
|
|
1551
|
+
description: 'Create and activate a standard table type whose row type is a dictionary structure. Non-unique standard key. '
|
|
1552
|
+
+ 'Sorted and hashed tables, data-element or table row types, built-in row types and changes to an existing table type are not supported here (ADT). '
|
|
1553
|
+
+ 'Requires approval_id from a request_approval change of type TTYP.',
|
|
1554
|
+
isMutating: true,
|
|
1555
|
+
requiresSap: true,
|
|
1556
|
+
input_schema: {
|
|
1557
|
+
type: 'object',
|
|
1558
|
+
properties: {
|
|
1559
|
+
name: { type: 'string', description: 'Name of the new table type (Z*/Y*)' },
|
|
1560
|
+
description: { type: 'string', description: 'Short description' },
|
|
1561
|
+
package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
|
|
1562
|
+
row_type: { type: 'string', description: 'The active dictionary structure each row has (e.g. BAPIRET2)' },
|
|
1563
|
+
transport: { type: 'string', description: 'Target transport (optional — required for workbench packages)' },
|
|
1564
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
1565
|
+
},
|
|
1566
|
+
required: ['name', 'description', 'package', 'row_type', 'approval_id'],
|
|
1567
|
+
},
|
|
1568
|
+
handler: writeToolHandler(CREATE_TABLE_TYPE_SPEC),
|
|
1569
|
+
});
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
import { toObjectUrl, localPackageMessage } from '@cspeach/sap-client';
|
|
28
28
|
/** At most this many fitting requests are named in a refusal. */
|
|
29
29
|
export const MAX_FITTING_SHOWN = 5;
|
|
30
|
+
/** Part 2c types: no transport check is sent for them (as before their rows existed). */
|
|
31
|
+
const NO_FIT_CHECK_TYPES = new Set(['FUNC', 'FINC', 'INCL', 'TTYP']);
|
|
30
32
|
function debug(msg) {
|
|
31
33
|
if (process.env['CSPEACH_DEBUG'] !== '1')
|
|
32
34
|
return;
|
|
@@ -42,6 +44,13 @@ export async function checkTransportFit(adt, o) {
|
|
|
42
44
|
return { ok: true };
|
|
43
45
|
const object = String(o.type ?? '').toUpperCase().split('/')[0].trim();
|
|
44
46
|
const objectName = String(o.name ?? '').trim().toUpperCase();
|
|
47
|
+
// Part 2c fix round 1: these types had no url before their rows landed, so no
|
|
48
|
+
// check was sent. The check for them was never probed (a program include is
|
|
49
|
+
// R3TR PROG in TADIR), so they keep that behaviour.
|
|
50
|
+
if (NO_FIT_CHECK_TYPES.has(object)) {
|
|
51
|
+
debug(`no fit check for ${object} ${objectName} (not probed)`);
|
|
52
|
+
return { ok: true };
|
|
53
|
+
}
|
|
45
54
|
let uri;
|
|
46
55
|
try {
|
|
47
56
|
uri = toObjectUrl(object, objectName).toLowerCase();
|
package/dist/tools/verify.js
CHANGED
|
@@ -150,6 +150,16 @@ export function inactiveBodyMentions(body, type, name) {
|
|
|
150
150
|
catch { /* unknown type word: the name check stands */ }
|
|
151
151
|
return byName || byUri;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Part 2c Task 5 — a program and a program include share one name space (one
|
|
155
|
+
* name is never both), so a `/programs/includes/X` entry is the same object
|
|
156
|
+
* for a caller that typed PROG, and the other way round. Before this, an
|
|
157
|
+
* inactive include checked as PROG read as "another object" and passed.
|
|
158
|
+
*/
|
|
159
|
+
function sameNameSpace(type) {
|
|
160
|
+
const t = type.toUpperCase();
|
|
161
|
+
return t === 'INCL' ? 'PROG' : t;
|
|
162
|
+
}
|
|
153
163
|
/**
|
|
154
164
|
* Task 10 / D6 — is this same-named inactive-list entry provably ANOTHER
|
|
155
165
|
* object? In a RAP stack the DDLS, BDEF and DDLX share one name, so a
|
|
@@ -178,7 +188,7 @@ export function isOtherObject(entry, type, name) {
|
|
|
178
188
|
}
|
|
179
189
|
if (!ours)
|
|
180
190
|
return false;
|
|
181
|
-
return theirs.type
|
|
191
|
+
return sameNameSpace(theirs.type) !== sameNameSpace(ours.type)
|
|
182
192
|
|| theirs.name.toUpperCase() !== ours.name.toUpperCase();
|
|
183
193
|
}
|
|
184
194
|
/** The same-named entries of the parsed list, and whether any of them may be
|
|
@@ -94,17 +94,59 @@ export function snapshotChainState(snap) {
|
|
|
94
94
|
export function writtenWithSyntaxErrors(r) {
|
|
95
95
|
return r.writeStatus !== undefined && r.writeStatus >= 200 && r.writeStatus < 300 && r.syntaxResult?.hasErrors === true;
|
|
96
96
|
}
|
|
97
|
+
/** Part 2c — the members SAP was sent besides `name`, as `TYPE NAME` (D2/D5). */
|
|
98
|
+
export function alsoActivatedOf(members, name) {
|
|
99
|
+
const upper = String(name ?? '').trim().toUpperCase();
|
|
100
|
+
return (members ?? []).filter((m) => m.name.toUpperCase() !== upper).map((m) => `${m.type} ${m.name}`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Part 2c (D2/D5; Task 5 review I-1, I-2) — what an activation that swept a
|
|
104
|
+
* function group's members says, as envelope keys. Empty when the result
|
|
105
|
+
* lists no `members` (every type but FUNC/FINC/FUGR), so those envelopes stay
|
|
106
|
+
* byte-identical.
|
|
107
|
+
* - `warnings` (opt-in): the activation's warnings in SAP's words, e.g. a
|
|
108
|
+
* member left out because it is inactive for another user.
|
|
109
|
+
* - success: `also_activated` — the members activated with the target.
|
|
110
|
+
* - failure: `sent_with` — the members sent with it, never called activated;
|
|
111
|
+
* and `sent_with_note` when an error does not name its part, because it
|
|
112
|
+
* may be a member's error, not the target's.
|
|
113
|
+
*/
|
|
114
|
+
export function sweepFields(a, name, opts) {
|
|
115
|
+
if (!a.members)
|
|
116
|
+
return {};
|
|
117
|
+
const upper = String(name ?? '').trim().toUpperCase();
|
|
118
|
+
const msgs = a.messages ?? [];
|
|
119
|
+
const others = alsoActivatedOf(a.members, upper);
|
|
120
|
+
const warnings = opts.warnings
|
|
121
|
+
? { warnings: msgs.filter((m) => m.severity === 'warning').map((m) => m.shortText ?? '').filter(Boolean) }
|
|
122
|
+
: {};
|
|
123
|
+
if (others.length === 0)
|
|
124
|
+
return warnings;
|
|
125
|
+
if (a.success)
|
|
126
|
+
return { ...warnings, also_activated: others };
|
|
127
|
+
const unnamed = msgs.some((m) => m.severity === 'error' && !String(m.objectName ?? '').trim());
|
|
128
|
+
return {
|
|
129
|
+
...warnings,
|
|
130
|
+
sent_with: others,
|
|
131
|
+
...(unnamed
|
|
132
|
+
? { sent_with_note: `These parts went to SAP together with ${upper}. An error that does not name its part may come from one of them, not from ${upper}.` }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
97
136
|
export async function verifyWriteActivation(ctx, r, name, type) {
|
|
98
137
|
const a = r.activationResult;
|
|
99
138
|
if (!a)
|
|
100
139
|
return undefined;
|
|
101
140
|
const errors = (a.messages ?? []).filter((m) => m.severity === 'error').map((m) => m.shortText ?? '').filter(Boolean).slice(0, 5);
|
|
141
|
+
// A failed activation never lists members as activated (sweepFields).
|
|
142
|
+
const sweep = a.members ? { sweep: sweepFields(a, name, { warnings: true }) } : {};
|
|
102
143
|
if (!a.success)
|
|
103
|
-
return { success: false, verified: false, errors };
|
|
144
|
+
return { success: false, verified: false, errors, ...sweep };
|
|
145
|
+
const also = sweep;
|
|
104
146
|
const v = await verifyActive(ctx.adt, name, type);
|
|
105
147
|
if (v.verifyError !== undefined)
|
|
106
|
-
return { success: true, verified: false, errors, verifyError: v.verifyError };
|
|
107
|
-
return v.active ? { success: true, verified: true, errors } : { success: true, verified: false, errors, stillInactive: true };
|
|
148
|
+
return { success: true, verified: false, errors, verifyError: v.verifyError, ...also };
|
|
149
|
+
return v.active ? { success: true, verified: true, errors, ...also } : { success: true, verified: false, errors, stillInactive: true, ...also };
|
|
108
150
|
}
|
|
109
151
|
/** Round 8 item 5 + A-I1 — the change-frame state from the verified activation. */
|
|
110
152
|
export function chainActivation(w) {
|
|
@@ -134,7 +176,10 @@ export function recordOwnFixAfterWrite(ctx, type, name, transport, w) {
|
|
|
134
176
|
export function activatedByWrite(w) {
|
|
135
177
|
if (!w)
|
|
136
178
|
return {};
|
|
137
|
-
return { activated_by_write: {
|
|
179
|
+
return { activated_by_write: {
|
|
180
|
+
success: w.success, verified: w.verified, errors: w.errors,
|
|
181
|
+
...(w.sweep ?? {}),
|
|
182
|
+
} };
|
|
138
183
|
}
|
|
139
184
|
/** A-I1 minor — after a verified activation on write, say so instead of "activation_allowed". */
|
|
140
185
|
export function nextStepAfterWrite(syntaxOk, w) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.43",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
55
55
|
"@azure/msal-node": "6.0.1",
|
|
56
56
|
"@cspeach/register-core": "0.4.0",
|
|
57
|
-
"@cspeach/sap-client": "0.1.
|
|
57
|
+
"@cspeach/sap-client": "0.1.28",
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"@inkjs/ui": "^2.0.0",
|
|
60
60
|
"@inquirer/prompts": "^7.0.0",
|