abapgit-agent 1.12.1 → 1.13.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.
@@ -8,7 +8,7 @@ grand_parent: ABAP Development
8
8
 
9
9
  # Creating CDS Views
10
10
 
11
- **Searchable keywords**: CDS, DDL, DDLS, CDS view, @AbapCatalog, @AccessControl, association, projection, consumption
11
+ **Searchable keywords**: CDS, DDL, DDLS, CDS view, @AbapCatalog, @AccessControl, association, projection, consumption, GROUP BY, aggregation, subquery, JOIN
12
12
 
13
13
  ## TOPICS IN THIS FILE
14
14
  1. File Naming - line 96
@@ -145,20 +145,13 @@ where devclass not like '$%'
145
145
 
146
146
  1. **Avoid reserved words** - Field names like `PACKAGE`, `CLASS`, `INTERFACE` are reserved in CDS. Use alternatives like `PackageName`, `ClassName`.
147
147
 
148
- 2. **Workflow for creating CDS views** - See `../CLAUDE.md` for complete workflow guidance:
149
- - Independent CDS views: `syntax → commit → pull --files`
150
- - Dependent CDS views (with associations to NEW views): Create underlying view first, then dependent view
151
- - See CLAUDE.md section on "Working with dependent objects"
148
+ 2. **Workflow for creating CDS views** run: `abapgit-agent ref --topic workflow-detailed`
152
149
 
153
150
  3. **System support** - CDS views require SAP systems with CDS capability (S/4HANA, SAP BW/4HANA, or ABAP 7.51+). Older systems will show error: "Object type DDLS is not supported by this system"
154
151
 
155
152
  ### Activating CDS Views
156
153
 
157
- **For standard workflow, see `../CLAUDE.md`**
158
-
159
- **CDS-specific notes:**
160
- - Single independent DDLS file: `abapgit-agent pull --files src/zc_view.ddls.asddls`
161
- - CDS views with associations to OTHER NEW views: Create target view first (see `../CLAUDE.md` for workflow)
154
+ See `abapgit-agent ref --topic workflow-detailed` for the complete workflow.
162
155
 
163
156
  ## CDS View Entity Features
164
157
 
@@ -196,6 +189,39 @@ where devclass not like '$%'
196
189
 
197
190
  ## CDS Best Practices and Common Patterns
198
191
 
192
+ ### No Inline Subqueries — Use JOIN + GROUP BY Instead
193
+
194
+ **CDS view entities do NOT support inline subqueries in JOIN.** This is a hard syntax error:
195
+
196
+ ❌ **WRONG — inline subquery (syntax error)**:
197
+ ```abap
198
+ define view entity ZC_MyView as select from zheader as Header
199
+ inner join ( // ← NOT SUPPORTED
200
+ select docid, sum( amount ) as Total
201
+ from zitems
202
+ group by docid
203
+ ) as Items on Items.docid = Header.docid ...
204
+ ```
205
+
206
+ ✅ **CORRECT — JOIN base tables directly, use GROUP BY on the outer view**:
207
+ ```abap
208
+ define view entity ZC_MyView
209
+ as select from zheader as Header
210
+ inner join zitems as Item
211
+ on Item.docid = Header.docid
212
+ {
213
+ key Header.docid as DocId,
214
+ Header.description as Description,
215
+ count(*) as NumberOfItems,
216
+ sum(Item.amount) as TotalAmount
217
+ }
218
+ group by Header.docid, Header.description
219
+ ```
220
+
221
+ **This works.** CDS supports JOIN + GROUP BY + aggregation functions in a single view. Only inline subqueries in the FROM/JOIN clause are unsupported.
222
+
223
+ ---
224
+
199
225
  ### Key Field Ordering (STRICT RULE)
200
226
 
201
227
  CDS views enforce strict key field ordering that differs from regular SQL:
@@ -235,6 +261,8 @@ CDS views enforce strict key field ordering that differs from regular SQL:
235
261
 
236
262
  ### Currency/Amount Field Aggregation
237
263
 
264
+ > **Design principle**: Prefer a **single CDS view** with JOIN + GROUP BY. Only split into multiple layered views when requirements specifically need reusable intermediate aggregations shared across different consumers.
265
+
238
266
  When aggregating currency or amount fields in CDS views, use semantic annotations instead of complex casting:
239
267
 
240
268
  ❌ **WRONG - Complex casting (will fail)**:
@@ -290,6 +290,49 @@ METHOD test_aggregation.
290
290
  ENDMETHOD.
291
291
  ```
292
292
 
293
+ ### Testing CDS Views that Select from Another CDS View
294
+
295
+ > **Note:** This pattern applies when your design **already has** a CDS view that selects from another CDS view. It does NOT mean you should split a single view into two — use a single CDS view with GROUP BY / JOIN when the business logic fits.
296
+
297
+ When your CDS view selects from **another CDS view** (not a base table), `create` will raise `CX_CDS_FAILURE`. Use `create_for_multiple_cds` instead and list all CDS entities in the dependency chain.
298
+
299
+ ```abap
300
+ METHOD class_setup.
301
+ " ZC_TopView selects from ZC_IntermediateView (another CDS view entity)
302
+ " → must use create_for_multiple_cds and list all CDS entities
303
+ mo_cds_env_static = cl_cds_test_environment=>create_for_multiple_cds(
304
+ i_for_entities = VALUE #(
305
+ ( 'ZC_TOPVIEW' ) " the view under test
306
+ ( 'ZC_INTERMEDIATEVIEW' ) " the CDS view it selects from
307
+ ) ).
308
+ ENDMETHOD.
309
+ ```
310
+
311
+ Insert test data into the intermediate CDS view (not the base tables), since that is what the top-level view reads:
312
+
313
+ ```abap
314
+ METHOD test_read.
315
+ DATA lt_source TYPE TABLE OF zc_intermediateview WITH EMPTY KEY.
316
+ lt_source = VALUE #(
317
+ ( field1 = 'A' field2 = 100 )
318
+ ( field1 = 'B' field2 = 200 ) ).
319
+ mo_cds_env->insert_test_data( i_data = lt_source ).
320
+
321
+ SELECT * FROM zc_topview INTO TABLE @DATA(lt_result).
322
+
323
+ cl_abap_unit_assert=>assert_equals(
324
+ exp = 2 act = lines( lt_result ) msg = 'Expected 2 rows' ).
325
+ ENDMETHOD.
326
+ ```
327
+
328
+ **Rules:**
329
+ - List the view under test **and all CDS views it depends on** in `i_for_entities`
330
+ - Insert data into the **direct source** of the top-level view (the intermediate CDS view)
331
+ - Order in `i_for_entities` does not matter
332
+ - If you get `CX_CDS_FAILURE` when using `create`, switch to `create_for_multiple_cds`
333
+
334
+ ---
335
+
293
336
  ### Key Classes for CDS Testing
294
337
 
295
338
  | Item | Type/Usage |
@@ -304,7 +347,8 @@ ENDMETHOD.
304
347
 
305
348
  | Method | Purpose |
306
349
  |--------|---------|
307
- | `CL_CDS_TEST_ENVIRONMENT=>create( i_for_entity = ... )` | Create test environment (returns `if_cds_test_environment`) |
350
+ | `CL_CDS_TEST_ENVIRONMENT=>create( i_for_entity = ... )` | Create test environment for a CDS view over base tables |
351
+ | `CL_CDS_TEST_ENVIRONMENT=>create_for_multiple_cds( i_for_entities = ... )` | Create test environment when the CDS view selects from another CDS view |
308
352
  | `insert_test_data( i_data = ... )` | Insert test data into test doubles |
309
353
  | `clear_doubles` | Clear test data before each test method |
310
354
  | `destroy` | Clean up after test class |
@@ -314,7 +358,7 @@ ENDMETHOD.
314
358
  1. **Use interface type**: `DATA mo_cds_env TYPE REF TO if_cds_test_environment` - the CREATE method returns an interface reference
315
359
  2. **CLASS-METHODS required**: `class_setup` and `class_teardown` must be declared with `CLASS-METHODS` (not `METHODS`)
316
360
  3. **Table type declaration**: Must declare `DATA lt_tab TYPE TABLE OF <type> WITH EMPTY KEY` before using `VALUE #()`
317
- 4. **Auto-created dependencies**: CDS framework auto-creates test doubles for base tables - do not specify `i_dependency_list`
361
+ 4. **Auto-created dependencies**: When the CDS view selects only from **base tables**, the framework auto-creates test doubles do not specify `i_dependency_list`. When the CDS view selects from **another CDS view**, use `create_for_multiple_cds` instead (see section below).
318
362
  5. **Aggregations**: For CDS views with SUM/COUNT/GROUP BY, insert test data into base tables (SFLIGHT, SCARR, etc.)
319
363
  6. **Clear doubles**: Call `clear_doubles` in `setup` method before each test
320
364
  7. **Enable associations**: Set `test_associations = 'X'` only if testing CDS associations
package/bin/abapgit-agent CHANGED
@@ -54,6 +54,7 @@ async function main() {
54
54
  debug: require('../src/commands/debug'),
55
55
  run: require('../src/commands/run'),
56
56
  ref: require('../src/commands/ref'),
57
+ guide: require('../src/commands/guide'),
57
58
  init: require('../src/commands/init'),
58
59
  pull: require('../src/commands/pull'),
59
60
  upgrade: require('../src/commands/upgrade'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -8,7 +8,9 @@
8
8
  "src/",
9
9
  "abap/guidelines/",
10
10
  "abap/CLAUDE.md",
11
+ "abap/CLAUDE.slim.md",
11
12
  "abap/.github/copilot-instructions.md",
13
+ "abap/.github/copilot-instructions.slim.md",
12
14
  ".abapGitAgent.example"
13
15
  ],
14
16
  "bin": {
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+
7
+ // Marker present in the old full-guide CLAUDE.md (copied by init before this feature)
8
+ const FULL_GUIDE_MARKER = 'Claude Code Instructions';
9
+
10
+ // Marker present in the slim stub (so we can detect it's already migrated)
11
+ const SLIM_STUB_MARKER = 'abapgit-agent guide';
12
+
13
+ // Marker present in the old full copilot-instructions.md
14
+ const COPILOT_FULL_MARKER = '# ABAP Development with abapGit';
15
+
16
+ // Marker present in the slim copilot stub
17
+ const COPILOT_SLIM_MARKER = 'abapgit-agent guide';
18
+
19
+ module.exports = {
20
+ name: 'guide',
21
+ description: 'Show bundled ABAP development guide',
22
+ requiresAbapConfig: false,
23
+
24
+ _findBundledGuide() {
25
+ const candidates = [
26
+ path.join(__dirname, '..', '..', 'abap', 'CLAUDE.md'),
27
+ path.join(__dirname, '..', '..', '..', 'abap', 'CLAUDE.md')
28
+ ];
29
+ return candidates.find(p => fs.existsSync(p)) || null;
30
+ },
31
+
32
+ _findSlimStub() {
33
+ const candidates = [
34
+ path.join(__dirname, '..', '..', 'abap', 'CLAUDE.slim.md'),
35
+ path.join(__dirname, '..', '..', '..', 'abap', 'CLAUDE.slim.md')
36
+ ];
37
+ return candidates.find(p => fs.existsSync(p)) || null;
38
+ },
39
+
40
+ _findCopilotSlimStub() {
41
+ const candidates = [
42
+ path.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.slim.md'),
43
+ path.join(__dirname, '..', '..', '..', 'abap', '.github', 'copilot-instructions.slim.md')
44
+ ];
45
+ return candidates.find(p => fs.existsSync(p)) || null;
46
+ },
47
+
48
+ _getBundledGuidelineNames() {
49
+ const candidates = [
50
+ path.join(__dirname, '..', '..', 'abap', 'guidelines'),
51
+ path.join(__dirname, '..', '..', '..', 'abap', 'guidelines')
52
+ ];
53
+ const guidelinesDir = candidates.find(p => fs.existsSync(p));
54
+ if (!guidelinesDir) return new Set();
55
+ return new Set(
56
+ fs.readdirSync(guidelinesDir).filter(f => f.endsWith('.md'))
57
+ );
58
+ },
59
+
60
+ async _confirm(question) {
61
+ return new Promise((resolve) => {
62
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
63
+ rl.question(question, (answer) => {
64
+ rl.close();
65
+ const normalized = answer.trim().toLowerCase();
66
+ resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
67
+ });
68
+ });
69
+ },
70
+
71
+ async _runMigrate(args) {
72
+ const dryRun = args.includes('--dry-run');
73
+ const yes = args.includes('--yes') || args.includes('-y');
74
+ const cwd = process.cwd();
75
+
76
+ const bundledNames = this._getBundledGuidelineNames();
77
+ const slimStubPath = this._findSlimStub();
78
+ const copilotSlimStubPath = this._findCopilotSlimStub();
79
+
80
+ // --- Scan guidelines/ ---
81
+ const guidelinesDir = path.join(cwd, 'guidelines');
82
+ const toDelete = []; // standard files matching bundled names
83
+ const toKeep = []; // *.local.md or other non-standard files
84
+
85
+ if (fs.existsSync(guidelinesDir)) {
86
+ for (const name of fs.readdirSync(guidelinesDir)) {
87
+ if (!name.endsWith('.md')) continue;
88
+ if (bundledNames.has(name)) {
89
+ toDelete.push(path.join(guidelinesDir, name));
90
+ } else {
91
+ toKeep.push(name);
92
+ }
93
+ }
94
+ }
95
+
96
+ // --- Scan CLAUDE.md ---
97
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
98
+ let claudeMdAction = 'none'; // 'replace' | 'already-slim' | 'custom' | 'missing'
99
+ if (fs.existsSync(claudeMdPath)) {
100
+ const content = fs.readFileSync(claudeMdPath, 'utf8');
101
+ if (content.includes(SLIM_STUB_MARKER)) {
102
+ claudeMdAction = 'already-slim';
103
+ } else if (content.includes(FULL_GUIDE_MARKER)) {
104
+ claudeMdAction = 'replace';
105
+ } else {
106
+ claudeMdAction = 'custom';
107
+ }
108
+ } else {
109
+ claudeMdAction = 'missing';
110
+ }
111
+
112
+ // --- Scan .github/copilot-instructions.md ---
113
+ const copilotMdPath = path.join(cwd, '.github', 'copilot-instructions.md');
114
+ let copilotAction = 'none'; // 'replace' | 'already-slim' | 'custom' | 'missing'
115
+ if (fs.existsSync(copilotMdPath)) {
116
+ const content = fs.readFileSync(copilotMdPath, 'utf8');
117
+ if (content.includes(COPILOT_SLIM_MARKER)) {
118
+ copilotAction = 'already-slim';
119
+ } else if (content.includes(COPILOT_FULL_MARKER)) {
120
+ copilotAction = 'replace';
121
+ } else {
122
+ copilotAction = 'custom';
123
+ }
124
+ } else {
125
+ copilotAction = 'missing';
126
+ }
127
+
128
+ // --- Nothing to do? ---
129
+ const nothingToDo = toDelete.length === 0 && claudeMdAction !== 'replace' && copilotAction !== 'replace';
130
+ if (nothingToDo) {
131
+ console.log('');
132
+ console.log('✅ Already clean — nothing to migrate.');
133
+ if (claudeMdAction === 'already-slim') {
134
+ console.log(' CLAUDE.md is already the slim stub.');
135
+ } else if (claudeMdAction === 'custom') {
136
+ console.log(' CLAUDE.md has custom content — left untouched.');
137
+ } else if (claudeMdAction === 'missing') {
138
+ console.log(' No CLAUDE.md found.');
139
+ }
140
+ if (copilotAction === 'already-slim') {
141
+ console.log(' .github/copilot-instructions.md is already the slim stub.');
142
+ } else if (copilotAction === 'custom') {
143
+ console.log(' .github/copilot-instructions.md has custom content — left untouched.');
144
+ }
145
+ if (toDelete.length === 0 && fs.existsSync(guidelinesDir)) {
146
+ console.log(' No standard guideline files found in guidelines/.');
147
+ }
148
+ console.log('');
149
+ return;
150
+ }
151
+
152
+ // --- Preview ---
153
+ console.log('');
154
+ console.log('🔄 guide --migrate: switch to bundled guidelines');
155
+ console.log('');
156
+
157
+ if (toDelete.length > 0) {
158
+ console.log(`Files to remove (${toDelete.length} standard guideline file${toDelete.length > 1 ? 's' : ''}):`);
159
+ toDelete.forEach(f => console.log(` ${path.relative(cwd, f)}`));
160
+ console.log('');
161
+ }
162
+
163
+ if (toKeep.length > 0) {
164
+ console.log('Files to keep (project-specific):');
165
+ toKeep.forEach(f => console.log(` guidelines/${f}`));
166
+ console.log('');
167
+ }
168
+
169
+ if (claudeMdAction === 'replace') {
170
+ if (slimStubPath) {
171
+ console.log('CLAUDE.md: detected as full guide → will replace with slim stub');
172
+ console.log(" (run 'abapgit-agent guide' to read the full guide on demand)");
173
+ } else {
174
+ console.log('CLAUDE.md: detected as full guide → ⚠️ slim stub not found, will skip');
175
+ }
176
+ console.log('');
177
+ }
178
+
179
+ if (copilotAction === 'replace') {
180
+ if (copilotSlimStubPath) {
181
+ console.log('.github/copilot-instructions.md: detected as full guide → will replace with slim stub');
182
+ console.log(' (Copilot uses the slim stub; full guide available online)');
183
+ } else {
184
+ console.log('.github/copilot-instructions.md: detected as full guide → ⚠️ slim stub not found, will skip');
185
+ }
186
+ console.log('');
187
+ }
188
+
189
+ // After deletions, would guidelines/ be empty?
190
+ const dirWillBeEmpty = fs.existsSync(guidelinesDir) && toKeep.length === 0;
191
+ if (dirWillBeEmpty) {
192
+ console.log('guidelines/ will be removed (no project-specific files remain).');
193
+ console.log('');
194
+ }
195
+
196
+ if (dryRun) {
197
+ console.log('ℹ️ Dry run — no changes made.');
198
+ console.log('');
199
+ return;
200
+ }
201
+
202
+ // --- Confirm ---
203
+ if (!yes) {
204
+ const proceed = await this._confirm('Proceed? [y/N] ');
205
+ if (!proceed) {
206
+ console.log('Migration cancelled.');
207
+ console.log('');
208
+ return;
209
+ }
210
+ }
211
+
212
+ // --- Execute ---
213
+ console.log('');
214
+ let deletedCount = 0;
215
+
216
+ for (const filePath of toDelete) {
217
+ fs.unlinkSync(filePath);
218
+ console.log(`🗑️ Removed ${path.relative(cwd, filePath)}`);
219
+ deletedCount++;
220
+ }
221
+
222
+ if (dirWillBeEmpty) {
223
+ // Verify no other files remain before removing the directory
224
+ const remaining = fs.readdirSync(guidelinesDir);
225
+ if (remaining.length === 0) {
226
+ fs.rmdirSync(guidelinesDir);
227
+ console.log('🗑️ Removed guidelines/');
228
+ }
229
+ }
230
+
231
+ if (claudeMdAction === 'replace' && slimStubPath) {
232
+ const slimContent = fs.readFileSync(slimStubPath, 'utf8');
233
+ fs.writeFileSync(claudeMdPath, slimContent);
234
+ console.log('✅ Replaced CLAUDE.md with slim stub');
235
+ }
236
+
237
+ if (copilotAction === 'replace' && copilotSlimStubPath) {
238
+ const slimContent = fs.readFileSync(copilotSlimStubPath, 'utf8');
239
+ fs.writeFileSync(copilotMdPath, slimContent);
240
+ console.log('✅ Replaced .github/copilot-instructions.md with slim stub');
241
+ }
242
+
243
+ console.log('');
244
+ console.log('✅ Migration complete.');
245
+ console.log(' Standard guidelines are now read from the package automatically.');
246
+ console.log(" Run 'abapgit-agent ref \"<pattern>\"' or 'abapgit-agent ref --topic <topic>' to search them.");
247
+ console.log('');
248
+ },
249
+
250
+ async execute(args) {
251
+ if (args.includes('--migrate')) {
252
+ return this._runMigrate(args);
253
+ }
254
+
255
+ const filePath = this._findBundledGuide();
256
+
257
+ if (!filePath) {
258
+ console.error('❌ Bundled CLAUDE.md not found. Make sure abapgit-agent is properly installed.');
259
+ process.exit(1);
260
+ }
261
+
262
+ if (args.includes('--path')) {
263
+ console.log(filePath);
264
+ return;
265
+ }
266
+
267
+ const content = fs.readFileSync(filePath, 'utf8');
268
+
269
+ if (args.includes('--json')) {
270
+ console.log(JSON.stringify({ path: filePath, content }));
271
+ return;
272
+ }
273
+
274
+ console.log(content);
275
+ }
276
+ };
@@ -78,6 +78,19 @@ Commands:
78
78
  - Use full URL: https://github.com/user/repo.git
79
79
  - Or short name: user/repo or user/repo (assumes github.com)
80
80
 
81
+ guide [--path] [--json]
82
+ Show the full ABAP development guide bundled with the package.
83
+ Reads directly from the installed npm package — always up-to-date.
84
+ - Use --path to print only the file path (useful for pagers)
85
+ - Use --json for machine-readable output
86
+
87
+ guide --migrate [--dry-run] [--yes]
88
+ Migrate repo from locally-copied guidelines to bundled fallback.
89
+ Removes standard guideline files copied by old 'init', replaces full CLAUDE.md
90
+ with slim stub. Project-specific files (*.local.md) are never removed.
91
+ - Use --dry-run to preview changes without applying them
92
+ - Use --yes to skip confirmation prompt
93
+
81
94
  health
82
95
  Check if ABAP REST API is healthy
83
96
 
@@ -107,6 +120,8 @@ Examples:
107
120
  abapgit-agent dump --user DEVELOPER --detail 1 # Full detail of first result
108
121
  abapgit-agent ref "CORRESPONDING" # Search for pattern
109
122
  abapgit-agent ref --topic exceptions # View exceptions topic
123
+ abapgit-agent guide # Full ABAP dev guide
124
+ abapgit-agent guide --path # Path to guide file
110
125
  abapgit-agent health # Health check
111
126
  abapgit-agent status # Configuration status
112
127