abap-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akshay Palimkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # abap-mcp
2
+
3
+ **MCP server for SAP ABAP — offline static analysis, ABAP Cloud readiness, and RAP scaffolding.**
4
+ No SAP system. No credentials. Works on ABAP source wherever your AI agent works: a git checkout,
5
+ an abapGit export, a code review, CI.
6
+
7
+ Built on [abaplint](https://abaplint.org) (the open-source ABAP parser/linter) and the
8
+ [Model Context Protocol](https://modelcontextprotocol.io). TypeScript, 100% local — the server
9
+ makes **zero network calls** and touches **no filesystem**: sources go in as text, findings come
10
+ back as structured JSON.
11
+
12
+ ## Why this exists
13
+
14
+ Every other ABAP MCP server is either a **bridge to a live SAP system** (ADT/RFC — needs
15
+ credentials, a system, and trust) or a **documentation search**. But AI coding agents spend most
16
+ of their time where the *files* are — editing abapGit repos, reviewing diffs, generating code —
17
+ long before anything reaches a system. This server gives agents the missing feedback loop at that
18
+ layer:
19
+
20
+ - *"Does this ABAP parse? Is it clean?"* → `lint_abap`
21
+ - *"How far is this classic report from ABAP Cloud?"* → `check_cloud_readiness`
22
+ - *"Start me a correct RAP business object."* → `scaffold_rap_bo`
23
+ - *"What's in this 4,000-line class?"* → `get_abap_outline`
24
+
25
+ ## Quickstart
26
+
27
+ ```bash
28
+ # Claude Code
29
+ claude mcp add abap-mcp -- npx -y abap-mcp
30
+
31
+ # or any MCP client (.mcp.json / mcp.json):
32
+ {
33
+ "mcpServers": {
34
+ "abap-mcp": { "command": "npx", "args": ["-y", "abap-mcp"] }
35
+ }
36
+ }
37
+ ```
38
+
39
+ From a clone instead:
40
+
41
+ ```bash
42
+ npm install && npm run build
43
+ claude mcp add abap-mcp -- node /path/to/abap-mcp/dist/cli.js
44
+ ```
45
+
46
+ Then ask your agent things like *"lint this class against ABAP Cloud"*, *"is zold_report
47
+ cloud-ready?"*, or *"scaffold a RAP BO for entity Booking on table zbooking, draft enabled"*.
48
+
49
+ ## Tools
50
+
51
+ | Tool | What it does |
52
+ | --- | --- |
53
+ | `lint_abap` | abaplint static analysis over ABAP/CDS/BDEF sources → structured findings with rule docs links. Presets: `style` (default, snippet-friendly), `full`, `syntax-only`; per-rule overrides. |
54
+ | `check_cloud_readiness` | Dual-parse diff (classic baseline vs `Cloud`): statements that are valid today but illegal in ABAP Cloud become categorized blockers (dynpro, list output, native SQL, …) with a transparent score; code broken at the baseline is reported separately, not counted as migration work. |
55
+ | `scaffold_rap_bo` | Generates the canonical RAP managed-BO stack (root view, behavior definition `strict(2)` + optional draft, behavior class + handler locals, projection, metadata extension, OData V4 service definition) plus suggested table DDL, activation order and next steps. |
56
+ | `list_abap_rules` | Browse abaplint's ~180 rules (filter by text or tag). |
57
+ | `explain_abap_rule` | One rule in depth — rationale (often Clean ABAP), examples, docs URL. |
58
+ | `format_abap` | Offline pretty-printer (keyword case + indentation). |
59
+ | `get_abap_outline` | Classes/methods/visibility/interfaces/FORMs of a source — navigate big objects without reading them whole. |
60
+
61
+ ## Honesty box — what this is *not*
62
+
63
+ - **Not ATC.** Readiness here is *language-level*: statements ABAP Cloud removed. Whether your
64
+ code calls **released APIs only** requires a system's released-API list (ATC check
65
+ `SAP_CP_READINESS`) — out of scope for an offline tool, and the readiness report says so on
66
+ every call.
67
+ - **Scaffold validation is tiered.** Generated classes and CDS views are round-tripped through
68
+ abaplint at Cloud level before they're returned (the generator and the linter share one
69
+ parser). Behavior/service definitions are outside abaplint's checked surface — they are
70
+ golden-tested canonical templates, and ADT activation is the final arbiter. Each generated
71
+ file is labeled `validated: "abaplint" | "template"`.
72
+ - **Text-in only, by design.** No filesystem walking, no network — the entire attack surface is
73
+ a parser over strings you explicitly pass. For linting whole directories, use the
74
+ [abaplint CLI](https://abaplint.org) in CI, or the
75
+ [mcp-kit `wrap-abaplint` recipe](https://github.com/lumivarahq/mcp-kit) this server grew out of.
76
+
77
+ ## Develop
78
+
79
+ ```bash
80
+ npm install
81
+ npm run check # typecheck + 80 tests + build — the CI gate
82
+ node dist/cli.js # stdio MCP server
83
+ npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
84
+ ```
85
+
86
+ Tool descriptions are CI-graded (a rubric test enforces verb-first names, when-to-use,
87
+ non-goals, described params, worked examples — the
88
+ [mcp-kit](https://github.com/lumivarahq/mcp-kit) discipline; the full mcp-kit lint scores all
89
+ seven tools 100/100).
90
+
91
+ ## Design
92
+
93
+ The decision log — why offline, why abaplint, why a dual-parse readiness diff, why the
94
+ scaffolder validates its own output, what was deliberately left out — lives in
95
+ [`docs/DESIGN.md`](docs/DESIGN.md).
96
+
97
+ ## Credits
98
+
99
+ - [abaplint](https://github.com/abaplint/abaplint) by Lars Hvam — the parser and rule engine
100
+ underneath every tool here (MIT).
101
+ - [mcp-kit](https://github.com/lumivarahq/mcp-kit) — the production-MCP patterns this server
102
+ follows (typed tool specs, transport discipline, description lint).
103
+
104
+ MIT © Akshay Palimkar. Not affiliated with or endorsed by SAP SE. "SAP", "ABAP" and "RAP" are
105
+ trademarks of SAP SE; this is an independent open-source tool for developers working with them.
@@ -0,0 +1,46 @@
1
+ export interface AbapSource {
2
+ filename?: string | undefined;
3
+ source: string;
4
+ }
5
+ export interface Finding {
6
+ rule: string;
7
+ message: string;
8
+ severity: string;
9
+ file: string;
10
+ line: number;
11
+ column: number;
12
+ /** First ~100 chars of the offending line, for fix-it-without-reopening flows. */
13
+ excerpt: string;
14
+ docsUrl: string;
15
+ }
16
+ export declare const MAX_FILES = 32;
17
+ export declare const MAX_FILE_CHARS = 100000;
18
+ export declare const MAX_FINDINGS = 500;
19
+ /** ABAP language versions a caller may target. */
20
+ export declare const ABAP_VERSIONS: readonly ["Cloud", "v750", "v751", "v752", "v753", "v754", "v755", "v756", "v757", "v758"];
21
+ export type AbapVersion = (typeof ABAP_VERSIONS)[number];
22
+ /**
23
+ * Infer an abapGit-conventional filename from the source's first meaningful
24
+ * statement, so agents can lint snippets without knowing the convention.
25
+ */
26
+ export declare function inferFilename(source: string, given?: string): string;
27
+ export interface RunOptions {
28
+ version: AbapVersion;
29
+ /** abaplint rule config; merged over the preset. */
30
+ rules?: Record<string, unknown> | undefined;
31
+ /**
32
+ * "style" — abaplint's default ruleset minus whole-program semantic
33
+ * checks, so isolated snippets don't drown in noise about
34
+ * objects that simply weren't provided.
35
+ * "full" — abaplint's default ruleset as-is (expects you to provide
36
+ * every referenced dev object).
37
+ * "syntax-only" — parser + CDS parser errors only.
38
+ */
39
+ preset: "style" | "full" | "syntax-only";
40
+ }
41
+ export interface RunResult {
42
+ findings: Finding[];
43
+ truncated: boolean;
44
+ fileCount: number;
45
+ }
46
+ export declare function runAbaplint(files: AbapSource[], opts: RunOptions): RunResult;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The abaplint engine wrapper.
3
+ *
4
+ * Design rules:
5
+ * - A fresh in-memory Registry per call — no shared state, safe under
6
+ * concurrent tool calls (pattern proven in RAP Dojo's /api/lint-abap).
7
+ * - abaplint PARSES the input, it never executes it. Inputs are still
8
+ * bounded (file count / chars) to keep each call cheap.
9
+ * - Filenames drive abaplint's object typing (zcl_x.clas.abap = a class).
10
+ * Callers may omit them; we infer from the source's leading statement.
11
+ */
12
+ import * as abaplint from "@abaplint/core";
13
+ export const MAX_FILES = 32;
14
+ export const MAX_FILE_CHARS = 100_000;
15
+ export const MAX_FINDINGS = 500;
16
+ /** ABAP language versions a caller may target. */
17
+ export const ABAP_VERSIONS = [
18
+ "Cloud",
19
+ "v750",
20
+ "v751",
21
+ "v752",
22
+ "v753",
23
+ "v754",
24
+ "v755",
25
+ "v756",
26
+ "v757",
27
+ "v758",
28
+ ];
29
+ const FILENAME_RE = /^[a-zA-Z0-9_#-]+\.(clas\.abap|clas\.locals_imp\.abap|clas\.locals_def\.abap|clas\.testclasses\.abap|prog\.abap|intf\.abap|fugr\.abap|ddls\.asddls|bdef\.asbdef|srvd\.srvdsrv|ddlx\.asddlx)$/;
30
+ /**
31
+ * Infer an abapGit-conventional filename from the source's first meaningful
32
+ * statement, so agents can lint snippets without knowing the convention.
33
+ */
34
+ export function inferFilename(source, given) {
35
+ if (given !== undefined) {
36
+ if (!FILENAME_RE.test(given)) {
37
+ throw new Error(`Filename "${given}" is not an abapGit-style name (e.g. zcl_foo.clas.abap, zfoo.prog.abap, zr_foo.ddls.asddls).`);
38
+ }
39
+ return given.toLowerCase();
40
+ }
41
+ const head = source
42
+ .split("\n")
43
+ .map((l) => l.trim())
44
+ .filter((l) => l.length > 0 && !l.startsWith("*") && !l.startsWith('"'))
45
+ .slice(0, 5)
46
+ .join("\n");
47
+ const classMatch = /^\s*CLASS\s+(\w+)\s+DEFINITION/im.exec(head);
48
+ if (classMatch?.[1] !== undefined)
49
+ return `${classMatch[1].toLowerCase()}.clas.abap`;
50
+ const intfMatch = /^\s*INTERFACE\s+(\w+)/im.exec(head);
51
+ if (intfMatch?.[1] !== undefined)
52
+ return `${intfMatch[1].toLowerCase()}.intf.abap`;
53
+ if (/^\s*(@\w|define\s+(root\s+)?view)/im.test(head))
54
+ return "zsnippet.ddls.asddls";
55
+ if (/^\s*(managed|unmanaged|abstract|projection|interface;)/im.test(head))
56
+ return "zsnippet.bdef.asbdef";
57
+ const progMatch = /^\s*(REPORT|PROGRAM)\s+(\w+)/im.exec(head);
58
+ if (progMatch?.[2] !== undefined)
59
+ return `${progMatch[2].toLowerCase()}.prog.abap`;
60
+ return "zsnippet.prog.abap";
61
+ }
62
+ function boundFiles(files) {
63
+ if (files.length === 0)
64
+ throw new Error("Provide at least one source file.");
65
+ if (files.length > MAX_FILES)
66
+ throw new Error(`At most ${MAX_FILES} files per call.`);
67
+ const used = new Set();
68
+ return files.map((f) => {
69
+ if (f.source.length > MAX_FILE_CHARS) {
70
+ throw new Error(`File ${f.filename ?? "(unnamed)"} exceeds ${MAX_FILE_CHARS} characters; split it or lint the relevant part.`);
71
+ }
72
+ let filename = inferFilename(f.source, f.filename);
73
+ if (used.has(filename)) {
74
+ // Identical names would silently overwrite each other in the registry.
75
+ // Generic snippet fallbacks get a unique suffix; names derived from a
76
+ // declaration (or given explicitly) are a caller error — two files
77
+ // can't define the same object.
78
+ if (f.filename === undefined && filename.startsWith("zsnippet.")) {
79
+ let n = 2;
80
+ while (used.has(filename.replace("zsnippet.", `zsnippet${n}.`)))
81
+ n += 1;
82
+ filename = filename.replace("zsnippet.", `zsnippet${n}.`);
83
+ }
84
+ else {
85
+ throw new Error(`Duplicate filename "${filename}" — each file must define a distinct object; pass unique filenames.`);
86
+ }
87
+ }
88
+ used.add(filename);
89
+ return { filename, source: f.source };
90
+ });
91
+ }
92
+ function buildConfig(opts) {
93
+ let raw;
94
+ if (opts.preset === "syntax-only") {
95
+ raw = {
96
+ global: { files: "/**/*" },
97
+ syntax: { version: opts.version, errorNamespace: "^(Z|Y)" },
98
+ rules: { parser_error: true, cds_parser_error: true },
99
+ };
100
+ }
101
+ else {
102
+ const def = abaplint.Config.getDefault().get();
103
+ def.syntax.version = opts.version;
104
+ if (opts.preset === "style") {
105
+ // Semantic whole-program checks false-positive on isolated snippets.
106
+ def.rules["check_syntax"] = false;
107
+ }
108
+ raw = def;
109
+ }
110
+ if (opts.rules !== undefined) {
111
+ const rules = raw["rules"];
112
+ for (const [k, v] of Object.entries(opts.rules))
113
+ rules[k] = v;
114
+ }
115
+ return new abaplint.Config(JSON.stringify(raw));
116
+ }
117
+ export function runAbaplint(files, opts) {
118
+ const bounded = boundFiles(files);
119
+ const registry = new abaplint.Registry(buildConfig(opts));
120
+ const lines = new Map();
121
+ for (const f of bounded) {
122
+ registry.addFile(new abaplint.MemoryFile(f.filename, f.source));
123
+ lines.set(f.filename, f.source.split("\n"));
124
+ }
125
+ registry.parse();
126
+ const issues = registry.findIssues();
127
+ const findings = issues.slice(0, MAX_FINDINGS).map((issue) => {
128
+ const start = issue.getStart();
129
+ const fileLines = lines.get(issue.getFilename());
130
+ const excerpt = (fileLines?.[start.getRow() - 1] ?? "").trim().slice(0, 100);
131
+ return {
132
+ rule: issue.getKey(),
133
+ message: issue.getMessage(),
134
+ severity: String(issue.getSeverity()),
135
+ file: issue.getFilename(),
136
+ line: start.getRow(),
137
+ column: start.getCol(),
138
+ excerpt,
139
+ docsUrl: `https://rules.abaplint.org/${issue.getKey()}/`,
140
+ };
141
+ });
142
+ return { findings, truncated: issues.length > MAX_FINDINGS, fileCount: bounded.length };
143
+ }
@@ -0,0 +1 @@
1
+ export declare function formatAbap(source: string, filename?: string): string;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pretty-print ABAP via abaplint's PrettyPrinter (keyword casing + indent).
3
+ * Only meaningful for ABAP sources — CDS/BDEF artifacts are not reformatted.
4
+ */
5
+ import * as abaplint from "@abaplint/core";
6
+ import { invalidInput } from "../errors.js";
7
+ import { inferFilename, MAX_FILE_CHARS } from "./engine.js";
8
+ export function formatAbap(source, filename) {
9
+ if (source.length > MAX_FILE_CHARS) {
10
+ throw invalidInput(`Source exceeds ${MAX_FILE_CHARS} characters.`);
11
+ }
12
+ const name = inferFilename(source, filename);
13
+ if (!name.endsWith(".abap")) {
14
+ throw invalidInput(`format_abap handles ABAP sources only (got "${name}"). CDS and behavior definitions are not reformatted.`);
15
+ }
16
+ const config = abaplint.Config.getDefault();
17
+ const registry = new abaplint.Registry(config);
18
+ registry.addFile(new abaplint.MemoryFile(name, source));
19
+ registry.parse();
20
+ const obj = registry.getFirstObject();
21
+ if (!(obj instanceof abaplint.ABAPObject)) {
22
+ throw invalidInput("Could not parse the source as an ABAP object.");
23
+ }
24
+ const file = obj.getABAPFiles()[0];
25
+ if (file === undefined) {
26
+ throw invalidInput("Could not parse the source as an ABAP object.");
27
+ }
28
+ // "Fails cleanly on source it cannot parse" is the tool contract — returning
29
+ // broken code as "formatted" would launder syntax errors. Gate on the
30
+ // parse/structure issues only (style findings are not format blockers).
31
+ const SYNTAX_KEYS = new Set(["parser_error", "structure", "cds_parser_error"]);
32
+ const syntaxIssues = registry.findIssues().filter((i) => SYNTAX_KEYS.has(i.getKey()));
33
+ const firstIssue = syntaxIssues[0];
34
+ if (firstIssue !== undefined) {
35
+ throw invalidInput(`Source does not parse cleanly (${syntaxIssues.length} syntax/structure issue(s)); fix before formatting. First: line ${firstIssue.getStart().getRow()}: ${firstIssue.getMessage()}`);
36
+ }
37
+ return new abaplint.PrettyPrinter(file, config).run();
38
+ }
@@ -0,0 +1,25 @@
1
+ import type { AbapSource } from "./engine.js";
2
+ export interface MethodOutline {
3
+ name: string;
4
+ visibility: "public" | "protected" | "private";
5
+ }
6
+ export interface ClassOutline {
7
+ name: string;
8
+ isGlobal: boolean;
9
+ isFinal: boolean;
10
+ isAbstract: boolean;
11
+ isForTesting: boolean;
12
+ superClass: string | null;
13
+ interfaces: string[];
14
+ methods: MethodOutline[];
15
+ attributes: string[];
16
+ constants: string[];
17
+ }
18
+ export interface FileOutline {
19
+ file: string;
20
+ classes: ClassOutline[];
21
+ interfaces: string[];
22
+ forms: string[];
23
+ parseable: boolean;
24
+ }
25
+ export declare function outlineAbap(files: AbapSource[]): FileOutline[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Structural outline of ABAP sources — lets an agent navigate a large class
3
+ * or legacy include without reading every line into context.
4
+ */
5
+ import * as abaplint from "@abaplint/core";
6
+ import { inferFilename, MAX_FILE_CHARS, MAX_FILES } from "./engine.js";
7
+ import { invalidInput } from "../errors.js";
8
+ const VISIBILITY = {
9
+ 1: "private",
10
+ 2: "protected",
11
+ 3: "public",
12
+ };
13
+ export function outlineAbap(files) {
14
+ if (files.length === 0)
15
+ throw invalidInput("Provide at least one source file.");
16
+ if (files.length > MAX_FILES)
17
+ throw invalidInput(`At most ${MAX_FILES} files per call.`);
18
+ const config = abaplint.Config.getDefault();
19
+ const registry = new abaplint.Registry(config);
20
+ const names = [];
21
+ for (const f of files) {
22
+ if (f.source.length > MAX_FILE_CHARS) {
23
+ throw invalidInput(`File ${f.filename ?? "(unnamed)"} exceeds ${MAX_FILE_CHARS} characters.`);
24
+ }
25
+ const name = inferFilename(f.source, f.filename);
26
+ names.push(name);
27
+ registry.addFile(new abaplint.MemoryFile(name, f.source));
28
+ }
29
+ registry.parse();
30
+ const out = [];
31
+ for (const name of names) {
32
+ let found = false;
33
+ for (const obj of registry.getObjects()) {
34
+ if (!(obj instanceof abaplint.ABAPObject))
35
+ continue;
36
+ for (const file of obj.getABAPFiles()) {
37
+ if (file.getFilename() !== name)
38
+ continue;
39
+ found = true;
40
+ const info = file.getInfo();
41
+ out.push({
42
+ file: name,
43
+ parseable: true,
44
+ classes: info.listClassDefinitions().map((c) => ({
45
+ name: c.name,
46
+ isGlobal: c.isGlobal,
47
+ isFinal: c.isFinal,
48
+ isAbstract: c.isAbstract,
49
+ isForTesting: c.isForTesting,
50
+ superClass: c.superClassName?.toLowerCase() ?? null,
51
+ interfaces: c.interfaces.map((i) => i.name.toLowerCase()),
52
+ methods: c.methods.map((m) => ({
53
+ name: m.name,
54
+ visibility: VISIBILITY[m.visibility] ?? "private",
55
+ })),
56
+ attributes: c.attributes.map((a) => a.name),
57
+ constants: c.constants.map((k) => k.name),
58
+ })),
59
+ interfaces: info.listInterfaceDefinitions().map((i) => i.name),
60
+ forms: info.listFormDefinitions().map((f) => f.name),
61
+ });
62
+ }
63
+ }
64
+ if (!found) {
65
+ // Non-ABAP artifacts (CDS, BDEF) or unparseable sources get an empty outline.
66
+ out.push({ file: name, parseable: false, classes: [], interfaces: [], forms: [] });
67
+ }
68
+ }
69
+ return out;
70
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ABAP Cloud / Clean Core readiness — the dual-run diff.
3
+ *
4
+ * One abaplint pass at `version: Cloud` and one at a classic baseline
5
+ * (default v758). A finding present at Cloud but absent at the baseline is a
6
+ * *cloud blocker* (the statement is fine classic ABAP that ABAP Cloud no
7
+ * longer allows). A finding present at the baseline is just *broken code* —
8
+ * reporting it as a migration item would overstate the migration.
9
+ *
10
+ * Honest scope: this is the static, parser-level slice of readiness. The
11
+ * other half — "does this call only RELEASED SAP APIs?" — requires the
12
+ * system's released-API list (ATC check SAP_CP_READINESS) and is out of
13
+ * scope for an offline tool. The report says so.
14
+ */
15
+ import type { AbapSource, AbapVersion, Finding } from "./engine.js";
16
+ export interface ReadinessCategory {
17
+ category: string;
18
+ label: string;
19
+ count: number;
20
+ findings: Finding[];
21
+ }
22
+ export interface ReadinessReport {
23
+ verdict: "ready" | "minor-rework" | "moderate-rework" | "significant-rework";
24
+ score: number;
25
+ cloudBlockerCount: number;
26
+ categories: ReadinessCategory[];
27
+ /** Findings that fail even at the classic baseline — fix these first; they are not migration items. */
28
+ brokenAtBaseline: Finding[];
29
+ baselineVersion: AbapVersion;
30
+ scopeNote: string;
31
+ }
32
+ export declare const SCOPE_NOTE: string;
33
+ export declare function checkCloudReadiness(files: AbapSource[], baselineVersion?: AbapVersion): ReadinessReport;
@@ -0,0 +1,56 @@
1
+ import { runAbaplint } from "./engine.js";
2
+ export const SCOPE_NOTE = "Static parser-level analysis (abaplint). It detects statements and syntax that ABAP Cloud removes, " +
3
+ "but NOT usage of unreleased SAP APIs — that requires a system's released-API list (ATC / SAP_CP_READINESS). " +
4
+ "Treat 'ready' as 'no language-level blockers', not as a full Clean Core certification.";
5
+ /** Map an offending line to a human category by its leading keyword(s). */
6
+ function categorize(excerpt) {
7
+ const head = excerpt.toUpperCase();
8
+ if (/^(WRITE|ULINE|SKIP|FORMAT|NEW-PAGE|TOP-OF-PAGE|END-OF-PAGE|PRINT-CONTROL)\b/.test(head))
9
+ return { category: "list-output", label: "Classic list output (WRITE…) — no UI in ABAP Cloud; expose data via RAP/OData instead" };
10
+ if (/^(CALL SCREEN|SET SCREEN|LEAVE SCREEN|MODULE|LOOP AT SCREEN|SET PF-STATUS|SET TITLEBAR|CALL DIALOG|SUPPRESS DIALOG)\b/.test(head))
11
+ return { category: "dynpro", label: "Dynpro / classic UI — rebuild the UI as a Fiori app on a RAP service" };
12
+ if (/^(SELECT-OPTIONS|PARAMETERS|SELECTION-SCREEN|AT SELECTION-SCREEN|INITIALIZATION|START-OF-SELECTION|END-OF-SELECTION|AT LINE-SELECTION|AT USER-COMMAND)\b/.test(head))
13
+ return { category: "report-events", label: "Report / selection-screen events — wrap the logic in a class; use RAP or an application job" };
14
+ if (/^(REPORT|PROGRAM|SUBMIT)\b/.test(head))
15
+ return { category: "report-program", label: "Executable program statements — ABAP Cloud has classes only; SUBMIT has no released equivalent" };
16
+ if (/^(EXEC SQL|ENDEXEC)\b/.test(head))
17
+ return { category: "native-sql", label: "Native SQL — use ABAP SQL (or AMDP where released)" };
18
+ if (/^(CALL FUNCTION .*DESTINATION|CALL FUNCTION .*STARTING NEW TASK|RECEIVE RESULTS)\b/.test(head))
19
+ return { category: "rfc", label: "Direct RFC patterns — use released connectivity (bgPF, HTTP, released RFC wrappers)" };
20
+ if (/^(FORM|PERFORM|ENDFORM)\b/.test(head))
21
+ return { category: "subroutines", label: "FORM subroutines — obsolete; move logic into class methods" };
22
+ if (/^(CALL TRANSACTION|LEAVE TO TRANSACTION|SET PARAMETER|GET PARAMETER|AUTHORITY-CHECK)\b/.test(head))
23
+ return { category: "transaction-glue", label: "Transaction / SPA-GPA / classic auth glue — re-model on released APIs" };
24
+ return { category: "other", label: "Other statements ABAP Cloud does not allow" };
25
+ }
26
+ export function checkCloudReadiness(files, baselineVersion = "v758") {
27
+ const cloud = runAbaplint(files, { version: "Cloud", preset: "syntax-only" });
28
+ const baseline = runAbaplint(files, { version: baselineVersion, preset: "syntax-only" });
29
+ const baselineKeys = new Set(baseline.findings.map((f) => `${f.file}:${f.line}:${f.rule}`));
30
+ const blockers = [];
31
+ for (const f of cloud.findings) {
32
+ if (!baselineKeys.has(`${f.file}:${f.line}:${f.rule}`))
33
+ blockers.push(f);
34
+ }
35
+ const byCategory = new Map();
36
+ for (const f of blockers) {
37
+ const { category, label } = categorize(f.excerpt);
38
+ const entry = byCategory.get(category) ?? { category, label, count: 0, findings: [] };
39
+ entry.count += 1;
40
+ entry.findings.push(f);
41
+ byCategory.set(category, entry);
42
+ }
43
+ const n = blockers.length;
44
+ // Transparent, documented formula — a conversation starter, not an oracle.
45
+ const score = Math.max(0, 100 - 5 * n);
46
+ const verdict = n === 0 ? "ready" : n <= 5 ? "minor-rework" : n <= 20 ? "moderate-rework" : "significant-rework";
47
+ return {
48
+ verdict,
49
+ score,
50
+ cloudBlockerCount: n,
51
+ categories: [...byCategory.values()].sort((a, b) => b.count - a.count),
52
+ brokenAtBaseline: baseline.findings,
53
+ baselineVersion,
54
+ scopeNote: SCOPE_NOTE,
55
+ };
56
+ }
@@ -0,0 +1,15 @@
1
+ export interface RuleSummary {
2
+ key: string;
3
+ title: string;
4
+ shortDescription: string;
5
+ tags: string[];
6
+ docsUrl: string;
7
+ }
8
+ export interface RuleDetail extends RuleSummary {
9
+ extendedInformation: string;
10
+ }
11
+ export declare function listRules(query?: string, tag?: string): RuleSummary[];
12
+ export declare function explainRule(key: string): RuleDetail & {
13
+ badExample?: string;
14
+ goodExample?: string;
15
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Rule catalog — thin projection over abaplint's own rule metadata, so the
3
+ * documentation an agent reads is the analyzer's, not a copy that can drift.
4
+ */
5
+ import * as abaplint from "@abaplint/core";
6
+ import { notFound } from "../errors.js";
7
+ function metadata() {
8
+ return abaplint.ArtifactsRules.getRules().map((r) => r.getMetadata());
9
+ }
10
+ function toSummary(m) {
11
+ return {
12
+ key: m.key,
13
+ title: m.title,
14
+ shortDescription: m.shortDescription,
15
+ tags: m.tags ?? [],
16
+ docsUrl: `https://rules.abaplint.org/${m.key}/`,
17
+ };
18
+ }
19
+ export function listRules(query, tag) {
20
+ const q = query?.toLowerCase();
21
+ return metadata()
22
+ .filter((m) => {
23
+ if (tag !== undefined && !(m.tags ?? []).some((t) => t.toLowerCase() === tag.toLowerCase()))
24
+ return false;
25
+ if (q !== undefined) {
26
+ const hay = `${m.key} ${m.title} ${m.shortDescription}`.toLowerCase();
27
+ if (!hay.includes(q))
28
+ return false;
29
+ }
30
+ return true;
31
+ })
32
+ .map(toSummary)
33
+ .sort((a, b) => a.key.localeCompare(b.key));
34
+ }
35
+ export function explainRule(key) {
36
+ const m = metadata().find((x) => x.key === key.toLowerCase());
37
+ if (m === undefined) {
38
+ throw notFound(`No abaplint rule named "${key}". Use list_abap_rules to browse valid keys.`, {
39
+ key,
40
+ });
41
+ }
42
+ const detail = {
43
+ ...toSummary(m),
44
+ extendedInformation: m.extendedInformation ?? "",
45
+ };
46
+ if (m.badExample !== undefined)
47
+ detail.badExample = m.badExample;
48
+ if (m.goodExample !== undefined)
49
+ detail.goodExample = m.goodExample;
50
+ return detail;
51
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * RAP managed-BO scaffolder.
3
+ *
4
+ * Generates the canonical ABAP-Cloud stack for one root entity — the same
5
+ * shape as SAP's /DMO reference apps and the ADT wizards:
6
+ *
7
+ * root view entity (ZR_) → behavior definition (managed, strict(2))
8
+ * projection view (ZC_) → projection behavior definition
9
+ * behavior implementation class (ZBP_) + handler locals
10
+ * metadata extension (UI annotations) + service definition (ZUI_…_V4)
11
+ *
12
+ * Everything the generator emits that abaplint can parse is round-tripped
13
+ * through abaplint at version Cloud before it is returned: the generator and
14
+ * the linter share one parser, so the scaffold can never drift into syntax
15
+ * the lint would reject. BDEF/SRVD artifacts are outside abaplint's checked
16
+ * surface (verified empirically) — those are covered by golden tests and
17
+ * marked validated:"template".
18
+ */
19
+ import type { Finding } from "./engine.js";
20
+ export interface ScaffoldField {
21
+ /** snake_case table field name, e.g. "agency_id". */
22
+ name: string;
23
+ /** Suggested DDL type for the table source, e.g. "abap.char(6)". */
24
+ type?: string | undefined;
25
+ }
26
+ export interface ScaffoldOptions {
27
+ /** Entity in UpperCamelCase, e.g. "Travel". Drives all artifact names. */
28
+ entityName: string;
29
+ /** Persistent SQL table, e.g. "ztravel". */
30
+ sqlTable: string;
31
+ /** snake_case key field name, e.g. "travel_id". */
32
+ keyField: string;
33
+ /** True (default): UUID key, managed numbering. False: caller supplies the key on create. */
34
+ managedUuidKey: boolean;
35
+ fields: ScaffoldField[];
36
+ draft: boolean;
37
+ /** Dev namespace prefix, "Z" or "Y". */
38
+ prefix: "Z" | "Y";
39
+ }
40
+ export interface ScaffoldFile {
41
+ filename: string;
42
+ content: string;
43
+ /** "abaplint" = machine-validated through the parser; "template" = golden-tested template. */
44
+ validated: "abaplint" | "template";
45
+ }
46
+ export interface ScaffoldResult {
47
+ files: ScaffoldFile[];
48
+ activationOrder: string[];
49
+ nextSteps: string[];
50
+ suggestedTableDdl: string;
51
+ /** Non-empty only if the generated sources failed abaplint — should never happen. */
52
+ validationIssues: Finding[];
53
+ }
54
+ export declare function snakeToCamel(snake: string): string;
55
+ export declare function scaffoldRapBo(opts: ScaffoldOptions): ScaffoldResult;