@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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "id": "1a6ac3e6",
3
- "createdAt": "2026-10-16T13:54:35.800Z"
2
+ "id": "4d0ce47b",
3
+ "createdAt": "2026-10-16T21:06:21.305Z"
4
4
  }
@@ -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']);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 18 read-only SAP tools wired to @cspeach/sap-client AdtClient.
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
- void rawXml;
212
- return { content: JSON.stringify({ name, type, version, ...rest }, null, 2) };
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
+ });
@@ -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 { auditLog, SapError } from '@cspeach/sap-client';
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
- what_will_happen: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
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: (_a, _r, pre) => {
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
- onThrow: (err, _a, pre) => ({ error: 'create_failed', detail: String(err), ...orphanRequest(devcTransportCreated(pre)) }),
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
- // A message class has no source and is never activated: its messages
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
- activationResults.push({ name: obj.name, type: obj.type, success: res.success, messages: res.messages });
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();
@@ -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.toUpperCase() !== ours.type.toUpperCase()
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: { success: w.success, verified: w.verified, errors: w.errors } };
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.42",
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.27",
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",