@abapify/adt-atc 0.4.34 → 0.4.38

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/README.md CHANGED
@@ -1,165 +1,43 @@
1
1
  # @abapify/adt-atc
2
2
 
3
- [![version](https://img.shields.io/github/package-json/v/abapify/adt-cli?filename=packages/adt-atc/package.json)](https://github.com/abapify/adt-cli/pkgs/npm/%40abapify%2Fadt-atc)
3
+ [![npm](https://img.shields.io/npm/v/%40abapify%2Fadt-atc)](https://www.npmjs.com/package/@abapify/adt-atc)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/abapify/adt-cli/blob/main/LICENSE)
4
5
 
5
- ABAP Test Cockpit (ATC) CLI plugin for `@abapify/adt-cli`.
6
+ ABAP Test Cockpit (ATC) CLI plugin for adt-cli.
6
7
 
7
- ## Installation
8
+ ## Install
8
9
 
9
10
  ```bash
10
- npm install @abapify/adt-atc
11
+ npm install @abapify/adt-atc @abapify/adt-cli
11
12
  ```
12
13
 
13
14
  ## Usage
14
15
 
15
- ### As a CLI Plugin
16
+ Register the command plugin in your project config:
16
17
 
17
- Add to your `adt.config.ts`:
18
-
19
- ```typescript
18
+ ```ts
19
+ // adt.config.ts
20
20
  export default {
21
21
  commands: ['@abapify/adt-atc/commands/atc'],
22
22
  };
23
23
  ```
24
24
 
25
- Then use the command:
25
+ Then:
26
26
 
27
27
  ```bash
28
- # Run ATC on a transport
29
- adt atc -t TRLK942970
30
-
31
- # Run ATC on a package
32
- adt atc -p ZMY_PACKAGE
33
-
34
- # Run ATC on a specific object
35
- adt atc -o /sap/bc/adt/oo/classes/zcl_my_class
36
-
37
- # Output as SARIF (for GitHub Code Scanning)
38
- adt atc -t TRLK942970 --format sarif --output atc-results.sarif
39
-
40
- # Output as GitLab Code Quality
41
- adt atc -t TRLK942970 --format gitlab --output gl-code-quality.json
42
- ```
43
-
44
- ### Options
45
-
46
- | Option | Description |
47
- | ----------------------------- | ---------------------------------------------------- |
48
- | `-p, --package <package>` | Run ATC on package |
49
- | `-o, --object <uri>` | Run ATC on specific object |
50
- | `-t, --transport <transport>` | Run ATC on transport request |
51
- | `--variant <variant>` | ATC check variant (default: from system customizing) |
52
- | `--max-results <number>` | Maximum number of results (default: 100) |
53
- | `--format <format>` | Output format: console, json, gitlab, sarif |
54
- | `--output <file>` | Output file (required for gitlab/sarif format) |
55
- | `--resolver <name>` | Resolve report paths (built-in: `abapgit`) |
56
-
57
- ### Programmatic Usage
58
-
59
- ```typescript
60
- import {
61
- atcCommand,
62
- outputSarifReport,
63
- outputGitLabCodeQuality,
64
- } from '@abapify/adt-atc';
65
- import type { AtcResult, AtcFinding } from '@abapify/adt-atc';
66
-
67
- // Use the formatters directly
68
- const result: AtcResult = {
69
- checkVariant: 'DEFAULT',
70
- totalFindings: 1,
71
- errorCount: 0,
72
- warningCount: 1,
73
- infoCount: 0,
74
- findings: [
75
- {
76
- checkId: 'CL_CI_TEST_SELECT',
77
- checkTitle: 'Analysis of WHERE Condition for SELECT',
78
- messageId: '001',
79
- priority: 2,
80
- messageText: 'Table SFLIGHT: No WHERE condition',
81
- objectUri: '/sap/bc/adt/oo/classes/zcl_my_class',
82
- objectType: 'CLAS',
83
- objectName: 'ZCL_MY_CLASS',
84
- location: '/sap/bc/adt/oo/classes/zcl_my_class/source/main#start=10,0',
85
- // Optional: resolved by the atc command from the run's check variant
86
- // form template (check categories are best-effort enrichment)
87
- checkCategory: 'CI_PERFORMANCE',
88
- },
89
- ],
90
- };
91
-
92
- await outputSarifReport(result, 'atc-results.sarif');
93
- await outputGitLabCodeQuality(result, 'gl-code-quality.json');
94
- ```
95
-
96
- ## Check Categories
97
-
98
- Each finding can carry a `checkCategory` (e.g., `CI_PERFORMANCE`,
99
- `CI_SECURITY`): the name of the ATC check category its check is assigned
100
- to. The `atc` command resolves it automatically from the run's check
101
- variant form template (`GET /sap/bc/adt/atc/checkvariants/formtemplate`)
102
- by resolving each finding's documentation page to its implementing class
103
- (the worklist `checkId` is a GUID), then mapping that class to a category.
104
- The `enrichResultWithCategories` helper only does a direct `checkId` lookup;
105
- the command uses the documentation bridge for normal worklist findings.
106
- This is best-effort: unavailable templates, unreadable pages, or uncatalogued
107
- checks leave the category unset without failing the run. Categories are
108
- resolved from the variant's catalog as fetched after the run, so they
109
- reflect the variant configuration in effect at that time (including any
110
- edits made in the meantime).
111
-
112
- The enrichment helpers are exported for reuse:
113
-
114
- ```typescript
115
- import {
116
- parseCheckCategoryIndex,
117
- fetchCheckCategoryIndex,
118
- enrichResultWithCategories,
119
- } from '@abapify/adt-atc';
120
-
121
- // Build the implementing class -> category name index from a parsed form template
122
- const index = parseCheckCategoryIndex(formTemplateDocument);
123
-
124
- // Or fetch it through an ADT client (never throws)
125
- const fetched = await fetchCheckCategoryIndex(client, 'DEFAULT');
126
-
127
- // Enrich a result in place (direct lookup: only resolves findings whose
128
- // checkId is already the implementing class name; normal worklist findings
129
- // are enriched by the command via the documentation bridge)
130
- enrichResultWithCategories(result, index);
28
+ adt atc --package ZMY_PKG --format sarif --output atc.sarif
131
29
  ```
132
30
 
133
- The GitLab Code Quality formatter emits the category as `check_category`
134
- per finding; the SARIF formatter sets it on each rule's
135
- `properties.category`.
136
-
137
- ## Output Formats
138
-
139
- ### Console (default)
140
-
141
- Human-readable output with emoji indicators and clickable ADT links (in supported terminals).
142
-
143
- ### JSON
144
-
145
- Raw JSON output of the ATC results for further processing.
146
-
147
- ### SARIF
148
-
149
- [SARIF](https://sarifweb.azurewebsites.net/) format for GitHub Code Scanning integration.
31
+ ## Features
150
32
 
151
- ### GitLab Code Quality
33
+ - Run ABAP Test Cockpit checks on packages, objects, or transports
34
+ - Console, JSON, GitLab Code Quality, and SARIF output
35
+ - Custom check variants and finding resolvers for path / line mapping
152
36
 
153
- [GitLab Code Quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html) format for GitLab CI/CD integration.
37
+ ## Documentation
154
38
 
155
- For abapGit repositories, add `--resolver abapgit`. The resolver uses the
156
- full ATC location URI to map compound objects such as function modules to
157
- their serialized filenames, then scans the configured source tree so both
158
- PREFIX and FULL folder logic point to the actual Git path. When multiple
159
- files share the same basename (e.g. multiple `package.devc.xml` files in
160
- different package directories), the resolver rejects the ambiguous match
161
- rather than guessing.
39
+ **[adt-cli.netlify.app/sdk/packages/adt-atc](https://adt-cli.netlify.app/sdk/packages/adt-atc)**
162
40
 
163
- ## License
41
+ ---
164
42
 
165
- MIT
43
+ Part of [**abapify**](https://github.com/abapify/adt-cli) — open, typed tooling for SAP ABAP Development Tools (ADT): CLI, MCP server, and SDK.
@@ -160,7 +160,7 @@ function toPosix(pathValue) {
160
160
  return sep === "/" ? pathValue : pathValue.split(sep).join("/");
161
161
  }
162
162
  function normalizeStartingFolder(value) {
163
- return (value || "src").trim().replaceAll("\\", "/").replace(/^\/+|\/+$/g, "") || "src";
163
+ return (value || "src").trim().replaceAll("\\", "/").replace(/^\/+/, "").replace(/(?<!\/)\/+$/, "") || "src";
164
164
  }
165
165
  function parseFolderLogic(value) {
166
166
  const normalized = value?.trim().toLowerCase();
@@ -172,8 +172,8 @@ function parseAbapGitMetadata(root) {
172
172
  try {
173
173
  const xml = readFileSync(metadataPath, "utf8");
174
174
  return {
175
- folderLogic: parseFolderLogic(xml.match(/<FOLDER_LOGIC>\s*([^<]+?)\s*<\/FOLDER_LOGIC>/i)?.[1]),
176
- startingFolder: normalizeStartingFolder(xml.match(/<STARTING_FOLDER>\s*([^<]+?)\s*<\/STARTING_FOLDER>/i)?.[1])
175
+ folderLogic: parseFolderLogic(xml.match(/<FOLDER_LOGIC>([^<]*)<\/FOLDER_LOGIC>/i)?.[1]),
176
+ startingFolder: normalizeStartingFolder(xml.match(/<STARTING_FOLDER>([^<]*)<\/STARTING_FOLDER>/i)?.[1])
177
177
  };
178
178
  } catch {
179
179
  return;
@@ -186,7 +186,7 @@ function parseConfiguredFormat(root) {
186
186
  join(root, "adt.config.js"),
187
187
  join(root, "adt.config.mjs")
188
188
  ].filter((value) => Boolean(value));
189
- for (const configPath of [...new Set(configPaths.map((value) => resolve(value)))]) {
189
+ for (const configPath of new Set(configPaths.map((value) => resolve(value)))) {
190
190
  if (!existsSync(configPath)) continue;
191
191
  try {
192
192
  const config = readFileSync(configPath, "utf8");
@@ -269,7 +269,7 @@ function parseMethodRanges(lines) {
269
269
  let currentMethod = null;
270
270
  let methodStart = 0;
271
271
  for (let i = 0; i < lines.length; i++) {
272
- const match = lines[i].match(/^\s*METHOD\s+(\w+)/i);
272
+ const match = /^\s*METHOD\s+(\w+)/i.exec(lines[i]);
273
273
  if (match) {
274
274
  currentMethod = match[1].toLowerCase();
275
275
  methodStart = i + 1;
@@ -328,21 +328,20 @@ function createAbapGitResolver(srcRoot = "src/") {
328
328
  const startingFolder = metadata?.startingFolder ?? "src";
329
329
  console.log(`📂 Finding resolver: ${lookup.size} files indexed (format=${format}, folderLogic=${folderLogic}, startingFolder=${startingFolder})`);
330
330
  }
331
- return { async resolve(objectType, objectName, atcLine, methodName, atcLocation) {
332
- const candidateSegments = ((atcLocation ? adtUriToAbapGitPath(atcLocation) : null) ?? `${objectName.toLowerCase()}.${objectType.toLowerCase()}.abap`).split("/");
333
- const expectedFilename = candidateSegments[candidateSegments.length - 1];
334
- if (!expectedFilename) return null;
331
+ return { resolve(objectType, objectName, atcLine, methodName, atcLocation) {
332
+ const expectedFilename = ((atcLocation ? adtUriToAbapGitPath(atcLocation) : null) ?? `${objectName.toLowerCase()}.${objectType.toLowerCase()}.abap`).split("/").at(-1);
333
+ if (!expectedFilename) return Promise.resolve(null);
335
334
  const resolvedPath = lookup.get(expectedFilename.toLowerCase());
336
- if (!resolvedPath) return null;
335
+ if (!resolvedPath) return Promise.resolve(null);
337
336
  const fileLine = convertLine(atcLine, methodName, resolvedPath);
338
337
  const gitPath = toPosix(relative(repository.root, resolvedPath));
339
- if (!gitPath || gitPath.startsWith("../")) return null;
340
- return {
338
+ if (!gitPath || gitPath.startsWith("../")) return Promise.resolve(null);
339
+ return Promise.resolve({
341
340
  path: gitPath,
342
341
  line: fileLine
343
- };
342
+ });
344
343
  } };
345
344
  }
346
345
  export { createAbapGitResolver as n, adtUriToAbapGitPath as r, abapgit_exports as t };
347
346
 
348
- //# sourceMappingURL=abapgit-B2E5I-u0.mjs.map
347
+ //# sourceMappingURL=abapgit-DrCkrhFZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abapgit-DrCkrhFZ.mjs","names":["resolvePath"],"sources":["../src/resolvers/adt-uri-to-abapgit-path.ts","../src/resolvers/abapgit.ts"],"sourcesContent":["/**\n * Convert an ADT URI to an on-disk abapGit filename.\n *\n * This mapper belongs next to the ATC resolver because ATC findings carry\n * ADT locations while the GitLab report needs an abapGit file name. The\n * abapGit format plugin re-exports it for consumers that already use that\n * package.\n */\n\n/** Apply abapGit namespace transform: `/MYNS/zcl_foo` → `(myns)zcl_foo`. */\nfunction applyNamespace(raw: string): string {\n const nsMatch = /^\\/([^/]+)\\/(.*)$/.exec(raw);\n if (nsMatch) {\n const [, ns, rest] = nsMatch;\n return `(${ns.toLowerCase()})${rest.toLowerCase()}`;\n }\n return raw.toLowerCase();\n}\n\nfunction stripFragment(uri: string): string {\n const hashIndex = uri.indexOf('#');\n return hashIndex >= 0 ? uri.slice(0, hashIndex) : uri;\n}\n\n// Class include → abapGit suffix. Kept here so the built-in ATC package does\n// not depend on the format plugin (which depends on @abapify/adt-atc).\n// `main` maps to the primary class source (no suffix).\nconst CLASS_INCLUDE_TO_SUFFIX: Record<string, string | undefined> = {\n testclasses: 'testclasses',\n definitions: 'locals_def',\n implementations: 'locals_imp',\n macros: 'macros',\n localtypes: 'locals_types',\n};\n\ninterface Rule {\n test: RegExp;\n build: (match: RegExpMatchArray) => string;\n}\n\n// Order matters – most specific first.\nconst RULES: Rule[] = [\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/includes\\/([^/]+)$/,\n build: (match) => {\n const name = applyNamespace(decodeURIComponent(match[1]));\n const include = match[2].toLowerCase();\n if (include === 'main') return `src/${name}.clas.abap`;\n const suffix = CLASS_INCLUDE_TO_SUFFIX[include];\n return suffix\n ? `src/${name}.clas.${suffix}.abap`\n : `src/${name}.clas.${include}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/source\\/main$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n\n // Interfaces\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/interfaces\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.intf.abap`,\n },\n\n // Programs\n {\n test: /^\\/sap\\/bc\\/adt\\/programs\\/programs\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.prog.abap`,\n },\n\n // Function group metadata and source components\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/source\\/main$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n return `src/${group}.fugr.l${group}top.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.fugr.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/fmodules\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const functionModule = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${functionModule}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/includes\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const include = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${include}.abap`;\n },\n },\n\n // CDS and RAP source objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/ddl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.ddls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/acm\\/dcl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dcls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/bo\\/behaviordefinitions\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.bdef.abdl`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/srvd\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvd.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/businessservices\\/bindings\\/([^/]+?)(?:\\/publishedstates)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvb.xml`,\n },\n\n // DDIC objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/domains\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.doma.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/dataelements\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dtel.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/(?:structures|tables)\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.tabl.xml`,\n },\n\n // Package — abapGit serializes every package to the fixed filename\n // `package.devc.xml`, regardless of the package name.\n {\n test: /^\\/sap\\/bc\\/adt\\/packages\\/([^/]+)$/,\n build: () => 'src/package.devc.xml',\n },\n];\n\n/**\n * Convert an ADT URI to the abapGit on-disk filename. The returned path is\n * rooted at the configured abapGit starting folder (`src/` by default);\n * callers resolve that prefix against the repository's actual source tree.\n */\nexport function adtUriToAbapGitPath(uri: string): string | null {\n if (!uri) return null;\n const clean = stripFragment(uri).trim();\n for (const rule of RULES) {\n const match = rule.test.exec(clean);\n if (match) {\n try {\n return rule.build(match);\n } catch (error) {\n if (error instanceof URIError) return null;\n throw error;\n }\n }\n }\n return null;\n}\n","/**\n * Built-in abapGit Finding Resolver.\n *\n * ATC identifies a finding by an ADT URI, while GitLab needs the path of the\n * serialized abapGit file. The resolver owns that format-specific mapping and\n * scans the checked-out repository so PREFIX and FULL layouts both resolve to\n * the path that actually exists in Git.\n */\n\nimport {\n existsSync,\n readFileSync,\n readdirSync,\n realpathSync,\n type Dirent,\n} from 'node:fs';\nimport {\n basename,\n join,\n relative,\n resolve as resolvePath,\n sep,\n} from 'node:path';\nimport type { FindingResolver, ResolvedLocation } from '../types';\nimport { adtUriToAbapGitPath } from './adt-uri-to-abapgit-path';\n\ntype FolderLogic = 'prefix' | 'full' | 'full-with-root';\n\ninterface AbapGitMetadata {\n folderLogic?: FolderLogic;\n startingFolder: string;\n}\n\ninterface ResolverRepository {\n root: string;\n sourceRoot: string;\n metadata?: AbapGitMetadata;\n configuredFormat?: string;\n configuredFolderLogic?: FolderLogic;\n}\n\nfunction toPosix(pathValue: string): string {\n return sep === '/' ? pathValue : pathValue.split(sep).join('/');\n}\n\nfunction normalizeStartingFolder(value: string | undefined): string {\n const normalized = (value || 'src')\n .trim()\n .replaceAll('\\\\', '/')\n .replace(/^\\/+/, '')\n .replace(/(?<!\\/)\\/+$/, '');\n return normalized || 'src';\n}\n\nfunction parseFolderLogic(value: string | undefined): FolderLogic | undefined {\n const normalized = value?.trim().toLowerCase();\n if (\n normalized === 'prefix' ||\n normalized === 'full' ||\n normalized === 'full-with-root'\n ) {\n return normalized;\n }\n return undefined;\n}\n\nfunction parseAbapGitMetadata(root: string): AbapGitMetadata | undefined {\n const metadataPath = join(root, '.abapgit.xml');\n if (!existsSync(metadataPath)) return undefined;\n\n try {\n const xml = readFileSync(metadataPath, 'utf8');\n return {\n folderLogic: parseFolderLogic(\n xml.match(/<FOLDER_LOGIC>([^<]*)<\\/FOLDER_LOGIC>/i)?.[1],\n ),\n startingFolder: normalizeStartingFolder(\n xml.match(/<STARTING_FOLDER>([^<]*)<\\/STARTING_FOLDER>/i)?.[1],\n ),\n };\n } catch {\n return undefined;\n }\n}\n\nfunction parseConfiguredFormat(root: string): {\n format?: string;\n folderLogic?: FolderLogic;\n} {\n const configPaths = [\n process.env.ADT_CONFIG_PATH,\n join(root, 'adt.config.ts'),\n join(root, 'adt.config.js'),\n join(root, 'adt.config.mjs'),\n ].filter((value): value is string => Boolean(value));\n\n for (const configPath of new Set(\n configPaths.map((value) => resolvePath(value)),\n )) {\n if (!existsSync(configPath)) continue;\n try {\n const config = readFileSync(configPath, 'utf8');\n const formatId = config.match(\n /\\bformat\\s*:\\s*\\{[\\s\\S]*?\\bid\\s*:\\s*['\"]([^'\"]+)['\"]/i,\n )?.[1];\n const folderLogic = parseFolderLogic(\n config.match(/\\bfolderLogic\\s*:\\s*['\"]([^'\"]+)['\"]/i)?.[1],\n );\n if (formatId || folderLogic) {\n return {\n ...(formatId ? { format: formatId.trim().toLowerCase() } : {}),\n ...(folderLogic ? { folderLogic } : {}),\n };\n }\n } catch {\n // An unreadable optional config must not disable report generation.\n }\n }\n return {};\n}\n\nfunction findRepositoryRoot(): string {\n const configuredRoot =\n process.env.CI_PROJECT_DIR?.trim() || process.env.SOURCE_REPO_DIR?.trim();\n if (configuredRoot) return resolvePath(configuredRoot);\n\n // Preserve the old API behaviour for callers that pass an explicit source\n // path: paths are still reported relative to the current working directory.\n return process.cwd();\n}\n\nfunction resolveRepository(srcRoot: string): ResolverRepository {\n const usesConfiguredSourceRoot = srcRoot === 'src/' || srcRoot === 'src';\n const root = usesConfiguredSourceRoot ? findRepositoryRoot() : process.cwd();\n const metadata = parseAbapGitMetadata(root);\n const configured = parseConfiguredFormat(root);\n const configuredSourceFolder = metadata?.startingFolder ?? 'src';\n const sourcePath = usesConfiguredSourceRoot\n ? join(root, configuredSourceFolder)\n : resolvePath(process.cwd(), srcRoot);\n\n // Guard against path traversal via STARTING_FOLDER — the resolved source\n // root must stay inside the repository root. Use realpath to resolve\n // symlinks before the containment check. If it escapes, fall back to\n // the default `src` folder. Guard existsSync before realpathSync so a\n // missing source folder does not throw and abort report generation.\n const realRoot = existsSync(root) ? realpathSync(root) : root;\n const realSourcePath = existsSync(sourcePath)\n ? realpathSync(sourcePath)\n : sourcePath;\n const safeSourcePath =\n realSourcePath === realRoot || realSourcePath.startsWith(realRoot + sep)\n ? sourcePath\n : join(root, 'src');\n\n return {\n root,\n sourceRoot: safeSourcePath,\n metadata,\n configuredFormat: configured.format,\n configuredFolderLogic: configured.folderLogic,\n };\n}\n\n// ── Source tree and file contents ───────────────────────────────────────\n\nconst fileCache = new Map<string, string[]>();\n\nconst SOURCE_EXTENSIONS = ['.abap', '.xml', '.acds', '.abdl'];\n\nfunction hasSourceExtension(filename: string): boolean {\n return SOURCE_EXTENSIONS.some((ext) => filename.endsWith(ext));\n}\n\nfunction indexEntry(\n entry: Dirent,\n current: string,\n result: string[],\n stack: string[],\n): void {\n const fullPath = join(current, entry.name);\n if (entry.isDirectory()) {\n stack.push(fullPath);\n } else if (entry.isFile() && hasSourceExtension(entry.name)) {\n result.push(fullPath);\n }\n}\n\nfunction collectSourceFiles(root: string): string[] {\n const result: string[] = [];\n const stack: string[] = [root];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n let entries: Dirent[];\n try {\n entries = readdirSync(current, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n indexEntry(entry, current, result, stack);\n }\n }\n\n return result;\n}\n\nfunction getFileLines(filePath: string): string[] | null {\n const cached = fileCache.get(filePath);\n if (cached) return cached;\n try {\n const lines = readFileSync(filePath, 'utf8').split('\\n');\n fileCache.set(filePath, lines);\n return lines;\n } catch {\n return null;\n }\n}\n\n// ── Method range parsing ────────────────────────────────────────────────\n\ninterface MethodRange {\n name: string;\n startLine: number;\n length: number;\n}\n\nfunction parseMethodRanges(lines: string[]): MethodRange[] {\n const ranges: MethodRange[] = [];\n let currentMethod: string | null = null;\n let methodStart = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const match = /^\\s*METHOD\\s+(\\w+)/i.exec(lines[i]);\n if (match) {\n currentMethod = match[1].toLowerCase();\n methodStart = i + 1;\n }\n if (currentMethod && /^\\s*ENDMETHOD/i.test(lines[i])) {\n const endLine = i + 1;\n ranges.push({\n name: currentMethod,\n startLine: methodStart,\n length: endLine - methodStart + 1,\n });\n currentMethod = null;\n }\n }\n return ranges;\n}\n\nfunction convertLine(\n atcLine: number,\n methodName: string | undefined,\n filePath: string,\n): number {\n if (!filePath.endsWith('.clas.abap')) return atcLine;\n\n const lines = getFileLines(filePath);\n if (!lines) return atcLine;\n\n const ranges = parseMethodRanges(lines);\n if (ranges.length === 0) return atcLine;\n\n if (methodName) {\n const method = ranges.find(\n (range) => range.name === methodName.toLowerCase(),\n );\n if (method) return method.startLine + atcLine - 1;\n }\n\n // Without a method name the ATC line is already relative to the class\n // file, so return it as-is instead of guessing a method.\n return atcLine;\n}\n\n// ── Resolver factory ────────────────────────────────────────────────────\n\n/**\n * Create a built-in abapGit finding resolver.\n *\n * The full ATC location is used to derive the canonical abapGit basename.\n * The actual checked-out path is then selected from the configured source\n * tree, which naturally preserves PREFIX/FULL package directories and the\n * repository's STARTING_FOLDER.\n */\nexport function createAbapGitResolver(srcRoot = 'src/'): FindingResolver {\n const repository = resolveRepository(srcRoot);\n const lookup = new Map<string, string>();\n\n try {\n if (existsSync(repository.sourceRoot)) {\n const files = collectSourceFiles(repository.sourceRoot).sort(\n (left, right) => left.localeCompare(right),\n );\n\n const ambiguous = new Set<string>();\n for (const filePath of files) {\n const name = basename(filePath).toLowerCase();\n if (lookup.has(name)) {\n ambiguous.add(name);\n } else {\n lookup.set(name, filePath);\n }\n }\n for (const name of ambiguous) lookup.delete(name);\n }\n } catch {\n // Source scan failed — resolver will return null for all findings.\n }\n\n if (lookup.size > 0) {\n const metadata = repository.metadata;\n const format = repository.configuredFormat ?? 'abapgit';\n const folderLogic =\n metadata?.folderLogic ?? repository.configuredFolderLogic ?? 'prefix';\n const startingFolder = metadata?.startingFolder ?? 'src';\n console.log(\n `📂 Finding resolver: ${lookup.size} files indexed (format=${format}, folderLogic=${folderLogic}, startingFolder=${startingFolder})`,\n );\n }\n\n return {\n resolve(\n objectType: string,\n objectName: string,\n atcLine: number,\n methodName?: string,\n atcLocation?: string,\n ): Promise<ResolvedLocation | null> {\n const canonicalPath = atcLocation\n ? adtUriToAbapGitPath(atcLocation)\n : null;\n const candidatePath =\n canonicalPath ??\n `${objectName.toLowerCase()}.${objectType.toLowerCase()}.abap`;\n const candidateSegments = candidatePath.split('/');\n const expectedFilename = candidateSegments.at(-1);\n if (!expectedFilename) return Promise.resolve(null);\n const resolvedPath = lookup.get(expectedFilename.toLowerCase());\n\n if (!resolvedPath) return Promise.resolve(null);\n\n const fileLine = convertLine(atcLine, methodName, resolvedPath);\n const gitPath = toPosix(relative(repository.root, resolvedPath));\n if (!gitPath || gitPath.startsWith('../')) return Promise.resolve(null);\n\n return Promise.resolve({ path: gitPath, line: fileLine });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAUA,SAAS,eAAe,KAAqB;CAC3C,MAAM,UAAU,oBAAoB,KAAK,GAAG;CAC5C,IAAI,SAAS;EACX,MAAM,GAAG,IAAI,QAAQ;EACrB,OAAO,IAAI,GAAG,YAAY,EAAE,GAAG,KAAK,YAAY;CAClD;CACA,OAAO,IAAI,YAAY;AACzB;AAEA,SAAS,cAAc,KAAqB;CAC1C,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,OAAO,aAAa,IAAI,IAAI,MAAM,GAAG,SAAS,IAAI;AACpD;AAKA,MAAM,0BAA8D;CAClE,aAAa;CACb,aAAa;CACb,iBAAiB;CACjB,QAAQ;CACR,YAAY;AACd;AAQA,MAAM,QAAgB;CACpB;EACE,MAAM;EACN,QAAQ,UAAU;GAChB,MAAM,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC;GACxD,MAAM,UAAU,MAAM,EAAE,CAAC,YAAY;GACrC,IAAI,YAAY,QAAQ,OAAO,OAAO,KAAK;GAC3C,MAAM,SAAS,wBAAwB;GACvC,OAAO,SACH,OAAO,KAAK,QAAQ,OAAO,SAC3B,OAAO,KAAK,QAAQ,QAAQ;EAClC;CACF;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CAGA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CAGA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CAGA;EACE,MAAM;EACN,QAAQ,UAAU;GAChB,MAAM,QAAQ,eAAe,mBAAmB,MAAM,EAAE,CAAC;GACzD,OAAO,OAAO,MAAM,SAAS,MAAM;EACrC;CACF;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UAAU;GAGhB,OAAO,OAFO,eAAe,mBAAmB,MAAM,EAAE,CAEtC,EAAE,QADG,eAAe,mBAAmB,MAAM,EAAE,CACxB,EAAE;EAC7C;CACF;CACA;EACE,MAAM;EACN,QAAQ,UAAU;GAGhB,OAAO,OAFO,eAAe,mBAAmB,MAAM,EAAE,CAEtC,EAAE,QADJ,eAAe,mBAAmB,MAAM,EAAE,CACxB,EAAE;EACtC;CACF;CAGA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CAGA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CACA;EACE,MAAM;EACN,QAAQ,UACN,OAAO,eAAe,mBAAmB,MAAM,EAAE,CAAC,EAAE;CACxD;CAIA;EACE,MAAM;EACN,aAAa;CACf;AACF;;;;;;AAOA,SAAgB,oBAAoB,KAA4B;CAC9D,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,QAAQ,cAAc,GAAG,CAAC,CAAC,KAAK;CACtC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK;EAClC,IAAI,OACF,IAAI;GACF,OAAO,KAAK,MAAM,KAAK;EACzB,SAAS,OAAO;GACd,IAAI,iBAAiB,UAAU,OAAO;GACtC,MAAM;EACR;CAEJ;CACA,OAAO;AACT;;;;;;;;;;;AC5IA,SAAS,QAAQ,WAA2B;CAC1C,OAAO,QAAQ,MAAM,YAAY,UAAU,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;AAChE;AAEA,SAAS,wBAAwB,OAAmC;CAMlE,QALoB,SAAS,MAAA,CAC1B,KAAK,CAAC,CACN,WAAW,MAAM,GAAG,CAAC,CACrB,QAAQ,QAAQ,EAAE,CAAC,CACnB,QAAQ,eAAe,EACV,KAAK;AACvB;AAEA,SAAS,iBAAiB,OAAoD;CAC5E,MAAM,aAAa,OAAO,KAAK,CAAC,CAAC,YAAY;CAC7C,IACE,eAAe,YACf,eAAe,UACf,eAAe,kBAEf,OAAO;AAGX;AAEA,SAAS,qBAAqB,MAA2C;CACvE,MAAM,eAAe,KAAK,MAAM,cAAc;CAC9C,IAAI,CAAC,WAAW,YAAY,GAAG,OAAO,KAAA;CAEtC,IAAI;EACF,MAAM,MAAM,aAAa,cAAc,MAAM;EAC7C,OAAO;GACL,aAAa,iBACX,IAAI,MAAM,wCAAwC,CAAC,GAAG,EACxD;GACA,gBAAgB,wBACd,IAAI,MAAM,8CAA8C,CAAC,GAAG,EAC9D;EACF;CACF,QAAQ;EACN;CACF;AACF;AAEA,SAAS,sBAAsB,MAG7B;CACA,MAAM,cAAc;EAClB,QAAQ,IAAI;EACZ,KAAK,MAAM,eAAe;EAC1B,KAAK,MAAM,eAAe;EAC1B,KAAK,MAAM,gBAAgB;CAC7B,CAAC,CAAC,QAAQ,UAA2B,QAAQ,KAAK,CAAC;CAEnD,KAAK,MAAM,cAAc,IAAI,IAC3B,YAAY,KAAK,UAAUA,QAAY,KAAK,CAAC,CAC/C,GAAG;EACD,IAAI,CAAC,WAAW,UAAU,GAAG;EAC7B,IAAI;GACF,MAAM,SAAS,aAAa,YAAY,MAAM;GAC9C,MAAM,WAAW,OAAO,MACtB,uDACF,CAAC,GAAG;GACJ,MAAM,cAAc,iBAClB,OAAO,MAAM,uCAAuC,CAAC,GAAG,EAC1D;GACA,IAAI,YAAY,aACd,OAAO;IACL,GAAI,WAAW,EAAE,QAAQ,SAAS,KAAK,CAAC,CAAC,YAAY,EAAE,IAAI,CAAC;IAC5D,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;GACvC;EAEJ,QAAQ,CAER;CACF;CACA,OAAO,CAAC;AACV;AAEA,SAAS,qBAA6B;CACpC,MAAM,iBACJ,QAAQ,IAAI,gBAAgB,KAAK,KAAK,QAAQ,IAAI,iBAAiB,KAAK;CAC1E,IAAI,gBAAgB,OAAOA,QAAY,cAAc;CAIrD,OAAO,QAAQ,IAAI;AACrB;AAEA,SAAS,kBAAkB,SAAqC;CAC9D,MAAM,2BAA2B,YAAY,UAAU,YAAY;CACnE,MAAM,OAAO,2BAA2B,mBAAmB,IAAI,QAAQ,IAAI;CAC3E,MAAM,WAAW,qBAAqB,IAAI;CAC1C,MAAM,aAAa,sBAAsB,IAAI;CAC7C,MAAM,yBAAyB,UAAU,kBAAkB;CAC3D,MAAM,aAAa,2BACf,KAAK,MAAM,sBAAsB,IACjCA,QAAY,QAAQ,IAAI,GAAG,OAAO;CAOtC,MAAM,WAAW,WAAW,IAAI,IAAI,aAAa,IAAI,IAAI;CACzD,MAAM,iBAAiB,WAAW,UAAU,IACxC,aAAa,UAAU,IACvB;CAMJ,OAAO;EACL;EACA,YANA,mBAAmB,YAAY,eAAe,WAAW,WAAW,GAAG,IACnE,aACA,KAAK,MAAM,KAAK;EAKpB;EACA,kBAAkB,WAAW;EAC7B,uBAAuB,WAAW;CACpC;AACF;AAIA,MAAM,4BAAY,IAAI,IAAsB;AAE5C,MAAM,oBAAoB;CAAC;CAAS;CAAQ;CAAS;AAAO;AAE5D,SAAS,mBAAmB,UAA2B;CACrD,OAAO,kBAAkB,MAAM,QAAQ,SAAS,SAAS,GAAG,CAAC;AAC/D;AAEA,SAAS,WACP,OACA,SACA,QACA,OACM;CACN,MAAM,WAAW,KAAK,SAAS,MAAM,IAAI;CACzC,IAAI,MAAM,YAAY,GACpB,MAAM,KAAK,QAAQ;MACd,IAAI,MAAM,OAAO,KAAK,mBAAmB,MAAM,IAAI,GACxD,OAAO,KAAK,QAAQ;AAExB;AAEA,SAAS,mBAAmB,MAAwB;CAClD,MAAM,SAAmB,CAAC;CAC1B,MAAM,QAAkB,CAAC,IAAI;CAE7B,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,UAAU,MAAM,IAAI;EAC1B,IAAI,CAAC,SAAS;EACd,IAAI;EACJ,IAAI;GACF,UAAU,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;EACxD,QAAQ;GACN;EACF;EACA,KAAK,MAAM,SAAS,SAClB,WAAW,OAAO,SAAS,QAAQ,KAAK;CAE5C;CAEA,OAAO;AACT;AAEA,SAAS,aAAa,UAAmC;CACvD,MAAM,SAAS,UAAU,IAAI,QAAQ;CACrC,IAAI,QAAQ,OAAO;CACnB,IAAI;EACF,MAAM,QAAQ,aAAa,UAAU,MAAM,CAAC,CAAC,MAAM,IAAI;EACvD,UAAU,IAAI,UAAU,KAAK;EAC7B,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAUA,SAAS,kBAAkB,OAAgC;CACzD,MAAM,SAAwB,CAAC;CAC/B,IAAI,gBAA+B;CACnC,IAAI,cAAc;CAElB,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,QAAQ,sBAAsB,KAAK,MAAM,EAAE;EACjD,IAAI,OAAO;GACT,gBAAgB,MAAM,EAAE,CAAC,YAAY;GACrC,cAAc,IAAI;EACpB;EACA,IAAI,iBAAiB,iBAAiB,KAAK,MAAM,EAAE,GAAG;GACpD,MAAM,UAAU,IAAI;GACpB,OAAO,KAAK;IACV,MAAM;IACN,WAAW;IACX,QAAQ,UAAU,cAAc;GAClC,CAAC;GACD,gBAAgB;EAClB;CACF;CACA,OAAO;AACT;AAEA,SAAS,YACP,SACA,YACA,UACQ;CACR,IAAI,CAAC,SAAS,SAAS,YAAY,GAAG,OAAO;CAE7C,MAAM,QAAQ,aAAa,QAAQ;CACnC,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,SAAS,kBAAkB,KAAK;CACtC,IAAI,OAAO,WAAW,GAAG,OAAO;CAEhC,IAAI,YAAY;EACd,MAAM,SAAS,OAAO,MACnB,UAAU,MAAM,SAAS,WAAW,YAAY,CACnD;EACA,IAAI,QAAQ,OAAO,OAAO,YAAY,UAAU;CAClD;CAIA,OAAO;AACT;;;;;;;;;AAYA,SAAgB,sBAAsB,UAAU,QAAyB;CACvE,MAAM,aAAa,kBAAkB,OAAO;CAC5C,MAAM,yBAAS,IAAI,IAAoB;CAEvC,IAAI;EACF,IAAI,WAAW,WAAW,UAAU,GAAG;GACrC,MAAM,QAAQ,mBAAmB,WAAW,UAAU,CAAC,CAAC,MACrD,MAAM,UAAU,KAAK,cAAc,KAAK,CAC3C;GAEA,MAAM,4BAAY,IAAI,IAAY;GAClC,KAAK,MAAM,YAAY,OAAO;IAC5B,MAAM,OAAO,SAAS,QAAQ,CAAC,CAAC,YAAY;IAC5C,IAAI,OAAO,IAAI,IAAI,GACjB,UAAU,IAAI,IAAI;SAElB,OAAO,IAAI,MAAM,QAAQ;GAE7B;GACA,KAAK,MAAM,QAAQ,WAAW,OAAO,OAAO,IAAI;EAClD;CACF,QAAQ,CAER;CAEA,IAAI,OAAO,OAAO,GAAG;EACnB,MAAM,WAAW,WAAW;EAC5B,MAAM,SAAS,WAAW,oBAAoB;EAC9C,MAAM,cACJ,UAAU,eAAe,WAAW,yBAAyB;EAC/D,MAAM,iBAAiB,UAAU,kBAAkB;EACnD,QAAQ,IACN,wBAAwB,OAAO,KAAK,yBAAyB,OAAO,gBAAgB,YAAY,mBAAmB,eAAe,EACpI;CACF;CAEA,OAAO,EACL,QACE,YACA,YACA,SACA,YACA,aACkC;EAQlC,MAAM,qBAPgB,cAClB,oBAAoB,WAAW,IAC/B,SAGF,GAAG,WAAW,YAAY,EAAE,GAAG,WAAW,YAAY,EAAE,OAAA,CAClB,MAAM,GACL,CAAC,CAAC,GAAG,EAAE;EAChD,IAAI,CAAC,kBAAkB,OAAO,QAAQ,QAAQ,IAAI;EAClD,MAAM,eAAe,OAAO,IAAI,iBAAiB,YAAY,CAAC;EAE9D,IAAI,CAAC,cAAc,OAAO,QAAQ,QAAQ,IAAI;EAE9C,MAAM,WAAW,YAAY,SAAS,YAAY,YAAY;EAC9D,MAAM,UAAU,QAAQ,SAAS,WAAW,MAAM,YAAY,CAAC;EAC/D,IAAI,CAAC,WAAW,QAAQ,WAAW,KAAK,GAAG,OAAO,QAAQ,QAAQ,IAAI;EAEtE,OAAO,QAAQ,QAAQ;GAAE,MAAM;GAAS,MAAM;EAAS,CAAC;CAC1D,EACF;AACF"}
@@ -6,13 +6,18 @@ import { writeFile } from "node:fs/promises";
6
6
  *
7
7
  * Outputs ATC findings in SARIF format for GitHub Code Scanning.
8
8
  */
9
+ function sarifLevel(priority) {
10
+ if (priority === 1) return "error";
11
+ if (priority === 2) return "warning";
12
+ return "note";
13
+ }
9
14
  async function outputSarifReport(result, outputFile, _targetName) {
10
15
  const rules = Array.from(new Map(result.findings.map((f) => [f.checkId, {
11
16
  id: f.checkId,
12
17
  name: f.checkTitle || f.checkId,
13
18
  shortDescription: { text: f.checkTitle || f.checkId },
14
19
  fullDescription: { text: f.checkTitle || f.checkId },
15
- defaultConfiguration: { level: f.priority === 1 ? "error" : f.priority === 2 ? "warning" : "note" },
20
+ defaultConfiguration: { level: sarifLevel(f.priority) },
16
21
  properties: {
17
22
  tags: ["abap", "code-quality"],
18
23
  precision: "high",
@@ -25,7 +30,7 @@ async function outputSarifReport(result, outputFile, _targetName) {
25
30
  const line = lineMatch ? Number.parseInt(lineMatch[1], 10) : 1;
26
31
  return {
27
32
  ruleId: finding.checkId,
28
- level: finding.priority === 1 ? "error" : finding.priority === 2 ? "warning" : "note",
33
+ level: sarifLevel(finding.priority),
29
34
  message: { text: finding.messageText },
30
35
  locations: [{ physicalLocation: {
31
36
  artifactLocation: { uri: filePath },
@@ -82,7 +87,7 @@ async function outputSarifReport(result, outputFile, _targetName) {
82
87
  */
83
88
  function extractMethodName(location) {
84
89
  if (!location) return void 0;
85
- const match = location.match(/\/methods\/(\w+)/i) || location.match(/[;?&]name=(\w+)/i);
90
+ const match = /\/methods\/(\w+)/i.exec(location) || /[;?&]name=(\w+)/i.exec(location);
86
91
  return match ? match[1].toLowerCase() : void 0;
87
92
  }
88
93
  function prefixPath(finding) {
@@ -240,7 +245,7 @@ function enrichResultWithCategories(result, index) {
240
245
  * @returns The check class name, or undefined when the page does not carry it
241
246
  */
242
247
  function parseCheckClassFromDocumentation(html) {
243
- return html.match(/Check\s*Class[^<]{0,80}<\/h3>\s*<p>\s*[^/<]{0,120}\/\s*((?:\/[A-Za-z0-9_]{1,8}\/)?[A-Za-z][A-Za-z0-9_]*)\s*\//u)?.[1];
248
+ return /Check\s*Class[^<]{0,80}<\/h3>\s*<p>\s*[^/<]{0,120}\/\s*((?:\/\w{1,8}\/)?[A-Za-z]\w*)\s*\//u.exec(html)?.[1];
244
249
  }
245
250
  /**
246
251
  * Only ATC documentation paths may be fetched — a worklist-provided URI
@@ -397,7 +402,9 @@ function displayAtcResults(result, systemName) {
397
402
  if (result.findings && result.findings.length > 0) {
398
403
  console.log(`\n📄 Findings:`);
399
404
  result.findings.slice(0, 5).forEach((finding) => {
400
- const priorityIcon = finding.priority === 1 ? "❌" : finding.priority === 2 ? "⚠️" : "ℹ️";
405
+ let priorityIcon = "ℹ️";
406
+ if (finding.priority === 1) priorityIcon = "❌";
407
+ else if (finding.priority === 2) priorityIcon = "⚠️";
401
408
  const objectLink = finding.location ? adtLink(finding.objectName, finding.location, systemName) : finding.objectName;
402
409
  console.log(` ${priorityIcon} ${finding.checkTitle || finding.checkId}`);
403
410
  console.log(` ${finding.messageText}`);
@@ -407,9 +414,91 @@ function displayAtcResults(result, systemName) {
407
414
  }
408
415
  console.log(`\n💡 Use --debug for detailed results`);
409
416
  }
417
+ /** Resolve the single ATC target from the mutually exclusive options. */
418
+ async function resolveTargets(options, ctx) {
419
+ if (options.fromFile) {
420
+ const { readFileSync } = await import("node:fs");
421
+ const targetUris = readFileSync(options.fromFile, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
422
+ if (targetUris.length === 0) {
423
+ ctx.logger.error(`❌ No objects found in ${options.fromFile}`);
424
+ process.exit(1);
425
+ }
426
+ const targetName = `${targetUris.length} objects from ${options.fromFile}`;
427
+ ctx.logger.info(`📄 Target: ${targetName}`);
428
+ return {
429
+ targetUris,
430
+ targetName
431
+ };
432
+ }
433
+ if (options.transport) {
434
+ const targetName = `Transport ${options.transport}`;
435
+ ctx.logger.info(`🚚 Target: ${targetName}`);
436
+ return {
437
+ targetUris: [`/sap/bc/adt/cts/transportrequests/${options.transport}`],
438
+ targetName
439
+ };
440
+ }
441
+ if (options.package) {
442
+ const targetName = `Package ${options.package.toUpperCase()}`;
443
+ ctx.logger.info(`📦 Target: ${targetName}`);
444
+ return {
445
+ targetUris: [`/sap/bc/adt/packages/${options.package.toUpperCase()}`],
446
+ targetName
447
+ };
448
+ }
449
+ const targetName = options.object;
450
+ ctx.logger.info(`📄 Target: ${targetName}`);
451
+ return {
452
+ targetUris: [options.object],
453
+ targetName
454
+ };
455
+ }
410
456
  /**
411
- * ATC Command Plugin
457
+ * Determine the check variant: the explicit `--variant`, the system
458
+ * default from ATC customizing, or `DEFAULT`.
412
459
  */
460
+ async function resolveCheckVariant(client, options, ctx) {
461
+ ctx.logger.info("📋 Reading ATC customizing...");
462
+ const customizing = await client.adt.atc.customizing.get();
463
+ if (options.variant) return options.variant;
464
+ const variantProp = customizing.customizing.properties.property?.find((p) => p.name === "systemCheckVariant");
465
+ return variantProp?.value ? variantProp.value : "DEFAULT";
466
+ }
467
+ /** Extract the worklist ID — SAP can return a plain string or an object. */
468
+ function extractWorklistId(worklistCreateResponse, ctx) {
469
+ if (typeof worklistCreateResponse === "string") {
470
+ const idMatch = /id="([^"]+)"/.exec(worklistCreateResponse);
471
+ return idMatch ? idMatch[1] : worklistCreateResponse.trim();
472
+ }
473
+ if ("worklist" in worklistCreateResponse && worklistCreateResponse.worklist?.id) return worklistCreateResponse.worklist.id;
474
+ if ("worklistRun" in worklistCreateResponse && worklistCreateResponse.worklistRun?.worklistId) return worklistCreateResponse.worklistRun.worklistId;
475
+ ctx.logger.error("❌ Failed to get worklist ID from response");
476
+ process.exit(1);
477
+ }
478
+ /** Load the finding resolver plugin when requested. */
479
+ async function loadFindingResolver(options, ctx) {
480
+ if (!options.resolver) return void 0;
481
+ const name = options.resolver;
482
+ if (name === "abapgit") {
483
+ const { createAbapGitResolver } = await import("./abapgit-DrCkrhFZ.mjs").then((n) => n.t);
484
+ return createAbapGitResolver();
485
+ }
486
+ try {
487
+ ctx.logger.info(`📂 Loading external resolver: ${name}`);
488
+ const mod = await import(name);
489
+ if (typeof mod.createFindingResolver === "function") return mod.createFindingResolver();
490
+ ctx.logger.warn(`⚠️ Module ${name} does not export createFindingResolver()`);
491
+ } catch (e) {
492
+ ctx.logger.warn(`⚠️ Failed to load resolver "${name}": ${e instanceof Error ? e.message : e}`);
493
+ }
494
+ }
495
+ /** Output the ATC results in the requested format. */
496
+ async function outputAtcResults(result, options, targetName, resolver, ctx) {
497
+ if (options.format === "json") console.log(JSON.stringify(result, null, 2));
498
+ else if (options.format === "gitlab" && options.output) await outputGitLabCodeQuality(result, options.output, { resolver });
499
+ else if (options.format === "sarif" && options.output) await outputSarifReport(result, options.output, targetName);
500
+ else displayAtcResults(result, ctx.adtSystemName);
501
+ }
413
502
  const atcCommand = {
414
503
  name: "atc",
415
504
  description: "Run ABAP Test Cockpit (ATC) checks",
@@ -480,51 +569,11 @@ const atcCommand = {
480
569
  }
481
570
  const client = await ctx.getAdtClient();
482
571
  ctx.logger.info("🔍 Running ABAP Test Cockpit checks...");
483
- let targetUris;
484
- let targetName;
485
- if (options.fromFile) {
486
- const { readFileSync } = await import("fs");
487
- targetUris = readFileSync(options.fromFile, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
488
- if (targetUris.length === 0) {
489
- ctx.logger.error(`❌ No objects found in ${options.fromFile}`);
490
- process.exit(1);
491
- }
492
- targetName = `${targetUris.length} objects from ${options.fromFile}`;
493
- ctx.logger.info(`📄 Target: ${targetName}`);
494
- } else if (options.transport) {
495
- targetUris = [`/sap/bc/adt/cts/transportrequests/${options.transport}`];
496
- targetName = `Transport ${options.transport}`;
497
- ctx.logger.info(`🚚 Target: ${targetName}`);
498
- } else if (options.package) {
499
- targetUris = [`/sap/bc/adt/packages/${options.package.toUpperCase()}`];
500
- targetName = `Package ${options.package.toUpperCase()}`;
501
- ctx.logger.info(`📦 Target: ${targetName}`);
502
- } else {
503
- targetUris = [options.object];
504
- targetName = options.object;
505
- ctx.logger.info(`📄 Target: ${targetName}`);
506
- }
507
- ctx.logger.info("📋 Reading ATC customizing...");
508
- const customizing = await client.adt.atc.customizing.get();
509
- let checkVariant = options.variant;
510
- if (!checkVariant) {
511
- const variantProp = customizing.customizing.properties.property?.find((p) => p.name === "systemCheckVariant");
512
- if (variantProp?.value) checkVariant = variantProp.value;
513
- else checkVariant = "DEFAULT";
514
- }
572
+ const { targetUris, targetName } = await resolveTargets(options, ctx);
573
+ const checkVariant = await resolveCheckVariant(client, options, ctx);
515
574
  ctx.logger.info(`🎯 Check variant: ${checkVariant}`);
516
575
  ctx.logger.info("📋 Creating ATC worklist...");
517
- const worklistCreateResponse = await client.adt.atc.worklists.create({ checkVariant });
518
- let worklistId;
519
- if (typeof worklistCreateResponse === "string") {
520
- const idMatch = worklistCreateResponse.match(/id="([^"]+)"/);
521
- worklistId = idMatch ? idMatch[1] : worklistCreateResponse.trim();
522
- } else if ("worklist" in worklistCreateResponse && worklistCreateResponse.worklist?.id) worklistId = worklistCreateResponse.worklist.id;
523
- else if ("worklistRun" in worklistCreateResponse && worklistCreateResponse.worklistRun?.worklistId) worklistId = worklistCreateResponse.worklistRun.worklistId;
524
- else {
525
- ctx.logger.error("❌ Failed to get worklist ID from response");
526
- process.exit(1);
527
- }
576
+ const worklistId = extractWorklistId(await client.adt.atc.worklists.create({ checkVariant }), ctx);
528
577
  ctx.logger.info(`📋 Worklist created: ${worklistId}`);
529
578
  ctx.logger.info("⏳ Running ATC analysis...");
530
579
  const runData = { run: {
@@ -542,28 +591,10 @@ const atcCommand = {
542
591
  if (enriched > 0) ctx.logger.info(`🏷️ Enriched ${enriched}/${result.totalFindings} finding(s) with check categories`);
543
592
  const bridged = await resolveCategoriesViaDocumentation(client, result.findings, categoryIndex, { logger: ctx.logger });
544
593
  if (bridged > 0) ctx.logger.info(`🏷 Bridged ${bridged} finding(s) to check categories via ATC documentation`);
545
- let resolver;
546
- if (options.resolver) {
547
- const name = options.resolver;
548
- if (name === "abapgit") {
549
- const { createAbapGitResolver } = await import("./abapgit-B2E5I-u0.mjs").then((n) => n.t);
550
- resolver = createAbapGitResolver();
551
- } else try {
552
- ctx.logger.info(`📂 Loading external resolver: ${name}`);
553
- const mod = await import(name);
554
- if (typeof mod.createFindingResolver === "function") resolver = mod.createFindingResolver();
555
- else ctx.logger.warn(`⚠️ Module ${name} does not export createFindingResolver()`);
556
- } catch (e) {
557
- ctx.logger.warn(`⚠️ Failed to load resolver "${name}": ${e instanceof Error ? e.message : e}`);
558
- }
559
- }
560
- if (options.format === "json") console.log(JSON.stringify(result, null, 2));
561
- else if (options.format === "gitlab" && options.output) await outputGitLabCodeQuality(result, options.output, { resolver });
562
- else if (options.format === "sarif" && options.output) await outputSarifReport(result, options.output, targetName);
563
- else displayAtcResults(result, ctx.adtSystemName);
594
+ await outputAtcResults(result, options, targetName, await loadFindingResolver(options, ctx), ctx);
564
595
  if (result.errorCount > 0) process.exit(1);
565
596
  }
566
597
  };
567
598
  export { enrichResultWithCategories as a, outputGitLabCodeQuality as c, enrichFindingsWithCategories as i, outputSarifReport as l, parseCheckClassFromDocumentation as n, fetchCheckCategoryIndex as o, resolveCategoriesViaDocumentation as r, parseCheckCategoryIndex as s, atcCommand as t };
568
599
 
569
- //# sourceMappingURL=atc-CIPiGB5o.mjs.map
600
+ //# sourceMappingURL=atc-kyCLseda.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atc-kyCLseda.mjs","names":[],"sources":["../src/formatters/sarif.ts","../src/formatters/gitlab.ts","../src/check-categories.ts","../src/documentation-bridge.ts","../src/commands/atc.ts"],"sourcesContent":["/**\n * SARIF Formatter\n *\n * Outputs ATC findings in SARIF format for GitHub Code Scanning.\n */\n\nimport { writeFile } from 'node:fs/promises';\nimport type { AtcResult, AtcFinding } from '../types';\n\nfunction sarifLevel(priority: number): 'error' | 'warning' | 'note' {\n if (priority === 1) return 'error';\n if (priority === 2) return 'warning';\n return 'note';\n}\n\nexport async function outputSarifReport(\n result: AtcResult,\n outputFile: string,\n _targetName?: string,\n): Promise<void> {\n // Transform ATC findings to SARIF format\n const rules = Array.from(\n new Map(\n result.findings.map((f) => [\n f.checkId,\n {\n id: f.checkId,\n name: f.checkTitle || f.checkId,\n shortDescription: { text: f.checkTitle || f.checkId },\n fullDescription: { text: f.checkTitle || f.checkId },\n defaultConfiguration: {\n level: sarifLevel(f.priority),\n },\n properties: {\n tags: ['abap', 'code-quality'],\n precision: 'high',\n ...(f.checkCategory && { category: f.checkCategory }),\n },\n },\n ]),\n ).values(),\n );\n\n const results = result.findings.map((finding: AtcFinding) => {\n const filePath = `src/${finding.objectType.toLowerCase()}/${finding.objectName.toLowerCase()}.${finding.objectType.toLowerCase()}`;\n\n // Parse line number from location if available\n const lineMatch = finding.location?.match(/start=(\\d+)/);\n const line = lineMatch ? Number.parseInt(lineMatch[1], 10) : 1;\n\n return {\n ruleId: finding.checkId,\n level: sarifLevel(finding.priority),\n message: { text: finding.messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: filePath },\n region: {\n startLine: line,\n startColumn: 1,\n },\n },\n },\n ],\n partialFingerprints: {\n primaryLocationLineHash: `${finding.checkId}-${finding.objectName}-${line}`,\n },\n };\n });\n\n const sarifReport = {\n $schema: 'https://json.schemastore.org/sarif-2.1.0.json',\n version: '2.1.0',\n runs: [\n {\n tool: {\n driver: {\n name: 'ABAP Test Cockpit',\n version: '1.0.0',\n informationUri:\n 'https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/4ec5711c6e391014adc9fffe4e204223.html',\n rules: Array.from(rules),\n },\n },\n results,\n },\n ],\n };\n\n await writeFile(outputFile, JSON.stringify(sarifReport, null, 2));\n console.log(`\\n📄 SARIF report written to: ${outputFile}`);\n console.log(\n `📊 ${result.totalFindings} issues exported for GitHub Code Scanning`,\n );\n}\n","/**\n * GitLab Code Quality Formatter\n *\n * Outputs ATC findings in GitLab Code Quality format.\n *\n * This formatter is a pure output formatter — it maps ATC findings to the\n * GitLab Code Quality JSON structure. Path resolution and line number\n * conversion are delegated to an optional FindingResolver.\n *\n * Without a resolver, the formatter produces PREFIX-style paths and raw\n * ATC (method-relative) line numbers. With a resolver (e.g., from\n * @abapify/adt-plugin-abapgit), it produces correct git paths and\n * file-relative line numbers.\n *\n * @example\n * ```typescript\n * // Without resolver (standalone CLI usage)\n * await outputGitLabCodeQuality(result, 'report.json');\n *\n * // With resolver (inside an abapgit repo)\n * import { createFindingResolver } from '@abapify/adt-plugin-abapgit';\n * const resolver = await createFindingResolver();\n * await outputGitLabCodeQuality(result, 'report.json', { resolver });\n * ```\n */\n\nimport { writeFile } from 'node:fs/promises';\nimport type { AtcResult, AtcFinding, FindingResolver } from '../types';\n\n/**\n * Options for the GitLab Code Quality formatter\n */\nexport interface GitLabFormatterOptions {\n /** Optional resolver for path/line resolution (e.g., from abapgit plugin) */\n resolver?: FindingResolver;\n}\n\n// ── Extract method name from ATC location URI ───────────────────────────\n// Format 1: /sap/bc/adt/oo/classes/zcl_foo/methods/my_method#start=21,0\n// Format 2: ...#type=CLAS%2FOM;name=MY_METHOD;start=21\n\nfunction extractMethodName(location?: string): string | undefined {\n if (!location) return undefined;\n const match =\n /\\/methods\\/(\\w+)/i.exec(location) || /[;?&]name=(\\w+)/i.exec(location);\n return match ? match[1].toLowerCase() : undefined;\n}\n\n// ── Construct PREFIX-style fallback path ─────────────────────────────────\n\nfunction prefixPath(finding: AtcFinding): string {\n return `src/${finding.objectType.toLowerCase()}/${finding.objectName.toLowerCase()}.${finding.objectType.toLowerCase()}.abap`;\n}\n\n// ── Main formatter ──────────────────────────────────────────────────────\n\nexport async function outputGitLabCodeQuality(\n result: AtcResult,\n outputFile: string,\n options?: GitLabFormatterOptions,\n): Promise<void> {\n const resolver = options?.resolver;\n\n // Transform ATC findings to GitLab Code Quality format\n const gitlabReport = await Promise.all(\n result.findings.map(async (finding: AtcFinding) => {\n // Map ATC priority to GitLab severity\n let severity: string;\n switch (finding.priority) {\n case 1:\n severity = 'blocker';\n break;\n case 2:\n severity = 'major';\n break;\n case 3:\n severity = 'minor';\n break;\n default:\n severity = 'info';\n break;\n }\n\n // Parse ATC line number from location\n const lineMatch = finding.location?.match(/start=(\\d+)/);\n const atcLine = lineMatch ? Number.parseInt(lineMatch[1], 10) : 1;\n\n // Extract method name from ATC location URI\n const methodName = extractMethodName(finding.location);\n\n // Resolve path and line via plugin, or fall back to PREFIX-style\n let filePath: string;\n let fileLine: number;\n\n if (resolver) {\n const resolved = await resolver.resolve(\n finding.objectType,\n finding.objectName,\n atcLine,\n methodName,\n finding.location,\n );\n if (resolved) {\n filePath = resolved.path;\n fileLine = resolved.line;\n } else {\n filePath = prefixPath(finding);\n fileLine = atcLine;\n }\n } else {\n filePath = prefixPath(finding);\n fileLine = atcLine;\n }\n\n // Create unique fingerprint\n const fingerprint = `${finding.checkId}-${finding.objectName}-${fileLine}`;\n\n return {\n description: finding.messageText,\n check_name: finding.checkTitle || finding.checkId,\n // The worklist checkId as SAP reports it. On live systems this is a\n // per-instance GUID (stable for the finding, but not a catalog\n // identity); checkTitle is a display name that SAP can rename, so\n // downstream consumers should key on this field rather than the\n // title.\n check_id: finding.checkId || undefined,\n fingerprint,\n severity,\n location: {\n path: filePath,\n lines: {\n begin: fileLine,\n end: fileLine,\n },\n },\n // Extra fields for downstream processing\n ...(methodName && { method: methodName }),\n ...(finding.location && { atc_location: finding.location }),\n ...(finding.checkCategory && {\n check_category: finding.checkCategory,\n }),\n };\n }),\n );\n\n await writeFile(outputFile, JSON.stringify(gitlabReport, null, 2));\n console.log(`\\n📄 GitLab Code Quality report written to: ${outputFile}`);\n console.log(`📊 ${result.totalFindings} issues exported`);\n}\n","/**\n * Check Category Enrichment\n *\n * Resolves ATC findings to their check category (e.g., 'CI_PERFORMANCE',\n * 'CI_SECURITY') through the check variant form template\n * (GET /sap/bc/adt/atc/checkvariants/formtemplate?chkvName={name}).\n *\n * The form template is the check catalog of the run's check variant: every\n * available check category lists its checks, and each check carries its\n * implementing class. When a finding's checkId matches a catalog\n * implementing class, the index resolves it directly. Live worklist\n * checkIds are check-instance GUIDs (observed on a live S/4 system,\n * 2026-10-22); those resolve through the documentation bridge in\n * ./documentation-bridge.ts.\n *\n * Enrichment is best-effort by design: without the form template, or for a\n * checkId outside the catalog, findings keep no category and the run still\n * succeeds.\n */\n\n/**\n * Structural shape of the parsed check variant form template document\n * (GET /sap/bc/adt/atc/checkvariants/formtemplate).\n *\n * Only the fields needed for the checkId -> category index are declared.\n */\nexport interface FormTemplateDocument {\n formTemplate?: {\n availableCheckCategories?: {\n availableCheckCategory?: FormTemplateCategoryEntry[];\n };\n };\n}\n\n/** One available check category with the checks it contains. */\nexport interface FormTemplateCategoryEntry {\n checkCategoryObject?: {\n /** Category name (the enrichment key, e.g., 'CI_PERFORMANCE') */\n name?: string;\n /** Category display title (e.g., 'Performance') */\n description?: string;\n };\n check?: FormTemplateCheckEntry[];\n}\n\n/** One check entry of a category. */\nexport interface FormTemplateCheckEntry {\n checkObject?: {\n /** Check name (e.g., 'CI_ANALYZE_WHERE') */\n name?: string;\n content?: {\n /** The check's category assignment (the category name) */\n category?: { name?: string };\n /**\n * Implementing class of the check (e.g., 'CL_CI_TEST_SELECT_TAW_A').\n * Worklist findings carry a check-instance GUID instead; the\n * documentation bridge resolves that GUID to this class name.\n */\n implementingClass?: { name?: string };\n };\n };\n}\n\n/**\n * Build a checkId -> check category name index from a parsed check variant\n * form template document.\n *\n * The checkId key is the implementing class name; the value is the category\n * object name (a stable key like 'CI_PERFORMANCE', not the display title).\n *\n * @param doc - Parsed form template document (nullable for convenience)\n * @returns Map of checkId -> category name; empty when the document is\n * missing or malformed\n */\nexport function parseCheckCategoryIndex(\n doc: FormTemplateDocument | null | undefined,\n): Map<string, string> {\n const index = new Map<string, string>();\n const categories =\n doc?.formTemplate?.availableCheckCategories?.availableCheckCategory ?? [];\n for (const category of categories) {\n collectCategoryChecks(category, index);\n }\n return index;\n}\n\n/** Index every check of one category under the category's name. */\nfunction collectCategoryChecks(\n category: FormTemplateCategoryEntry,\n index: Map<string, string>,\n): void {\n const categoryName = category.checkCategoryObject?.name;\n if (!categoryName) return;\n const checkIds = (category.check ?? [])\n .map((check) => check.checkObject?.content?.implementingClass?.name)\n .filter((checkId): checkId is string => !!checkId);\n for (const checkId of checkIds) {\n index.set(checkId, categoryName);\n }\n}\n\n/** Structural client surface needed to fetch a form template. */\nexport interface FormTemplateClient {\n adt: {\n atc: {\n checkvariants: {\n /** GET /sap/bc/adt/atc/checkvariants/formtemplate{?chkvName} */\n formtemplate: (params?: {\n chkvName?: string;\n }) => Promise<FormTemplateDocument>;\n };\n };\n };\n}\n\n/**\n * Fetch the check variant form template and build the checkId -> category\n * name index for a run.\n *\n * Best-effort: any error (network, 404, malformed document) is swallowed\n * and reported through the optional logger so a failed enrichment never\n * fails the ATC run itself.\n *\n * @param client - ADT client with the checkvariants.formtemplate surface\n * @param checkVariant - The check variant name of the run (e.g., 'DEFAULT')\n * @param logger - Optional CLI logger for diagnostics\n * @returns checkId -> category name index; empty when unavailable\n */\nexport async function fetchCheckCategoryIndex(\n client: FormTemplateClient,\n checkVariant: string,\n logger?: {\n warn: (message: string) => void;\n info?: (message: string) => void;\n },\n): Promise<Map<string, string>> {\n try {\n const doc = await client.adt.atc.checkvariants.formtemplate({\n chkvName: checkVariant,\n });\n return parseCheckCategoryIndex(doc);\n } catch (error) {\n logger?.warn?.(\n `⚠️ Check category enrichment unavailable: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return new Map();\n }\n}\n\n/**\n * Enrich findings with their check category in place.\n *\n * Generic over any finding shape carrying a checkId (and an optional\n * checkCategory field), so both the CLI plugin's AtcFinding and other\n * canonical finding types can be enriched with the same index.\n *\n * @param findings - Findings to enrich (mutated in place)\n * @param index - checkId -> category name index\n * @returns Number of findings that received a category\n */\nexport function enrichFindingsWithCategories<\n T extends { checkId: string; checkCategory?: string },\n>(findings: T[], index: Map<string, string>): number {\n let enriched = 0;\n for (const finding of findings) {\n const checkCategory = index.get(finding.checkId);\n if (checkCategory) {\n finding.checkCategory = checkCategory;\n enriched++;\n }\n }\n return enriched;\n}\n\n/**\n * Enrich an ATC result with check categories in place.\n *\n * @param result - The run result (findings mutated in place)\n * @param index - checkId -> category name index\n * @returns Number of findings that received a category\n */\nexport function enrichResultWithCategories<\n T extends { checkId: string; checkCategory?: string },\n>(result: { findings?: T[] }, index: Map<string, string>): number {\n return enrichFindingsWithCategories(result.findings ?? [], index);\n}\n","/**\n * Documentation Bridge for Check Category Enrichment\n *\n * ATC worklist findings carry a check-instance GUID as their checkId — not\n * the implementing class name the check variant form template catalog lists\n * (observed on a live S/4 system, 2026-10-22). The finding's documentation\n * page (the worklist atom:link) names the check class in its header line,\n * which resolves through the form-template category index.\n *\n * Everything here is best-effort: fetch errors are reported through the\n * optional logger and skipped; findings keep no category when their page\n * cannot be fetched or parsed.\n */\n\n/** Structural surface for a bounded documentation read. The real ADT client\n * provides readTextBounded; mocks without it simply skip the bridge. */\nexport interface DocumentationReadingClient {\n readTextBounded?: (\n uri: string,\n maxBytes: number,\n options?: { headers?: Record<string, string> },\n ) => Promise<string>;\n}\n\n/**\n * Parse the implementing check class from an ATC finding documentation page.\n *\n * Live shape (2026-10-22): the page opens with\n * \"Appl. Comp. Check / Check Class / Message Code\" followed by\n * `<p>BC-ABA-LA / CL_CI_TEST_SELECT_TAW_A / 0501</p>`. The check class in\n * that line is the implementing class the check variant form template lists\n * — the bridge from the finding's GUID checkId to the check catalog.\n *\n * The match is bounded (no unbounded quantifiers on page input) and accepts\n * an optional ABAP namespace prefix (e.g. `/NSP/CL_EXAMPLE`).\n *\n * @param html - The documentation page HTML\n * @returns The check class name, or undefined when the page does not carry it\n */\nexport function parseCheckClassFromDocumentation(\n html: string,\n): string | undefined {\n const match =\n /Check\\s*Class[^<]{0,80}<\\/h3>\\s*<p>\\s*[^/<]{0,120}\\/\\s*((?:\\/\\w{1,8}\\/)?[A-Za-z]\\w*)\\s*\\//u.exec(\n html,\n );\n return match?.[1];\n}\n\n/** Finding shape the bridge enriches in place. */\nexport interface DocumentationBridgeFinding {\n checkId: string;\n checkCategory?: string;\n documentationUri?: string;\n}\n\n/**\n * Only ATC documentation paths may be fetched — a worklist-provided URI\n * must never drive an authenticated read outside the documentation space.\n */\nconst ATC_DOCUMENTATION_URI =\n /^\\/sap\\/bc\\/adt\\/documentation\\/atc\\/documents\\/[\\w/%-]+$/u;\n\n/** Shared state for the per-URI documentation fetch. */\ninterface DocumentationFetchContext {\n read: NonNullable<DocumentationReadingClient['readTextBounded']>;\n maxBytes: number;\n maxReads: number;\n reads: number;\n classByDocumentationUri: Map<string, string | undefined>;\n logger?: { warn: (message: string) => void };\n}\n\n/**\n * Read one documentation page and resolve its check class, with a shared\n * per-URI cache so repeated findings never re-fetch the same page.\n */\nasync function resolveCheckClassForUri(\n documentationUri: string,\n context: DocumentationFetchContext,\n): Promise<void> {\n if (context.classByDocumentationUri.has(documentationUri)) return;\n if (!ATC_DOCUMENTATION_URI.test(documentationUri)) {\n context.logger?.warn?.(\n `⚠️ Skipping non-ATC documentation URI: ${documentationUri}`,\n );\n context.classByDocumentationUri.set(documentationUri, undefined);\n return;\n }\n if (context.reads >= context.maxReads) {\n context.classByDocumentationUri.set(documentationUri, undefined);\n return;\n }\n context.reads++;\n try {\n const html = await context.read(documentationUri, context.maxBytes, {\n headers: { Accept: 'application/vnd.sap.adt.docu.v1+html' },\n });\n context.classByDocumentationUri.set(\n documentationUri,\n parseCheckClassFromDocumentation(html),\n );\n } catch (error) {\n context.logger?.warn?.(\n `⚠️ ATC documentation read failed for ${documentationUri}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n context.classByDocumentationUri.set(documentationUri, undefined);\n }\n}\n\n/**\n * Bridge finding categories through the ATC documentation page.\n *\n * After the direct form-template index pass (which resolves findings whose\n * checkId already matches a catalog implementing class), this resolves the\n * GUID-checkId findings: each finding's documentation page names the check\n * class, which resolves through the same index.\n *\n * Best-effort: per-documentation-URI fetch errors are reported through the\n * optional logger and skipped; a finding keeps no category when its page\n * cannot be fetched or parsed. Reads are deduplicated per URI.\n *\n * @param client - ADT client with an optional bounded text read\n * @param findings - Findings to enrich in place\n * @param classToCategory - checkId/class -> category name index (the\n * form-template catalog: parseCheckCategoryIndex output)\n * @param options - maxBytes (default 64 KiB) and an optional logger\n * @returns Number of findings that received a category\n */\nexport async function resolveCategoriesViaDocumentation(\n client: DocumentationReadingClient,\n findings: DocumentationBridgeFinding[],\n classToCategory: ReadonlyMap<string, string>,\n options: {\n maxBytes?: number;\n maxReads?: number;\n logger?: { warn: (message: string) => void };\n } = {},\n): Promise<number> {\n const read = client.readTextBounded;\n if (!read || classToCategory.size === 0) return 0;\n\n const pending = findings.filter(\n (finding) => !finding.checkCategory && finding.documentationUri,\n );\n if (pending.length === 0) return 0;\n\n const context: DocumentationFetchContext = {\n read,\n maxBytes: options.maxBytes ?? 64 * 1024,\n maxReads: options.maxReads ?? 500,\n reads: 0,\n classByDocumentationUri: new Map<string, string | undefined>(),\n logger: options.logger,\n };\n let enriched = 0;\n for (const finding of pending) {\n await resolveCheckClassForUri(finding.documentationUri!, context);\n const checkClass = context.classByDocumentationUri.get(\n finding.documentationUri!,\n );\n const checkCategory = checkClass\n ? classToCategory.get(checkClass)\n : undefined;\n if (checkCategory) {\n finding.checkCategory = checkCategory;\n enriched++;\n }\n }\n return enriched;\n}\n","/**\n * ATC Command Plugin\n *\n * CLI-agnostic command for running ABAP Test Cockpit checks.\n * Uses the CliContext.getAdtClient() factory for API access.\n */\n\nimport type { CliCommandPlugin, CliContext } from '@abapify/adt-plugin';\nimport chalk from 'chalk';\nimport { outputSarifReport, outputGitLabCodeQuality } from '../formatters';\nimport {\n fetchCheckCategoryIndex,\n enrichResultWithCategories,\n} from '../check-categories';\nimport { resolveCategoriesViaDocumentation } from '../documentation-bridge';\nimport type { AtcResult, AtcFinding, OutputFormat } from '../types';\n\n// Client type - the plugin receives a typed client from context\n// The client.adt.atc namespace provides all ATC operations\ninterface AdtClient {\n adt: {\n atc: {\n customizing: {\n get: () => Promise<{\n customizing: {\n properties: { property?: Array<{ name: string; value?: string }> };\n };\n }>;\n };\n worklists: {\n create: (params: {\n checkVariant: string;\n }) => Promise<\n | string\n | { worklistRun?: { worklistId: string }; worklist?: { id: string } }\n >;\n get: (\n id: string,\n params: { includeExemptedFindings: string },\n ) => Promise<WorklistResponse>;\n };\n checkvariants: {\n formtemplate: (params?: {\n chkvName?: string;\n }) => Promise<import('../check-categories').FormTemplateDocument>;\n };\n runs: {\n post: (\n params: { worklistId: string },\n body: {\n run: {\n maximumVerdicts: number;\n objectSets: {\n objectSet: Array<{\n kind: string;\n objectReferences: {\n objectReference: Array<{ uri: string }>;\n };\n }>;\n };\n };\n },\n ) => Promise<void>;\n };\n };\n };\n /**\n * Bounded documentation text read (the real ADT client provides it).\n * Optional: without it the documentation category bridge is skipped.\n */\n readTextBounded?: (\n uri: string,\n maxBytes: number,\n options?: { headers?: Record<string, string> },\n ) => Promise<string>;\n}\n\n// Worklist response type\ninterface WorklistResponse {\n worklist?: {\n id: string;\n timestamp: string;\n objects: {\n object?: Array<{\n uri?: string;\n type?: string;\n name?: string;\n findings: {\n finding?: Array<{\n uri?: string;\n location?: string;\n priority?: string;\n checkId?: string;\n checkTitle?: string;\n messageId?: string;\n messageTitle?: string;\n /** Documentation link: the page that names the check class. */\n link?: Array<{ href?: string; rel?: string }>;\n }>;\n };\n }>;\n };\n };\n worklistRun?: { worklistId: string };\n}\n\n/**\n * Create OSC 8 hyperlink for terminal\n */\nfunction hyperlink(text: string, url: string): string {\n const OSC = '\\x1b]';\n const BEL = '\\x07';\n const SEP = ';';\n return `${OSC}8${SEP}${SEP}${url}${BEL}${text}${OSC}8${SEP}${SEP}${BEL}`;\n}\n\n/**\n * Create ADT Eclipse link\n */\nfunction adtLink(name: string, uri: string, systemName?: string): string {\n if (!systemName || !name || !uri) {\n return name ? chalk.cyan(name) : '';\n }\n const path = uri.startsWith('/sap/bc/adt') ? uri : `/sap/bc/adt${uri}`;\n const url = `adt://${systemName}${path}`;\n return hyperlink(chalk.cyan(name), url);\n}\n\n/**\n * The trusted documentation link of one worklist finding.\n *\n * Live shape (2026-10-22): <atom:link rel=\"http://www.sap.com/adt/\n * relations/documentation\" href=\"/sap/bc/adt/documentation/atc/documents/\n * itemid/{guid}/index/{n}\"/> inside the atcfinding:finding element.\n */\nfunction documentationUriFromFinding(\n finding: NonNullable<\n NonNullable<\n NonNullable<WorklistResponse['worklist']>['objects']['object']\n >[number]['findings']['finding']\n >[number],\n): string | undefined {\n return finding.link?.find(\n (link) => link.rel === 'http://www.sap.com/adt/relations/documentation',\n )?.href;\n}\n\n/**\n * Convert worklist response to AtcResult\n */\nfunction convertWorklistToResult(\n worklistResponse: WorklistResponse,\n checkVariant: string,\n): AtcResult {\n const findings: AtcFinding[] = [];\n\n if (worklistResponse.worklist) {\n const objects = worklistResponse.worklist.objects.object || [];\n\n for (const obj of objects) {\n const objFindings = obj.findings.finding || [];\n for (const finding of objFindings) {\n findings.push({\n checkId: finding.checkId || '',\n checkTitle: finding.checkTitle || '',\n messageId: finding.messageId || '',\n priority: Number.parseInt(finding.priority || '3', 10),\n messageText: finding.messageTitle || '',\n objectUri: obj.uri || '',\n objectType: obj.type || '',\n objectName: obj.name || '',\n location: finding.location,\n findingUri: finding.uri,\n documentationUri: documentationUriFromFinding(finding),\n });\n }\n }\n }\n\n const errorCount = findings.filter((f) => f.priority === 1).length;\n const warningCount = findings.filter((f) => f.priority === 2).length;\n const infoCount = findings.filter((f) => f.priority >= 3).length;\n\n return {\n checkVariant,\n totalFindings: findings.length,\n errorCount,\n warningCount,\n infoCount,\n findings,\n };\n}\n\n/**\n * Display ATC results in console\n */\nfunction displayAtcResults(result: AtcResult, systemName?: string): void {\n if (result.totalFindings === 0) {\n console.log(`\\n✅ ATC check passed - No issues found!`);\n console.log(` 🎯 Variant: ${result.checkVariant}`);\n return;\n }\n\n console.log(`\\n📊 ATC Results Summary:`);\n console.log(` 🎯 Variant: ${result.checkVariant}`);\n if (result.errorCount > 0) console.log(` ❌ Errors: ${result.errorCount}`);\n if (result.warningCount > 0)\n console.log(` ⚠️ Warnings: ${result.warningCount}`);\n if (result.infoCount > 0) console.log(` ℹ️ Info: ${result.infoCount}`);\n console.log(` 📋 Total Issues: ${result.totalFindings}`);\n\n if (result.findings && result.findings.length > 0) {\n console.log(`\\n📄 Findings:`);\n result.findings.slice(0, 5).forEach((finding) => {\n let priorityIcon = 'ℹ️';\n if (finding.priority === 1) priorityIcon = '❌';\n else if (finding.priority === 2) priorityIcon = '⚠️';\n const objectLink = finding.location\n ? adtLink(finding.objectName, finding.location, systemName)\n : finding.objectName;\n console.log(\n ` ${priorityIcon} ${finding.checkTitle || finding.checkId}`,\n );\n console.log(` ${finding.messageText}`);\n console.log(` Object: ${objectLink} (${finding.objectType})`);\n });\n\n if (result.findings.length > 5) {\n console.log(` ... (${result.findings.length - 5} more findings)`);\n }\n }\n\n console.log(`\\n💡 Use --debug for detailed results`);\n}\n\n/**\n * ATC Command Plugin\n */\n/** CLI options parsed for the `atc` command. */\ninterface AtcCliOptions {\n package?: string;\n object?: string;\n transport?: string;\n fromFile?: string;\n variant?: string;\n maxResults?: string;\n format?: OutputFormat;\n output?: string;\n resolver?: string;\n}\n\n/** Resolve the single ATC target from the mutually exclusive options. */\nasync function resolveTargets(\n options: AtcCliOptions,\n ctx: CliContext,\n): Promise<{ targetUris: string[]; targetName: string }> {\n if (options.fromFile) {\n // Read URIs from file\n const { readFileSync } = await import('node:fs');\n const fileContent = readFileSync(options.fromFile, 'utf-8');\n const targetUris = fileContent\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith('#')); // Skip empty lines and comments\n\n if (targetUris.length === 0) {\n ctx.logger.error(`❌ No objects found in ${options.fromFile}`);\n process.exit(1);\n }\n const targetName = `${targetUris.length} objects from ${options.fromFile}`;\n ctx.logger.info(`📄 Target: ${targetName}`);\n return { targetUris, targetName };\n }\n if (options.transport) {\n const targetName = `Transport ${options.transport}`;\n ctx.logger.info(`🚚 Target: ${targetName}`);\n return {\n targetUris: [`/sap/bc/adt/cts/transportrequests/${options.transport}`],\n targetName,\n };\n }\n if (options.package) {\n const targetName = `Package ${options.package.toUpperCase()}`;\n ctx.logger.info(`📦 Target: ${targetName}`);\n return {\n targetUris: [`/sap/bc/adt/packages/${options.package.toUpperCase()}`],\n targetName,\n };\n }\n const targetName = options.object!;\n ctx.logger.info(`📄 Target: ${targetName}`);\n return { targetUris: [options.object!], targetName };\n}\n\n/**\n * Determine the check variant: the explicit `--variant`, the system\n * default from ATC customizing, or `DEFAULT`.\n */\nasync function resolveCheckVariant(\n client: AdtClient,\n options: AtcCliOptions,\n ctx: CliContext,\n): Promise<string> {\n // Step 1: Get ATC customizing to find default check variant\n ctx.logger.info('📋 Reading ATC customizing...');\n const customizing = await client.adt.atc.customizing.get();\n\n if (options.variant) return options.variant;\n\n // Find systemCheckVariant in properties\n const variantProp = customizing.customizing.properties.property?.find(\n (p) => p.name === 'systemCheckVariant',\n );\n return variantProp?.value ? variantProp.value : 'DEFAULT';\n}\n\n/** Extract the worklist ID — SAP can return a plain string or an object. */\nfunction extractWorklistId(\n worklistCreateResponse:\n | string\n | { worklistRun?: { worklistId: string }; worklist?: { id: string } },\n ctx: CliContext,\n): string {\n if (typeof worklistCreateResponse === 'string') {\n // Plain string response - extract ID from XML or use as-is\n const idMatch = /id=\"([^\"]+)\"/.exec(worklistCreateResponse);\n return idMatch ? idMatch[1] : worklistCreateResponse.trim();\n }\n if (\n 'worklist' in worklistCreateResponse &&\n worklistCreateResponse.worklist?.id\n ) {\n return worklistCreateResponse.worklist.id;\n }\n if (\n 'worklistRun' in worklistCreateResponse &&\n worklistCreateResponse.worklistRun?.worklistId\n ) {\n return worklistCreateResponse.worklistRun.worklistId;\n }\n ctx.logger.error('❌ Failed to get worklist ID from response');\n process.exit(1);\n}\n\n/** Load the finding resolver plugin when requested. */\nasync function loadFindingResolver(\n options: AtcCliOptions,\n ctx: CliContext,\n): Promise<import('../types').FindingResolver | undefined> {\n if (!options.resolver) return undefined;\n\n const name = options.resolver;\n if (name === 'abapgit') {\n // Built-in abapgit resolver — no external dependencies\n const { createAbapGitResolver } = await import('../resolvers/abapgit');\n return createAbapGitResolver();\n }\n\n // External resolver — try dynamic import\n try {\n ctx.logger.info(`📂 Loading external resolver: ${name}`);\n const mod = await import(name);\n if (typeof mod.createFindingResolver === 'function') {\n return mod.createFindingResolver();\n }\n ctx.logger.warn(\n `⚠️ Module ${name} does not export createFindingResolver()`,\n );\n } catch (e) {\n ctx.logger.warn(\n `⚠️ Failed to load resolver \"${name}\": ${e instanceof Error ? e.message : e}`,\n );\n }\n return undefined;\n}\n\n/** Output the ATC results in the requested format. */\nasync function outputAtcResults(\n result: AtcResult,\n options: AtcCliOptions,\n targetName: string,\n resolver: import('../types').FindingResolver | undefined,\n ctx: CliContext,\n): Promise<void> {\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (options.format === 'gitlab' && options.output) {\n await outputGitLabCodeQuality(result, options.output, { resolver });\n } else if (options.format === 'sarif' && options.output) {\n await outputSarifReport(result, options.output, targetName);\n } else {\n displayAtcResults(result, ctx.adtSystemName);\n }\n}\n\nexport const atcCommand: CliCommandPlugin = {\n name: 'atc',\n description: 'Run ABAP Test Cockpit (ATC) checks',\n\n options: [\n { flags: '-p, --package <package>', description: 'Run ATC on package' },\n {\n flags: '-o, --object <uri>',\n description:\n 'Run ATC on specific object (e.g., /sap/bc/adt/oo/classes/zcl_my_class)',\n },\n {\n flags: '-t, --transport <transport>',\n description: 'Run ATC on transport request (e.g., TRLK942970)',\n },\n {\n flags: '-f, --from-file <file>',\n description: 'Run ATC on objects listed in file (one URI per line)',\n },\n {\n flags: '--variant <variant>',\n description: 'ATC check variant (default: from system customizing)',\n },\n {\n flags: '--max-results <number>',\n description: 'Maximum number of results',\n default: '100',\n },\n {\n flags: '--format <format>',\n description: 'Output format: console, json, gitlab, sarif',\n default: 'console',\n },\n {\n flags: '--output <file>',\n description: 'Output file (required for gitlab/sarif format)',\n },\n {\n flags: '--resolver <name>',\n description:\n 'Finding resolver plugin for path/line resolution (e.g., abapgit)',\n },\n ],\n\n async execute(args, ctx: CliContext) {\n const options = args as AtcCliOptions;\n\n // Validate mutually exclusive options\n const targetCount = [\n options.package,\n options.object,\n options.transport,\n options.fromFile,\n ].filter(Boolean).length;\n\n if (targetCount === 0) {\n ctx.logger.error(\n '❌ One of --package, --object, --transport, or --from-file is required',\n );\n process.exit(1);\n }\n\n if (targetCount > 1) {\n ctx.logger.error(\n '❌ Only one of --package, --object, --transport, or --from-file can be specified',\n );\n process.exit(1);\n }\n\n // Validate output file for gitlab/sarif formats\n if (\n (options.format === 'gitlab' || options.format === 'sarif') &&\n !options.output\n ) {\n ctx.logger.error(`❌ --output is required for ${options.format} format`);\n process.exit(1);\n }\n\n // Get ADT client from context\n if (!ctx.getAdtClient) {\n ctx.logger.error(\n '❌ ADT client not available. This command requires authentication.',\n );\n ctx.logger.error(' Run: adt auth login');\n process.exit(1);\n }\n\n const client = (await ctx.getAdtClient()) as AdtClient;\n\n ctx.logger.info('🔍 Running ABAP Test Cockpit checks...');\n\n // Determine target(s)\n const { targetUris, targetName } = await resolveTargets(options, ctx);\n\n // Determine the check variant\n const checkVariant = await resolveCheckVariant(client, options, ctx);\n ctx.logger.info(`🎯 Check variant: ${checkVariant}`);\n\n // Step 2: Create worklist (just with checkVariant, no objects)\n ctx.logger.info('📋 Creating ATC worklist...');\n const worklistCreateResponse = await client.adt.atc.worklists.create({\n checkVariant,\n });\n\n // Extract worklist ID - SAP can return plain string or object\n const worklistId = extractWorklistId(worklistCreateResponse, ctx);\n ctx.logger.info(`📋 Worklist created: ${worklistId}`);\n\n // Step 3: Run ATC checks with objects\n ctx.logger.info('⏳ Running ATC analysis...');\n const maxResults = Number.parseInt(options.maxResults || '100', 10);\n const runData = {\n run: {\n maximumVerdicts: maxResults,\n objectSets: {\n objectSet: [\n {\n kind: 'inclusive',\n objectReferences: {\n objectReference: targetUris.map((uri) => ({ uri })),\n },\n },\n ],\n },\n },\n };\n await client.adt.atc.runs.post({ worklistId }, runData);\n\n // Step 4: Get results\n ctx.logger.info('📊 Fetching results...');\n const worklistResult = await client.adt.atc.worklists.get(worklistId, {\n includeExemptedFindings: 'false',\n });\n\n // Convert to AtcResult\n const result = convertWorklistToResult(\n worklistResult as WorklistResponse,\n checkVariant,\n );\n\n // Enrich findings with check categories from the run's check variant\n // form template (best-effort: never fails the run itself). The catalog is\n // fetched after the run, so categories reflect the variant configuration\n // in effect at that time.\n const categoryIndex = await fetchCheckCategoryIndex(\n client,\n checkVariant,\n ctx.logger,\n );\n const enriched = enrichResultWithCategories(result, categoryIndex);\n if (enriched > 0) {\n ctx.logger.info(\n `🏷️ Enriched ${enriched}/${result.totalFindings} finding(s) with check categories`,\n );\n }\n\n // Bridge remaining findings through their ATC documentation page: the\n // worklist checkId is a check-instance GUID, not the implementing class\n // the form-template catalog lists (live evidence, 2026-10-22), so\n // the direct index match cannot resolve them. The documentation page\n // names the check class, which resolves through the same index.\n // Best-effort: never fails the run.\n const bridged = await resolveCategoriesViaDocumentation(\n client,\n result.findings,\n categoryIndex,\n { logger: ctx.logger },\n );\n if (bridged > 0) {\n ctx.logger.info(\n `🏷 Bridged ${bridged} finding(s) to check categories via ATC documentation`,\n );\n }\n\n // Load finding resolver if requested\n const resolver = await loadFindingResolver(options, ctx);\n\n // Output based on format\n await outputAtcResults(result, options, targetName, resolver, ctx);\n\n // Exit with error code if there are errors\n if (result.errorCount > 0) {\n process.exit(1);\n }\n },\n};\n\nexport default atcCommand;\n"],"mappings":"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"}
@@ -1,7 +1,4 @@
1
1
  import { CliCommandPlugin } from "@abapify/adt-plugin";
2
- /**
3
- * ATC Command Plugin
4
- */
5
2
  declare const atcCommand: CliCommandPlugin;
6
3
  export { atcCommand, atcCommand as default };
7
4
  //# sourceMappingURL=atc.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"atc.d.mts","names":[],"sources":["../../src/commands/atc.ts"],"mappings":";;;;cA6Oa,YAAY"}
1
+ {"version":3,"file":"atc.d.mts","names":[],"sources":["../../src/commands/atc.ts"],"mappings":";cA2Ya,YAAY"}
@@ -1 +1 @@
1
- export { t as atcCommand, t as default } from "../atc-CIPiGB5o.mjs";
1
+ export { t as atcCommand, t as default } from "../atc-kyCLseda.mjs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/commands/atc-customizing.ts","../src/types.ts","../src/formatters/sarif.ts","../src/formatters/gitlab.ts","../src/check-categories.ts","../src/documentation-bridge.ts","../src/resolvers/abapgit.ts","../src/resolvers/adt-uri-to-abapgit-path.ts"],"mappings":";;cAiCa,uBAAuB;;;;;;;;;UCxBnB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;EAOA;;;;;;;EAOA;;;;;UAMe;EACf;EACA;EACA;EACA;EACA;EACA,UAAU;;;;;KAMA;;;;UAKK;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;;;UAsBe;;;;;;;;;;;EAWf,QACE,oBACA,oBACA,iBACA,qBACA,uBACC,QAAQ;;iBC1FS,kBACpB,QAAQ,WACR,oBACA,uBACC;;;;UCmBc;;EAEf,WAAW;;iBAsBS,wBACpB,QAAQ,WACR,oBACA,UAAU,yBACT;;;;;;;;;;;;;;;;;;;;;;;;;;UClCc;EACf;IACE;MACE,yBAAyB;;;;;UAMd;EACf;;IAEE;;IAEA;;EAEF,QAAQ;;;UAIO;EACf;;IAEE;IACA;;MAEE;QAAa;;;;;;;MAMb;QAAsB;;;;;;;;;;;;;;;;iBAgBZ,wBACd,KAAK,0CACJ;;UA0Bc;EACf;IACE;MACE;;QAEE,eAAe;UACb;cACI,QAAQ;;;;;;;;;;;;;;;;;;iBAmBA,wBACpB,QAAQ,oBACR,sBACA;EACE,OAAO;EACP,QAAQ;IAET,QAAQ;;;;;;;;;;;;iBA2BK,6BACd;EAAY;EAAiB;GAC7B,UAAU,KAAK,OAAO;;;;;;;;iBAmBR,2BACd;EAAY;EAAiB;GAC7B;EAAU,WAAW;GAAO,OAAO;;;;;;;;;;;;;;;;UCzKpB;EACf,mBACE,aACA,kBACA;IAAY,UAAU;QACnB;;;;;;;;;;;;;;;;;iBAkBS,iCACd;;UASe;EACf;EACA;EACA;;;;;;;;;;;;;;;;;;;;;iBA8EoB,kCACpB,QAAQ,4BACR,UAAU,8BACV,iBAAiB,6BACjB;EACE;EACA;EACA;IAAW,OAAO;;IAEnB;;;;;;;;;iBCoJa,sBAAsB,mBAAmB;;;;;;;;;;;;;;iBCzHzC,oBAAoB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/commands/atc-customizing.ts","../src/types.ts","../src/formatters/sarif.ts","../src/formatters/gitlab.ts","../src/check-categories.ts","../src/documentation-bridge.ts","../src/resolvers/abapgit.ts","../src/resolvers/adt-uri-to-abapgit-path.ts"],"mappings":";;cAiCa,uBAAuB;;;;;;;;;UCxBnB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;EAOA;;;;;;;EAOA;;;;;UAMe;EACf;EACA;EACA;EACA;EACA;EACA,UAAU;;;;;KAMA;;;;UAKK;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;;;UAsBe;;;;;;;;;;;EAWf,QACE,oBACA,oBACA,iBACA,qBACA,uBACC,QAAQ;;iBCpFS,kBACpB,QAAQ,WACR,oBACA,uBACC;;;;UCac;;EAEf,WAAW;;iBAsBS,wBACpB,QAAQ,WACR,oBACA,UAAU,yBACT;;;;;;;;;;;;;;;;;;;;;;;;;;UClCc;EACf;IACE;MACE,yBAAyB;;;;;UAMd;EACf;;IAEE;;IAEA;;EAEF,QAAQ;;;UAIO;EACf;;IAEE;IACA;;MAEE;QAAa;;;;;;;MAMb;QAAsB;;;;;;;;;;;;;;;;iBAgBZ,wBACd,KAAK,0CACJ;;UA0Bc;EACf;IACE;MACE;;QAEE,eAAe;UACb;cACI,QAAQ;;;;;;;;;;;;;;;;;;iBAmBA,wBACpB,QAAQ,oBACR,sBACA;EACE,OAAO;EACP,QAAQ;IAET,QAAQ;;;;;;;;;;;;iBA2BK,6BACd;EAAY;EAAiB;GAC7B,UAAU,KAAK,OAAO;;;;;;;;iBAmBR,2BACd;EAAY;EAAiB;GAC7B;EAAU,WAAW;GAAO,OAAO;;;;;;;;;;;;;;;;UCzKpB;EACf,mBACE,aACA,kBACA;IAAY,UAAU;QACnB;;;;;;;;;;;;;;;;;iBAkBS,iCACd;;UAUe;EACf;EACA;EACA;;;;;;;;;;;;;;;;;;;;;iBA8EoB,kCACpB,QAAQ,4BACR,UAAU,8BACV,iBAAiB,6BACjB;EACE;EACA;EACA;IAAW,OAAO;;IAEnB;;;;;;;;;iBCoJa,sBAAsB,mBAAmB;;;;;;;;;;;;;;iBC1HzC,oBAAoB"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as createAbapGitResolver, r as adtUriToAbapGitPath } from "./abapgit-B2E5I-u0.mjs";
2
- import { a as enrichResultWithCategories, c as outputGitLabCodeQuality, i as enrichFindingsWithCategories, l as outputSarifReport, n as parseCheckClassFromDocumentation, o as fetchCheckCategoryIndex, r as resolveCategoriesViaDocumentation, s as parseCheckCategoryIndex, t as atcCommand } from "./atc-CIPiGB5o.mjs";
1
+ import { n as createAbapGitResolver, r as adtUriToAbapGitPath } from "./abapgit-DrCkrhFZ.mjs";
2
+ import { a as enrichResultWithCategories, c as outputGitLabCodeQuality, i as enrichFindingsWithCategories, l as outputSarifReport, n as parseCheckClassFromDocumentation, o as fetchCheckCategoryIndex, r as resolveCategoriesViaDocumentation, s as parseCheckCategoryIndex, t as atcCommand } from "./atc-kyCLseda.mjs";
3
3
  // src/commands/atc-customizing.ts
4
4
  const atcCustomizingCommand = {
5
5
  name: "atc-customizing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abapify/adt-atc",
3
- "version": "0.4.34",
3
+ "version": "0.4.38",
4
4
  "description": "ABAP Test Cockpit (ATC) CLI plugin for adt-cli",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -28,7 +28,7 @@
28
28
  "gitlab"
29
29
  ],
30
30
  "dependencies": {
31
- "@abapify/adt-plugin": "0.4.34",
31
+ "@abapify/adt-plugin": "0.4.38",
32
32
  "chalk": "^5.3.0"
33
33
  },
34
34
  "module": "./dist/index.mjs",
@@ -1 +0,0 @@
1
- {"version":3,"file":"abapgit-B2E5I-u0.mjs","names":["resolvePath"],"sources":["../src/resolvers/adt-uri-to-abapgit-path.ts","../src/resolvers/abapgit.ts"],"sourcesContent":["/**\n * Convert an ADT URI to an on-disk abapGit filename.\n *\n * This mapper belongs next to the ATC resolver because ATC findings carry\n * ADT locations while the GitLab report needs an abapGit file name. The\n * abapGit format plugin re-exports it for consumers that already use that\n * package.\n */\n\n/** Apply abapGit namespace transform: `/MYNS/zcl_foo` → `(myns)zcl_foo`. */\nfunction applyNamespace(raw: string): string {\n const nsMatch = /^\\/([^/]+)\\/(.*)$/.exec(raw);\n if (nsMatch) {\n const [, ns, rest] = nsMatch;\n return `(${ns.toLowerCase()})${rest.toLowerCase()}`;\n }\n return raw.toLowerCase();\n}\n\nfunction stripFragment(uri: string): string {\n const hashIndex = uri.indexOf('#');\n return hashIndex >= 0 ? uri.slice(0, hashIndex) : uri;\n}\n\n// Class include → abapGit suffix. Kept here so the built-in ATC package does\n// not depend on the format plugin (which depends on @abapify/adt-atc).\n// `main` maps to the primary class source (no suffix).\nconst CLASS_INCLUDE_TO_SUFFIX: Record<string, string | undefined> = {\n testclasses: 'testclasses',\n definitions: 'locals_def',\n implementations: 'locals_imp',\n macros: 'macros',\n localtypes: 'locals_types',\n};\n\ninterface Rule {\n test: RegExp;\n build: (match: RegExpMatchArray) => string;\n}\n\n// Order matters – most specific first.\nconst RULES: Rule[] = [\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/includes\\/([^/]+)$/,\n build: (match) => {\n const name = applyNamespace(decodeURIComponent(match[1]));\n const include = match[2].toLowerCase();\n if (include === 'main') return `src/${name}.clas.abap`;\n const suffix = CLASS_INCLUDE_TO_SUFFIX[include];\n return suffix\n ? `src/${name}.clas.${suffix}.abap`\n : `src/${name}.clas.${include}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/source\\/main$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n\n // Interfaces\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/interfaces\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.intf.abap`,\n },\n\n // Programs\n {\n test: /^\\/sap\\/bc\\/adt\\/programs\\/programs\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.prog.abap`,\n },\n\n // Function group metadata and source components\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/source\\/main$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n return `src/${group}.fugr.l${group}top.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.fugr.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/fmodules\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const functionModule = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${functionModule}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/includes\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const include = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${include}.abap`;\n },\n },\n\n // CDS and RAP source objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/ddl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.ddls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/acm\\/dcl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dcls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/bo\\/behaviordefinitions\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.bdef.abdl`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/srvd\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvd.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/businessservices\\/bindings\\/([^/]+?)(?:\\/publishedstates)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvb.xml`,\n },\n\n // DDIC objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/domains\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.doma.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/dataelements\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dtel.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/(?:structures|tables)\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.tabl.xml`,\n },\n\n // Package — abapGit serializes every package to the fixed filename\n // `package.devc.xml`, regardless of the package name.\n {\n test: /^\\/sap\\/bc\\/adt\\/packages\\/([^/]+)$/,\n build: () => 'src/package.devc.xml',\n },\n];\n\n/**\n * Convert an ADT URI to the abapGit on-disk filename. The returned path is\n * rooted at the configured abapGit starting folder (`src/` by default);\n * callers resolve that prefix against the repository's actual source tree.\n */\nexport function adtUriToAbapGitPath(uri: string): string | null {\n if (!uri) return null;\n const clean = stripFragment(uri).trim();\n for (const rule of RULES) {\n const match = rule.test.exec(clean);\n if (match) {\n try {\n return rule.build(match);\n } catch (error) {\n if (error instanceof URIError) return null;\n throw error;\n }\n }\n }\n return null;\n}\n","/**\n * Built-in abapGit Finding Resolver.\n *\n * ATC identifies a finding by an ADT URI, while GitLab needs the path of the\n * serialized abapGit file. The resolver owns that format-specific mapping and\n * scans the checked-out repository so PREFIX and FULL layouts both resolve to\n * the path that actually exists in Git.\n */\n\nimport {\n existsSync,\n readFileSync,\n readdirSync,\n realpathSync,\n type Dirent,\n} from 'node:fs';\nimport {\n basename,\n join,\n relative,\n resolve as resolvePath,\n sep,\n} from 'node:path';\nimport type { FindingResolver, ResolvedLocation } from '../types';\nimport { adtUriToAbapGitPath } from './adt-uri-to-abapgit-path';\n\ntype FolderLogic = 'prefix' | 'full' | 'full-with-root';\n\ninterface AbapGitMetadata {\n folderLogic?: FolderLogic;\n startingFolder: string;\n}\n\ninterface ResolverRepository {\n root: string;\n sourceRoot: string;\n metadata?: AbapGitMetadata;\n configuredFormat?: string;\n configuredFolderLogic?: FolderLogic;\n}\n\nfunction toPosix(pathValue: string): string {\n return sep === '/' ? pathValue : pathValue.split(sep).join('/');\n}\n\nfunction normalizeStartingFolder(value: string | undefined): string {\n const normalized = (value || 'src')\n .trim()\n .replaceAll('\\\\', '/')\n .replace(/^\\/+|\\/+$/g, '');\n return normalized || 'src';\n}\n\nfunction parseFolderLogic(value: string | undefined): FolderLogic | undefined {\n const normalized = value?.trim().toLowerCase();\n if (\n normalized === 'prefix' ||\n normalized === 'full' ||\n normalized === 'full-with-root'\n ) {\n return normalized;\n }\n return undefined;\n}\n\nfunction parseAbapGitMetadata(root: string): AbapGitMetadata | undefined {\n const metadataPath = join(root, '.abapgit.xml');\n if (!existsSync(metadataPath)) return undefined;\n\n try {\n const xml = readFileSync(metadataPath, 'utf8');\n return {\n folderLogic: parseFolderLogic(\n xml.match(/<FOLDER_LOGIC>\\s*([^<]+?)\\s*<\\/FOLDER_LOGIC>/i)?.[1],\n ),\n startingFolder: normalizeStartingFolder(\n xml.match(/<STARTING_FOLDER>\\s*([^<]+?)\\s*<\\/STARTING_FOLDER>/i)?.[1],\n ),\n };\n } catch {\n return undefined;\n }\n}\n\nfunction parseConfiguredFormat(root: string): {\n format?: string;\n folderLogic?: FolderLogic;\n} {\n const configPaths = [\n process.env.ADT_CONFIG_PATH,\n join(root, 'adt.config.ts'),\n join(root, 'adt.config.js'),\n join(root, 'adt.config.mjs'),\n ].filter((value): value is string => Boolean(value));\n\n for (const configPath of [\n ...new Set(configPaths.map((value) => resolvePath(value))),\n ]) {\n if (!existsSync(configPath)) continue;\n try {\n const config = readFileSync(configPath, 'utf8');\n const formatId = config.match(\n /\\bformat\\s*:\\s*\\{[\\s\\S]*?\\bid\\s*:\\s*['\"]([^'\"]+)['\"]/i,\n )?.[1];\n const folderLogic = parseFolderLogic(\n config.match(/\\bfolderLogic\\s*:\\s*['\"]([^'\"]+)['\"]/i)?.[1],\n );\n if (formatId || folderLogic) {\n return {\n ...(formatId ? { format: formatId.trim().toLowerCase() } : {}),\n ...(folderLogic ? { folderLogic } : {}),\n };\n }\n } catch {\n // An unreadable optional config must not disable report generation.\n }\n }\n return {};\n}\n\nfunction findRepositoryRoot(): string {\n const configuredRoot =\n process.env.CI_PROJECT_DIR?.trim() || process.env.SOURCE_REPO_DIR?.trim();\n if (configuredRoot) return resolvePath(configuredRoot);\n\n // Preserve the old API behaviour for callers that pass an explicit source\n // path: paths are still reported relative to the current working directory.\n return process.cwd();\n}\n\nfunction resolveRepository(srcRoot: string): ResolverRepository {\n const usesConfiguredSourceRoot = srcRoot === 'src/' || srcRoot === 'src';\n const root = usesConfiguredSourceRoot ? findRepositoryRoot() : process.cwd();\n const metadata = parseAbapGitMetadata(root);\n const configured = parseConfiguredFormat(root);\n const configuredSourceFolder = metadata?.startingFolder ?? 'src';\n const sourcePath = usesConfiguredSourceRoot\n ? join(root, configuredSourceFolder)\n : resolvePath(process.cwd(), srcRoot);\n\n // Guard against path traversal via STARTING_FOLDER — the resolved source\n // root must stay inside the repository root. Use realpath to resolve\n // symlinks before the containment check. If it escapes, fall back to\n // the default `src` folder. Guard existsSync before realpathSync so a\n // missing source folder does not throw and abort report generation.\n const realRoot = existsSync(root) ? realpathSync(root) : root;\n const realSourcePath = existsSync(sourcePath)\n ? realpathSync(sourcePath)\n : sourcePath;\n const safeSourcePath =\n realSourcePath === realRoot || realSourcePath.startsWith(realRoot + sep)\n ? sourcePath\n : join(root, 'src');\n\n return {\n root,\n sourceRoot: safeSourcePath,\n metadata,\n configuredFormat: configured.format,\n configuredFolderLogic: configured.folderLogic,\n };\n}\n\n// ── Source tree and file contents ───────────────────────────────────────\n\nconst fileCache = new Map<string, string[]>();\n\nconst SOURCE_EXTENSIONS = ['.abap', '.xml', '.acds', '.abdl'];\n\nfunction hasSourceExtension(filename: string): boolean {\n return SOURCE_EXTENSIONS.some((ext) => filename.endsWith(ext));\n}\n\nfunction indexEntry(\n entry: Dirent,\n current: string,\n result: string[],\n stack: string[],\n): void {\n const fullPath = join(current, entry.name);\n if (entry.isDirectory()) {\n stack.push(fullPath);\n } else if (entry.isFile() && hasSourceExtension(entry.name)) {\n result.push(fullPath);\n }\n}\n\nfunction collectSourceFiles(root: string): string[] {\n const result: string[] = [];\n const stack: string[] = [root];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n let entries: Dirent[];\n try {\n entries = readdirSync(current, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n indexEntry(entry, current, result, stack);\n }\n }\n\n return result;\n}\n\nfunction getFileLines(filePath: string): string[] | null {\n const cached = fileCache.get(filePath);\n if (cached) return cached;\n try {\n const lines = readFileSync(filePath, 'utf8').split('\\n');\n fileCache.set(filePath, lines);\n return lines;\n } catch {\n return null;\n }\n}\n\n// ── Method range parsing ────────────────────────────────────────────────\n\ninterface MethodRange {\n name: string;\n startLine: number;\n length: number;\n}\n\nfunction parseMethodRanges(lines: string[]): MethodRange[] {\n const ranges: MethodRange[] = [];\n let currentMethod: string | null = null;\n let methodStart = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const match = lines[i].match(/^\\s*METHOD\\s+(\\w+)/i);\n if (match) {\n currentMethod = match[1].toLowerCase();\n methodStart = i + 1;\n }\n if (currentMethod && /^\\s*ENDMETHOD/i.test(lines[i])) {\n const endLine = i + 1;\n ranges.push({\n name: currentMethod,\n startLine: methodStart,\n length: endLine - methodStart + 1,\n });\n currentMethod = null;\n }\n }\n return ranges;\n}\n\nfunction convertLine(\n atcLine: number,\n methodName: string | undefined,\n filePath: string,\n): number {\n if (!filePath.endsWith('.clas.abap')) return atcLine;\n\n const lines = getFileLines(filePath);\n if (!lines) return atcLine;\n\n const ranges = parseMethodRanges(lines);\n if (ranges.length === 0) return atcLine;\n\n if (methodName) {\n const method = ranges.find(\n (range) => range.name === methodName.toLowerCase(),\n );\n if (method) return method.startLine + atcLine - 1;\n }\n\n // Without a method name the ATC line is already relative to the class\n // file, so return it as-is instead of guessing a method.\n return atcLine;\n}\n\n// ── Resolver factory ────────────────────────────────────────────────────\n\n/**\n * Create a built-in abapGit finding resolver.\n *\n * The full ATC location is used to derive the canonical abapGit basename.\n * The actual checked-out path is then selected from the configured source\n * tree, which naturally preserves PREFIX/FULL package directories and the\n * repository's STARTING_FOLDER.\n */\nexport function createAbapGitResolver(srcRoot = 'src/'): FindingResolver {\n const repository = resolveRepository(srcRoot);\n const lookup = new Map<string, string>();\n\n try {\n if (existsSync(repository.sourceRoot)) {\n const files = collectSourceFiles(repository.sourceRoot).sort(\n (left, right) => left.localeCompare(right),\n );\n\n const ambiguous = new Set<string>();\n for (const filePath of files) {\n const name = basename(filePath).toLowerCase();\n if (lookup.has(name)) {\n ambiguous.add(name);\n } else {\n lookup.set(name, filePath);\n }\n }\n for (const name of ambiguous) lookup.delete(name);\n }\n } catch {\n // Source scan failed — resolver will return null for all findings.\n }\n\n if (lookup.size > 0) {\n const metadata = repository.metadata;\n const format = repository.configuredFormat ?? 'abapgit';\n const folderLogic =\n metadata?.folderLogic ?? repository.configuredFolderLogic ?? 'prefix';\n const startingFolder = metadata?.startingFolder ?? 'src';\n console.log(\n `📂 Finding resolver: ${lookup.size} files indexed (format=${format}, folderLogic=${folderLogic}, startingFolder=${startingFolder})`,\n );\n }\n\n return {\n async resolve(\n objectType: string,\n objectName: string,\n atcLine: number,\n methodName?: string,\n atcLocation?: string,\n ): Promise<ResolvedLocation | null> {\n const canonicalPath = atcLocation\n ? adtUriToAbapGitPath(atcLocation)\n : null;\n const candidatePath =\n canonicalPath ??\n `${objectName.toLowerCase()}.${objectType.toLowerCase()}.abap`;\n const candidateSegments = candidatePath.split('/');\n const expectedFilename = candidateSegments[candidateSegments.length - 1];\n if (!expectedFilename) return null;\n const resolvedPath = lookup.get(expectedFilename.toLowerCase());\n\n if (!resolvedPath) return null;\n\n const fileLine = convertLine(atcLine, methodName, resolvedPath);\n const gitPath = toPosix(relative(repository.root, resolvedPath));\n if (!gitPath || gitPath.startsWith('../')) return null;\n\n return { path: gitPath, line: fileLine };\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"atc-CIPiGB5o.mjs","names":[],"sources":["../src/formatters/sarif.ts","../src/formatters/gitlab.ts","../src/check-categories.ts","../src/documentation-bridge.ts","../src/commands/atc.ts"],"sourcesContent":["/**\n * SARIF Formatter\n *\n * Outputs ATC findings in SARIF format for GitHub Code Scanning.\n */\n\nimport { writeFile } from 'node:fs/promises';\nimport type { AtcResult, AtcFinding } from '../types';\n\nexport async function outputSarifReport(\n result: AtcResult,\n outputFile: string,\n _targetName?: string,\n): Promise<void> {\n // Transform ATC findings to SARIF format\n const rules = Array.from(\n new Map(\n result.findings.map((f) => [\n f.checkId,\n {\n id: f.checkId,\n name: f.checkTitle || f.checkId,\n shortDescription: { text: f.checkTitle || f.checkId },\n fullDescription: { text: f.checkTitle || f.checkId },\n defaultConfiguration: {\n level:\n f.priority === 1\n ? 'error'\n : f.priority === 2\n ? 'warning'\n : 'note',\n },\n properties: {\n tags: ['abap', 'code-quality'],\n precision: 'high',\n ...(f.checkCategory && { category: f.checkCategory }),\n },\n },\n ]),\n ).values(),\n );\n\n const results = result.findings.map((finding: AtcFinding) => {\n const filePath = `src/${finding.objectType.toLowerCase()}/${finding.objectName.toLowerCase()}.${finding.objectType.toLowerCase()}`;\n\n // Parse line number from location if available\n const lineMatch = finding.location?.match(/start=(\\d+)/);\n const line = lineMatch ? Number.parseInt(lineMatch[1], 10) : 1;\n\n return {\n ruleId: finding.checkId,\n level:\n finding.priority === 1\n ? 'error'\n : finding.priority === 2\n ? 'warning'\n : 'note',\n message: { text: finding.messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: filePath },\n region: {\n startLine: line,\n startColumn: 1,\n },\n },\n },\n ],\n partialFingerprints: {\n primaryLocationLineHash: `${finding.checkId}-${finding.objectName}-${line}`,\n },\n };\n });\n\n const sarifReport = {\n $schema: 'https://json.schemastore.org/sarif-2.1.0.json',\n version: '2.1.0',\n runs: [\n {\n tool: {\n driver: {\n name: 'ABAP Test Cockpit',\n version: '1.0.0',\n informationUri:\n 'https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/4ec5711c6e391014adc9fffe4e204223.html',\n rules: Array.from(rules),\n },\n },\n results,\n },\n ],\n };\n\n await writeFile(outputFile, JSON.stringify(sarifReport, null, 2));\n console.log(`\\n📄 SARIF report written to: ${outputFile}`);\n console.log(\n `📊 ${result.totalFindings} issues exported for GitHub Code Scanning`,\n );\n}\n","/**\n * GitLab Code Quality Formatter\n *\n * Outputs ATC findings in GitLab Code Quality format.\n *\n * This formatter is a pure output formatter — it maps ATC findings to the\n * GitLab Code Quality JSON structure. Path resolution and line number\n * conversion are delegated to an optional FindingResolver.\n *\n * Without a resolver, the formatter produces PREFIX-style paths and raw\n * ATC (method-relative) line numbers. With a resolver (e.g., from\n * @abapify/adt-plugin-abapgit), it produces correct git paths and\n * file-relative line numbers.\n *\n * @example\n * ```typescript\n * // Without resolver (standalone CLI usage)\n * await outputGitLabCodeQuality(result, 'report.json');\n *\n * // With resolver (inside an abapgit repo)\n * import { createFindingResolver } from '@abapify/adt-plugin-abapgit';\n * const resolver = await createFindingResolver();\n * await outputGitLabCodeQuality(result, 'report.json', { resolver });\n * ```\n */\n\nimport { writeFile } from 'node:fs/promises';\nimport type { AtcResult, AtcFinding, FindingResolver } from '../types';\n\n/**\n * Options for the GitLab Code Quality formatter\n */\nexport interface GitLabFormatterOptions {\n /** Optional resolver for path/line resolution (e.g., from abapgit plugin) */\n resolver?: FindingResolver;\n}\n\n// ── Extract method name from ATC location URI ───────────────────────────\n// Format 1: /sap/bc/adt/oo/classes/zcl_foo/methods/my_method#start=21,0\n// Format 2: ...#type=CLAS%2FOM;name=MY_METHOD;start=21\n\nfunction extractMethodName(location?: string): string | undefined {\n if (!location) return undefined;\n const match =\n location.match(/\\/methods\\/(\\w+)/i) || location.match(/[;?&]name=(\\w+)/i);\n return match ? match[1].toLowerCase() : undefined;\n}\n\n// ── Construct PREFIX-style fallback path ─────────────────────────────────\n\nfunction prefixPath(finding: AtcFinding): string {\n return `src/${finding.objectType.toLowerCase()}/${finding.objectName.toLowerCase()}.${finding.objectType.toLowerCase()}.abap`;\n}\n\n// ── Main formatter ──────────────────────────────────────────────────────\n\nexport async function outputGitLabCodeQuality(\n result: AtcResult,\n outputFile: string,\n options?: GitLabFormatterOptions,\n): Promise<void> {\n const resolver = options?.resolver;\n\n // Transform ATC findings to GitLab Code Quality format\n const gitlabReport = await Promise.all(\n result.findings.map(async (finding: AtcFinding) => {\n // Map ATC priority to GitLab severity\n let severity: string;\n switch (finding.priority) {\n case 1:\n severity = 'blocker';\n break;\n case 2:\n severity = 'major';\n break;\n case 3:\n severity = 'minor';\n break;\n default:\n severity = 'info';\n break;\n }\n\n // Parse ATC line number from location\n const lineMatch = finding.location?.match(/start=(\\d+)/);\n const atcLine = lineMatch ? Number.parseInt(lineMatch[1], 10) : 1;\n\n // Extract method name from ATC location URI\n const methodName = extractMethodName(finding.location);\n\n // Resolve path and line via plugin, or fall back to PREFIX-style\n let filePath: string;\n let fileLine: number;\n\n if (resolver) {\n const resolved = await resolver.resolve(\n finding.objectType,\n finding.objectName,\n atcLine,\n methodName,\n finding.location,\n );\n if (resolved) {\n filePath = resolved.path;\n fileLine = resolved.line;\n } else {\n filePath = prefixPath(finding);\n fileLine = atcLine;\n }\n } else {\n filePath = prefixPath(finding);\n fileLine = atcLine;\n }\n\n // Create unique fingerprint\n const fingerprint = `${finding.checkId}-${finding.objectName}-${fileLine}`;\n\n return {\n description: finding.messageText,\n check_name: finding.checkTitle || finding.checkId,\n // The worklist checkId as SAP reports it. On live systems this is a\n // per-instance GUID (stable for the finding, but not a catalog\n // identity); checkTitle is a display name that SAP can rename, so\n // downstream consumers should key on this field rather than the\n // title.\n check_id: finding.checkId || undefined,\n fingerprint,\n severity,\n location: {\n path: filePath,\n lines: {\n begin: fileLine,\n end: fileLine,\n },\n },\n // Extra fields for downstream processing\n ...(methodName && { method: methodName }),\n ...(finding.location && { atc_location: finding.location }),\n ...(finding.checkCategory && {\n check_category: finding.checkCategory,\n }),\n };\n }),\n );\n\n await writeFile(outputFile, JSON.stringify(gitlabReport, null, 2));\n console.log(`\\n📄 GitLab Code Quality report written to: ${outputFile}`);\n console.log(`📊 ${result.totalFindings} issues exported`);\n}\n","/**\n * Check Category Enrichment\n *\n * Resolves ATC findings to their check category (e.g., 'CI_PERFORMANCE',\n * 'CI_SECURITY') through the check variant form template\n * (GET /sap/bc/adt/atc/checkvariants/formtemplate?chkvName={name}).\n *\n * The form template is the check catalog of the run's check variant: every\n * available check category lists its checks, and each check carries its\n * implementing class. When a finding's checkId matches a catalog\n * implementing class, the index resolves it directly. Live worklist\n * checkIds are check-instance GUIDs (observed on a live S/4 system,\n * 2026-10-22); those resolve through the documentation bridge in\n * ./documentation-bridge.ts.\n *\n * Enrichment is best-effort by design: without the form template, or for a\n * checkId outside the catalog, findings keep no category and the run still\n * succeeds.\n */\n\n/**\n * Structural shape of the parsed check variant form template document\n * (GET /sap/bc/adt/atc/checkvariants/formtemplate).\n *\n * Only the fields needed for the checkId -> category index are declared.\n */\nexport interface FormTemplateDocument {\n formTemplate?: {\n availableCheckCategories?: {\n availableCheckCategory?: FormTemplateCategoryEntry[];\n };\n };\n}\n\n/** One available check category with the checks it contains. */\nexport interface FormTemplateCategoryEntry {\n checkCategoryObject?: {\n /** Category name (the enrichment key, e.g., 'CI_PERFORMANCE') */\n name?: string;\n /** Category display title (e.g., 'Performance') */\n description?: string;\n };\n check?: FormTemplateCheckEntry[];\n}\n\n/** One check entry of a category. */\nexport interface FormTemplateCheckEntry {\n checkObject?: {\n /** Check name (e.g., 'CI_ANALYZE_WHERE') */\n name?: string;\n content?: {\n /** The check's category assignment (the category name) */\n category?: { name?: string };\n /**\n * Implementing class of the check (e.g., 'CL_CI_TEST_SELECT_TAW_A').\n * Worklist findings carry a check-instance GUID instead; the\n * documentation bridge resolves that GUID to this class name.\n */\n implementingClass?: { name?: string };\n };\n };\n}\n\n/**\n * Build a checkId -> check category name index from a parsed check variant\n * form template document.\n *\n * The checkId key is the implementing class name; the value is the category\n * object name (a stable key like 'CI_PERFORMANCE', not the display title).\n *\n * @param doc - Parsed form template document (nullable for convenience)\n * @returns Map of checkId -> category name; empty when the document is\n * missing or malformed\n */\nexport function parseCheckCategoryIndex(\n doc: FormTemplateDocument | null | undefined,\n): Map<string, string> {\n const index = new Map<string, string>();\n const categories =\n doc?.formTemplate?.availableCheckCategories?.availableCheckCategory ?? [];\n for (const category of categories) {\n collectCategoryChecks(category, index);\n }\n return index;\n}\n\n/** Index every check of one category under the category's name. */\nfunction collectCategoryChecks(\n category: FormTemplateCategoryEntry,\n index: Map<string, string>,\n): void {\n const categoryName = category.checkCategoryObject?.name;\n if (!categoryName) return;\n const checkIds = (category.check ?? [])\n .map((check) => check.checkObject?.content?.implementingClass?.name)\n .filter((checkId): checkId is string => !!checkId);\n for (const checkId of checkIds) {\n index.set(checkId, categoryName);\n }\n}\n\n/** Structural client surface needed to fetch a form template. */\nexport interface FormTemplateClient {\n adt: {\n atc: {\n checkvariants: {\n /** GET /sap/bc/adt/atc/checkvariants/formtemplate{?chkvName} */\n formtemplate: (params?: {\n chkvName?: string;\n }) => Promise<FormTemplateDocument>;\n };\n };\n };\n}\n\n/**\n * Fetch the check variant form template and build the checkId -> category\n * name index for a run.\n *\n * Best-effort: any error (network, 404, malformed document) is swallowed\n * and reported through the optional logger so a failed enrichment never\n * fails the ATC run itself.\n *\n * @param client - ADT client with the checkvariants.formtemplate surface\n * @param checkVariant - The check variant name of the run (e.g., 'DEFAULT')\n * @param logger - Optional CLI logger for diagnostics\n * @returns checkId -> category name index; empty when unavailable\n */\nexport async function fetchCheckCategoryIndex(\n client: FormTemplateClient,\n checkVariant: string,\n logger?: {\n warn: (message: string) => void;\n info?: (message: string) => void;\n },\n): Promise<Map<string, string>> {\n try {\n const doc = await client.adt.atc.checkvariants.formtemplate({\n chkvName: checkVariant,\n });\n return parseCheckCategoryIndex(doc);\n } catch (error) {\n logger?.warn?.(\n `⚠️ Check category enrichment unavailable: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return new Map();\n }\n}\n\n/**\n * Enrich findings with their check category in place.\n *\n * Generic over any finding shape carrying a checkId (and an optional\n * checkCategory field), so both the CLI plugin's AtcFinding and other\n * canonical finding types can be enriched with the same index.\n *\n * @param findings - Findings to enrich (mutated in place)\n * @param index - checkId -> category name index\n * @returns Number of findings that received a category\n */\nexport function enrichFindingsWithCategories<\n T extends { checkId: string; checkCategory?: string },\n>(findings: T[], index: Map<string, string>): number {\n let enriched = 0;\n for (const finding of findings) {\n const checkCategory = index.get(finding.checkId);\n if (checkCategory) {\n finding.checkCategory = checkCategory;\n enriched++;\n }\n }\n return enriched;\n}\n\n/**\n * Enrich an ATC result with check categories in place.\n *\n * @param result - The run result (findings mutated in place)\n * @param index - checkId -> category name index\n * @returns Number of findings that received a category\n */\nexport function enrichResultWithCategories<\n T extends { checkId: string; checkCategory?: string },\n>(result: { findings?: T[] }, index: Map<string, string>): number {\n return enrichFindingsWithCategories(result.findings ?? [], index);\n}\n","/**\n * Documentation Bridge for Check Category Enrichment\n *\n * ATC worklist findings carry a check-instance GUID as their checkId — not\n * the implementing class name the check variant form template catalog lists\n * (observed on a live S/4 system, 2026-10-22). The finding's documentation\n * page (the worklist atom:link) names the check class in its header line,\n * which resolves through the form-template category index.\n *\n * Everything here is best-effort: fetch errors are reported through the\n * optional logger and skipped; findings keep no category when their page\n * cannot be fetched or parsed.\n */\n\n/** Structural surface for a bounded documentation read. The real ADT client\n * provides readTextBounded; mocks without it simply skip the bridge. */\nexport interface DocumentationReadingClient {\n readTextBounded?: (\n uri: string,\n maxBytes: number,\n options?: { headers?: Record<string, string> },\n ) => Promise<string>;\n}\n\n/**\n * Parse the implementing check class from an ATC finding documentation page.\n *\n * Live shape (2026-10-22): the page opens with\n * \"Appl. Comp. Check / Check Class / Message Code\" followed by\n * `<p>BC-ABA-LA / CL_CI_TEST_SELECT_TAW_A / 0501</p>`. The check class in\n * that line is the implementing class the check variant form template lists\n * — the bridge from the finding's GUID checkId to the check catalog.\n *\n * The match is bounded (no unbounded quantifiers on page input) and accepts\n * an optional ABAP namespace prefix (e.g. `/NSP/CL_EXAMPLE`).\n *\n * @param html - The documentation page HTML\n * @returns The check class name, or undefined when the page does not carry it\n */\nexport function parseCheckClassFromDocumentation(\n html: string,\n): string | undefined {\n const match = html.match(\n /Check\\s*Class[^<]{0,80}<\\/h3>\\s*<p>\\s*[^/<]{0,120}\\/\\s*((?:\\/[A-Za-z0-9_]{1,8}\\/)?[A-Za-z][A-Za-z0-9_]*)\\s*\\//u,\n );\n return match?.[1];\n}\n\n/** Finding shape the bridge enriches in place. */\nexport interface DocumentationBridgeFinding {\n checkId: string;\n checkCategory?: string;\n documentationUri?: string;\n}\n\n/**\n * Only ATC documentation paths may be fetched — a worklist-provided URI\n * must never drive an authenticated read outside the documentation space.\n */\nconst ATC_DOCUMENTATION_URI =\n /^\\/sap\\/bc\\/adt\\/documentation\\/atc\\/documents\\/[\\w/%-]+$/u;\n\n/** Shared state for the per-URI documentation fetch. */\ninterface DocumentationFetchContext {\n read: NonNullable<DocumentationReadingClient['readTextBounded']>;\n maxBytes: number;\n maxReads: number;\n reads: number;\n classByDocumentationUri: Map<string, string | undefined>;\n logger?: { warn: (message: string) => void };\n}\n\n/**\n * Read one documentation page and resolve its check class, with a shared\n * per-URI cache so repeated findings never re-fetch the same page.\n */\nasync function resolveCheckClassForUri(\n documentationUri: string,\n context: DocumentationFetchContext,\n): Promise<void> {\n if (context.classByDocumentationUri.has(documentationUri)) return;\n if (!ATC_DOCUMENTATION_URI.test(documentationUri)) {\n context.logger?.warn?.(\n `⚠️ Skipping non-ATC documentation URI: ${documentationUri}`,\n );\n context.classByDocumentationUri.set(documentationUri, undefined);\n return;\n }\n if (context.reads >= context.maxReads) {\n context.classByDocumentationUri.set(documentationUri, undefined);\n return;\n }\n context.reads++;\n try {\n const html = await context.read(documentationUri, context.maxBytes, {\n headers: { Accept: 'application/vnd.sap.adt.docu.v1+html' },\n });\n context.classByDocumentationUri.set(\n documentationUri,\n parseCheckClassFromDocumentation(html),\n );\n } catch (error) {\n context.logger?.warn?.(\n `⚠️ ATC documentation read failed for ${documentationUri}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n context.classByDocumentationUri.set(documentationUri, undefined);\n }\n}\n\n/**\n * Bridge finding categories through the ATC documentation page.\n *\n * After the direct form-template index pass (which resolves findings whose\n * checkId already matches a catalog implementing class), this resolves the\n * GUID-checkId findings: each finding's documentation page names the check\n * class, which resolves through the same index.\n *\n * Best-effort: per-documentation-URI fetch errors are reported through the\n * optional logger and skipped; a finding keeps no category when its page\n * cannot be fetched or parsed. Reads are deduplicated per URI.\n *\n * @param client - ADT client with an optional bounded text read\n * @param findings - Findings to enrich in place\n * @param classToCategory - checkId/class -> category name index (the\n * form-template catalog: parseCheckCategoryIndex output)\n * @param options - maxBytes (default 64 KiB) and an optional logger\n * @returns Number of findings that received a category\n */\nexport async function resolveCategoriesViaDocumentation(\n client: DocumentationReadingClient,\n findings: DocumentationBridgeFinding[],\n classToCategory: ReadonlyMap<string, string>,\n options: {\n maxBytes?: number;\n maxReads?: number;\n logger?: { warn: (message: string) => void };\n } = {},\n): Promise<number> {\n const read = client.readTextBounded;\n if (!read || classToCategory.size === 0) return 0;\n\n const pending = findings.filter(\n (finding) => !finding.checkCategory && finding.documentationUri,\n );\n if (pending.length === 0) return 0;\n\n const context: DocumentationFetchContext = {\n read,\n maxBytes: options.maxBytes ?? 64 * 1024,\n maxReads: options.maxReads ?? 500,\n reads: 0,\n classByDocumentationUri: new Map<string, string | undefined>(),\n logger: options.logger,\n };\n let enriched = 0;\n for (const finding of pending) {\n await resolveCheckClassForUri(finding.documentationUri!, context);\n const checkClass = context.classByDocumentationUri.get(\n finding.documentationUri!,\n );\n const checkCategory = checkClass\n ? classToCategory.get(checkClass)\n : undefined;\n if (checkCategory) {\n finding.checkCategory = checkCategory;\n enriched++;\n }\n }\n return enriched;\n}\n","/**\n * ATC Command Plugin\n *\n * CLI-agnostic command for running ABAP Test Cockpit checks.\n * Uses the CliContext.getAdtClient() factory for API access.\n */\n\nimport type { CliCommandPlugin, CliContext } from '@abapify/adt-plugin';\nimport chalk from 'chalk';\nimport { outputSarifReport, outputGitLabCodeQuality } from '../formatters';\nimport {\n fetchCheckCategoryIndex,\n enrichResultWithCategories,\n} from '../check-categories';\nimport { resolveCategoriesViaDocumentation } from '../documentation-bridge';\nimport type { AtcResult, AtcFinding, OutputFormat } from '../types';\n\n// Client type - the plugin receives a typed client from context\n// The client.adt.atc namespace provides all ATC operations\ninterface AdtClient {\n adt: {\n atc: {\n customizing: {\n get: () => Promise<{\n customizing: {\n properties: { property?: Array<{ name: string; value?: string }> };\n };\n }>;\n };\n worklists: {\n create: (params: {\n checkVariant: string;\n }) => Promise<\n | string\n | { worklistRun?: { worklistId: string }; worklist?: { id: string } }\n >;\n get: (\n id: string,\n params: { includeExemptedFindings: string },\n ) => Promise<WorklistResponse>;\n };\n checkvariants: {\n formtemplate: (params?: {\n chkvName?: string;\n }) => Promise<import('../check-categories').FormTemplateDocument>;\n };\n runs: {\n post: (\n params: { worklistId: string },\n body: {\n run: {\n maximumVerdicts: number;\n objectSets: {\n objectSet: Array<{\n kind: string;\n objectReferences: {\n objectReference: Array<{ uri: string }>;\n };\n }>;\n };\n };\n },\n ) => Promise<void>;\n };\n };\n };\n /**\n * Bounded documentation text read (the real ADT client provides it).\n * Optional: without it the documentation category bridge is skipped.\n */\n readTextBounded?: (\n uri: string,\n maxBytes: number,\n options?: { headers?: Record<string, string> },\n ) => Promise<string>;\n}\n\n// Worklist response type\ninterface WorklistResponse {\n worklist?: {\n id: string;\n timestamp: string;\n objects: {\n object?: Array<{\n uri?: string;\n type?: string;\n name?: string;\n findings: {\n finding?: Array<{\n uri?: string;\n location?: string;\n priority?: string;\n checkId?: string;\n checkTitle?: string;\n messageId?: string;\n messageTitle?: string;\n /** Documentation link: the page that names the check class. */\n link?: Array<{ href?: string; rel?: string }>;\n }>;\n };\n }>;\n };\n };\n worklistRun?: { worklistId: string };\n}\n\n/**\n * Create OSC 8 hyperlink for terminal\n */\nfunction hyperlink(text: string, url: string): string {\n const OSC = '\\x1b]';\n const BEL = '\\x07';\n const SEP = ';';\n return `${OSC}8${SEP}${SEP}${url}${BEL}${text}${OSC}8${SEP}${SEP}${BEL}`;\n}\n\n/**\n * Create ADT Eclipse link\n */\nfunction adtLink(name: string, uri: string, systemName?: string): string {\n if (!systemName || !name || !uri) {\n return name ? chalk.cyan(name) : '';\n }\n const path = uri.startsWith('/sap/bc/adt') ? uri : `/sap/bc/adt${uri}`;\n const url = `adt://${systemName}${path}`;\n return hyperlink(chalk.cyan(name), url);\n}\n\n/**\n * The trusted documentation link of one worklist finding.\n *\n * Live shape (2026-10-22): <atom:link rel=\"http://www.sap.com/adt/\n * relations/documentation\" href=\"/sap/bc/adt/documentation/atc/documents/\n * itemid/{guid}/index/{n}\"/> inside the atcfinding:finding element.\n */\nfunction documentationUriFromFinding(\n finding: NonNullable<\n NonNullable<\n NonNullable<WorklistResponse['worklist']>['objects']['object']\n >[number]['findings']['finding']\n >[number],\n): string | undefined {\n return finding.link?.find(\n (link) => link.rel === 'http://www.sap.com/adt/relations/documentation',\n )?.href;\n}\n\n/**\n * Convert worklist response to AtcResult\n */\nfunction convertWorklistToResult(\n worklistResponse: WorklistResponse,\n checkVariant: string,\n): AtcResult {\n const findings: AtcFinding[] = [];\n\n if (worklistResponse.worklist) {\n const objects = worklistResponse.worklist.objects.object || [];\n\n for (const obj of objects) {\n const objFindings = obj.findings.finding || [];\n for (const finding of objFindings) {\n findings.push({\n checkId: finding.checkId || '',\n checkTitle: finding.checkTitle || '',\n messageId: finding.messageId || '',\n priority: Number.parseInt(finding.priority || '3', 10),\n messageText: finding.messageTitle || '',\n objectUri: obj.uri || '',\n objectType: obj.type || '',\n objectName: obj.name || '',\n location: finding.location,\n findingUri: finding.uri,\n documentationUri: documentationUriFromFinding(finding),\n });\n }\n }\n }\n\n const errorCount = findings.filter((f) => f.priority === 1).length;\n const warningCount = findings.filter((f) => f.priority === 2).length;\n const infoCount = findings.filter((f) => f.priority >= 3).length;\n\n return {\n checkVariant,\n totalFindings: findings.length,\n errorCount,\n warningCount,\n infoCount,\n findings,\n };\n}\n\n/**\n * Display ATC results in console\n */\nfunction displayAtcResults(result: AtcResult, systemName?: string): void {\n if (result.totalFindings === 0) {\n console.log(`\\n✅ ATC check passed - No issues found!`);\n console.log(` 🎯 Variant: ${result.checkVariant}`);\n return;\n }\n\n console.log(`\\n📊 ATC Results Summary:`);\n console.log(` 🎯 Variant: ${result.checkVariant}`);\n if (result.errorCount > 0) console.log(` ❌ Errors: ${result.errorCount}`);\n if (result.warningCount > 0)\n console.log(` ⚠️ Warnings: ${result.warningCount}`);\n if (result.infoCount > 0) console.log(` ℹ️ Info: ${result.infoCount}`);\n console.log(` 📋 Total Issues: ${result.totalFindings}`);\n\n if (result.findings && result.findings.length > 0) {\n console.log(`\\n📄 Findings:`);\n result.findings.slice(0, 5).forEach((finding) => {\n const priorityIcon =\n finding.priority === 1 ? '❌' : finding.priority === 2 ? '⚠️' : 'ℹ️';\n const objectLink = finding.location\n ? adtLink(finding.objectName, finding.location, systemName)\n : finding.objectName;\n console.log(\n ` ${priorityIcon} ${finding.checkTitle || finding.checkId}`,\n );\n console.log(` ${finding.messageText}`);\n console.log(` Object: ${objectLink} (${finding.objectType})`);\n });\n\n if (result.findings.length > 5) {\n console.log(` ... (${result.findings.length - 5} more findings)`);\n }\n }\n\n console.log(`\\n💡 Use --debug for detailed results`);\n}\n\n/**\n * ATC Command Plugin\n */\nexport const atcCommand: CliCommandPlugin = {\n name: 'atc',\n description: 'Run ABAP Test Cockpit (ATC) checks',\n\n options: [\n { flags: '-p, --package <package>', description: 'Run ATC on package' },\n {\n flags: '-o, --object <uri>',\n description:\n 'Run ATC on specific object (e.g., /sap/bc/adt/oo/classes/zcl_my_class)',\n },\n {\n flags: '-t, --transport <transport>',\n description: 'Run ATC on transport request (e.g., TRLK942970)',\n },\n {\n flags: '-f, --from-file <file>',\n description: 'Run ATC on objects listed in file (one URI per line)',\n },\n {\n flags: '--variant <variant>',\n description: 'ATC check variant (default: from system customizing)',\n },\n {\n flags: '--max-results <number>',\n description: 'Maximum number of results',\n default: '100',\n },\n {\n flags: '--format <format>',\n description: 'Output format: console, json, gitlab, sarif',\n default: 'console',\n },\n {\n flags: '--output <file>',\n description: 'Output file (required for gitlab/sarif format)',\n },\n {\n flags: '--resolver <name>',\n description:\n 'Finding resolver plugin for path/line resolution (e.g., abapgit)',\n },\n ],\n\n async execute(args, ctx: CliContext) {\n const options = args as {\n package?: string;\n object?: string;\n transport?: string;\n fromFile?: string;\n variant?: string;\n maxResults?: string;\n format?: OutputFormat;\n output?: string;\n resolver?: string;\n };\n\n // Validate mutually exclusive options\n const targetCount = [\n options.package,\n options.object,\n options.transport,\n options.fromFile,\n ].filter(Boolean).length;\n\n if (targetCount === 0) {\n ctx.logger.error(\n '❌ One of --package, --object, --transport, or --from-file is required',\n );\n process.exit(1);\n }\n\n if (targetCount > 1) {\n ctx.logger.error(\n '❌ Only one of --package, --object, --transport, or --from-file can be specified',\n );\n process.exit(1);\n }\n\n // Validate output file for gitlab/sarif formats\n if (\n (options.format === 'gitlab' || options.format === 'sarif') &&\n !options.output\n ) {\n ctx.logger.error(`❌ --output is required for ${options.format} format`);\n process.exit(1);\n }\n\n // Get ADT client from context\n if (!ctx.getAdtClient) {\n ctx.logger.error(\n '❌ ADT client not available. This command requires authentication.',\n );\n ctx.logger.error(' Run: adt auth login');\n process.exit(1);\n }\n\n const client = (await ctx.getAdtClient()) as AdtClient;\n\n ctx.logger.info('🔍 Running ABAP Test Cockpit checks...');\n\n // Determine target(s)\n let targetUris: string[];\n let targetName: string;\n\n if (options.fromFile) {\n // Read URIs from file\n const { readFileSync } = await import('fs');\n const fileContent = readFileSync(options.fromFile, 'utf-8');\n targetUris = fileContent\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith('#')); // Skip empty lines and comments\n\n if (targetUris.length === 0) {\n ctx.logger.error(`❌ No objects found in ${options.fromFile}`);\n process.exit(1);\n }\n targetName = `${targetUris.length} objects from ${options.fromFile}`;\n ctx.logger.info(`📄 Target: ${targetName}`);\n } else if (options.transport) {\n targetUris = [`/sap/bc/adt/cts/transportrequests/${options.transport}`];\n targetName = `Transport ${options.transport}`;\n ctx.logger.info(`🚚 Target: ${targetName}`);\n } else if (options.package) {\n targetUris = [`/sap/bc/adt/packages/${options.package.toUpperCase()}`];\n targetName = `Package ${options.package.toUpperCase()}`;\n ctx.logger.info(`📦 Target: ${targetName}`);\n } else {\n targetUris = [options.object!];\n targetName = options.object!;\n ctx.logger.info(`📄 Target: ${targetName}`);\n }\n\n // Step 1: Get ATC customizing to find default check variant\n ctx.logger.info('📋 Reading ATC customizing...');\n const customizing = await client.adt.atc.customizing.get();\n\n // Find systemCheckVariant in properties\n let checkVariant = options.variant;\n if (!checkVariant) {\n const variantProp = customizing.customizing.properties.property?.find(\n (p) => p.name === 'systemCheckVariant',\n );\n if (variantProp?.value) {\n checkVariant = variantProp.value;\n } else {\n checkVariant = 'DEFAULT';\n }\n }\n ctx.logger.info(`🎯 Check variant: ${checkVariant}`);\n\n // Step 2: Create worklist (just with checkVariant, no objects)\n ctx.logger.info('📋 Creating ATC worklist...');\n const worklistCreateResponse = await client.adt.atc.worklists.create({\n checkVariant,\n });\n\n // Extract worklist ID - SAP can return plain string or object\n let worklistId: string;\n if (typeof worklistCreateResponse === 'string') {\n // Plain string response - extract ID from XML or use as-is\n const idMatch = worklistCreateResponse.match(/id=\"([^\"]+)\"/);\n worklistId = idMatch ? idMatch[1] : worklistCreateResponse.trim();\n } else if (\n 'worklist' in worklistCreateResponse &&\n worklistCreateResponse.worklist?.id\n ) {\n worklistId = worklistCreateResponse.worklist.id;\n } else if (\n 'worklistRun' in worklistCreateResponse &&\n worklistCreateResponse.worklistRun?.worklistId\n ) {\n worklistId = worklistCreateResponse.worklistRun.worklistId;\n } else {\n ctx.logger.error('❌ Failed to get worklist ID from response');\n process.exit(1);\n }\n ctx.logger.info(`📋 Worklist created: ${worklistId}`);\n\n // Step 3: Run ATC checks with objects\n ctx.logger.info('⏳ Running ATC analysis...');\n const maxResults = Number.parseInt(options.maxResults || '100', 10);\n const runData = {\n run: {\n maximumVerdicts: maxResults,\n objectSets: {\n objectSet: [\n {\n kind: 'inclusive',\n objectReferences: {\n objectReference: targetUris.map((uri) => ({ uri })),\n },\n },\n ],\n },\n },\n };\n await client.adt.atc.runs.post({ worklistId }, runData);\n\n // Step 4: Get results\n ctx.logger.info('📊 Fetching results...');\n const worklistResult = await client.adt.atc.worklists.get(worklistId, {\n includeExemptedFindings: 'false',\n });\n\n // Convert to AtcResult\n const result = convertWorklistToResult(\n worklistResult as WorklistResponse,\n checkVariant,\n );\n\n // Enrich findings with check categories from the run's check variant\n // form template (best-effort: never fails the run itself). The catalog is\n // fetched after the run, so categories reflect the variant configuration\n // in effect at that time.\n const categoryIndex = await fetchCheckCategoryIndex(\n client,\n checkVariant,\n ctx.logger,\n );\n const enriched = enrichResultWithCategories(result, categoryIndex);\n if (enriched > 0) {\n ctx.logger.info(\n `🏷️ Enriched ${enriched}/${result.totalFindings} finding(s) with check categories`,\n );\n }\n\n // Bridge remaining findings through their ATC documentation page: the\n // worklist checkId is a check-instance GUID, not the implementing class\n // the form-template catalog lists (live evidence, 2026-10-22), so\n // the direct index match cannot resolve them. The documentation page\n // names the check class, which resolves through the same index.\n // Best-effort: never fails the run.\n const bridged = await resolveCategoriesViaDocumentation(\n client,\n result.findings,\n categoryIndex,\n { logger: ctx.logger },\n );\n if (bridged > 0) {\n ctx.logger.info(\n `🏷 Bridged ${bridged} finding(s) to check categories via ATC documentation`,\n );\n }\n\n // Load finding resolver if requested\n let resolver: import('../types').FindingResolver | undefined;\n if (options.resolver) {\n const name = options.resolver;\n if (name === 'abapgit') {\n // Built-in abapgit resolver — no external dependencies\n const { createAbapGitResolver } = await import('../resolvers/abapgit');\n resolver = createAbapGitResolver();\n } else {\n // External resolver — try dynamic import\n try {\n ctx.logger.info(`📂 Loading external resolver: ${name}`);\n const mod = await import(name);\n if (typeof mod.createFindingResolver === 'function') {\n resolver = mod.createFindingResolver();\n } else {\n ctx.logger.warn(\n `⚠️ Module ${name} does not export createFindingResolver()`,\n );\n }\n } catch (e) {\n ctx.logger.warn(\n `⚠️ Failed to load resolver \"${name}\": ${e instanceof Error ? e.message : e}`,\n );\n }\n }\n }\n\n // Output based on format\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (options.format === 'gitlab' && options.output) {\n await outputGitLabCodeQuality(result, options.output, { resolver });\n } else if (options.format === 'sarif' && options.output) {\n await outputSarifReport(result, options.output, targetName);\n } else {\n displayAtcResults(result, ctx.adtSystemName);\n }\n\n // Exit with error code if there are errors\n if (result.errorCount > 0) {\n process.exit(1);\n }\n },\n};\n\nexport default atcCommand;\n"],"mappings":"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"}