This commit is contained in:
Ingo Schnabel
2026-09-24 16:36:15 +02:00
parent 7043c8ab0b
commit 3f63c94469
71 changed files with 10460 additions and 189 deletions

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ replay_pid*
/.mvn/
/.idea/
# local symlink to external project
/korin-natjav

View File

@@ -13,7 +13,7 @@ import picocli.CommandLine.Option;
description = "Per-language LoC/SLoC rollup of a project (plus total)")
final class LocCommand extends AbstractProjectCommand {
@Option(names = "--language", description = "Filter by language (natural/java)")
@Option(names = "--language", description = "Filter by language (natural/java/natjav)")
@Nullable String language;
@Option(names = "--source-file", description = "Filter by source file")

View File

@@ -59,7 +59,7 @@ final class ProjectCommand implements Callable<Integer> {
@SuppressWarnings("NullAway.Init")
@Option(names = {"-l", "--language"}, required = true,
description = "Project source language (natural/java)")
description = "Project source language (natural/java/natjav)")
String language;
@Option(names = {"-g", "--generated-dir"},
@@ -97,7 +97,7 @@ final class ProjectCommand implements Callable<Integer> {
description = "Directory name to skip when scanning the root (case-insensitive); repeatable")
List<String> excludeDirs = List.of();
@Option(names = {"-l", "--language"}, description = "Project source language (natural/java)")
@Option(names = {"-l", "--language"}, description = "Project source language (natural/java/natjav)")
String language = "";
@Option(names = {"-g", "--generated-dir"},

View File

@@ -4,4 +4,4 @@
server.url=http://localhost:8787
# Stamped by manage-ac.sh (stamp_cli_version) from ac-code-server's agenticcode.version
# at build time. "dev" means this jar wasn't built via manage-ac.sh.
version=294
version=346

View File

@@ -29,6 +29,10 @@
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-java</artifactId>
</dependency>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-natjav</artifactId>
</dependency>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-neo4j-store</artifactId>

View File

@@ -61,7 +61,7 @@ public class ProjectResource {
.build();
}
private static final List<String> SUPPORTED_LANGUAGES = List.of("natural", "java");
private static final List<String> SUPPORTED_LANGUAGES = List.of("natural", "java", "natjav");
private static @Nullable String normalize(@Nullable String value) {
return value == null || value.isBlank() ? null : value.strip();

View File

@@ -11,6 +11,10 @@ import com.agenticcode.parsercore.ast.spi.LineCounter;
import com.agenticcode.parserjava.JavaCoarseScanner;
import com.agenticcode.parserjava.JavaLineCounter;
import com.agenticcode.parserjava.JavaParser;
import com.agenticcode.parsernatjav.NatJavCoarseScanner;
import com.agenticcode.parsernatjav.NatJavDdmResolver;
import com.agenticcode.parsernatjav.NatJavLineCounter;
import com.agenticcode.parsernatjav.NatJavParser;
import com.agenticcode.parsernatural.CopycodeResolver;
import com.agenticcode.parsernatural.NaturalCoarseScanner;
import com.agenticcode.parsernatural.NaturalLineCounter;
@@ -39,6 +43,9 @@ public class AstIngestService {
private final NaturalCoarseScanner naturalScanner = new NaturalCoarseScanner();
private final LineCounter javaLineCounter = new JavaLineCounter();
private final LineCounter naturalLineCounter = new NaturalLineCounter();
private final NatJavParser natJavParser = new NatJavParser();
private final NatJavCoarseScanner natJavScanner = new NatJavCoarseScanner();
private final LineCounter natJavLineCounter = new NatJavLineCounter();
/**
* Item 179 (DIAGNOSTIC): when false, {@link NodeType#COMMENT} nodes and their {@code DOCUMENTS}
@@ -110,10 +117,24 @@ public class AstIngestService {
*/
public LanguageParser.ParseResult parse(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes) {
if (language == SourceFiles.Language.JAVA) {
return javaParser.parse(sourceFile, content);
}
return naturalParser.parse(sourceFile, content, copycodes);
return parse(language, sourceFile, content, copycodes, NatJavDdmResolver.NONE);
}
/**
* As {@link #parse(SourceFiles.Language, String, String, CopycodeResolver)}, with the project's
* NatJav DDM index (item D) so a view resolves to its table and columns. Ignored by the other
* languages, which have no DDM concept.
*/
public LanguageParser.ParseResult parse(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes, NatJavDdmResolver ddms) {
// Exhaustive switch, not an if/else with a Natural fallthrough: a language added without a
// parser must fail to compile, not silently hand its files to the Natural parser and store
// a plausible-looking but wrong graph.
return switch (language) {
case JAVA -> javaParser.parse(sourceFile, content);
case NATJAV -> natJavParser.parse(sourceFile, content, ddms);
case NATURAL -> naturalParser.parse(sourceFile, content, copycodes);
};
}
/**
@@ -131,10 +152,20 @@ public class AstIngestService {
*/
public LanguageParser.ParseResult coarseScan(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes) {
if (language == SourceFiles.Language.JAVA) {
return javaScanner.scan(sourceFile, content);
}
return naturalScanner.scan(sourceFile, content, copycodes);
return coarseScan(language, sourceFile, content, copycodes, NatJavDdmResolver.NONE);
}
/**
* As {@link #coarseScan(SourceFiles.Language, String, String, CopycodeResolver)}, with the
* project's NatJav DDM index.
*/
public LanguageParser.ParseResult coarseScan(SourceFiles.Language language, String sourceFile, String content,
CopycodeResolver copycodes, NatJavDdmResolver ddms) {
return switch (language) {
case JAVA -> javaScanner.scan(sourceFile, content);
case NATJAV -> natJavScanner.scan(sourceFile, content, ddms);
case NATURAL -> naturalScanner.scan(sourceFile, content, copycodes);
};
}
/**
@@ -142,7 +173,11 @@ public class AstIngestService {
* the Tier-1 coarse scanners use, so a module's metrics are identical at any ingest depth.
*/
public LocMetrics count(SourceFiles.Language language, String content) {
LineCounter counter = language == SourceFiles.Language.JAVA ? javaLineCounter : naturalLineCounter;
LineCounter counter = switch (language) {
case JAVA -> javaLineCounter;
case NATJAV -> natJavLineCounter;
case NATURAL -> naturalLineCounter;
};
return counter.count(content);
}

View File

@@ -0,0 +1,146 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.parsernatjav.NatJavDdm;
import com.agenticcode.parsernatjav.NatJavDdmResolver;
import org.jboss.logging.Logger;
import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A NatJav project's data definition modules, backing {@link NatJavDdmResolver}. Built once per
* ingest from the project's {@code *_DDM.java} files (243 of them in the reference corpus), so a
* view can be given the table and column names that live in the DDM rather than in the view's own
* declaration.
*
* <p>The same split as {@link CopycodeLibrary} on the Natural side: the parser SPI stays
* file-agnostic and the ingest layer, which has filesystem access, supplies the index. Unlike
* copycodes these files <em>are</em> ingested in their own right — a DDM is a module with its
* columns — so this index is a second, cheap read of files the walk already knows about.
*/
final class NatJavDdmLibrary implements NatJavDdmResolver {
private static final Logger LOG = Logger.getLogger(NatJavDdmLibrary.class);
/**
* Filename suffix of a generated DDM class. Every {@code NaturalDdm} subclass in the corpus
* carries it and nothing else does, so the scan need not read non-DDM files at all.
*/
private static final String SUFFIX = "_DDM.java";
/**
* Filename suffix of a generated data area, whose {@code @ViewOf} declarations are indexed so a
* program using the area can resolve a statement's operand to a table.
*/
private static final String AREA_SUFFIX = "_DA.java";
/**
* {@code @ViewOf <ddm> <field> = view(...)} inside a data area — read off the text rather than
* parsed, because only two identifiers are needed and the area may hold thousands of fields.
*/
private static final java.util.regex.Pattern AREA_VIEW = java.util.regex.Pattern.compile(
"@ViewOf\\s+([\\w.]+)\\s+([\\w$]+)\\s*=\\s*view\\s*\\(");
/**
* A data area's field declaration, reduced to what item E1 needs: the Java identifier and the
* Natural name literal the transpiler preserved. Read off the text rather than parsed — the
* corpus holds 739,346 of these and only two identifiers per line are wanted.
*/
private static final java.util.regex.Pattern AREA_FIELD = java.util.regex.Pattern.compile(
"@L\\d\\s+(?:@\\w+\\s+[\\w.]+\\s+|Group\\s+|[A-Z]\\s+)?([\\w$]+)\\s*=\\s*[A-Za-z]+\\("
+ "\\s*(?:\\d+\\s*(?:,\\s*\\d+\\s*)?,\\s*)?\"([^\"]+)\"");
private final Map<String, Ddm> byClassName;
/**
* {@code <areaClass>#<viewField>} → the DDM class that view reads.
*/
private final Map<String, String> areaViews;
/**
* {@code <areaClass>#<javaFieldName>} → the field's Natural name.
*/
private final Map<String, String> areaFields;
private NatJavDdmLibrary(Map<String, Ddm> byClassName, Map<String, String> areaViews,
Map<String, String> areaFields) {
this.byClassName = byClassName;
this.areaViews = areaViews;
this.areaFields = areaFields;
}
private static String areaViewKey(String areaClassName, String viewName) {
return areaClassName.toUpperCase(java.util.Locale.ROOT) + "#" + viewName;
}
/**
* @param candidates the project's ingestible files
* @return the index, or {@link NatJavDdmResolver#NONE} when the project holds no DDM
*/
static NatJavDdmResolver scan(List<ProjectIngestService.Candidate> candidates) {
Map<String, Ddm> byClassName = new HashMap<>();
Map<String, String> areaViews = new HashMap<>();
Map<String, String> areaFields = new HashMap<>();
for (ProjectIngestService.Candidate candidate : candidates) {
if (candidate.kind().language() != SourceFiles.Language.NATJAV) {
continue;
}
String fileName = candidate.file().getFileName().toString();
String className = SourceFiles.stem(candidate.file());
try {
if (fileName.endsWith(SUFFIX)) {
Ddm ddm = NatJavDdm.parse(SourceFiles.read(candidate.file()), className);
if (ddm != null) {
byClassName.put(className, ddm);
}
} else if (fileName.endsWith(AREA_SUFFIX)) {
String text = SourceFiles.read(candidate.file());
java.util.regex.Matcher matcher = AREA_VIEW.matcher(text);
while (matcher.find()) {
String ddmClass = matcher.group(1);
int dot = ddmClass.lastIndexOf('.');
areaViews.put(areaViewKey(className, matcher.group(2)),
dot >= 0 ? ddmClass.substring(dot + 1) : ddmClass);
}
java.util.regex.Matcher fields = AREA_FIELD.matcher(text);
while (fields.find()) {
areaFields.putIfAbsent(areaViewKey(className, fields.group(1)), fields.group(2));
}
}
} catch (IOException | RuntimeException e) {
// Unreadable or unparseable here is reported as a failure when the file is ingested;
// the views referring to it simply resolve no columns.
LOG.debugf("NatJav DDM '%s' could not be indexed: %s", className, e.toString());
}
}
// All three, not just the DDMs: a project may hold data areas without a single *_DDM.java,
// and bailing on the DDM count alone threw away the areaViews/areaFields just computed —
// silently costing every cross-area dataflow edge, with no log line to say so.
if (byClassName.isEmpty() && areaViews.isEmpty() && areaFields.isEmpty()) {
return NatJavDdmResolver.NONE;
}
LOG.infof("NatJav: indexed %d DDMs, %d data-area views and %d data-area fields",
byClassName.size(), areaViews.size(), areaFields.size());
return new NatJavDdmLibrary(byClassName, areaViews, areaFields);
}
@Override
public @Nullable Ddm resolve(String ddmClassName) {
return byClassName.get(ddmClassName.toUpperCase(java.util.Locale.ROOT));
}
@Override
public @Nullable String resolveAreaField(String areaClassName, String javaFieldName) {
return areaFields.get(areaViewKey(areaClassName, javaFieldName));
}
@Override
public @Nullable Ddm resolveAreaView(String areaClassName, String viewName) {
String ddmClass = areaViews.get(areaViewKey(areaClassName, viewName));
return ddmClass == null ? null : resolve(ddmClass);
}
}

View File

@@ -0,0 +1,248 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jboss.logging.Logger;
import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Disambiguates NatJav module names that two files claim, before the ingest either merges them into
* one ambiguous node or drops both as duplicates.
*
* <p>A NatJav module's identity is the Natural name from its {@code @Natural(moduleName=...)}, which
* is right for the other 22,790 files of the reference corpus but leaves two collision classes:
*
* <ul>
* <li><b>Library homonyms.</b> Natural module names are unique <em>per library</em>, not globally,
* so {@code KORIN} and {@code ABFRAGE} may each hold an {@code ACOMMON} (and each its own GDA
* {@code COMMON}). Both are real, different modules. They are qualified with the library —
* the top-level package — as {@code KORIN.ACOMMON}, and only where the collision actually
* occurs, so the other 22,790 modules stay addressable by their bare Natural name.</li>
* <li><b>A copied header the migration did not update.</b> {@code ZB80BM6.java} declares
* {@code moduleName="ZB80BM5"} — provably wrong: {@code ZB80BM5.java} exists, and the two files
* differ in 178 of ~570 lines. Three module pairs and three data-area pairs in the corpus are
* like this. Here the class name is the trustworthy half, so the file whose class name matches
* the declared name keeps it and the impostor falls back to its own.</li>
* </ul>
*
* <p>The distinction between the two is which file <em>owns</em> the name: the one whose class name
* (less the transpiler's {@code _DA}/{@code _DDM} suffix) is the declared name. Exactly one owner
* means the others are impostors; none or several means the declaration is not in dispute and the
* files differ by library instead.
*
* <p>This has to be a cross-file pass — a parser looking at one file cannot know a name is contested
* — so it reads only each file's head, which is where the annotation and the class declaration are.
*/
final class NatJavIdentities {
private static final Logger LOG = Logger.getLogger(NatJavIdentities.class);
/**
* Property recording the {@code moduleName} a file declared but did not get, so the contradiction
* stays visible in the graph instead of being silently corrected away.
*/
static final String NAME_CONFLICT = "naturalNameConflict";
/**
* Bytes of each file read to find the annotation. The generated header (licence comment, package,
* import, Javadoc, annotation, class declaration) is well under this in every corpus file.
*/
private static final int HEAD_BYTES = 8192;
private static final Pattern MODULE_NAME =
Pattern.compile("@Natural\\s*\\(\\s*moduleName\\s*=\\s*\"([^\"]+)\"");
private NatJavIdentities() {
}
/**
* Builds the rename map for a NatJav project.
*
* @param root the project root
* @param candidates every ingestible file of the project (the whole walk, so a collision is seen
* even when only one of the two files is being ingested)
* @return project-relative source file → the name its module node must carry instead of the one
* it declares. Empty for a project with no contested name, and for every non-NatJav project.
*/
static Map<String, Rename> scan(Path root, List<ProjectIngestService.Candidate> candidates) {
Map<String, List<Claim>> byDeclaredName = new LinkedHashMap<>();
for (ProjectIngestService.Candidate candidate : candidates) {
if (candidate.kind().language() != SourceFiles.Language.NATJAV) {
continue;
}
Claim claim = claimOf(root, candidate.file());
byDeclaredName.computeIfAbsent(claim.declaredName(), n -> new ArrayList<>()).add(claim);
}
Map<String, Rename> renames = new HashMap<>();
byDeclaredName.forEach((declaredName, claims) -> {
if (claims.size() < 2) {
return;
}
List<Claim> owners = claims.stream().filter(c -> c.ownsDeclaredName(declaredName)).toList();
if (owners.size() == 1) {
for (Claim claim : claims) {
if (claim != owners.get(0)) {
renames.put(claim.sourceFile(), new Rename(claim.baseName(), declaredName));
LOG.infof("NatJav: '%s' also declares moduleName '%s', which %s owns — ingesting it as '%s'",
claim.sourceFile(), declaredName, owners.get(0).sourceFile(), claim.baseName());
}
}
return;
}
// Not a contested declaration: the files agree, they just live in different libraries.
long libraries = claims.stream().map(Claim::library).distinct().count();
// A file directly in the project root has no library, and qualifying it would name the
// module ".ACOMMON" — an identity every later query would have to be handed verbatim.
boolean allNamed = claims.stream().noneMatch(c -> c.library().isEmpty());
if (allNamed && libraries == claims.size()) {
for (Claim claim : claims) {
renames.put(claim.sourceFile(), new Rename(claim.library() + "." + declaredName, null));
}
}
// Same library and no single owner: nothing here distinguishes the files, so they are left
// to the duplicate detection, which reports them rather than binding one arbitrarily.
});
return renames;
}
/**
* @return every name a NatJav file's module answers to besides its filename stem: the Natural
* name from its {@code @Natural(moduleName=...)}, and the renamed identity where this pass gave
* it one. The by-name ingest indexes candidates by stem — the <em>Java class</em> name — while
* the graph exposes the Natural name, so without this a targeted ingest of {@code V-MIT04} or
* {@code WSAIVP} reports "unresolved" for a module the graph carries under exactly that name.
*/
static Map<String, String> graphNames(Path root, List<ProjectIngestService.Candidate> candidates,
Map<String, Rename> renames) {
Map<String, String> byFile = new HashMap<>();
for (ProjectIngestService.Candidate candidate : candidates) {
if (candidate.kind().language() != SourceFiles.Language.NATJAV) {
continue;
}
Claim claim = claimOf(root, candidate.file());
Rename rename = renames.get(claim.sourceFile());
byFile.put(claim.sourceFile(), rename != null ? rename.name() : claim.declaredName());
}
return byFile;
}
/**
* Applies {@code renames} to the module/data-structure shell of one parse result.
*
* @return {@code result} unchanged when this file is not renamed
*/
static ParseResult apply(ParseResult result, String sourceFile, Map<String, Rename> renames) {
Rename rename = renames.get(sourceFile);
if (rename == null) {
return result;
}
List<AstNode> nodes = new ArrayList<>(result.nodes().size());
for (AstNode node : result.nodes()) {
nodes.add(isShell(node, sourceFile) ? renamed(node, rename) : node);
}
return new ParseResult(nodes, result.edges());
}
/**
* @return whether {@code node} is the file's own shell — the node standing for the file, which is
* the only one whose name this pass may change. Identified the way the parser builds it: it
* carries the file's path, starts at line 1 and holds the {@code className} property.
*/
private static boolean isShell(AstNode node, String sourceFile) {
return (node.type() == NodeType.MODULE || node.type() == NodeType.DATA_STRUCTURE)
&& node.sourceFile().equals(sourceFile)
&& node.startLine() == 1
&& node.properties() != null
&& node.properties().containsKey("className");
}
private static AstNode renamed(AstNode node, Rename rename) {
Map<String, String> properties = new LinkedHashMap<>();
if (node.properties() != null) {
properties.putAll(node.properties());
}
if (rename.conflictingName() != null) {
properties.put(NAME_CONFLICT, rename.conflictingName());
}
return new AstNode(node.id(), node.type(), rename.name(), node.sourceFile(), node.language(),
node.startLine(), node.endLine(), node.dataType(), node.value(), properties);
}
private static Claim claimOf(Path root, Path file) {
String className = SourceFiles.stem(file);
String head = head(file);
Matcher matcher = MODULE_NAME.matcher(head);
String declaredName = matcher.find() ? matcher.group(1) : className;
Path relative = root.relativize(file);
String library = relative.getNameCount() > 1 ? relative.getName(0).toString() : "";
return new Claim(relative.toString().replace('\\', '/'), className, declaredName, library);
}
private static String head(Path file) {
try (var stream = Files.newInputStream(file)) {
return new String(stream.readNBytes(HEAD_BYTES), StandardCharsets.ISO_8859_1);
} catch (IOException e) {
// Unreadable here means unreadable at parse time too, where it is reported as a failure.
return "";
}
}
/**
* One file's claim to a module name.
*
* @param sourceFile the project-relative path
* @param className the Java class name (the filename stem, upper-cased by {@link SourceFiles})
* @param declaredName the {@code @Natural(moduleName=...)}, or the class name if it declares none
* @param library the top-level directory, i.e. the Natural library
*/
private record Claim(String sourceFile, String className, String declaredName, String library) {
/**
* @return the class name without the transpiler's data-area / DDM suffix — what the class name
* says the Natural name is
*/
String baseName() {
for (String suffix : List.of("_DA", "_DDM")) {
if (className.endsWith(suffix)) {
return className.substring(0, className.length() - suffix.length());
}
}
return className;
}
/**
* @return whether this file is the natural owner of {@code declaredName}: its own class name
* says so, independently of the annotation that is in dispute
*/
boolean ownsDeclaredName(String name) {
return baseName().equalsIgnoreCase(name.toUpperCase(Locale.ROOT))
|| baseName().equals(name);
}
}
/**
* The name a file's module node must carry instead of the one it declares.
*
* @param name the replacement name
* @param conflictingName the declared name that was rejected because another file owns it, or
* {@code null} when the rename is a library qualification rather than a
* correction
*/
record Rename(String name, @Nullable String conflictingName) {
}
}

View File

@@ -6,6 +6,7 @@ import com.agenticcode.neo4jstore.graph.ProjectInfo;
import com.agenticcode.neo4jstore.graph.ProjectIngestInfo;
import com.agenticcode.parsercore.ast.model.*;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import com.agenticcode.parsernatjav.NatJavDdmResolver;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
@@ -188,9 +189,20 @@ public class ProjectIngestService {
*/
private static List<Identity> fileIdentities(Parsed p) {
if (p.candidate().kind().kind() == NodeType.DATA_STRUCTURE) {
// The stem is the data area's identity for Natural (the file IS named after the area) but
// not for NatJav, whose file is named after the Java class: `WSK386P_DA.java` holds the
// area `WSK385P`. Keying on the stem there checked a name the graph does not use, so two
// areas colliding under their real name passed the guard and landed as one ambiguous
// node — WSK385P, WSSC31P and YLRY401A each did exactly that.
if (p.candidate().kind().language() == SourceFiles.Language.NATJAV) {
return natJavShellIdentities(p);
}
String stem = SourceFiles.stem(p.candidate().file());
return List.of(new Identity("DATA_STRUCTURE " + stem, stem, NodeType.DATA_STRUCTURE));
}
if (p.candidate().kind().language() == SourceFiles.Language.NATJAV) {
return natJavShellIdentities(p);
}
return p.result().nodes().stream()
.filter(node -> node.type() == NodeType.MODULE && !node.sourceFile().isEmpty())
.map(node -> identity(node, p.candidate().kind().language()))
@@ -198,20 +210,45 @@ public class ProjectIngestService {
.toList();
}
/**
* @return the identity of a NatJav file, taken from the shell node the parser actually produced
* rather than re-deduced from the filename.
*
* <p>The two can disagree: {@code SourceFiles.classify} decides MODULE vs DATA_STRUCTURE from the
* {@code _DA} suffix while the parser decides it from the {@code extends} target. A {@code *_DA.java}
* that does not extend {@code NaturalData}, or a {@code NaturalData} class named otherwise, then
* matched neither branch's filter and contributed <b>no</b> identity at all — so a genuine name
* collision on that file went unreported.
*/
private static List<Identity> natJavShellIdentities(Parsed p) {
return p.result().nodes().stream()
.filter(node -> (node.type() == NodeType.MODULE || node.type() == NodeType.DATA_STRUCTURE)
&& !node.sourceFile().isEmpty()
&& node.startLine() == 1
&& node.properties() != null
&& node.properties().containsKey("className"))
.map(node -> new Identity(node.type().name() + " " + node.name(), node.name(), node.type()))
.distinct()
.toList();
}
/**
* Item 24: reads and parses one candidate file, applying the shell/user-exit metric enrichment.
* Pure per-file work (no shared mutable state), so it is safe to run on a virtual thread per file.
*/
private Parsed parseCandidate(Candidate candidate, Path root, boolean coarse,
CopycodeLibrary copycodes, Map<String, LocMetrics> userExit,
ProjectInfo project) throws IOException {
ProjectInfo project,
Map<String, NatJavIdentities.Rename> natJavRenames,
NatJavDdmResolver ddms) throws IOException {
String content = SourceFiles.read(candidate.file());
String sourceFile = relativeSourceFile(root, candidate.file());
ParseResult result = coarse
? astIngestService.coarseScan(candidate.kind().language(), sourceFile, content, copycodes)
: withShellMetrics(astIngestService.parse(candidate.kind().language(), sourceFile, content, copycodes),
content, candidate.kind().language());
? astIngestService.coarseScan(candidate.kind().language(), sourceFile, content, copycodes, ddms)
: withShellMetrics(astIngestService.parse(candidate.kind().language(), sourceFile, content,
copycodes, ddms), content, candidate.kind().language());
result = withUserExitMetrics(result, project.generatedDir(), userExit);
result = NatJavIdentities.apply(result, sourceFile, natJavRenames);
return new Parsed(candidate, result);
}
@@ -237,7 +274,8 @@ public class ProjectIngestService {
*/
private static final List<String> DEFAULT_EXCLUDE_DIRS = List.of("target");
private static List<Candidate> walk(Path root, List<String> excludeDirs) throws IOException {
private static List<Candidate> walk(Path root, List<String> excludeDirs,
@Nullable String projectLanguage) throws IOException {
List<String> effectiveExcludes = new ArrayList<>(excludeDirs);
for (String dir : DEFAULT_EXCLUDE_DIRS) {
if (!effectiveExcludes.contains(dir)) {
@@ -253,7 +291,7 @@ public class ProjectIngestService {
if (SourceFiles.isExcluded(root, file, effectiveExcludes)) {
continue;
}
SourceFiles.Kind kind = SourceFiles.classify(file);
SourceFiles.Kind kind = SourceFiles.classify(file, projectLanguage);
if (kind != null) {
candidates.add(new Candidate(file, kind));
}
@@ -268,11 +306,40 @@ public class ProjectIngestService {
* name a by-name walk reports as a duplicate.
*/
private static Map<NameKey, List<Candidate>> nameIndex(List<Candidate> candidates) {
return nameIndex(candidates, Map.of());
}
/**
* As {@link #nameIndex(List)}, additionally indexing each NatJav file under the name its module
* carries in the graph. The stem is the Java class name ({@code V_MIT04.java}); the graph knows
* the module as {@code V-MIT04}, so a seed given the name every query uses would otherwise never
* resolve. Both keys point at the same candidate.
*
* @param graphNames project-relative source file → the module's graph name, from
* {@link NatJavIdentities#graphNames}
*/
private static Map<NameKey, List<Candidate>> nameIndex(List<Candidate> candidates,
Map<String, String> graphNames) {
Map<NameKey, List<Candidate>> index = new HashMap<>();
for (Candidate candidate : candidates) {
index.computeIfAbsent(new NameKey(candidate.kind().kind(), SourceFiles.stem(candidate.file())),
String stem = SourceFiles.stem(candidate.file());
index.computeIfAbsent(new NameKey(candidate.kind().kind(), stem),
k -> new ArrayList<>()).add(candidate);
}
graphNames.forEach((sourceFile, graphName) -> {
for (Candidate candidate : candidates) {
if (!candidate.file().toString().replace('\\', '/').endsWith(sourceFile)
|| graphName.equalsIgnoreCase(SourceFiles.stem(candidate.file()))) {
continue;
}
List<Candidate> under = index.computeIfAbsent(
new NameKey(candidate.kind().kind(), graphName.toUpperCase(Locale.ROOT)),
k -> new ArrayList<>());
if (!under.contains(candidate)) {
under.add(candidate);
}
}
});
return index;
}
@@ -461,7 +528,7 @@ public class ProjectIngestService {
// must be reported, not accepted: it was silently listed as examined while
// nothing about it could ever be ingested, which is precisely the "I ingested
// 2 of your 3 files" invisibility this list exists to prevent.
|| SourceFiles.classify(root.resolve(sf).normalize()) == null)
|| SourceFiles.classify(root.resolve(sf).normalize(), project.language()) == null)
.toList();
List<String> ingestable = requested.stream().filter(sf -> !unresolved.contains(sf)).toList();
if (ingestable.isEmpty()) {
@@ -493,12 +560,19 @@ public class ProjectIngestService {
markIngestStarted(project, coarse ? "tier1" : level.name().toLowerCase(Locale.ROOT));
Path root = Path.of(project.root());
List<String> excludeDirs = ingestExcludeDirs(project);
List<Candidate> allCandidates = walk(root, excludeDirs);
List<Candidate> allCandidates = walk(root, excludeDirs, project.language());
// NatJav: two files may claim one module name (a library homonym, or a copied header the
// migration never updated). Decided once over the whole walk, before anything is parsed —
// a single file cannot tell that its name is contested. Empty for every other language.
Map<String, NatJavIdentities.Rename> natJavRenames = NatJavIdentities.scan(root, allCandidates);
// NatJav: a view names only its DDM class; the table and column names live in that file.
NatJavDdmResolver ddms = NatJavDdmLibrary.scan(allCandidates);
CopycodeLibrary copycodes = CopycodeLibrary.scan(root, excludeDirs);
// Item 129: opt-in skip of files whose content is byte-identical to what the graph holds.
List<Candidate> candidates = changedOnly ? changedCandidates(project, root, excludeDirs, allCandidates)
: allCandidates;
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), astIngestService);
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), project.language(),
astIngestService);
List<String> examinedFiles = candidates.stream()
.map(c -> relativeSourceFile(root, c.file()))
@@ -513,7 +587,7 @@ public class ProjectIngestService {
try (ExecutorService parseExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Parsed>> futures = candidates.stream()
.map(candidate -> parseExecutor.submit(
() -> parseCandidate(candidate, root, coarse, copycodes, userExit, project)))
() -> parseCandidate(candidate, root, coarse, copycodes, userExit, project, natJavRenames, ddms)))
.toList();
for (int i = 0; i < futures.size(); i++) {
try {
@@ -630,7 +704,9 @@ public class ProjectIngestService {
// fixtures* that its Java walk never ingests, so they had no stored hash, counted as changed,
// and disabled skipping entirely — 487 of 509 unchanged files re-parsed. An unknown language
// (a legacy project) keeps the conservative behaviour.
if (!"java".equalsIgnoreCase(project.language() == null ? "" : project.language())
if (!SourceFiles.Language.JAVA.name().equalsIgnoreCase(project.language() == null ? "" : project.language())
&& !SourceFiles.NATJAV_PROJECT_LANGUAGE.equalsIgnoreCase(
project.language() == null ? "" : project.language())
&& copycodeChanged(root, excludeDirs, stored)) {
LOG.infof("changedOnly refresh of '%s': a copycode changed, so every file is re-parsed "
+ "(copycode text is inlined at parse time; skipping would keep stale expansions)",
@@ -753,8 +829,11 @@ public class ProjectIngestService {
int depthLimit = Math.min(Math.max(1, maxDepth != null ? maxDepth : defaultDeepDepth), maxDeepDepth);
int nodeLimit = Math.max(1, maxNodes != null ? maxNodes : defaultDeepNodes);
Path root = Path.of(project.root());
List<Candidate> candidates = walk(root, ingestExcludeDirs(project));
Map<NameKey, List<Candidate>> index = nameIndex(candidates);
List<Candidate> candidates = walk(root, ingestExcludeDirs(project), project.language());
Map<String, NatJavIdentities.Rename> natJavRenames = NatJavIdentities.scan(root, candidates);
NatJavDdmResolver ddms = NatJavDdmLibrary.scan(candidates);
Map<NameKey, List<Candidate>> index =
nameIndex(candidates, NatJavIdentities.graphNames(root, candidates, natJavRenames));
// Reverse index (Java): interface/base simple name -> files that implement/extend it, so a
// targeted ingest also pulls in the implementations/subclasses of any interface it reaches.
Map<String, List<Candidate>> implementorsByBase = buildImplementorIndex(root, candidates);
@@ -767,7 +846,7 @@ public class ProjectIngestService {
String label = seeds.size() == 1
? "'" + moduleNames.iterator().next() + "'"
: seeds.size() + " modules";
return bfsIngest(project, root, index, implementorsByBase, seeds, depthLimit, nodeLimit, label);
return bfsIngest(project, root, index, implementorsByBase, natJavRenames, ddms, seeds, depthLimit, nodeLimit, label);
}
/**
@@ -787,8 +866,11 @@ public class ProjectIngestService {
@Nullable Integer maxNodes) throws IOException {
int nodeLimit = Math.max(1, maxNodes != null ? maxNodes : defaultDeepNodes);
Path root = Path.of(project.root());
List<Candidate> candidates = walk(root, ingestExcludeDirs(project));
Map<NameKey, List<Candidate>> index = nameIndex(candidates);
List<Candidate> candidates = walk(root, ingestExcludeDirs(project), project.language());
Map<String, NatJavIdentities.Rename> natJavRenames = NatJavIdentities.scan(root, candidates);
NatJavDdmResolver ddms = NatJavDdmLibrary.scan(candidates);
Map<NameKey, List<Candidate>> index =
nameIndex(candidates, NatJavIdentities.graphNames(root, candidates, natJavRenames));
Map<String, List<Candidate>> implementorsByBase = buildImplementorIndex(root, candidates);
Map<String, Candidate> byRelPath = new HashMap<>();
for (Candidate candidate : candidates) {
@@ -806,7 +888,7 @@ public class ProjectIngestService {
if (seeds.isEmpty()) {
return new IngestSummary(0, List.of(), List.of(), List.of(), List.of(), null);
}
return bfsIngest(project, root, index, implementorsByBase, seeds, defaultDeepDepth, nodeLimit,
return bfsIngest(project, root, index, implementorsByBase, natJavRenames, ddms, seeds, defaultDeepDepth, nodeLimit,
seeds.size() + " module(s)");
}
@@ -863,10 +945,13 @@ public class ProjectIngestService {
private IngestSummary bfsIngest(ProjectInfo project, Path root,
Map<NameKey, List<Candidate>> index,
Map<String, List<Candidate>> implementorsByBase,
Map<String, NatJavIdentities.Rename> natJavRenames,
NatJavDdmResolver ddms,
List<QueuedRef> seeds, int depthLimit, int nodeLimit, String label) {
long startedAt = System.nanoTime();
CopycodeLibrary copycodes = CopycodeLibrary.scan(root, ingestExcludeDirs(project));
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), astIngestService);
Map<String, LocMetrics> userExit = UserExitMetrics.scan(root, project.userExitDir(), project.excludeDirs(), project.language(),
astIngestService);
int ingested = 0;
// Item 62 (summary surface): collected as refs, not strings, so the data-literal false
// positives can be filtered once the whole tree is walked — see filterDataLiteralRefs.
@@ -922,9 +1007,10 @@ public class ProjectIngestService {
String content = SourceFiles.read(candidate.file());
String sourceFile = relativeSourceFile(root, candidate.file());
examinedFiles.add(sourceFile);
ParseResult result = withUserExitMetrics(withShellMetrics(
astIngestService.parse(candidate.kind().language(), sourceFile, content, copycodes),
content, candidate.kind().language()), project.generatedDir(), userExit);
ParseResult result = NatJavIdentities.apply(withUserExitMetrics(withShellMetrics(
astIngestService.parse(candidate.kind().language(), sourceFile, content, copycodes, ddms),
content, candidate.kind().language()), project.generatedDir(), userExit),
sourceFile, natJavRenames);
LOG.infof("Ingesting %s into project '%s' [%s]", moduleNames(result), project.name(), sourceFile);
// Full deep parse of this module — reconcile so a by-name refresh purges its stale
// nodes (renamed/removed fields) rather than shadowing them (item 58).
@@ -1080,7 +1166,7 @@ public class ProjectIngestService {
return implementorsByBase;
}
private record Candidate(Path file, SourceFiles.Kind kind) {
record Candidate(Path file, SourceFiles.Kind kind) {
}
private record Parsed(Candidate candidate, ParseResult result) {

View File

@@ -20,6 +20,9 @@ import java.util.Locale;
* {@code MODULE}s and {@code .lda}/{@code .pda}/{@code .gda} data areas (local/parameter/global) are
* {@link NodeType#DATA_STRUCTURE}s. Files with any other extension are not ingestible and classify
* to {@code null}.
*
* <p>NatJav (Natural transpiled to Java) breaks the extension-only rule: its files are {@code .java},
* so the <em>project's</em> declared language decides — see {@link #classify(Path, String)}.
*/
final class SourceFiles {
@@ -27,12 +30,44 @@ final class SourceFiles {
}
/**
* @return the classification of {@code file} by extension, or {@code null} if it is not an
* ingestible source file (e.g. {@code .meta}).
* The {@code language} of a project whose {@code .java} files are transpiled Natural rather than
* hand-written Java, and must therefore be read by the NatJav parser.
*/
static final String NATJAV_PROJECT_LANGUAGE = "natjav";
/**
* Suffix the maxenso transpiler appends to a Natural data area's class name. Every one of the
* 5,818 {@code NaturalData} classes in the reference corpus carries it and no other class does,
* so it classifies a file the way {@code .lda}/{@code .pda}/{@code .gda} does for Natural. The
* <em>name</em> of the resulting node still comes from the parser, which reads the class's
* {@code @Natural(moduleName=...)} — the suffix is not stripped by string surgery here.
*/
private static final String NATJAV_DATA_AREA_SUFFIX = "_da.java";
/**
* As {@link #classify(Path, String)} for a caller with no project in scope (so {@code .java} is
* hand-written Java).
*/
static @Nullable Kind classify(Path file) {
return classify(file, null);
}
/**
* @param file the file to classify
* @param projectLanguage the owning project's declared {@code language}, or {@code null} if the
* caller has no project in scope. Only {@code "natjav"} changes the
* outcome: a NatJav file is a {@code .java} file, so the extension alone
* cannot distinguish it from hand-written Java and the project must say.
* @return the classification of {@code file}, or {@code null} if it is not an ingestible source
* file (e.g. {@code .meta})
*/
static @Nullable Kind classify(Path file, @Nullable String projectLanguage) {
String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".java")) {
if (NATJAV_PROJECT_LANGUAGE.equalsIgnoreCase(projectLanguage)) {
return new Kind(Language.NATJAV,
name.endsWith(NATJAV_DATA_AREA_SUFFIX) ? NodeType.DATA_STRUCTURE : NodeType.MODULE);
}
return new Kind(Language.JAVA, NodeType.MODULE);
}
if (name.endsWith(".nat") || name.endsWith(".nsn")) {
@@ -84,7 +119,12 @@ final class SourceFiles {
return false;
}
enum Language {JAVA, NATURAL}
/**
* The parser a source file is read by. {@code NATJAV} is Natural transpiled to Java: its files
* are syntactically Java but map onto the Natural half of the unified AST, which is why it is a
* language of its own rather than a flavour of {@code JAVA}.
*/
enum Language {JAVA, NATURAL, NATJAV}
/**
* Classification of an ingestible source file.

View File

@@ -33,12 +33,16 @@ final class UserExitMetrics {
/**
* @return module stem (upper-case) → the user-exit file's LoC/SLoC, for every ingestible file
* under a {@code userExitDir} path component. Empty when {@code userExitDir} is {@code null} or the
* under a {@code userExitDir} path component. {@code projectLanguage} is what tells a NatJav
* project's {@code .java} files apart from hand-written Java, so the metrics are counted by the
* right {@code LineCounter}. Empty when {@code userExitDir} is {@code null} or the
* root cannot be walked. When two user-exit trees hold the same stem, the first in sorted walk
* order wins (deterministic; narrow the scope via {@code excludeDirs} if undesired).
*/
static Map<String, LocMetrics> scan(Path root, @org.jspecify.annotations.Nullable String userExitDir,
List<String> excludeDirs, AstIngestService astIngestService) {
List<String> excludeDirs,
@org.jspecify.annotations.Nullable String projectLanguage,
AstIngestService astIngestService) {
Map<String, LocMetrics> byStem = new HashMap<>();
if (userExitDir == null || userExitDir.isBlank()) {
return byStem;
@@ -53,7 +57,7 @@ final class UserExitMetrics {
if (!hasComponent(root, file, userExitDir) || SourceFiles.isExcluded(root, file, otherExcludes)) {
continue;
}
SourceFiles.Kind kind = SourceFiles.classify(file);
SourceFiles.Kind kind = SourceFiles.classify(file, projectLanguage);
if (kind == null) {
continue;
}

View File

@@ -3,7 +3,7 @@ quarkus.http.port=8787
# AgenticCode's own release counter (not the Maven project version) — bump this by hand for each
# release. Single source of truth for the startup log line, GET /api/version, and the OpenAPI
# info version (referenced below via property expression, not duplicated).
agenticcode.version=294
agenticcode.version=346
# OpenAPI / Swagger UI (item 48) — the generated spec is the contract the web-UI TS client
# is generated against. Served at /q/openapi (yaml/json); Swagger UI at /q/swagger-ui in dev.
mp.openapi.extensions.smallrye.info.title=AgenticCode API

View File

@@ -0,0 +1,186 @@
package com.agenticcode.codeserver.api;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* The first NatJav end-to-end test (roadmap item 187): a six-file NatJav project goes through
* project creation, a whole-root deep refresh and the REST API. Until this class existed, every
* NatJav behaviour was verified by parser unit tests and by the live reference corpus only.
*
* <p>The root reproduces the corpus's shape where it matters: libraries are top-level directories,
* {@code COMMON_DA.java} exists in two of them ({@code KORIN}, {@code ABFRAGE}), and a program in a
* third library ({@code CSTENTW}) includes the KORIN one — the case a directory heuristic would bind
* wrong and the {@code @Using} qualifier binds right (item 184). One assertion per fix it protects.
*/
@QuarkusTest
class NatJavIngestIT {
private static final String PROJECT = "natjav-it";
private static final String API = "/api/projects/" + PROJECT;
@TempDir
static Path root;
@BeforeAll
static void ingest() {
RestAssured.port = Integer.getInteger("quarkus.http.test-port", 8081);
// Libraries are directories; the same class name in two of them is the homonym under test.
copyFixture("fixtures/natjav/COMMON_DA.java", "KORIN/COMMON_DA.java");
copyFixture("fixtures/natjav/abfrage/COMMON_DA.java", "ABFRAGE/COMMON_DA.java");
copyFixture("fixtures/natjav/V_MIT04.java", "KORIN/Mitarbeiter/Verwaltung/V_MIT04.java");
copyFixture("fixtures/natjav/YPUMPRUE_DA.java", "KORIN/Mitarbeiter/Verwaltung/YPUMPRUE_DA.java");
copyFixture("fixtures/natjav/UMKTEST.java", "KORIN/UMKTEST.java");
copyFixture("fixtures/natjav/DISKTEST.java", "CSTENTW/DISKTEST.java");
given().contentType("application/json")
.body(new ProjectResource.ProjectRequest(null, root.toString(), null, "natjav", null, null))
.when().post(API)
.then().statusCode(201);
deepRefresh();
}
private static void deepRefresh() {
given().when().post(API + "/refresh?deep=true").then().statusCode(200);
}
/**
* Unlike the Natural fixtures, these need their directory: the top-level one is the library, which
* {@code NatJavIdentities} qualifies homonyms by.
*/
private static void copyFixture(String classpathResource, String relativeTarget) {
try (InputStream in = NatJavIngestIT.class.getClassLoader().getResourceAsStream(classpathResource)) {
if (in == null) {
throw new IllegalStateException("Resource not found: " + classpathResource);
}
Path target = root.resolve(relativeTarget);
Files.createDirectories(target.getParent());
Files.write(target, in.readAllBytes());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String dataStructures(String module) {
return given().when().get(API + "/modules/" + module + "/data-structures")
.then().statusCode(200).extract().asString();
}
/**
* Item A: the module answers to its Natural name, not to the Java class name it was parsed from.
* Looked up by file — there is no bare {@code GET /modules/{name}}, only sub-resources — and then
* addressed by that name, which is what every sub-resource call below relies on.
*/
@Test
void moduleIsAddressedByItsNaturalName() {
given().when().get(API + "/modules?sourceFile=KORIN/Mitarbeiter/Verwaltung/V_MIT04.java")
.then().statusCode(200)
.body("size()", equalTo(1))
.body("[0].name", equalTo("V-MIT04"))
.body("[0].moduleKind", equalTo("SUBPROGRAM"));
given().when().get(API + "/modules/V-MIT04/functions")
.then().statusCode(200);
}
/**
* Item 184: {@code COMMON_DA} has two real candidates. A module in a THIRD library includes
* {@code KORIN.COMMON_DA}, and the qualifier — not the module's own directory — decides.
*/
@Test
void homonymResolvesToTheLibraryTheUsingNames() {
given().when().get(API + "/modules/DISKTEST/data-structures")
.then().statusCode(200)
.body("find { it.name == 'KORIN.COMMON' }.relationship", equalTo("USING"))
.body("find { it.name == 'KORIN.COMMON' }.area", equalTo("GDA"))
.body("find { it.name == 'KORIN.COMMON' }.sourceFile", equalTo("KORIN/COMMON_DA.java"))
.body("find { it.name == 'KORIN.COMMON' }.fieldCount", greaterThan(0))
.body("findAll { it.name == 'ABFRAGE.COMMON' }", hasSize(0))
.body("findAll { it.name == 'COMMON_DA' }", hasSize(0));
}
/**
* Item 184, the unqualified form: {@code @Using COMMON_DA} carries no package, so only Java's
* same-package rule — the including file's own {@code package KORIN;} — can pick a candidate.
* With one candidate the class name alone would do; this needs the homonym to be load-bearing.
*/
@Test
void unqualifiedUsingResolvesThroughTheFilesOwnPackage() {
given().when().get(API + "/modules/UMKTEST/data-structures")
.then().statusCode(200)
.body("find { it.name == 'KORIN.COMMON' }.area", equalTo("GDA"))
.body("findAll { it.name == 'ABFRAGE.COMMON' }", hasSize(0))
.body("find { it.name == 'YPUMPRUE' }.area", equalTo("LDA"));
}
/**
* Item 186: {@code area} comes from the {@code @Using} scope on the INCLUDES edge, not from a file
* extension a {@code .java} area does not have — and only for a resolved definition.
*/
@Test
void areaComesFromTheUsingScopeOnResolvedRowsOnly() {
given().when().get(API + "/modules/DISKTEST/data-structures")
.then().statusCode(200)
.body("find { it.name == 'YPUMPRUE' }.area", equalTo("LDA"))
.body("find { it.name == 'YPUMPRUE' }.sourceFile", equalTo("KORIN/Mitarbeiter/Verwaltung/YPUMPRUE_DA.java"))
.body("find { it.name == 'YPUMPRUE' }.fieldCount", greaterThan(0))
// Unresolved: the file is not in the project, so UNKNOWN keeps meaning exactly that.
.body("find { it.name == 'NOFILE_DA' }.area", equalTo("UNKNOWN"))
.body("find { it.name == 'NOFILE_DA' }.sourceFile", nullValue())
.body("find { it.name == 'NOFILE_DA' }.fieldCount", equalTo(0));
// A PARAMETER include whose area is not in the project is unresolved too - scope alone must
// not promote it to PDA.
given().when().get(API + "/modules/V-MIT04/data-structures")
.then().statusCode(200)
.body("find { it.name == 'GWSTGP_DA' }.area", equalTo("UNKNOWN"))
.body("find { it.name == 'GWSTGP_DA' }.sourceFile", nullValue());
}
/** Item B: {@code callnat("MITZ1R", ...)} is a CALLNAT to a module the project does not hold. */
@Test
void callnatTargetIsAnUnresolvedCallee() {
given().when().get(API + "/modules/V-MIT04/callees")
.then().statusCode(200)
.body("items.find { it.name == 'MITZ1R' }.unresolved", equalTo(true))
.body("items.find { it.name == 'MITZ1R' }.edgeKind", equalTo("CALLNAT"));
}
/**
* Item 183: a second deep refresh over unchanged input must leave the graph as it found it. The
* leak this guards against minted one placeholder DATA_STRUCTURE per ingest, which the identifier
* search would show as a second hit for the same name.
*/
@Test
void secondDeepRefreshLeavesTheGraphIdentical() {
String disktestBefore = dataStructures("DISKTEST");
String umktestBefore = dataStructures("UMKTEST");
deepRefresh();
assertEquals(disktestBefore, dataStructures("DISKTEST"), "DISKTEST's data structures changed on re-ingest");
assertEquals(umktestBefore, dataStructures("UMKTEST"), "UMKTEST's data structures changed on re-ingest");
given().when().get(API + "/search/identifier?name=YPUMPRUE&type=DATA_STRUCTURE")
.then().statusCode(200)
.body("size()", equalTo(1))
.body("[0].sourceFile", not(emptyString()));
given().when().get(API + "/search/identifier?name=KORIN.COMMON&type=DATA_STRUCTURE")
.then().statusCode(200)
.body("size()", equalTo(1));
// The class-name placeholder was renamed, redirected and, having no children, reaped.
given().when().get(API + "/search/identifier?name=COMMON_DA&type=DATA_STRUCTURE")
.then().statusCode(200)
.body("size()", equalTo(0));
}
}

View File

@@ -0,0 +1,155 @@
package com.agenticcode.codeserver.service;
import com.agenticcode.parsercore.ast.model.NodeType;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.*;
/**
* The two ways two NatJav files end up claiming one module name, and the rule that tells them apart.
* The fixtures reproduce the reference corpus's real cases by name.
*/
class NatJavIdentitiesTest {
@TempDir
Path root;
/**
* Writes the head of a generated NatJav file — the annotation and the class declaration are all
* the scan reads.
*/
private Path file(String relativePath, @Nullable String moduleName, String baseClass) throws IOException {
Path path = root.resolve(relativePath);
Path parent = requireNonNull(path.getParent());
Files.createDirectories(parent);
String className = path.getFileName().toString().replace(".java", "");
String annotation = moduleName == null ? "@Natural" : "@Natural(moduleName=\"" + moduleName + "\")";
Files.writeString(path, "package " + parent.getFileName() + ";\n\n"
+ "import innowake.mee.natural.api.*;\n\n"
+ annotation + "\npublic class " + className + " extends " + baseClass + " {\n}\n");
return path;
}
private static NatJavIdentities.Rename require(Map<String, NatJavIdentities.Rename> renames, String key) {
NatJavIdentities.Rename rename = renames.get(key);
assertNotNull(rename, "expected a rename for " + key + ", got " + renames.keySet());
return rename;
}
private Map<String, NatJavIdentities.Rename> scan(List<Path> files) {
List<ProjectIngestService.Candidate> candidates = new ArrayList<>();
for (Path path : files) {
NodeType kind = path.getFileName().toString().endsWith("_DA.java")
? NodeType.DATA_STRUCTURE : NodeType.MODULE;
candidates.add(new ProjectIngestService.Candidate(path,
new SourceFiles.Kind(SourceFiles.Language.NATJAV, kind)));
}
return NatJavIdentities.scan(root, candidates);
}
/**
* Natural module names are unique per library, not globally: {@code KORIN} and {@code ABFRAGE}
* may each hold an {@code ACOMMON}. Both are real, so both are kept — qualified by library.
*/
@Test
void libraryHomonymsAreQualifiedByLibrary() throws IOException {
Path korin = file("KORIN/ACOMMON.java", null, "NaturalProgram");
Path abfrage = file("ABFRAGE/ACOMMON.java", null, "NaturalProgram");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(korin, abfrage));
assertEquals("KORIN.ACOMMON", require(renames, "KORIN/ACOMMON.java").name());
assertEquals("ABFRAGE.ACOMMON", require(renames, "ABFRAGE/ACOMMON.java").name());
assertNull(require(renames, "KORIN/ACOMMON.java").conflictingName(),
"a library qualification is not a correction — nothing was in dispute");
}
/**
* The GDA {@code COMMON} exists in both libraries as {@code COMMON_DA.java}; the qualification
* uses the Natural name, not the class name.
*/
@Test
void dataAreaHomonymsAreQualifiedToo() throws IOException {
Path korin = file("KORIN/COMMON_DA.java", "COMMON", "NaturalData");
Path abfrage = file("ABFRAGE/COMMON_DA.java", "COMMON", "NaturalData");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(korin, abfrage));
assertEquals("KORIN.COMMON", require(renames, "KORIN/COMMON_DA.java").name());
assertEquals("ABFRAGE.COMMON", require(renames, "ABFRAGE/COMMON_DA.java").name());
}
/**
* {@code ZB80BM6.java} declaring {@code moduleName="ZB80BM5"} is a copied header: {@code
* ZB80BM5.java} exists and owns the name. The impostor is ingested under its own name, and the
* declaration it lost is recorded rather than dropped.
*/
@Test
void copiedHeaderLosesToTheFileThatOwnsTheName() throws IOException {
Path owner = file("KORIN/Praemienkonto/ZB80BM5.java", null, "NaturalProgram");
Path impostor = file("KORIN/Praemienkonto/ZB80BM6.java", "ZB80BM5", "NaturalProgram");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(owner, impostor));
assertNull(renames.get("KORIN/Praemienkonto/ZB80BM5.java"), "the owner keeps its name");
NatJavIdentities.Rename rename = require(renames, "KORIN/Praemienkonto/ZB80BM6.java");
assertEquals("ZB80BM6", rename.name());
assertEquals("ZB80BM5", rename.conflictingName());
}
/**
* The same defect in a data area: {@code WSK386P_DA.java} declares {@code WSK385P}, which
* {@code WSK385P_DA.java} owns. The suffix the transpiler appends is not part of the name.
*/
@Test
void copiedHeaderInADataAreaFallsBackToTheClassNameWithoutTheSuffix() throws IOException {
Path owner = file("KORIN/Webservices/WSK385P_DA.java", "WSK385P", "NaturalData");
Path impostor = file("KORIN/Webservices/WSK386P_DA.java", "WSK385P", "NaturalData");
Map<String, NatJavIdentities.Rename> renames = scan(List.of(owner, impostor));
assertNull(renames.get("KORIN/Webservices/WSK385P_DA.java"));
assertEquals("WSK386P", require(renames, "KORIN/Webservices/WSK386P_DA.java").name());
}
@Test
void anUncontestedNameIsNeverRenamed() throws IOException {
Path a = file("KORIN/V_MIT04.java", "V-MIT04", "NaturalSubprogram");
Path b = file("KORIN/XABBRUCH.java", "#ABBRUCH", "NaturalProgram");
assertTrue(scan(List.of(a, b)).isEmpty(), "nothing collides, so nothing is renamed");
}
/**
* Two files in the <em>same</em> library with no owner between them are genuinely
* indistinguishable. Renaming one would invent a distinction, so both are left for the duplicate
* detection to report.
*/
@Test
void indistinguishableClaimsAreLeftToDuplicateDetection() throws IOException {
Path a = file("KORIN/AAA.java", "SHARED", "NaturalProgram");
Path b = file("KORIN/BBB.java", "SHARED", "NaturalProgram");
assertTrue(scan(List.of(a, b)).isEmpty());
}
@Test
void nonNatJavCandidatesAreIgnored() throws IOException {
Path path = file("KORIN/ACOMMON.java", null, "NaturalProgram");
List<ProjectIngestService.Candidate> javaCandidates = List.of(
new ProjectIngestService.Candidate(path,
new SourceFiles.Kind(SourceFiles.Language.JAVA, NodeType.MODULE)));
assertTrue(NatJavIdentities.scan(root, javaCandidates).isEmpty());
}
}

View File

@@ -11,7 +11,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
/**
* Unit tests for {@link SourceFiles#classify} — the extension → (language, node kind) mapping that
* gates which files the ingester picks up. Pins the Natural data-area extensions incl. {@code .gda}
* (roadmap item 46c) and that unknown extensions are not ingestible.
* (roadmap item 46c), that unknown extensions are not ingestible, and that a {@code .java} file is
* read as NatJav only when its project declares that language.
*/
class SourceFilesTest {
@@ -38,6 +39,45 @@ class SourceFilesTest {
}
}
/**
* NatJav is the one case the extension cannot decide: its files are {@code .java}. Without a
* project language they must stay plain Java, or every Java project would change meaning.
*/
@Test
void javaStaysJavaWithoutANatJavProject() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.JAVA, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), null));
assertEquals(new SourceFiles.Kind(SourceFiles.Language.JAVA, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), "java"));
}
@Test
void natJavProjectReadsJavaFilesAsNatJav() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), "natjav"));
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.MODULE),
SourceFiles.classify(Path.of("KORIN/ACOMMON.java"), "NatJav"), "language match is case-insensitive");
}
/**
* The {@code _DA} suffix is to NatJav what {@code .lda}/{@code .pda}/{@code .gda} is to Natural:
* the only file-level signal that this is a data area rather than a program.
*/
@Test
void natJavDataAreasAreDataStructures() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.DATA_STRUCTURE),
SourceFiles.classify(Path.of("KORIN/COMMON_DA.java"), "natjav"));
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATJAV, NodeType.MODULE),
SourceFiles.classify(Path.of("SYSTEM/BANKEZ_DDM.java"), "natjav"),
"a DDM is a module, not a data area");
}
@Test
void naturalExtensionsAreUnaffectedByTheProjectLanguage() {
assertEquals(new SourceFiles.Kind(SourceFiles.Language.NATURAL, NodeType.MODULE),
SourceFiles.classify(Path.of("WSUBPX0S.nat"), "natjav"));
}
@Test
void unknownExtensionsAreNotIngestible() {
assertNull(SourceFiles.classify(Path.of("WNAUTD0S.meta")));

View File

@@ -0,0 +1,652 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
import innowake.mee.natural.api.*;
@Natural(moduleName="COMMON")
public class COMMON_DA extends NaturalData {
@Global
/*
* GDA für LIBRARY KORIN INCLUSIVE
* REDEFINITIONEN
*/
@L1 A gPROGRAMM = A(8 ,"+PROGRAMM");
@L1 A gBPOL_PARM = A(17 ,"+BPOL-PARM");
@L1 N gBBST_POLNR = N(9,0 ,"+BBST-POLNR");
@L1 A gBBST_ART = A(9 ,"+BBST-ART");
@L1 A gABTEILUNG = A(5 ,"+ABTEILUNG");
@L1 A gPASSWORD = A(8 ,"+PASSWORD");
@L1 A gGETMAIL = A(1 ,"+GETMAIL");
@L1 A gERROR_PROGRAM = A(8 ,"+ERROR-PROGRAM");
@L1 A gERROR_STAT = A(1 ,"+ERROR-STAT");
@L1 N gERROR_LINE = N(4,0 ,"+ERROR-LINE");
@L1 N gERROR_NR = N(4,0 ,"+ERROR-NR");
@L1 A gERGEBNIS = A(79 ,"+ERGEBNIS");
@L1 A gSPOOL_PARM = A(80 ,"+SPOOL-PARM");
@L1 Redefine gSPOOL_PARM;
@L2 A xSPUSER = A(8 ,"#SPUSER");
@L2 A xSPDEST = A(8 ,"#SPDEST");
@L2 A xSPFORM = A(1 ,"#SPFORM");
@L1 A gINIT_VAR = A(240 ,"+INIT-VAR");
@L1 Redefine gINIT_VAR; // REDEF. BEGIN : +INIT-VAR
@L2 A xRA_INITVAR_GETMAIL = A(1 ,"#RA-INITVAR-GETMAIL");// GETMAIL duchgeführt Y/N
@L2 N xINIT_DATIME = N(15,0 ,"#INIT-DATIME");// STARTUP Datum/Zeitder
@L2 Redefine xINIT_DATIME; // letzten Startup-Durchführung
@L3 N xINIT_DATE = N(8,0 ,"#INIT-DATE");// "YYYYMMDDHHMMSSZ"
@L3 Redefine xINIT_DATE;
@L4 A xINIT_DATE_ALPHA = A(8 ,"#INIT-DATE-ALPHA");
@L3 N xINIT_TIME = N(7,0 ,"#INIT-TIME");
@L3 Redefine xINIT_TIME;
@L4 A xINIT_TIME_ALPHA = A(7 ,"#INIT-TIME-ALPHA");
@L2 A xINIT_USER = A(8 ,"#INIT-USER"); // STARTUP U-ID d. Current LOGON
@L2 A xINIT_ID = A(8 ,"#INIT-ID"); // STARTUP Terminal-ID
@L2 A xINIT_GROUP = A(8 ,"#INIT-GROUP"); // STARTUP GROUP-ID, über die
/* /* gelinkt */
@L2 A xSTARTUP_PROGRAM = A(8 ,"#STARTUP-PROGRAM");// STARTUP Startup-Programm
@L2 A xRESTART_PROGRAM = A(8 ,"#RESTART-PROGRAM");// STARTUP Restart-Programm
@L2 A xERROR_PROGRAM = A(8 ,"#ERROR-PROGRAM");// #ERR letztes fehlerhaftes PRG
/* /* STARTUP Error-Programm */
@L2 A xRA_INITVAR_CICS_NAME = A(8 ,"#RA-INITVAR-CICS-NAME");// STARTUP und APPLID
@L2 Redefine xRA_INITVAR_CICS_NAME; // "CSWVCICS"/"CSWVCICT"
@L3 A xCICSNAME = A(8 ,"#CICSNAME"); // Redef. aus Namensgründen
@L2 Redefine xRA_INITVAR_CICS_NAME;
@L3 A xCICS_NAME = A(8 ,"#CICS-NAME"); // Redef. aus Namensgründen
@L2 A FILLER = A(8 ,"FILLER");
@L2 A xUSER_NAME = A(32 ,"#USER-NAME"); // STARTUP User-Name
@L2 A xAPPL_NAME = A(32 ,"#APPL-NAME"); // STARTUP Anwendungs-Name
@L2 A xSYS_SCHALTER = A(8 ,"#SYS-SCHALTER");// DIV SYSTEM-Schalter
@L2 Redefine xSYS_SCHALTER;
@L3 A xSYS_RESTART = A(1 ,"#SYS-RESTART");// #ERR bei Absturz Restart Y/N
@L3 A xSYS_CANCEL = A(1 ,"#SYS-CANCEL");// #CAN BATCH CANCELNin #ERROR
@L3 A xERROR_STAT = A(1 ,"#ERROR-STAT");// #ERR letzter Fehler-Status
@L3 A xERROR_OPTN = A(1 ,"#ERROR-OPTN");// #ERR RETRY ON ERRORS Y/N
@L3 A xERROR_SAME = A(1 ,"#ERROR-SAME");// #ERR same ERROR wie zuvor Y/N
/* 0=NEIN 1=PROG 2=NUMMER 3=BEIDE */
@L3 A xERROR_LOG = A(1 ,"#ERROR-LOG"); // #ERR N=kein ERROR-Logging
@L3 A xMONITOR = A(1 ,"#MONITOR"); // STARTUP B=BATCH, C=CICS
/* /* P=COMPLETE */
@L3 A FILLER2 = A(1 ,"FILLER2");
@L2 A xERROR_PROG = A(8 ,"#ERROR-PROG");
@L2 N xERROR_NR = N(4,0 ,"#ERROR-NR"); // #ERR letzte Fehlernummer
@L2 Redefine xERROR_NR;
@L3 A xERRAX = A(4 ,"#ERRAX");
@L2 N xERROR_LINE = N(4,0 ,"#ERROR-LINE"); // #ERR letzte fehlerhafte Zeile
@L2 A xMESSAGE = A(72 ,"#MESSAGE"); // #ERR Fehlermeldung
@L2 Redefine xMESSAGE;
@L3 A xMANF = A(1 ,"#MANF");
@L3 A STAR = A(1 ,"STAR");
@L3 A RESTMSG = A(70 ,"RESTMSG");
@L1 A gBERECHTIGUNGEN = A("+BERECHTIGUNGEN");
@L1 A gVSU = A(250 ,"+VSU");
@L1 A gKIS = A(250 ,"+KIS");
@L1 Redefine gKIS; // REDEF. BEGIN : +KIS
@L2 N xRN_KIS_ZAHL = N(3,0 ,"#RN-KIS-ZAHL");
@L2 B xRA_KIS_KUISNT = B(3 ,"#RA-KIS-KUISNT").defineArray(range(1, 20));
@L2 Redefine xRA_KIS_KUISNT; // REDEF. BEGIN : #RA-KIS-KUISNT
@L3 B xRA_KIS_KUISN = B(3 ,"#RA-KIS-KUISN");
@L2 N xRN_KIS_OVZL = N(3,0 ,"#RN-KIS-OVZL");
@L2 B xRA_KIS_OVISN = B(3 ,"#RA-KIS-OVISN").defineArray(range(1, 60));
@L1 A gUMS = A(250 ,"+UMS");
@L1 Redefine gUMS; // REDEF. BEGIN : +UMS
@L2 A xRA_UMS_CODE = A(8 ,"#RA-UMS-CODE");
@L2 P xRN_UMS_LAST_VR = P(3,0 ,"#RN-UMS-LAST-VR");
@L2 A xRA_UMS_MD_KZ1_BKLZ_KZ = A(1 ,"#RA-UMS-MD-KZ1-BKLZ-KZ");
@L2 A xRA_UMS_MD_KZ1_TARIF = A(1 ,"#RA-UMS-MD-KZ1-TARIF");
@L2 A xRA_UMS_WAEHRUNG_MD = A(3 ,"#RA-UMS-WAEHRUNG-MD");
@L2 N xRN_UMS_LAST_LOAD = N(6,0 ,"#RN-UMS-LAST-LOAD");
@L2 A xUMS_VORG_OPID = A(3 ,"#UMS-VORG-OPID");
@L2 A xUMS_FORCE = A(1 ,"#UMS-FORCE"); // Beharrung auf eigene
/* * /* PF-Tasten-Belegung */
@L2 N xUMS_SNR_FIL = N(2,0 ,"#UMS-SNR-FIL");// Schaden-Filial-Nummer
@L2 A xUMS_SNR_KREIS = A(2 ,"#UMS-SNR-KREIS");// Schaden-Kreis zB.:'SA'
@L2 N xUMS_SNR_JAHR = N(4,0 ,"#UMS-SNR-JAHR");// Schaden-Jahr
@L2 N xUMS_SNR_LFD_NR = N(6,0 ,"#UMS-SNR-LFD-NR");// Schaden-Laufende-Nummer
@L2 A xUMS_SNR_STATUS = A(1 ,"#UMS-SNR-STATUS");// Status-Byte 'O','S','H'
@L2 A xUMS_ART = A(2 ,"#UMS-ART"); // Schadenart
@L2 A xUMS_REFERENT = A(8 ,"#UMS-REFERENT");// Referent
@L2 A xUMS_EVI_TYP = A(1 ,"#UMS-EVI-TYP"); // eigen oder fremd-Vi
@L2 P xUMS_IDENTCOUNTER = P(5,0 ,"#UMS-IDENTCOUNTER");
@L2 P xUMS_OUTPUTNR = P(5,0 ,"#UMS-OUTPUTNR");
@L2 P xUMS_EREIGNIS_DT = P(8,0 ,"#UMS-EREIGNIS-DT");// nur für LV (= Stornodatum)
@L2 P xUMS_SCN_HIST = P(3,0 ,"#UMS-SCN-HIST");// merken Hist-Lesestand
@L2 B xUMS_ANZEIGEDAT_KPL = B(3 ,"#UMS-ANZEIGEDAT-KPL");// für Aufruf SK10
@L2 N xUMS_SKT_FIL = N(2,0 ,"#UMS-SKT-FIL");// Filial-Nummer Kontostamm
@L2 A xUMS_SKT_KREIS = A(2 ,"#UMS-SKT-KREIS");// Schaden-Kreis Kontostamm
@L2 N xUMS_SKT_JAHR = N(4,0 ,"#UMS-SKT-JAHR");// Schaden-Jahr Kontostamm
@L2 N xUMS_SKT_LFD_NR = N(6,0 ,"#UMS-SKT-LFD-NR");// Schaden-Lfd-Nr Kontostamm
@L2 N xUMS_SKT_BRAN = N(2,0 ,"#UMS-SKT-BRAN");// Schaden-Branche Kontostamm
@L2 A xUMS_WORKSTATION_ID = A(8 ,"#UMS-WORKSTATION-ID");// Workstation (OD) *** frei ***
@L2 A xRA_UMS_BRGR = A(1 ,"#RA-UMS-BRGR").defineArray(range(1,3));
@L2 A xUMS_WF_ABT = A(8 ,"#UMS-WF-ABT"); // Workflow-Abteilung(für OD)
@L2 A xUMS_WF_STAT = A(8 ,"#UMS-WF-STAT"); // Workflow-Station(für OD)
@L2 A xUMS_WF_OPID = A(8 ,"#UMS-WF-OPID"); // Workflow-Opid(für OD)
@L2 P xUMS_WF_SORT_DOKU_ART = P(3,0 ,"#UMS-WF-SORT-DOKU-ART").defineArray(range(1,5));// WF-Dokument(für OD)
@L2 P xUMS_KEYCOUNTER = P(5,0 ,"#UMS-KEYCOUNTER").defineArray(range(1,10));// Identcounter aus File111
@L2 A xUMS_SKT_BELEGTYP = A(1 ,"#UMS-SKT-BELEGTYP");// Schaden-Konto-Belegtyp
@L2 P xUMS_SKT_POS_IDC = P(5,0 ,"#UMS-SKT-POS-IDC");// Identcounter Pos.Buchung-SKT
@L2 P xUMS_SKT_BRAN_IDC = P(5,0 ,"#UMS-SKT-BRAN-IDC");// Identcounter Bran.Buchung-SKT
@L2 P xUMS_SKT_ABST_IDC = P(5,0 ,"#UMS-SKT-ABST-IDC");// Identcounter Abst.Buchung-SKT
@L2 A xUMS_TEXT_FN95 = A(1 ,"#UMS-TEXT-FN95");// Text-Option im FN95
@L2 A xUMS_REGRESS_RATE = A(2 ,"#UMS-REGRESS-RATE");// Schaden-Regreß oder -Rate
@L2 P xUMS_REGRESS_NR = P(6,0 ,"#UMS-REGRESS-NR");// Schaden-Regreß-Nr.
@L2 A xUMS_DAT2_FN95 = A(1 ,"#UMS-DAT2-FN95");// 2. Datumszeile FN95 optional
@L2 N xUMS_PC_DAT = N(8,0 ,"#UMS-PC-DAT"); // Pc-Antragsdatum
@L2 N xUMS_PC_NUM = N(4,0 ,"#UMS-PC-NUM"); // Pc-Antragsnummer
@L2 A xUMS_DRUCKER_ID = A(10 ,"#UMS-DRUCKER-ID");// Drucker-Id (OD)
@L2 A xUMS_KEY_STAT_DI32 = A(8 ,"#UMS-KEY-STAT-DI32");// Station für AufrufDI32 (OD)
@L2 B xUMS_SKT_ANZEIGEDAT_KPL = B(3 ,"#UMS-SKT-ANZEIGEDAT-KPL");// Anzeigedatum-KPL für Sammelb.
@L2 P xUMS_WINDOW_NUM = P(2,0 ,"#UMS-WINDOW-NUM");// Image-Display (OD)
@L2 P xUMS_STORNO_GRUND = P(2,0 ,"#UMS-STORNO-GRUND");// stornogrund (nur LV)
@L2 A xUMS_DOI_KEY_ART = A(2 ,"#UMS-DOI-KEY-ART");// Kz f. Einschränkung Di30 (OD)
@L2 P xUMS_SKT_WERTEDATUM = P(8,0 ,"#UMS-SKT-WERTEDATUM");// Schaden-Konto-Wertedatum
@L2 P xUMS_SKT_BANKSCHLUESSEL = P(2,0 ,"#UMS-SKT-BANKSCHLUESSEL");// Schaden-Konto-Bankschlüssel
@L2 A xUMS_REST = A(28 ,"#UMS-REST"); // Bytes frei
@L1 A gPEL = A(250 ,"+PEL");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 A xRA_PEL_ABT = A(1 ,"#RA-PEL-ABT");
@L2 N xRN_PEL_VPOL = N(8,0 ,"#RN-PEL-VPOL");
@L2 N xRN_PEL_KPNR = N(8,0 ,"#RN-PEL-KPNR");
@L2 N xRN_PEL_GT = N(8,0 ,"#RN-PEL-GT");
@L2 N xRN_PEL_BR1 = N(8,0 ,"#RN-PEL-BR1");
@L2 N xRN_PEL_BR2 = N(8,0 ,"#RN-PEL-BR2");
@L2 N xRN_PEL_BUCH = N(8,0 ,"#RN-PEL-BUCH");
@L2 N xRN_PEL_EINL = N(6,0 ,"#RN-PEL-EINL");
@L2 A xRA_PEL_FREI = A(2 ,"#RA-PEL-FREI");
@L2 N xRN_PEL_TEXT = N(8,0 ,"#RN-PEL-TEXT");
@L2 N xRN_PEL_STAT = N(8,0 ,"#RN-PEL-STAT");
@L2 N xRN_PEL_AGT = N(8,0 ,"#RN-PEL-AGT");
@L2 N xRN_PEL_ABR1 = N(8,0 ,"#RN-PEL-ABR1");
@L2 N xRN_PEL_ABR2 = N(8,0 ,"#RN-PEL-ABR2");
@L2 N xRN_PEL_ATXT = N(8,0 ,"#RN-PEL-ATXT");
@L2 N xRN_PEL_INDX = N(2,0 ,"#RN-PEL-INDX");
@L2 N xRN_PEL_VDAT = N(6,0 ,"#RN-PEL-VDAT");
@L2 A xRA_PEL_EFKZ = A(1 ,"#RA-PEL-EFKZ");
@L2 A xRA_PEL_DRUC = A(4 ,"#RA-PEL-DRUC");
@L2 A xRA_PEL_VART = A(3 ,"#RA-PEL-VART");
@L2 N xRN_PEL_KREI = N(3,0 ,"#RN-PEL-KREI");
@L2 A xRA_PEL_VAE = A(3 ,"#RA-PEL-VAE");
@L2 A xRA_PEL_BNEU = A(3 ,"#RA-PEL-BNEU");
@L2 A xRA_PEL_FILL118 = A(118 ,"#RA-PEL-FILL118");
@L2 A xRA_PEL_MENU = A(2 ,"#RA-PEL-MENU");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 N xRN_KFZ_MENR = N(1,0 ,"#RN-KFZ-MENR");
@L2 N xRN_KFZ_IEVI = N(3,0 ,"#RN-KFZ-IEVI");
@L2 N xRN_KFZ_IVV = N(3,0 ,"#RN-KFZ-IVV");
@L2 N xRN_KFZ_HIST = N(3,0 ,"#RN-KFZ-HIST");
@L2 N xRN_KFZ_STND = N(3,0 ,"#RN-KFZ-STND");
@L2 A xRA_KFZ_EVBR = A(30 ,"#RA-KFZ-EVBR");
@L2 Redefine xRA_KFZ_EVBR;
@L3 A xRA_KFZ_BRA = A(3 ,"#RA-KFZ-BRA").defineArray(range(1, 10));
@L2 Redefine xRA_KFZ_EVBR;
@L3 N xRN_KFZ_BRA1 = N(3,0 ,"#RN-KFZ-BRA1");
@L3 N xRN_KFZ_BRA2 = N(3,0 ,"#RN-KFZ-BRA2");
@L3 N xRN_KFZ_BRA3 = N(3,0 ,"#RN-KFZ-BRA3");
@L3 N xRN_KFZ_BRA4 = N(3,0 ,"#RN-KFZ-BRA4");
@L3 N xRN_KFZ_BRA5 = N(3,0 ,"#RN-KFZ-BRA5");
@L2 N xRN_KFZ_VDAT = N(8,0 ,"#RN-KFZ-VDAT");
@L2 A xRA_KFZ_AREV = A(1 ,"#RA-KFZ-AREV");
@L2 A xRA_KFZ_SABE = A(3 ,"#RA-KFZ-SABE");
@L2 A xRA_KFZ_RAG = A(1 ,"#RA-KFZ-RAG");
@L2 P xRN_KFZ_OZW = P(3,0 ,"#RN-KFZ-OZW");
@L2 N xRN_KFZ_VVAE = N(8,0 ,"#RN-KFZ-VVAE");
@L2 A xRA_KFZ_VEF = A(1 ,"#RA-KFZ-VEF");
@L2 A xRA_KFZ_VVAR = A(3 ,"#RA-KFZ-VVAR");
@L2 N xRN_KFZ_VEGD = N(8,0 ,"#RN-KFZ-VEGD");
@L2 N xRN_KFZ_VELD = N(8,0 ,"#RN-KFZ-VELD");
@L2 A xRA_KFZ_VDRU = A(4 ,"#RA-KFZ-VDRU");
@L2 A xRA_KFZ_PZSG = A(3 ,"#RA-KFZ-PZSG");
@L2 N xRN_KFZ_LESE = N(3,0 ,"#RN-KFZ-LESE");
@L2 N xRN_KFZ_FZW = N(2,0 ,"#RN-KFZ-FZW");
@L2 N xRN_KFZ_VI10 = N(2,0 ,"#RN-KFZ-VI10");
@L2 N xRN_KFZ_VD = N(8,0 ,"#RN-KFZ-VD");
@L2 N xRN_KFZ_VB = N(8,0 ,"#RN-KFZ-VB");
@L2 A xRA_KFZ_KFKZ = A(11 ,"#RA-KFZ-KFKZ");
@L2 N xRN_KFZ_AWDT = N(8,0 ,"#RN-KFZ-AWDT");
@L2 A xRA_KFZ_AWKZ = A(1 ,"#RA-KFZ-AWKZ");
@L2 N xRN_KFZ_AEND = N(8,0 ,"#RN-KFZ-AEND");
@L2 A xRA_KFZ_VABT = A(3 ,"#RA-KFZ-VABT");
@L2 A xRA_KFZ_OPID = A(3 ,"#RA-KFZ-OPID");
@L2 N xRN_KFZ_DATV = N(8,0 ,"#RN-KFZ-DATV");
@L2 N xRN_KFZ_DATB = N(8,0 ,"#RN-KFZ-DATB");
@L2 A xRA_KFZ_FIL = A(2 ,"#RA-KFZ-FIL");
@L2 N xRN_KFZ_KRBR = N(4,0 ,"#RN-KFZ-KRBR");
@L2 Redefine xRN_KFZ_KRBR; // REDEF. BEGIN : #RN-KFZ-KRBR
@L3 N xRN_KFZ_KR = N(2,0 ,"#RN-KFZ-KR");
@L3 N xRN_KFZ_BR = N(2,0 ,"#RN-KFZ-BR");
@L2 A xRA_KFZ_VAE1 = A(3 ,"#RA-KFZ-VAE1");
@L2 A xRA_KFZ_VAE2 = A(3 ,"#RA-KFZ-VAE2");
@L2 A xRA_KFZ_SUMM = A(1 ,"#RA-KFZ-SUMM");
@L2 N xRN_KFZ_STAT = N(2,0 ,"#RN-KFZ-STAT");
@L2 A xRA_KFZ_GSNA = A(10 ,"#RA-KFZ-GSNA");
@L2 A xRA_KFZ_BVNR = A(7 ,"#RA-KFZ-BVNR");
@L2 N xRN_KFZ_ANTR = N(8,0 ,"#RN-KFZ-ANTR");
@L2 N xRN_KFZ_VINK = N(8,0 ,"#RN-KFZ-VINK");
@L2 N xRN_KFZ_FPNR = N(3,0 ,"#RN-KFZ-FPNR");
@L2 A xRA_KFZ_MAPA = A(1 ,"#RA-KFZ-MAPA");
@L2 L xRL_KFZ_PRVV = L("#RL-KFZ-PRVV");
@L2 L xRL_KFZ_ST01 = L("#RL-KFZ-ST01");
@L2 L xRL_KFZ_AUTO = L("#RL-KFZ-AUTO");
@L2 A xRA_KFZ_OPI2 = A(3 ,"#RA-KFZ-OPI2");
@L2 L xRL_KFZ_BLND = L("#RL-KFZ-BLND");
@L2 C xRC_KFZ_BLND = C("#RC-KFZ-BLND");
@L2 A xRA_KFZ_ATXT = A(10 ,"#RA-KFZ-ATXT");
@L2 A xRA_KFZ_KTXT = A(3 ,"#RA-KFZ-KTXT");
@L2 A xRA_KFZ_AMNU = A(8 ,"#RA-KFZ-AMNU");
@L2 A xRA_KFZ_MENU = A(2 ,"#RA-KFZ-MENU");
@L1 A gISN = A(250 ,"+ISN");
@L1 Redefine gISN; // REDEF. BEGIN : +ISN
@L2 A xRA_ISN_ISN = A(5 ,"#RA-ISN-ISN");
@L2 Redefine xRA_ISN_ISN; // REDEF. BEGIN : #RA-ISN-ISN
@L3 P xRN_ISN_AKUN = P(9,0 ,"#RN-ISN-AKUN");
@L2 P xRN_ISN_HKUN = P(9,0 ,"#RN-ISN-HKUN");
@L2 P xRN_ISN_VKUN = P(9,0 ,"#RN-ISN-VKUN");
@L2 P xRN_ISN_ORTE = P(9,0 ,"#RN-ISN-ORTE");
@L2 P xRN_ISN_KORP = P(9,0 ,"#RN-ISN-KORP");
@L2 P xRN_ISN_KTAB = P(9,0 ,"#RN-ISN-KTAB");
@L2 P xRN_ISN_ABKU = P(9,0 ,"#RN-ISN-ABKU");
@L2 P xRN_ISN_OPOL = P(9,0 ,"#RN-ISN-OPOL");
@L2 P xRN_ISN_SPOL = P(9,0 ,"#RN-ISN-SPOL");
@L2 P xRN_ISN_OEVI = P(9,0 ,"#RN-ISN-OEVI");
@L2 P xRN_ISN_SEVI = P(9,0 ,"#RN-ISN-SEVI");
@L2 P xRN_ISN_OVV = P(9,0 ,"#RN-ISN-OVV");
@L2 P xRN_ISN_SVV = P(9,0 ,"#RN-ISN-SVV");
@L2 P xRN_ISN_OBU = P(9,0 ,"#RN-ISN-OBU");
@L2 P xRN_ISN_SBU = P(9,0 ,"#RN-ISN-SBU");
@L2 P xRN_ISN_BTS = P(9,0 ,"#RN-ISN-BTS");
@L2 P xRN_ISN_BTK = P(9,0 ,"#RN-ISN-BTK");
@L2 P xRN_ISN_EKPO = P(9,0 ,"#RN-ISN-EKPO");
@L2 P xRN_ISN_EKVV = P(9,0 ,"#RN-ISN-EKVV");
@L2 P xRN_ISN_EKK1 = P(9,0 ,"#RN-ISN-EKK1");
@L2 P xRN_ISN_OPOS = P(9,0 ,"#RN-ISN-OPOS");
@L2 P xRN_ISN_SPOS = P(9,0 ,"#RN-ISN-SPOS");
@L2 P xRN_ISN_EKG1 = P(9,0 ,"#RN-ISN-EKG1");
@L2 P xRN_ISN_HGPO = P(9,0 ,"#RN-ISN-HGPO");
@L2 P xRN_ISN_HGKO = P(9,0 ,"#RN-ISN-HGKO");
@L2 P xRN_ISN_OLS = P(9,0 ,"#RN-ISN-OLS");
@L2 P xRN_ISN_SLS = P(9,0 ,"#RN-ISN-SLS");
@L2 P xRN_ISN_HLS = P(9,0 ,"#RN-ISN-HLS");
@L2 P xRN_ISN_VDNR = P(9,0 ,"#RN-ISN-VDNR");
@L2 P xRN_ISN_MDST = P(9,0 ,"#RN-ISN-MDST");
@L2 P xRN_ISN_KTOS = P(9,0 ,"#RN-ISN-KTOS");
@L2 P xRN_ISN_KTOO = P(9,0 ,"#RN-ISN-KTOO");
@L2 P xRN_ISN_ORTS = P(9,0 ,"#RN-ISN-ORTS");
@L2 P xRN_ISN_FPNR = P(9,0 ,"#RN-ISN-FPNR");
@L2 P xRN_ISN_WA49 = P(9,0 ,"#RN-ISN-WA49");
@L2 P xRN_ISN_SD34 = P(8,0 ,"#RN-ISN-SD34");
@L2 P xRN_ISN_SPER = P(9,0 ,"#RN-ISN-SPER");
@L2 P xRN_ISN_MDS = P(9,0 ,"#RN-ISN-MDS");
@L2 P xRN_ISN_SUCH = P(9,0 ,"#RN-ISN-SUCH");
@L2 P xRN_ISN_ABST = P(9,0 ,"#RN-ISN-ABST");
@L2 P xRN_ISN_UMKON = P(9,0 ,"#RN-ISN-UMKON");
@L2 P xRN_ISN_MUBU = P(9,0 ,"#RN-ISN-MUBU");// MUTTERBUCHUNG (SK)
@L2 P xRN_ISN_RESERVE = P(9,0 ,"#RN-ISN-RESERVE").defineArray(range(1, 5));
@L1 A gKUN = A(250 ,"+KUN");
@L1 Redefine gKUN; // REDEF. BEGIN : +KUN
@L2 A xRA_KUN_KUNA = A(27 ,"#RA-KUN-KUNA");
@L2 A xRA_KUN_GHKZ = A(1 ,"#RA-KUN-GHKZ");
@L2 N xRN_KUN_GHK1 = N(8,0 ,"#RN-KUN-GHK1");
@L2 N xRN_KUN_GHK2 = N(8,0 ,"#RN-KUN-GHK2");
@L2 N xRN_KUN_DATV = N(8,0 ,"#RN-KUN-DATV");
@L2 N xRN_KUN_DATB = N(8,0 ,"#RN-KUN-DATB");
@L2 A xRA_KUN_BRAX = A(30 ,"#RA-KUN-BRAX");
@L2 N xRN_KUN_FILV = N(2,0 ,"#RN-KUN-FILV");
@L2 N xRN_KUN_FILB = N(2,0 ,"#RN-KUN-FILB");
@L2 N xRN_KUN_ANZEI = N(2,0 ,"#RN-KUN-ANZEI");
@L2 N xRN_KUN_BILKR = N(2,0 ,"#RN-KUN-BILKR");
@L2 N xRN_KUN_POLK = N(2,0 ,"#RN-KUN-POLK");
@L2 N xRN_KUN_POLB = N(2,0 ,"#RN-KUN-POLB");
@L2 A xRA_KUN_BANKKU = A(5 ,"#RA-KUN-BANKKU");
@L2 A xRA_KUN_BANKOR = A(27 ,"#RA-KUN-BANKOR");
@L2 N xRN_KUN_BANKLE = N(5,0 ,"#RN-KUN-BANKLE");
@L2 N xRN_KUN_TDAT = N(8,0 ,"#RN-KUN-TDAT");
@L2 N xRN_KUN_VR = N(3,0 ,"#RN-KUN-VR");
@L2 N xRN_KUN_VGVR = N(3,0 ,"#RN-KUN-VGVR");
@L2 N xRN_KUN_STAT = N(3,0 ,"#RN-KUN-STAT");
@L2 N xRN_KUN_HGV = N(2,0 ,"#RN-KUN-HGV");
@L2 A xRA_KUN_UG1V = A(2 ,"#RA-KUN-UG1V");
@L2 A xRA_KUN_UG2V = A(2 ,"#RA-KUN-UG2V");
@L2 A xRA_KUN_UG3V = A(2 ,"#RA-KUN-UG3V");
@L2 A xRA_KUN_SPV = A(1 ,"#RA-KUN-SPV");
@L2 N xRN_KUN_HGB = N(2,0 ,"#RN-KUN-HGB");
@L2 A xRA_KUN_UG1B = A(2 ,"#RA-KUN-UG1B");
@L2 A xRA_KUN_UG2B = A(2 ,"#RA-KUN-UG2B");
@L2 A xRA_KUN_UG3B = A(2 ,"#RA-KUN-UG3B");
@L2 A xRA_KUN_SPB = A(1 ,"#RA-KUN-SPB");
@L2 A xRA_KUN_LEER1 = A(38 ,"#RA-KUN-LEER1");
@L2 N xRN_KUN_OVGEM = N(5,0 ,"#RN-KUN-OVGEM");
@L2 N xRN_KUN_OVOKZ = N(6,0 ,"#RN-KUN-OVOKZ");
@L2 N xRN_KUN_OVOBK = N(4,0 ,"#RN-KUN-OVOBK");
@L2 A xRA_KUN_OVNAM = A(15 ,"#RA-KUN-OVNAM");
@L2 A xRA_KUN_LEER2 = A(4 ,"#RA-KUN-LEER2");
@L2 A xRA_KUN_OVFIL = A(2 ,"#RA-KUN-OVFIL");
@L2 A xRA_KUN_OVSA = A(1 ,"#RA-KUN-OVSA");
@L1 A gKEY = A(250 ,"+KEY");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 N xRN_KEY_VKKZ = N(8,0 ,"#RN-KEY-VKKZ");
@L2 A xRA_KEY_VVA = A(2 ,"#RA-KEY-VVA");
@L2 N xRN_KEY_VVNR = N(8,0 ,"#RN-KEY-VVNR");
@L2 N xRN_KEY_VHNR = N(3,0 ,"#RN-KEY-VHNR");
@L2 N xRN_KEY_VGES = N(3,0 ,"#RN-KEY-VGES");
@L2 N xRN_KEY_VRES = N(1,0 ,"#RN-KEY-VRES");
@L2 N xRN_KEY_VVIN = N(2,0 ,"#RN-KEY-VVIN");
@L2 N xRN_KEY_VBRA = N(2,0 ,"#RN-KEY-VBRA");
@L2 N xRN_KEY_VBRN = N(1,0 ,"#RN-KEY-VBRN");
@L2 A xRA_KEY_VBTART = A(1 ,"#RA-KEY-VBTART");
@L2 A xRA_KEY_VBSTAT = A(1 ,"#RA-KEY-VBSTAT");
@L2 N xRN_KEY_VUGES = N(3,0 ,"#RN-KEY-VUGES");
@L2 N xRN_KEY_VBJAHR = N(2,0 ,"#RN-KEY-VBJAHR");
@L2 N xRN_KEY_AKTONR = N(7,0 ,"#RN-KEY-AKTONR");
@L2 Redefine xRN_KEY_AKTONR; // REDEF. BEGIN : #RN-KEY-AKTONR
@L3 A xRA_KEY_VKTO = A(7 ,"#RA-KEY-VKTO");
@L2 A xRA_KEY_KOSEL = A(1 ,"#RA-KEY-KOSEL");
@L2 A xRA_KEY_LEER1 = A(4 ,"#RA-KEY-LEER1");
@L2 N xRN_KEY_AKKZ = N(8,0 ,"#RN-KEY-AKKZ");
@L2 A xRA_KEY_AVA = A(2 ,"#RA-KEY-AVA");
@L2 N xRN_KEY_AVNR = N(8,0 ,"#RN-KEY-AVNR");
@L2 N xRN_KEY_AHNR = N(3,0 ,"#RN-KEY-AHNR");
@L2 N xRN_KEY_AGES = N(3,0 ,"#RN-KEY-AGES");
@L2 N xRN_KEY_ARES = N(1,0 ,"#RN-KEY-ARES");
@L2 N xRN_KEY_AVIN = N(2,0 ,"#RN-KEY-AVIN");
@L2 N xRN_KEY_ABRA = N(2,0 ,"#RN-KEY-ABRA");
@L2 N xRN_KEY_ABRN = N(1,0 ,"#RN-KEY-ABRN");
@L2 A xRA_KEY_ABTART = A(1 ,"#RA-KEY-ABTART");
@L2 A xRA_KEY_ABSTAT = A(1 ,"#RA-KEY-ABSTAT");
@L2 N xRN_KEY_AUGES = N(3,0 ,"#RN-KEY-AUGES");
@L2 N xRN_KEY_ABJAHR = N(2,0 ,"#RN-KEY-ABJAHR");
@L2 N xRN_KEY_KTONR = N(7,0 ,"#RN-KEY-KTONR");
@L2 N xRN_KEY_PROV = N(2,0 ,"#RN-KEY-PROV");
@L2 N xRN_KEY_PROD = N(2,0 ,"#RN-KEY-PROD");
@L2 A xRA_KEY_PRKZ = A(1 ,"#RA-KEY-PRKZ");
@L2 N xRN_KEY_AFGS = N(3,0 ,"#RN-KEY-AFGS");
@L2 A xRA_KEY_AFPO = A(14 ,"#RA-KEY-AFPO");
@L2 N xRN_KEY_VFGS = N(3,0 ,"#RN-KEY-VFGS");
@L2 A xRA_KEY_VFPO = A(14 ,"#RA-KEY-VFPO");
@L2 N xRN_KEY_FBRA = N(2,0 ,"#RN-KEY-FBRA");
@L2 N xRN_KEY_NOTD = N(6,0 ,"#RN-KEY-NOTD");
@L2 Redefine xRN_KEY_NOTD;
@L3 N xRN_KEY_NOTD_YYYY = N(4,0 ,"#RN-KEY-NOTD-YYYY");
@L3 N xRN_KEY_NOTD_MM = N(2,0 ,"#RN-KEY-NOTD-MM");
@L2 N xRN_KEY_LSNR = N(2,0 ,"#RN-KEY-LSNR");
@L2 N xRN_KEY_KBJ = N(4,0 ,"#RN-KEY-KBJ");
@L2 N xRN_KEY_KBJNR = N(1,0 ,"#RN-KEY-KBJNR");
@L2 A xRA_KEY_ZNAM = A(31 ,"#RA-KEY-ZNAM");
@L2 A xRA_KEY_VNAM = A(2 ,"#RA-KEY-VNAM");
@L2 A xRA_KEY_STRA = A(1 ,"#RA-KEY-STRA");
@L2 N xRN_KEY_HAUS = N(4,0 ,"#RN-KEY-HAUS");
@L2 N xRN_KEY_POST = N(6,0 ,"#RN-KEY-POST");
@L2 A xRA_KEY_KZEX = A(1 ,"#RA-KEY-KZEX");
@L2 A xRA_KEY_ACHT = A(1 ,"#RA-KEY-ACHT");
@L2 N xRN_KEY_ALFA = N(2,0 ,"#RN-KEY-ALFA");
@L2 N xRN_KEY_BERU = N(5,0 ,"#RN-KEY-BERU");
@L2 A xRA_KEY_LEER2 = A(4 ,"#RA-KEY-LEER2");
@L2 A xRA_KEY_BRSL = A(1 ,"#RA-KEY-BRSL");
@L2 A xRA_KEY_PSTA = A(1 ,"#RA-KEY-PSTA");
@L2 A xRA_KEY_BRAW = A(1 ,"#RA-KEY-BRAW");
@L2 N xRN_KEY_BRVO = N(2,0 ,"#RN-KEY-BRVO");
@L2 N xRN_KEY_BRBI = N(2,0 ,"#RN-KEY-BRBI");
@L2 N xRN_KEY_JRVO = N(2,0 ,"#RN-KEY-JRVO");
@L2 N xRN_KEY_JRBI = N(2,0 ,"#RN-KEY-JRBI");
@L2 A xRA_KEY_VNAW = A(1 ,"#RA-KEY-VNAW");
@L2 A xRA_KEY_KEIG = A(1 ,"#RA-KEY-KEIG");
@L2 A xRA_KEY_SBID = A(3 ,"#RA-KEY-SBID");
@L2 A xRA_KEY_KURO = A(2 ,"#RA-KEY-KURO");
@L2 A xRA_KEY_PRAE = A(1 ,"#RA-KEY-PRAE");
@L2 A xRA_KEY_PROV = A(1 ,"#RA-KEY-PROV");
@L2 A xRA_KEY_OFFE = A(1 ,"#RA-KEY-OFFE");
@L2 A xRA_KEY_AKTU = A(1 ,"#RA-KEY-AKTU");
@L2 A xRA_KEY_ALLE = A(1 ,"#RA-KEY-ALLE");
@L2 A xRA_KEY_AOFA = A(1 ,"#RA-KEY-AOFA");
@L2 N xRN_KEY_BUDT = N(8,0 ,"#RN-KEY-BUDT");
@L2 P xRN_KEY_BUNR = P(9,0 ,"#RN-KEY-BUNR");
@L2 A xRA_KEY_VKZ = A(1 ,"#RA-KEY-VKZ");
@L2 A xRA_KEY_ABGA = A(1 ,"#RA-KEY-ABGA");
@L2 A xRA_KEY_SART = A(2 ,"#RA-KEY-SART");
@L2 A xRA_KEY_PFIL = A(2 ,"#RA-KEY-PFIL");
@L2 L xRL_KEY_UHR = L("#RL-KEY-UHR");
@L2 A xRA_KEY_STAT = A(1 ,"#RA-KEY-STAT");
@L2 A xRA_KEY_GSMT = A(1 ,"#RA-KEY-GSMT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_1 = A(8 ,"#RA-KEY-08X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_VVO = A(22 ,"#RA-KEY-VVO");
@L2 A xRA_KEY_27X_1 = A(27 ,"#RA-KEY-27X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVO = A(22 ,"#RA-KEY-AVO");
@L2 A xRA_KEY_27X_2 = A(27 ,"#RA-KEY-27X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVO = A(22 ,"#RA-KEY-GVO");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_2 = A(8 ,"#RA-KEY-08X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_VBT = A(24 ,"#RA-KEY-VBT");
@L2 A xRA_KEY_24X = A(24 ,"#RA-KEY-24X"); // ZWISCHENRAUM
@L2 A xRA_KEY_ABT = A(24 ,"#RA-KEY-ABT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_3 = A(8 ,"#RA-KEY-08X-3");// ZWISCHENRAUM
@L2 A xRA_KEY_VVK = A(16 ,"#RA-KEY-VVK");
@L2 A xRA_KEY_33X_1 = A(33 ,"#RA-KEY-33X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVK = A(16 ,"#RA-KEY-AVK");
@L2 A xRA_KEY_33X_2 = A(33 ,"#RA-KEY-33X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVK = A(16 ,"#RA-KEY-GVK");
@L1 A gSTV = A(250 ,"+STV");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_APRG = A(8 ,"#RA-STV-APRG");
@L2 A xRA_STV_VPRG = A(8 ,"#RA-STV-VPRG");
@L2 A xRA_STV_FUNK = A(4 ,"#RA-STV-FUNK");
@L2 Redefine xRA_STV_FUNK; // REDEF. BEGIN : #RA-STV-FUNK
@L3 N xRN_STV_FUNR = N(2,0 ,"#RN-STV-FUNR");
@L3 A xRA_STV_FUNR = A(2 ,"#RA-STV-FUNR");
@L2 N xRN_STV_AUSW = N(2,0 ,"#RN-STV-AUSW");
@L2 A xRA_STV_CODE = A(4 ,"#RA-STV-CODE"); // 30
@L2 N xRN_STV_OBNR = N(8,0 ,"#RN-STV-OBNR");
@L2 A xRA_STV_OART = A(2 ,"#RA-STV-OART");
@L2 N xRN_STV_HINR = N(3,0 ,"#RN-STV-HINR");
@L2 N xRN_STV_VINR = N(2,0 ,"#RN-STV-VINR");
@L2 N xRN_STV_BRAN = N(2,0 ,"#RN-STV-BRAN");
@L2 N xRN_STV_BRNR = N(1,0 ,"#RN-STV-BRNR");
@L2 N xRN_STV_GENR = N(3,0 ,"#RN-STV-GENR");
@L2 A xRA_STV_GMOD = A(1 ,"#RA-STV-GMOD");
@L2 A xRA_STV_AMOD = A(1 ,"#RA-STV-AMOD");
@L2 A xRA_STV_VIKZ = A(1 ,"#RA-STV-VIKZ");
@L2 A xRA_STV_DATI = A(8 ,"#RA-STV-DATI");
@L2 N xRN_STV_DATA = N(3,0 ,"#RN-STV-DATA");
@L2 A xRA_STV_OPID = A(3 ,"#RA-STV-OPID");
@L2 A xRA_STV_PFKY = A(96 ,"#RA-STV-PFKY");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PFF = A(4 ,"#RA-STV-PFF");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PF = A(4 ,"#RA-STV-PF").defineArray(range(1, 24));
@L2 N xRN_STV_DATN = N(6,0 ,"#RN-STV-DATN");
@L2 A xRA_STV_MMOD = A(3 ,"#RA-STV-MMOD");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_241X = A(241 ,"#RA-STV-241X");
@L2 A xRA_STV_APAR = A(1 ,"#RA-STV-APAR");
@L2 P xRN_STV_CURS = P(6,0 ,"#RN-STV-CURS");
@L2 P xRN_STV_MARK = P(3,0 ,"#RN-STV-MARK");
@L2 B xRN_STV_FNUM = B(2 ,"#RN-STV-FNUM");
@L1 A gKEY1 = A(250 ,"+KEY1");
@L1 Redefine gKEY1; // REDEF. BEGIN : +KEY1
@L2 A xRA_KEY1_ASSTART = A(4 ,"#RA-KEY1-ASSTART");
@L2 N xRN_KEY1_POSVI = N(2,0 ,"#RN-KEY1-POSVI");
@L2 N xRN_KEY1_POSVII = N(3,0 ,"#RN-KEY1-POSVII");
@L2 A xRA_KEY1_RVART = A(4 ,"#RA-KEY1-RVART");
@L2 N xRN_KEY1_RVVON = N(8,0 ,"#RN-KEY1-RVVON");
@L2 N xRN_KEY1_RVBHJAHR = N(4,0 ,"#RN-KEY1-RVBHJAHR");
@L2 N xRN_KEY1_RVBBNR = N(2,0 ,"#RN-KEY1-RVBBNR");
@L2 P xRN_KEY1_SBVANZ = P(1,0 ,"#RN-KEY1-SBVANZ");
@L2 P xRN_KEY1_SBVINR = P(2,0 ,"#RN-KEY1-SBVINR").defineArray(range(1, 2));
@L2 P xRN_KEY1_SBVII = P(2,0 ,"#RN-KEY1-SBVII").defineArray(range(1, 2));
@L2 N xRN_KEY1_BRA = N(2,0 ,"#RN-KEY1-BRA").defineArray(range(1, 3));
@L2 N xRN_KEY1_GEBDT = N(8,0 ,"#RN-KEY1-GEBDT");
@L2 A xRA_KEY1_2X = A(2 ,"#RA-KEY1-2X");
@L2 A xRA_KEY1_WAEHRUNG = A(3 ,"#RA-KEY1-WAEHRUNG");
@L2 A xRA_KEY1_SK_ALLE = A(1 ,"#RA-KEY1-SK-ALLE");
@L2 N xRN_KEY1_HNR = N(4,0 ,"#RN-KEY1-HNR");
@L2 A xRA_KEY1_MTVKZ = A(1 ,"#RA-KEY1-MTVKZ");
@L2 A xRA_KEY1_VIL = A(1 ,"#RA-KEY1-VIL");
@L2 A xRA_KEY1_OKZ = A(3 ,"#RA-KEY1-OKZ");
@L2 N xRN_KEY1_OPLZ = N(6,0 ,"#RN-KEY1-OPLZ");
@L2 N xRN_KEY1_OHNR = N(4,0 ,"#RN-KEY1-OHNR");
@L2 P xRN_KEY1_SUBVR = P(3,0 ,"#RN-KEY1-SUBVR");
@L2 P xRN_KEY1_SUBKTO = P(7,0 ,"#RN-KEY1-SUBKTO");
@L2 P xRN_KEY1_SUBZWST = P(3,0 ,"#RN-KEY1-SUBZWST");
@L2 A xRA_KEY1_SKART = A(3 ,"#RA-KEY1-SKART");
@L2 A xRA_KEY1_TCSART = A(1 ,"#RA-KEY1-TCSART");
@L2 A xRA_KEY1_TCZ1 = A(2 ,"#RA-KEY1-TCZ1");
@L2 Redefine xRA_KEY1_TCZ1; // REDEF. BEGIN : #RA-KEY1-TCZ1
@L3 N xRN_KEY1_TCZ1 = N(2,0 ,"#RN-KEY1-TCZ1");
@L2 P xRN_KEY1_TCODE = P(5,0 ,"#RN-KEY1-TCODE");
@L2 P xRN_KEY1_TCZ2 = P(2,0 ,"#RN-KEY1-TCZ2");
@L2 A xRA_KEY1_NATV = A(3 ,"#RA-KEY1-NATV");
@L2 A xRA_KEY1_NATO = A(3 ,"#RA-KEY1-NATO");
@L2 N xRN_KEY1_SNR = N(8,0 ,"#RN-KEY1-SNR");
@L2 N xRN_KEY1_LNR = N(5,0 ,"#RN-KEY1-LNR");
@L2 A xRA_KEY1_ZISB = A(3 ,"#RA-KEY1-ZISB");
@L2 N xRN_KEY1_STAT = N(5,0 ,"#RN-KEY1-STAT");
@L2 N xRN_KEY1_LZ = N(2,0 ,"#RN-KEY1-LZ");
@L2 A xRA_KEY1_GRDHA = A(2 ,"#RA-KEY1-GRDHA");
@L2 A xRA_KEY1_NATOV = A(3 ,"#RA-KEY1-NATOV");
@L2 N xRN_KEY1_AWDT = N(8,0 ,"#RN-KEY1-AWDT");
@L2 A xRA_KEY1_KOSP = A(1 ,"#RA-KEY1-KOSP");
@L2 N xRN_KEY1_EDSTAT = N(2,0 ,"#RN-KEY1-EDSTAT");
@L2 A xRA_KEY1_FREI = A(7 ,"#RA-KEY1-FREI");
@L2 N xRN_KEY1_KDAT = N(8,0 ,"#RN-KEY1-KDAT");
@L2 N xRN_KEY1_KGRD = N(2,0 ,"#RN-KEY1-KGRD");
@L2 N xRN_KEY1_PERSNR = N(5,0 ,"#RN-KEY1-PERSNR");
@L2 A xRA_KEY1_RPART = A(2 ,"#RA-KEY1-RPART");
@L2 N xRN_KEY1_RPDAT = N(8,0 ,"#RN-KEY1-RPDAT");
@L2 A xRA_KEY1_RVKZ = A(1 ,"#RA-KEY1-RVKZ");
@L2 A xRA_KEY1_RVKZ2 = A(1 ,"#RA-KEY1-RVKZ2");
@L2 A xRA_KEY1_LK = A(30 ,"#RA-KEY1-LK");
@L2 A xRA_KEY1_SCHEMA = A(2 ,"#RA-KEY1-SCHEMA");
@L2 N xRN_KEY1_RSOBNR = N(8,0 ,"#RN-KEY1-RSOBNR");
@L2 A xRA_KEY1_RSKZ = A(1 ,"#RA-KEY1-RSKZ");
@L2 A xRA_KEY1_BRFA = A(3 ,"#RA-KEY1-BRFA");
@L2 A xRA_KEY1_BUCHART = A(3 ,"#RA-KEY1-BUCHART");
@L2 N xRN_KEY1_BANKSCHL = N(3,0 ,"#RN-KEY1-BANKSCHL");
@L2 N xRN_KEY1_WERTEDAT = N(8,0 ,"#RN-KEY1-WERTEDAT");
@L2 A xRA_KEY1_VORPROG = A(8 ,"#RA-KEY1-VORPROG");
@L2 N xRN_KEY1_GILT_BIS = N(8,0 ,"#RN-KEY1-GILT-BIS");
@L2 P xRN_KEY1_FNR = P(3,0 ,"#RN-KEY1-FNR");
@L2 A xRA_KEY1_KO55MELD = A(1 ,"#RA-KEY1-KO55MELD");
@L1 A gKEY2 = A(250 ,"+KEY2");
@L1 Redefine gKEY2; // REDEF. BEGIN : +KEY2
@L2 A xRA_KEY2_TARIF = A(7 ,"#RA-KEY2-TARIF");
@L2 A xRA_KEY2_BANKNA = A(27 ,"#RA-KEY2-BANKNA");
@L2 A xRA_KEY2_USERGRUPPE = A(8 ,"#RA-KEY2-USERGRUPPE");
@L2 N xRN_KEY2_KOSTST = N(5,0 ,"#RN-KEY2-KOSTST");
@L2 A xRA_KEY2_AVISO_OP = A(3 ,"#RA-KEY2-AVISO-OP");
@L2 N xRN_KEY2_AVISO_DT = N(8,0 ,"#RN-KEY2-AVISO-DT");
@L2 N xRN_KEY2_AVISO_GD = N(3,0 ,"#RN-KEY2-AVISO-GD");
@L2 N xRN_KEY2_AVISO_DTB = N(8,0 ,"#RN-KEY2-AVISO-DTB");
@L2 A xRA_KEY2_SPRACH_KZ = A(2 ,"#RA-KEY2-SPRACH-KZ");
@L2 A xRA_KEY2_GEWINN_VB = A(4 ,"#RA-KEY2-GEWINN-VB");
@L2 N xRN_KEY2_GEWINN_JAHR = N(4,0 ,"#RN-KEY2-GEWINN-JAHR");
@L2 A xRA_KEY2_ABST_SA = A(2 ,"#RA-KEY2-ABST-SA");
@L2 A xRA_KEY2_ABST_ZA = A(1 ,"#RA-KEY2-ABST-ZA");
@L2 P xRN_KEY2_ABST_ZR = P(9,0 ,"#RN-KEY2-ABST-ZR");
@L2 A xRA_KEY2_ABST_NK = A(1 ,"#RA-KEY2-ABST-NK");
@L2 A xRA_KEY2_ABST_NA = A(8 ,"#RA-KEY2-ABST-NA");
@L2 A xRA_KEY2_FAB = A(3 ,"#RA-KEY2-FAB");
@L2 A xRA_KEY2_TYP = A(10 ,"#RA-KEY2-TYP");
@L2 A xRA_KEY2_FZA = A(1 ,"#RA-KEY2-FZA");
@L2 N xRN_KEY2_LST = N(1,0 ,"#RN-KEY2-LST");
@L2 N xRN_KEY2_KTBJ = N(4,0 ,"#RN-KEY2-KTBJ");
@L2 N xRN_KEY2_KW = N(3,0 ,"#RN-KEY2-KW");
@L2 P xRN_KEY2_UEBWNR = P(6,0 ,"#RN-KEY2-UEBWNR");
@L2 P xRN_KEY2_UNTNR = P(4,0 ,"#RN-KEY2-UNTNR");
@L2 A xRA_KEY2_SERIE = A(1 ,"#RA-KEY2-SERIE");
@L2 P xRN_KEY2_PAKNR = P(9,0 ,"#RN-KEY2-PAKNR");
@L2 A xRA_KEY2_WHG_VON = A(3 ,"#RA-KEY2-WHG-VON");
@L2 A xRA_KEY2_WHG_NACH = A(3 ,"#RA-KEY2-WHG-NACH");
@L2 A xRA_KEY2_EXTSA = A(3 ,"#RA-KEY2-EXTSA");
@L2 N xRN_KEY2_GKNR = N(8,0 ,"#RN-KEY2-GKNR");
@L1 C xCV_WHBS = C("#CV-WHBS"); // GW FUER WIEDERHOLTE BS-AUSG.
@L1 C xCV_OUTP = C("#CV-OUTP"); // PROTECT BEI A+B - PROGRAMMEN
@L1 C xCV_STV = C("#CV-STV"); // KEY-FELDER STEUERLEISTE
@L1 L xWL_KEYWECHS = L("#WL-KEYWECHS"); // SCHALTER, OB KEYWECHSEL
@L1 L xWL_INTERN = L("#WL-INTERN"); // SCHALTER, OB BLINDER DURCHL.
@L1 A xWA_HELP_TASTE = A(4 ,"#WA-HELP-TASTE"); // PF-TASTE FUER 'HELP'
@L1 A xWA_MENU_TASTE = A(4 ,"#WA-MENU-TASTE"); // PF-TASTE FUER 'MENU'
@L1 P xWN_SEL_ISN = P(9,0 ,"#WN-SEL-ISN").defineArray(range(1,24));
@L1 A xWA_FUNKTION = A(5 ,"#WA-FUNKTION").defineArray(range(1,100));// FUNKTIONSTABELLE
@L1 A xWA_VORWEG_AUFRUF = A(8 ,"#WA-VORWEG-AUFRUF");// VORWEG-AUFRUF (Z.B. SCHWEBE)
@L1 L xWL_TEST = L("#WL-TEST"); // KENNZEICHEN, OB TEST
@L1 N xWN_WARTEN_SUMME = N(5,0 ,"#WN-WARTEN-SUMME");// ZAEHLER FUER SUSPEND
@L1 A xWA_AUSWAHL_KZ = A(1 ,"#WA-AUSWAHL-KZ").defineArray(range(1,24));// MARKIERUNGSLEISTE
@L1 N xWN_AUSWAHL_ANZAHL = N(2,0 ,"#WN-AUSWAHL-ANZAHL");
@L1 A xWA_AUSWAHL_GUELTIG = A(20 ,"#WA-AUSWAHL-GUELTIG");
/*
* Platz für neue globale Variable
* Definitionen in:
* YLNEUGLO/YCNEUGLO
*/
@L1 A gNEUGLO_01 = A(250 ,"+NEUGLO-01");
@L1 Redefine gNEUGLO_01;
@L2 A xBTC_STEUERUNG = A(8 ,"#BTC-STEUERUNG");
@L2 A xUMS_MIS_BILANZKREIS = A(1 ,"#UMS-MIS-BILANZKREIS").defineArray(range(1, 7));
@L2 A xUMS_MIS_BRAN = A(2 ,"#UMS-MIS-BRAN").defineArray(range(1, 10));
@L2 P xKEY1_SUCH_BETRAG = P(9,2 ,"#KEY1-SUCH-BETRAG");
@L2 P xKEY2_ARCHIV_DB = P(3,0 ,"#KEY2-ARCHIV-DB");
@L2 A xKEY2_EXTKZ = A(1 ,"#KEY2-EXTKZ");
@L2 N xRN_KEY_JAHRV = N(4,0 ,"#RN-KEY-JAHRV");
@L2 N xRN_KEY_JAHRB = N(4,0 ,"#RN-KEY-JAHRB");
@L2 A xRA_KEY_EXT_GRUND_NAME = A(31 ,"#RA-KEY-EXT-GRUND-NAME");
@L2 P xRN_KEY_EXT_BB_VON = P(8,0 ,"#RN-KEY-EXT-BB-VON");
@L2 P xRN_KEY_EXT_BB_BIS = P(8,0 ,"#RN-KEY-EXT-BB-BIS");
@L2 P xUMS_MA_SUB_SATZART_1 = P(3,0 ,"#UMS-MA-SUB-SATZART-1");
@L2 A xRA_KEY_EXT_ANTRAGSART = A(3 ,"#RA-KEY-EXT-ANTRAGSART");
@L2 P xFLV_TRANS = P(7,0 ,"#FLV-TRANS");
@L2 A xFLV_ISIN = A(12 ,"#FLV-ISIN");
@L2 P xFLV_BUNR = P(7,0 ,"#FLV-BUNR");
@L2 P xFLV_GILT_BIS = P(8,0 ,"#FLV-GILT-BIS");
@L2 P xFLV_DATUM = P(8,0 ,"#FLV-DATUM");
@L2 P xFLV_VV_VINR = P(3,0 ,"#FLV-VV-VINR");
@L2 P xFLV_VV_BRAN = P(3,0 ,"#FLV-VV-BRAN");
@L2 P xFLV_VV_BRNR = P(3,0 ,"#FLV-VV-BRNR");
@L2 P xFLV_VV_HINR = P(3,0 ,"#FLV-VV-HINR");
@L2 A xVLK_EXVERARB = A(30 ,"#VLK-EXVERARB");
@L2 P xRN_KEY_IVKNR = P(8,0 ,"#RN-KEY-IVKNR");
@L2 A xRA_SC_MELDUNGS_ART = A(1 ,"#RA-SC-MELDUNGS-ART");
@L2 A xRA_OS_GFALL = A(2 ,"#RA-OS-GFALL");
@L2 A xRA_OS_DOKART = A(4 ,"#RA-OS-DOKART");
@L2 A xRA_OS_BR_ART = A(3 ,"#RA-OS-BR-ART");
@L2 N xRN_KEY_YU_PERSNR = N(13,0 ,"#RN-KEY-YU-PERSNR");
@L2 N xSCN_VERS_BEGINN = N(8,0 ,"#SCN-VERS-BEGINN");
@L2 A xDI_ABTEILUNG = A(8 ,"#DI-ABTEILUNG");
@L2 P xDI_STAT_DZT = P(3,0 ,"#DI-STAT-DZT");
@L2 P xDI_STAT_DZT_BIS = P(3,0 ,"#DI-STAT-DZT-BIS");
@L2 A xDI_SPARTE = A(2 ,"#DI-SPARTE");
@L2 A xDI_STATION = A(8 ,"#DI-STATION");
@L2 A xDI_WM_OPID = A(8 ,"#DI-WM-OPID");
@L2 A xDI_AW_POL_FIL = A(1 ,"#DI-AW-POL-FIL");
@L2 A xDI_VAE_ART = A(3 ,"#DI-VAE-ART");
@L2 A xDI_WK_VAE_ART = A(3 ,"#DI-WK-VAE-ART");
@L2 A xDI_AW_SUB_VR = A(1 ,"#DI-AW-SUB-VR");
@L2 P xDI_SUB_VR = P(3,0 ,"#DI-SUB-VR");
@L2 P xDI_POL_KREIS = P(2,0 ,"#DI-POL-KREIS");
@L2 P xDI_POL_BRAN = P(2,0 ,"#DI-POL-BRAN");
@L2 A xDI_VAE_HINW = A(3 ,"#DI-VAE-HINW");
@L1 A gNEUGLO_02 = A(250 ,"+NEUGLO-02");
@L1 Redefine gNEUGLO_02;
@L2 A xDI_BEARB_GRP = A(3 ,"#DI-BEARB-GRP");
@L2 A xDI_AW_DRF = A(1 ,"#DI-AW-DRF");
@L2 A xDI_AW_E_ANT = A(1 ,"#DI-AW-E-ANT");
@L2 P xDI_PC_ANT_VKTO = P(7,0 ,"#DI-PC-ANT-VKTO");
@L2 P xDI_PLZ_VON = P(7,0 ,"#DI-PLZ-VON");
@L2 P xDI_PLZ_BIS = P(7,0 ,"#DI-PLZ-BIS");
@L2 A xDI_POSTNATION = A(3 ,"#DI-POSTNATION");
@L2 P xDI_WM_DATUM = P(8,0 ,"#DI-WM-DATUM");
@L2 P xDI_POL_FILIALE = P(2,0 ,"#DI-POL-FILIALE");
@L2 A xDI_AW_VAE_HINW = A(1 ,"#DI-AW-VAE-HINW");
@L2 A xDI_AW_VKTO = A(1 ,"#DI-AW-VKTO");
@L2 A xDI_AW_BEARB_GRP = A(1 ,"#DI-AW-BEARB-GRP");
@L2 A xDI_AW_KR_BR = A(1 ,"#DI-AW-KR-BR");
@L2 A xDI_AW_VAE_ART = A(1 ,"#DI-AW-VAE-ART");
@L2 P xDI_VR_KEY = P(3,0 ,"#DI-VR-KEY");
@L2 A xDI_OART = A(2 ,"#DI-OART");
@L2 P xDI_WM_DATUM_VON = P(8,0 ,"#DI-WM-DATUM-VON");
@L2 A xDI_WM_AUSWAHL = A(1 ,"#DI-WM-AUSWAHL");
@L2 A xRA_KEY_OIB = A(20 ,"#RA-KEY-OIB");
}

View File

@@ -0,0 +1,23 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package CSTENTW;
import innowake.mee.natural.api.*;
/**
* IT fixture (roadmap item 187), shaped like the corpus's CSTENTW/DISKRECH.java: a program in one
* library that includes the GDA of another. COMMON_DA exists in KORIN and ABFRAGE; only the qualifier
* says which one this module means.
*/
@Natural(moduleName="DISKTEST")
public class DISKTEST extends NaturalProgram {
@Using KORIN.COMMON_DA COMMON_DA = globalUsing(KORIN.COMMON_DA.class);
@Using KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA YPUMPRUE_DA = localUsing(KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA.class);
@Using KORIN.NOFILE_DA NOFILE_DA = localUsing(KORIN.NOFILE_DA.class);
@Override
public void run() {
}
}

View File

@@ -0,0 +1,22 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
import innowake.mee.natural.api.*;
/**
* IT fixture (roadmap item 187): the unqualified form. COMMON_DA is named without a package, so only
* Java's same-package rule — this file's own package, KORIN — can pick KORIN.COMMON over
* ABFRAGE.COMMON.
*/
@Natural(moduleName="UMKTEST")
public class UMKTEST extends NaturalProgram {
@Using COMMON_DA COMMON_DA = globalUsing(COMMON_DA.class);
@Using KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA YPUMPRUE_DA = localUsing(KORIN.Mitarbeiter.Verwaltung.YPUMPRUE_DA.class);
@Override
public void run() {
}
}

View File

@@ -0,0 +1,113 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * header created during migration process *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION Rule : Vkto-Daten
* * @OWNER P07
* **************************************************************
* **************************************************************
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* </pre>
*/
@Natural(moduleName="V-MIT04")
public class V_MIT04 extends NaturalSubprogram {
//
// VKTO-Daten zu Original od. Schwebe
//
@Using KORIN.Diverse.Framework.GWSTGP_DA GWSTGP_DA = parameterUsing(KORIN.Diverse.Framework.GWSTGP_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWSTGP_DA) */
public final KORIN.Diverse.Framework.GWSTGP_DA.GWSTGP GWSTGP = GWSTGP_DA.GWSTGP;
/* @@USING:END(KORIN.Diverse.Framework.GWSTGP_DA) */
@Using KORIN.Diverse.Framework.GWPDA_M_DA GWPDA_M_DA = parameterUsing(KORIN.Diverse.Framework.GWPDA_M_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWPDA_M_DA) */
public final KORIN.Diverse.Framework.GWPDA_M_DA.MSG_INFO MSG_INFO = GWPDA_M_DA.MSG_INFO;
/* @@USING:END(KORIN.Diverse.Framework.GWPDA_M_DA) */
@Using V_MIT04P_DA V_MIT04P_DA = parameterUsing(V_MIT04P_DA.class);
/* @@USING:BEGIN(V_MIT04P_DA) */
public final V_MIT04P_DA.V_MIT04P V_MIT04P = V_MIT04P_DA.V_MIT04P;
/* @@USING:END(V_MIT04P_DA) */
@Using KORIN.Diverse.Framework.GWLOGNAM_DA GWLOGNAM_DA = localUsing(KORIN.Diverse.Framework.GWLOGNAM_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWLOGNAM_DA) */
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_RETCODE xSTD_RETCODE = GWLOGNAM_DA.xSTD_RETCODE;
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_LOOPCODE xSTD_LOOPCODE = GWLOGNAM_DA.xSTD_LOOPCODE;
/* @@USING:END(KORIN.Diverse.Framework.GWLOGNAM_DA) */
/* * */
// Daten für: MITZ1R
@Using MITZ1RP_DA MITZ1RP_DA = localUsing(MITZ1RP_DA.class);
/* @@USING:BEGIN(MITZ1RP_DA) */
public final MITZ1RP_DA.MITZ1RR MITZ1RR = MITZ1RP_DA.MITZ1RR;
public final MITZ1RP_DA.MITZ1RP MITZ1RP = MITZ1RP_DA.MITZ1RP;
/* @@USING:END(MITZ1RP_DA) */
@Local
@L1 Group xVON_MITZ1R = group("#VON-MITZ1R"); // <PRIME-KEY-VON>
@L2 A xGESAMT = A(7 ,"#GESAMT");
@L2 Redefine xGESAMT;
@L3 A SVKTO = A(7 ,"SVKTO");
@L3 Redefine SVKTO;
@L4 P VR_KEY = P(3,0 ,"VR-KEY");
@L4 P VKTO = P(7,0 ,"VKTO");
@L4 A STATUS_BYTE = A(1 ,"STATUS-BYTE");
/* * */
@Override
public void run() {
/* * */
reset(V_MIT04P.OUTPUTS);
/* * */
if (((NE(V_MIT04P.xVR_KEY, 0)
&& NE(V_MIT04P.xVKTO, 0))
&& NE(V_MIT04P.xSTATUS_BYTE, " "))) {
assign(xVON_MITZ1R.VR_KEY, V_MIT04P.xVR_KEY);
assign(xVON_MITZ1R.VKTO, V_MIT04P.xVKTO);
assign(xVON_MITZ1R.STATUS_BYTE, V_MIT04P.xSTATUS_BYTE);
//
//
assign(MITZ1RR.xVON_KEY, xVON_MITZ1R.xGESAMT);
assign(MITZ1RR.xFUNCTION, "Read");
callnat("MITZ1R", GWSTGP, MSG_INFO, MITZ1RR, MITZ1RP);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
escapeRoutine();
}
if ((NE(MITZ1RR.xSW_FOUND, "J")
&& EQ(V_MIT04P.xSCHWEBE_GILT, "J"))) {
assign(xVON_MITZ1R.STATUS_BYTE, "S");
assign(MITZ1RR.xVON_KEY, xVON_MITZ1R.xGESAMT);
assign(MITZ1RR.xFUNCTION, "Read");
callnat("MITZ1R", GWSTGP, MSG_INFO, MITZ1RR, MITZ1RP);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
escapeRoutine();
}
}
if (EQ(MITZ1RR.xSW_FOUND, "J")) {
assign(V_MIT04P.xZS_NAME, MITZ1RP.ZS_NAME.get(1));
assign(V_MIT04P.xVKTO_ORGBEZEICHNUNG, MITZ1RP.VKTO_ORGBEZEICHNUNG);
assign(V_MIT04P.xVKTO_STATUS_TEXT, MITZ1RP.VKTO_STATUS_TEXT);
assign(V_MIT04P.xVKTO_VERWENDUNG, MITZ1RP.VKTO_VERWENDUNG);
assign(V_MIT04P.xVKTO_VERWENDUNG_TEXT, MITZ1RP.VKTO_VERWENDUNG_TEXT);
if (GT(V_MIT04P.xVKTO_ORGBEZEICHNUNG, " ")) {
assign(V_MIT04P.xMAP_NAME, V_MIT04P.xVKTO_ORGBEZEICHNUNG);
} else {
assign(V_MIT04P.xMAP_NAME, V_MIT04P.xZS_NAME);
}
} else {
// KONTONUMMER :1: NICHT IN MITARBEITER-DATEI GESPEICHERT
assign(MSG_INFO.xxMSG_NR, 684);
assign(MSG_INFO.xxMSG_DATA.get(1), V_MIT04P.xVKTO);
escapeRoutine();
}
}
/* * */
}
}

View File

@@ -0,0 +1,24 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
@Natural(moduleName="YPUMPRUE")
public class YPUMPRUE_DA extends NaturalData {
@Parameter
@L1 Group YPUMPRUE = group("YPUMPRUE");
@L2 Group INPUTS = group("INPUTS");
@L3 N VR_KEY = N(3,0 ,"VR-KEY");
@L3 N VKTO = N(7,0 ,"VKTO");
@L3 N VKTO_ZIEL = N(7,0 ,"VKTO-ZIEL");
@L3 A KZ_PR_UMK_ABWEICH_KVA = A(1 ,"KZ-PR-UMK-ABWEICH-KVA");
@L2 Group OUTPUTS = group("OUTPUTS");
@L3 A VKTO_NAME = A(31 ,"VKTO-NAME");
@L3 N FNUM = N(4,0 ,"FNUM");
@L3 A FEHLER = A(78 ,"FEHLER");
}

View File

@@ -0,0 +1,652 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package ABFRAGE;
import innowake.mee.natural.api.*;
@Natural(moduleName="COMMON")
public class COMMON_DA extends NaturalData {
@Global
/*
* GDA für LIBRARY KORIN INCLUSIVE
* REDEFINITIONEN
*/
@L1 A gPROGRAMM = A(8 ,"+PROGRAMM");
@L1 A gBPOL_PARM = A(17 ,"+BPOL-PARM");
@L1 N gBBST_POLNR = N(9,0 ,"+BBST-POLNR");
@L1 A gBBST_ART = A(9 ,"+BBST-ART");
@L1 A gABTEILUNG = A(5 ,"+ABTEILUNG");
@L1 A gPASSWORD = A(8 ,"+PASSWORD");
@L1 A gGETMAIL = A(1 ,"+GETMAIL");
@L1 A gERROR_PROGRAM = A(8 ,"+ERROR-PROGRAM");
@L1 A gERROR_STAT = A(1 ,"+ERROR-STAT");
@L1 N gERROR_LINE = N(4,0 ,"+ERROR-LINE");
@L1 N gERROR_NR = N(4,0 ,"+ERROR-NR");
@L1 A gERGEBNIS = A(79 ,"+ERGEBNIS");
@L1 A gSPOOL_PARM = A(80 ,"+SPOOL-PARM");
@L1 Redefine gSPOOL_PARM;
@L2 A xSPUSER = A(8 ,"#SPUSER");
@L2 A xSPDEST = A(8 ,"#SPDEST");
@L2 A xSPFORM = A(1 ,"#SPFORM");
@L1 A gINIT_VAR = A(240 ,"+INIT-VAR");
@L1 Redefine gINIT_VAR; // REDEF. BEGIN : +INIT-VAR
@L2 A xRA_INITVAR_GETMAIL = A(1 ,"#RA-INITVAR-GETMAIL");// GETMAIL duchgeführt Y/N
@L2 N xINIT_DATIME = N(15,0 ,"#INIT-DATIME");// STARTUP Datum/Zeitder
@L2 Redefine xINIT_DATIME; // letzten Startup-Durchführung
@L3 N xINIT_DATE = N(8,0 ,"#INIT-DATE");// "YYYYMMDDHHMMSSZ"
@L3 Redefine xINIT_DATE;
@L4 A xINIT_DATE_ALPHA = A(8 ,"#INIT-DATE-ALPHA");
@L3 N xINIT_TIME = N(7,0 ,"#INIT-TIME");
@L3 Redefine xINIT_TIME;
@L4 A xINIT_TIME_ALPHA = A(7 ,"#INIT-TIME-ALPHA");
@L2 A xINIT_USER = A(8 ,"#INIT-USER"); // STARTUP U-ID d. Current LOGON
@L2 A xINIT_ID = A(8 ,"#INIT-ID"); // STARTUP Terminal-ID
@L2 A xINIT_GROUP = A(8 ,"#INIT-GROUP"); // STARTUP GROUP-ID, über die
/* /* gelinkt */
@L2 A xSTARTUP_PROGRAM = A(8 ,"#STARTUP-PROGRAM");// STARTUP Startup-Programm
@L2 A xRESTART_PROGRAM = A(8 ,"#RESTART-PROGRAM");// STARTUP Restart-Programm
@L2 A xERROR_PROGRAM = A(8 ,"#ERROR-PROGRAM");// #ERR letztes fehlerhaftes PRG
/* /* STARTUP Error-Programm */
@L2 A xRA_INITVAR_CICS_NAME = A(8 ,"#RA-INITVAR-CICS-NAME");// STARTUP und APPLID
@L2 Redefine xRA_INITVAR_CICS_NAME; // "CSWVCICS"/"CSWVCICT"
@L3 A xCICSNAME = A(8 ,"#CICSNAME"); // Redef. aus Namensgründen
@L2 Redefine xRA_INITVAR_CICS_NAME;
@L3 A xCICS_NAME = A(8 ,"#CICS-NAME"); // Redef. aus Namensgründen
@L2 A FILLER = A(8 ,"FILLER");
@L2 A xUSER_NAME = A(32 ,"#USER-NAME"); // STARTUP User-Name
@L2 A xAPPL_NAME = A(32 ,"#APPL-NAME"); // STARTUP Anwendungs-Name
@L2 A xSYS_SCHALTER = A(8 ,"#SYS-SCHALTER");// DIV SYSTEM-Schalter
@L2 Redefine xSYS_SCHALTER;
@L3 A xSYS_RESTART = A(1 ,"#SYS-RESTART");// #ERR bei Absturz Restart Y/N
@L3 A xSYS_CANCEL = A(1 ,"#SYS-CANCEL");// #CAN BATCH CANCELNin #ERROR
@L3 A xERROR_STAT = A(1 ,"#ERROR-STAT");// #ERR letzter Fehler-Status
@L3 A xERROR_OPTN = A(1 ,"#ERROR-OPTN");// #ERR RETRY ON ERRORS Y/N
@L3 A xERROR_SAME = A(1 ,"#ERROR-SAME");// #ERR same ERROR wie zuvor Y/N
/* 0=NEIN 1=PROG 2=NUMMER 3=BEIDE */
@L3 A xERROR_LOG = A(1 ,"#ERROR-LOG"); // #ERR N=kein ERROR-Logging
@L3 A xMONITOR = A(1 ,"#MONITOR"); // STARTUP B=BATCH, C=CICS
/* /* P=COMPLETE */
@L3 A FILLER2 = A(1 ,"FILLER2");
@L2 A xERROR_PROG = A(8 ,"#ERROR-PROG");
@L2 N xERROR_NR = N(4,0 ,"#ERROR-NR"); // #ERR letzte Fehlernummer
@L2 Redefine xERROR_NR;
@L3 A xERRAX = A(4 ,"#ERRAX");
@L2 N xERROR_LINE = N(4,0 ,"#ERROR-LINE"); // #ERR letzte fehlerhafte Zeile
@L2 A xMESSAGE = A(72 ,"#MESSAGE"); // #ERR Fehlermeldung
@L2 Redefine xMESSAGE;
@L3 A xMANF = A(1 ,"#MANF");
@L3 A STAR = A(1 ,"STAR");
@L3 A RESTMSG = A(70 ,"RESTMSG");
@L1 A gBERECHTIGUNGEN = A("+BERECHTIGUNGEN");
@L1 A gVSU = A(250 ,"+VSU");
@L1 A gKIS = A(250 ,"+KIS");
@L1 Redefine gKIS; // REDEF. BEGIN : +KIS
@L2 N xRN_KIS_ZAHL = N(3,0 ,"#RN-KIS-ZAHL");
@L2 B xRA_KIS_KUISNT = B(3 ,"#RA-KIS-KUISNT").defineArray(range(1, 20));
@L2 Redefine xRA_KIS_KUISNT; // REDEF. BEGIN : #RA-KIS-KUISNT
@L3 B xRA_KIS_KUISN = B(3 ,"#RA-KIS-KUISN");
@L2 N xRN_KIS_OVZL = N(3,0 ,"#RN-KIS-OVZL");
@L2 B xRA_KIS_OVISN = B(3 ,"#RA-KIS-OVISN").defineArray(range(1, 60));
@L1 A gUMS = A(250 ,"+UMS");
@L1 Redefine gUMS; // REDEF. BEGIN : +UMS
@L2 A xRA_UMS_CODE = A(8 ,"#RA-UMS-CODE");
@L2 P xRN_UMS_LAST_VR = P(3,0 ,"#RN-UMS-LAST-VR");
@L2 A xRA_UMS_MD_KZ1_BKLZ_KZ = A(1 ,"#RA-UMS-MD-KZ1-BKLZ-KZ");
@L2 A xRA_UMS_MD_KZ1_TARIF = A(1 ,"#RA-UMS-MD-KZ1-TARIF");
@L2 A xRA_UMS_WAEHRUNG_MD = A(3 ,"#RA-UMS-WAEHRUNG-MD");
@L2 N xRN_UMS_LAST_LOAD = N(6,0 ,"#RN-UMS-LAST-LOAD");
@L2 A xUMS_VORG_OPID = A(3 ,"#UMS-VORG-OPID");
@L2 A xUMS_FORCE = A(1 ,"#UMS-FORCE"); // Beharrung auf eigene
/* * /* PF-Tasten-Belegung */
@L2 N xUMS_SNR_FIL = N(2,0 ,"#UMS-SNR-FIL");// Schaden-Filial-Nummer
@L2 A xUMS_SNR_KREIS = A(2 ,"#UMS-SNR-KREIS");// Schaden-Kreis zB.:'SA'
@L2 N xUMS_SNR_JAHR = N(4,0 ,"#UMS-SNR-JAHR");// Schaden-Jahr
@L2 N xUMS_SNR_LFD_NR = N(6,0 ,"#UMS-SNR-LFD-NR");// Schaden-Laufende-Nummer
@L2 A xUMS_SNR_STATUS = A(1 ,"#UMS-SNR-STATUS");// Status-Byte 'O','S','H'
@L2 A xUMS_ART = A(2 ,"#UMS-ART"); // Schadenart
@L2 A xUMS_REFERENT = A(8 ,"#UMS-REFERENT");// Referent
@L2 A xUMS_EVI_TYP = A(1 ,"#UMS-EVI-TYP"); // eigen oder fremd-Vi
@L2 P xUMS_IDENTCOUNTER = P(5,0 ,"#UMS-IDENTCOUNTER");
@L2 P xUMS_OUTPUTNR = P(5,0 ,"#UMS-OUTPUTNR");
@L2 P xUMS_EREIGNIS_DT = P(8,0 ,"#UMS-EREIGNIS-DT");// nur für LV (= Stornodatum)
@L2 P xUMS_SCN_HIST = P(3,0 ,"#UMS-SCN-HIST");// merken Hist-Lesestand
@L2 B xUMS_ANZEIGEDAT_KPL = B(3 ,"#UMS-ANZEIGEDAT-KPL");// für Aufruf SK10
@L2 N xUMS_SKT_FIL = N(2,0 ,"#UMS-SKT-FIL");// Filial-Nummer Kontostamm
@L2 A xUMS_SKT_KREIS = A(2 ,"#UMS-SKT-KREIS");// Schaden-Kreis Kontostamm
@L2 N xUMS_SKT_JAHR = N(4,0 ,"#UMS-SKT-JAHR");// Schaden-Jahr Kontostamm
@L2 N xUMS_SKT_LFD_NR = N(6,0 ,"#UMS-SKT-LFD-NR");// Schaden-Lfd-Nr Kontostamm
@L2 N xUMS_SKT_BRAN = N(2,0 ,"#UMS-SKT-BRAN");// Schaden-Branche Kontostamm
@L2 A xUMS_WORKSTATION_ID = A(8 ,"#UMS-WORKSTATION-ID");// Workstation (OD) *** frei ***
@L2 A xRA_UMS_BRGR = A(1 ,"#RA-UMS-BRGR").defineArray(range(1,3));
@L2 A xUMS_WF_ABT = A(8 ,"#UMS-WF-ABT"); // Workflow-Abteilung(für OD)
@L2 A xUMS_WF_STAT = A(8 ,"#UMS-WF-STAT"); // Workflow-Station(für OD)
@L2 A xUMS_WF_OPID = A(8 ,"#UMS-WF-OPID"); // Workflow-Opid(für OD)
@L2 P xUMS_WF_SORT_DOKU_ART = P(3,0 ,"#UMS-WF-SORT-DOKU-ART").defineArray(range(1,5));// WF-Dokument(für OD)
@L2 P xUMS_KEYCOUNTER = P(5,0 ,"#UMS-KEYCOUNTER").defineArray(range(1,10));// Identcounter aus File111
@L2 A xUMS_SKT_BELEGTYP = A(1 ,"#UMS-SKT-BELEGTYP");// Schaden-Konto-Belegtyp
@L2 P xUMS_SKT_POS_IDC = P(5,0 ,"#UMS-SKT-POS-IDC");// Identcounter Pos.Buchung-SKT
@L2 P xUMS_SKT_BRAN_IDC = P(5,0 ,"#UMS-SKT-BRAN-IDC");// Identcounter Bran.Buchung-SKT
@L2 P xUMS_SKT_ABST_IDC = P(5,0 ,"#UMS-SKT-ABST-IDC");// Identcounter Abst.Buchung-SKT
@L2 A xUMS_TEXT_FN95 = A(1 ,"#UMS-TEXT-FN95");// Text-Option im FN95
@L2 A xUMS_REGRESS_RATE = A(2 ,"#UMS-REGRESS-RATE");// Schaden-Regreß oder -Rate
@L2 P xUMS_REGRESS_NR = P(6,0 ,"#UMS-REGRESS-NR");// Schaden-Regreß-Nr.
@L2 A xUMS_DAT2_FN95 = A(1 ,"#UMS-DAT2-FN95");// 2. Datumszeile FN95 optional
@L2 N xUMS_PC_DAT = N(8,0 ,"#UMS-PC-DAT"); // Pc-Antragsdatum
@L2 N xUMS_PC_NUM = N(4,0 ,"#UMS-PC-NUM"); // Pc-Antragsnummer
@L2 A xUMS_DRUCKER_ID = A(10 ,"#UMS-DRUCKER-ID");// Drucker-Id (OD)
@L2 A xUMS_KEY_STAT_DI32 = A(8 ,"#UMS-KEY-STAT-DI32");// Station für AufrufDI32 (OD)
@L2 B xUMS_SKT_ANZEIGEDAT_KPL = B(3 ,"#UMS-SKT-ANZEIGEDAT-KPL");// Anzeigedatum-KPL für Sammelb.
@L2 P xUMS_WINDOW_NUM = P(2,0 ,"#UMS-WINDOW-NUM");// Image-Display (OD)
@L2 P xUMS_STORNO_GRUND = P(2,0 ,"#UMS-STORNO-GRUND");// stornogrund (nur LV)
@L2 A xUMS_DOI_KEY_ART = A(2 ,"#UMS-DOI-KEY-ART");// Kz f. Einschränkung Di30 (OD)
@L2 P xUMS_SKT_WERTEDATUM = P(8,0 ,"#UMS-SKT-WERTEDATUM");// Schaden-Konto-Wertedatum
@L2 P xUMS_SKT_BANKSCHLUESSEL = P(2,0 ,"#UMS-SKT-BANKSCHLUESSEL");// Schaden-Konto-Bankschlüssel
@L2 A xUMS_REST = A(28 ,"#UMS-REST"); // Bytes frei
@L1 A gPEL = A(250 ,"+PEL");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 A xRA_PEL_ABT = A(1 ,"#RA-PEL-ABT");
@L2 N xRN_PEL_VPOL = N(8,0 ,"#RN-PEL-VPOL");
@L2 N xRN_PEL_KPNR = N(8,0 ,"#RN-PEL-KPNR");
@L2 N xRN_PEL_GT = N(8,0 ,"#RN-PEL-GT");
@L2 N xRN_PEL_BR1 = N(8,0 ,"#RN-PEL-BR1");
@L2 N xRN_PEL_BR2 = N(8,0 ,"#RN-PEL-BR2");
@L2 N xRN_PEL_BUCH = N(8,0 ,"#RN-PEL-BUCH");
@L2 N xRN_PEL_EINL = N(6,0 ,"#RN-PEL-EINL");
@L2 A xRA_PEL_FREI = A(2 ,"#RA-PEL-FREI");
@L2 N xRN_PEL_TEXT = N(8,0 ,"#RN-PEL-TEXT");
@L2 N xRN_PEL_STAT = N(8,0 ,"#RN-PEL-STAT");
@L2 N xRN_PEL_AGT = N(8,0 ,"#RN-PEL-AGT");
@L2 N xRN_PEL_ABR1 = N(8,0 ,"#RN-PEL-ABR1");
@L2 N xRN_PEL_ABR2 = N(8,0 ,"#RN-PEL-ABR2");
@L2 N xRN_PEL_ATXT = N(8,0 ,"#RN-PEL-ATXT");
@L2 N xRN_PEL_INDX = N(2,0 ,"#RN-PEL-INDX");
@L2 N xRN_PEL_VDAT = N(6,0 ,"#RN-PEL-VDAT");
@L2 A xRA_PEL_EFKZ = A(1 ,"#RA-PEL-EFKZ");
@L2 A xRA_PEL_DRUC = A(4 ,"#RA-PEL-DRUC");
@L2 A xRA_PEL_VART = A(3 ,"#RA-PEL-VART");
@L2 N xRN_PEL_KREI = N(3,0 ,"#RN-PEL-KREI");
@L2 A xRA_PEL_VAE = A(3 ,"#RA-PEL-VAE");
@L2 A xRA_PEL_BNEU = A(3 ,"#RA-PEL-BNEU");
@L2 A xRA_PEL_FILL118 = A(118 ,"#RA-PEL-FILL118");
@L2 A xRA_PEL_MENU = A(2 ,"#RA-PEL-MENU");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 N xRN_KFZ_MENR = N(1,0 ,"#RN-KFZ-MENR");
@L2 N xRN_KFZ_IEVI = N(3,0 ,"#RN-KFZ-IEVI");
@L2 N xRN_KFZ_IVV = N(3,0 ,"#RN-KFZ-IVV");
@L2 N xRN_KFZ_HIST = N(3,0 ,"#RN-KFZ-HIST");
@L2 N xRN_KFZ_STND = N(3,0 ,"#RN-KFZ-STND");
@L2 A xRA_KFZ_EVBR = A(30 ,"#RA-KFZ-EVBR");
@L2 Redefine xRA_KFZ_EVBR;
@L3 A xRA_KFZ_BRA = A(3 ,"#RA-KFZ-BRA").defineArray(range(1, 10));
@L2 Redefine xRA_KFZ_EVBR;
@L3 N xRN_KFZ_BRA1 = N(3,0 ,"#RN-KFZ-BRA1");
@L3 N xRN_KFZ_BRA2 = N(3,0 ,"#RN-KFZ-BRA2");
@L3 N xRN_KFZ_BRA3 = N(3,0 ,"#RN-KFZ-BRA3");
@L3 N xRN_KFZ_BRA4 = N(3,0 ,"#RN-KFZ-BRA4");
@L3 N xRN_KFZ_BRA5 = N(3,0 ,"#RN-KFZ-BRA5");
@L2 N xRN_KFZ_VDAT = N(8,0 ,"#RN-KFZ-VDAT");
@L2 A xRA_KFZ_AREV = A(1 ,"#RA-KFZ-AREV");
@L2 A xRA_KFZ_SABE = A(3 ,"#RA-KFZ-SABE");
@L2 A xRA_KFZ_RAG = A(1 ,"#RA-KFZ-RAG");
@L2 P xRN_KFZ_OZW = P(3,0 ,"#RN-KFZ-OZW");
@L2 N xRN_KFZ_VVAE = N(8,0 ,"#RN-KFZ-VVAE");
@L2 A xRA_KFZ_VEF = A(1 ,"#RA-KFZ-VEF");
@L2 A xRA_KFZ_VVAR = A(3 ,"#RA-KFZ-VVAR");
@L2 N xRN_KFZ_VEGD = N(8,0 ,"#RN-KFZ-VEGD");
@L2 N xRN_KFZ_VELD = N(8,0 ,"#RN-KFZ-VELD");
@L2 A xRA_KFZ_VDRU = A(4 ,"#RA-KFZ-VDRU");
@L2 A xRA_KFZ_PZSG = A(3 ,"#RA-KFZ-PZSG");
@L2 N xRN_KFZ_LESE = N(3,0 ,"#RN-KFZ-LESE");
@L2 N xRN_KFZ_FZW = N(2,0 ,"#RN-KFZ-FZW");
@L2 N xRN_KFZ_VI10 = N(2,0 ,"#RN-KFZ-VI10");
@L2 N xRN_KFZ_VD = N(8,0 ,"#RN-KFZ-VD");
@L2 N xRN_KFZ_VB = N(8,0 ,"#RN-KFZ-VB");
@L2 A xRA_KFZ_KFKZ = A(11 ,"#RA-KFZ-KFKZ");
@L2 N xRN_KFZ_AWDT = N(8,0 ,"#RN-KFZ-AWDT");
@L2 A xRA_KFZ_AWKZ = A(1 ,"#RA-KFZ-AWKZ");
@L2 N xRN_KFZ_AEND = N(8,0 ,"#RN-KFZ-AEND");
@L2 A xRA_KFZ_VABT = A(3 ,"#RA-KFZ-VABT");
@L2 A xRA_KFZ_OPID = A(3 ,"#RA-KFZ-OPID");
@L2 N xRN_KFZ_DATV = N(8,0 ,"#RN-KFZ-DATV");
@L2 N xRN_KFZ_DATB = N(8,0 ,"#RN-KFZ-DATB");
@L2 A xRA_KFZ_FIL = A(2 ,"#RA-KFZ-FIL");
@L2 N xRN_KFZ_KRBR = N(4,0 ,"#RN-KFZ-KRBR");
@L2 Redefine xRN_KFZ_KRBR; // REDEF. BEGIN : #RN-KFZ-KRBR
@L3 N xRN_KFZ_KR = N(2,0 ,"#RN-KFZ-KR");
@L3 N xRN_KFZ_BR = N(2,0 ,"#RN-KFZ-BR");
@L2 A xRA_KFZ_VAE1 = A(3 ,"#RA-KFZ-VAE1");
@L2 A xRA_KFZ_VAE2 = A(3 ,"#RA-KFZ-VAE2");
@L2 A xRA_KFZ_SUMM = A(1 ,"#RA-KFZ-SUMM");
@L2 N xRN_KFZ_STAT = N(2,0 ,"#RN-KFZ-STAT");
@L2 A xRA_KFZ_GSNA = A(10 ,"#RA-KFZ-GSNA");
@L2 A xRA_KFZ_BVNR = A(7 ,"#RA-KFZ-BVNR");
@L2 N xRN_KFZ_ANTR = N(8,0 ,"#RN-KFZ-ANTR");
@L2 N xRN_KFZ_VINK = N(8,0 ,"#RN-KFZ-VINK");
@L2 N xRN_KFZ_FPNR = N(3,0 ,"#RN-KFZ-FPNR");
@L2 A xRA_KFZ_MAPA = A(1 ,"#RA-KFZ-MAPA");
@L2 L xRL_KFZ_PRVV = L("#RL-KFZ-PRVV");
@L2 L xRL_KFZ_ST01 = L("#RL-KFZ-ST01");
@L2 L xRL_KFZ_AUTO = L("#RL-KFZ-AUTO");
@L2 A xRA_KFZ_OPI2 = A(3 ,"#RA-KFZ-OPI2");
@L2 L xRL_KFZ_BLND = L("#RL-KFZ-BLND");
@L2 C xRC_KFZ_BLND = C("#RC-KFZ-BLND");
@L2 A xRA_KFZ_ATXT = A(10 ,"#RA-KFZ-ATXT");
@L2 A xRA_KFZ_KTXT = A(3 ,"#RA-KFZ-KTXT");
@L2 A xRA_KFZ_AMNU = A(8 ,"#RA-KFZ-AMNU");
@L2 A xRA_KFZ_MENU = A(2 ,"#RA-KFZ-MENU");
@L1 A gISN = A(250 ,"+ISN");
@L1 Redefine gISN; // REDEF. BEGIN : +ISN
@L2 A xRA_ISN_ISN = A(5 ,"#RA-ISN-ISN");
@L2 Redefine xRA_ISN_ISN; // REDEF. BEGIN : #RA-ISN-ISN
@L3 P xRN_ISN_AKUN = P(9,0 ,"#RN-ISN-AKUN");
@L2 P xRN_ISN_HKUN = P(9,0 ,"#RN-ISN-HKUN");
@L2 P xRN_ISN_VKUN = P(9,0 ,"#RN-ISN-VKUN");
@L2 P xRN_ISN_ORTE = P(9,0 ,"#RN-ISN-ORTE");
@L2 P xRN_ISN_KORP = P(9,0 ,"#RN-ISN-KORP");
@L2 P xRN_ISN_KTAB = P(9,0 ,"#RN-ISN-KTAB");
@L2 P xRN_ISN_ABKU = P(9,0 ,"#RN-ISN-ABKU");
@L2 P xRN_ISN_OPOL = P(9,0 ,"#RN-ISN-OPOL");
@L2 P xRN_ISN_SPOL = P(9,0 ,"#RN-ISN-SPOL");
@L2 P xRN_ISN_OEVI = P(9,0 ,"#RN-ISN-OEVI");
@L2 P xRN_ISN_SEVI = P(9,0 ,"#RN-ISN-SEVI");
@L2 P xRN_ISN_OVV = P(9,0 ,"#RN-ISN-OVV");
@L2 P xRN_ISN_SVV = P(9,0 ,"#RN-ISN-SVV");
@L2 P xRN_ISN_OBU = P(9,0 ,"#RN-ISN-OBU");
@L2 P xRN_ISN_SBU = P(9,0 ,"#RN-ISN-SBU");
@L2 P xRN_ISN_BTS = P(9,0 ,"#RN-ISN-BTS");
@L2 P xRN_ISN_BTK = P(9,0 ,"#RN-ISN-BTK");
@L2 P xRN_ISN_EKPO = P(9,0 ,"#RN-ISN-EKPO");
@L2 P xRN_ISN_EKVV = P(9,0 ,"#RN-ISN-EKVV");
@L2 P xRN_ISN_EKK1 = P(9,0 ,"#RN-ISN-EKK1");
@L2 P xRN_ISN_OPOS = P(9,0 ,"#RN-ISN-OPOS");
@L2 P xRN_ISN_SPOS = P(9,0 ,"#RN-ISN-SPOS");
@L2 P xRN_ISN_EKG1 = P(9,0 ,"#RN-ISN-EKG1");
@L2 P xRN_ISN_HGPO = P(9,0 ,"#RN-ISN-HGPO");
@L2 P xRN_ISN_HGKO = P(9,0 ,"#RN-ISN-HGKO");
@L2 P xRN_ISN_OLS = P(9,0 ,"#RN-ISN-OLS");
@L2 P xRN_ISN_SLS = P(9,0 ,"#RN-ISN-SLS");
@L2 P xRN_ISN_HLS = P(9,0 ,"#RN-ISN-HLS");
@L2 P xRN_ISN_VDNR = P(9,0 ,"#RN-ISN-VDNR");
@L2 P xRN_ISN_MDST = P(9,0 ,"#RN-ISN-MDST");
@L2 P xRN_ISN_KTOS = P(9,0 ,"#RN-ISN-KTOS");
@L2 P xRN_ISN_KTOO = P(9,0 ,"#RN-ISN-KTOO");
@L2 P xRN_ISN_ORTS = P(9,0 ,"#RN-ISN-ORTS");
@L2 P xRN_ISN_FPNR = P(9,0 ,"#RN-ISN-FPNR");
@L2 P xRN_ISN_WA49 = P(9,0 ,"#RN-ISN-WA49");
@L2 P xRN_ISN_SD34 = P(8,0 ,"#RN-ISN-SD34");
@L2 P xRN_ISN_SPER = P(9,0 ,"#RN-ISN-SPER");
@L2 P xRN_ISN_MDS = P(9,0 ,"#RN-ISN-MDS");
@L2 P xRN_ISN_SUCH = P(9,0 ,"#RN-ISN-SUCH");
@L2 P xRN_ISN_ABST = P(9,0 ,"#RN-ISN-ABST");
@L2 P xRN_ISN_UMKON = P(9,0 ,"#RN-ISN-UMKON");
@L2 P xRN_ISN_MUBU = P(9,0 ,"#RN-ISN-MUBU");// MUTTERBUCHUNG (SK)
@L2 P xRN_ISN_RESERVE = P(9,0 ,"#RN-ISN-RESERVE").defineArray(range(1, 5));
@L1 A gKUN = A(250 ,"+KUN");
@L1 Redefine gKUN; // REDEF. BEGIN : +KUN
@L2 A xRA_KUN_KUNA = A(27 ,"#RA-KUN-KUNA");
@L2 A xRA_KUN_GHKZ = A(1 ,"#RA-KUN-GHKZ");
@L2 N xRN_KUN_GHK1 = N(8,0 ,"#RN-KUN-GHK1");
@L2 N xRN_KUN_GHK2 = N(8,0 ,"#RN-KUN-GHK2");
@L2 N xRN_KUN_DATV = N(8,0 ,"#RN-KUN-DATV");
@L2 N xRN_KUN_DATB = N(8,0 ,"#RN-KUN-DATB");
@L2 A xRA_KUN_BRAX = A(30 ,"#RA-KUN-BRAX");
@L2 N xRN_KUN_FILV = N(2,0 ,"#RN-KUN-FILV");
@L2 N xRN_KUN_FILB = N(2,0 ,"#RN-KUN-FILB");
@L2 N xRN_KUN_ANZEI = N(2,0 ,"#RN-KUN-ANZEI");
@L2 N xRN_KUN_BILKR = N(2,0 ,"#RN-KUN-BILKR");
@L2 N xRN_KUN_POLK = N(2,0 ,"#RN-KUN-POLK");
@L2 N xRN_KUN_POLB = N(2,0 ,"#RN-KUN-POLB");
@L2 A xRA_KUN_BANKKU = A(5 ,"#RA-KUN-BANKKU");
@L2 A xRA_KUN_BANKOR = A(27 ,"#RA-KUN-BANKOR");
@L2 N xRN_KUN_BANKLE = N(5,0 ,"#RN-KUN-BANKLE");
@L2 N xRN_KUN_TDAT = N(8,0 ,"#RN-KUN-TDAT");
@L2 N xRN_KUN_VR = N(3,0 ,"#RN-KUN-VR");
@L2 N xRN_KUN_VGVR = N(3,0 ,"#RN-KUN-VGVR");
@L2 N xRN_KUN_STAT = N(3,0 ,"#RN-KUN-STAT");
@L2 N xRN_KUN_HGV = N(2,0 ,"#RN-KUN-HGV");
@L2 A xRA_KUN_UG1V = A(2 ,"#RA-KUN-UG1V");
@L2 A xRA_KUN_UG2V = A(2 ,"#RA-KUN-UG2V");
@L2 A xRA_KUN_UG3V = A(2 ,"#RA-KUN-UG3V");
@L2 A xRA_KUN_SPV = A(1 ,"#RA-KUN-SPV");
@L2 N xRN_KUN_HGB = N(2,0 ,"#RN-KUN-HGB");
@L2 A xRA_KUN_UG1B = A(2 ,"#RA-KUN-UG1B");
@L2 A xRA_KUN_UG2B = A(2 ,"#RA-KUN-UG2B");
@L2 A xRA_KUN_UG3B = A(2 ,"#RA-KUN-UG3B");
@L2 A xRA_KUN_SPB = A(1 ,"#RA-KUN-SPB");
@L2 A xRA_KUN_LEER1 = A(38 ,"#RA-KUN-LEER1");
@L2 N xRN_KUN_OVGEM = N(5,0 ,"#RN-KUN-OVGEM");
@L2 N xRN_KUN_OVOKZ = N(6,0 ,"#RN-KUN-OVOKZ");
@L2 N xRN_KUN_OVOBK = N(4,0 ,"#RN-KUN-OVOBK");
@L2 A xRA_KUN_OVNAM = A(15 ,"#RA-KUN-OVNAM");
@L2 A xRA_KUN_LEER2 = A(4 ,"#RA-KUN-LEER2");
@L2 A xRA_KUN_OVFIL = A(2 ,"#RA-KUN-OVFIL");
@L2 A xRA_KUN_OVSA = A(1 ,"#RA-KUN-OVSA");
@L1 A gKEY = A(250 ,"+KEY");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 N xRN_KEY_VKKZ = N(8,0 ,"#RN-KEY-VKKZ");
@L2 A xRA_KEY_VVA = A(2 ,"#RA-KEY-VVA");
@L2 N xRN_KEY_VVNR = N(8,0 ,"#RN-KEY-VVNR");
@L2 N xRN_KEY_VHNR = N(3,0 ,"#RN-KEY-VHNR");
@L2 N xRN_KEY_VGES = N(3,0 ,"#RN-KEY-VGES");
@L2 N xRN_KEY_VRES = N(1,0 ,"#RN-KEY-VRES");
@L2 N xRN_KEY_VVIN = N(2,0 ,"#RN-KEY-VVIN");
@L2 N xRN_KEY_VBRA = N(2,0 ,"#RN-KEY-VBRA");
@L2 N xRN_KEY_VBRN = N(1,0 ,"#RN-KEY-VBRN");
@L2 A xRA_KEY_VBTART = A(1 ,"#RA-KEY-VBTART");
@L2 A xRA_KEY_VBSTAT = A(1 ,"#RA-KEY-VBSTAT");
@L2 N xRN_KEY_VUGES = N(3,0 ,"#RN-KEY-VUGES");
@L2 N xRN_KEY_VBJAHR = N(2,0 ,"#RN-KEY-VBJAHR");
@L2 N xRN_KEY_AKTONR = N(7,0 ,"#RN-KEY-AKTONR");
@L2 Redefine xRN_KEY_AKTONR; // REDEF. BEGIN : #RN-KEY-AKTONR
@L3 A xRA_KEY_VKTO = A(7 ,"#RA-KEY-VKTO");
@L2 A xRA_KEY_KOSEL = A(1 ,"#RA-KEY-KOSEL");
@L2 A xRA_KEY_LEER1 = A(4 ,"#RA-KEY-LEER1");
@L2 N xRN_KEY_AKKZ = N(8,0 ,"#RN-KEY-AKKZ");
@L2 A xRA_KEY_AVA = A(2 ,"#RA-KEY-AVA");
@L2 N xRN_KEY_AVNR = N(8,0 ,"#RN-KEY-AVNR");
@L2 N xRN_KEY_AHNR = N(3,0 ,"#RN-KEY-AHNR");
@L2 N xRN_KEY_AGES = N(3,0 ,"#RN-KEY-AGES");
@L2 N xRN_KEY_ARES = N(1,0 ,"#RN-KEY-ARES");
@L2 N xRN_KEY_AVIN = N(2,0 ,"#RN-KEY-AVIN");
@L2 N xRN_KEY_ABRA = N(2,0 ,"#RN-KEY-ABRA");
@L2 N xRN_KEY_ABRN = N(1,0 ,"#RN-KEY-ABRN");
@L2 A xRA_KEY_ABTART = A(1 ,"#RA-KEY-ABTART");
@L2 A xRA_KEY_ABSTAT = A(1 ,"#RA-KEY-ABSTAT");
@L2 N xRN_KEY_AUGES = N(3,0 ,"#RN-KEY-AUGES");
@L2 N xRN_KEY_ABJAHR = N(2,0 ,"#RN-KEY-ABJAHR");
@L2 N xRN_KEY_KTONR = N(7,0 ,"#RN-KEY-KTONR");
@L2 N xRN_KEY_PROV = N(2,0 ,"#RN-KEY-PROV");
@L2 N xRN_KEY_PROD = N(2,0 ,"#RN-KEY-PROD");
@L2 A xRA_KEY_PRKZ = A(1 ,"#RA-KEY-PRKZ");
@L2 N xRN_KEY_AFGS = N(3,0 ,"#RN-KEY-AFGS");
@L2 A xRA_KEY_AFPO = A(14 ,"#RA-KEY-AFPO");
@L2 N xRN_KEY_VFGS = N(3,0 ,"#RN-KEY-VFGS");
@L2 A xRA_KEY_VFPO = A(14 ,"#RA-KEY-VFPO");
@L2 N xRN_KEY_FBRA = N(2,0 ,"#RN-KEY-FBRA");
@L2 N xRN_KEY_NOTD = N(6,0 ,"#RN-KEY-NOTD");
@L2 Redefine xRN_KEY_NOTD;
@L3 N xRN_KEY_NOTD_YYYY = N(4,0 ,"#RN-KEY-NOTD-YYYY");
@L3 N xRN_KEY_NOTD_MM = N(2,0 ,"#RN-KEY-NOTD-MM");
@L2 N xRN_KEY_LSNR = N(2,0 ,"#RN-KEY-LSNR");
@L2 N xRN_KEY_KBJ = N(4,0 ,"#RN-KEY-KBJ");
@L2 N xRN_KEY_KBJNR = N(1,0 ,"#RN-KEY-KBJNR");
@L2 A xRA_KEY_ZNAM = A(31 ,"#RA-KEY-ZNAM");
@L2 A xRA_KEY_VNAM = A(2 ,"#RA-KEY-VNAM");
@L2 A xRA_KEY_STRA = A(1 ,"#RA-KEY-STRA");
@L2 N xRN_KEY_HAUS = N(4,0 ,"#RN-KEY-HAUS");
@L2 N xRN_KEY_POST = N(6,0 ,"#RN-KEY-POST");
@L2 A xRA_KEY_KZEX = A(1 ,"#RA-KEY-KZEX");
@L2 A xRA_KEY_ACHT = A(1 ,"#RA-KEY-ACHT");
@L2 N xRN_KEY_ALFA = N(2,0 ,"#RN-KEY-ALFA");
@L2 N xRN_KEY_BERU = N(5,0 ,"#RN-KEY-BERU");
@L2 A xRA_KEY_LEER2 = A(4 ,"#RA-KEY-LEER2");
@L2 A xRA_KEY_BRSL = A(1 ,"#RA-KEY-BRSL");
@L2 A xRA_KEY_PSTA = A(1 ,"#RA-KEY-PSTA");
@L2 A xRA_KEY_BRAW = A(1 ,"#RA-KEY-BRAW");
@L2 N xRN_KEY_BRVO = N(2,0 ,"#RN-KEY-BRVO");
@L2 N xRN_KEY_BRBI = N(2,0 ,"#RN-KEY-BRBI");
@L2 N xRN_KEY_JRVO = N(2,0 ,"#RN-KEY-JRVO");
@L2 N xRN_KEY_JRBI = N(2,0 ,"#RN-KEY-JRBI");
@L2 A xRA_KEY_VNAW = A(1 ,"#RA-KEY-VNAW");
@L2 A xRA_KEY_KEIG = A(1 ,"#RA-KEY-KEIG");
@L2 A xRA_KEY_SBID = A(3 ,"#RA-KEY-SBID");
@L2 A xRA_KEY_KURO = A(2 ,"#RA-KEY-KURO");
@L2 A xRA_KEY_PRAE = A(1 ,"#RA-KEY-PRAE");
@L2 A xRA_KEY_PROV = A(1 ,"#RA-KEY-PROV");
@L2 A xRA_KEY_OFFE = A(1 ,"#RA-KEY-OFFE");
@L2 A xRA_KEY_AKTU = A(1 ,"#RA-KEY-AKTU");
@L2 A xRA_KEY_ALLE = A(1 ,"#RA-KEY-ALLE");
@L2 A xRA_KEY_AOFA = A(1 ,"#RA-KEY-AOFA");
@L2 N xRN_KEY_BUDT = N(8,0 ,"#RN-KEY-BUDT");
@L2 P xRN_KEY_BUNR = P(9,0 ,"#RN-KEY-BUNR");
@L2 A xRA_KEY_VKZ = A(1 ,"#RA-KEY-VKZ");
@L2 A xRA_KEY_ABGA = A(1 ,"#RA-KEY-ABGA");
@L2 A xRA_KEY_SART = A(2 ,"#RA-KEY-SART");
@L2 A xRA_KEY_PFIL = A(2 ,"#RA-KEY-PFIL");
@L2 L xRL_KEY_UHR = L("#RL-KEY-UHR");
@L2 A xRA_KEY_STAT = A(1 ,"#RA-KEY-STAT");
@L2 A xRA_KEY_GSMT = A(1 ,"#RA-KEY-GSMT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_1 = A(8 ,"#RA-KEY-08X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_VVO = A(22 ,"#RA-KEY-VVO");
@L2 A xRA_KEY_27X_1 = A(27 ,"#RA-KEY-27X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVO = A(22 ,"#RA-KEY-AVO");
@L2 A xRA_KEY_27X_2 = A(27 ,"#RA-KEY-27X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVO = A(22 ,"#RA-KEY-GVO");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_2 = A(8 ,"#RA-KEY-08X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_VBT = A(24 ,"#RA-KEY-VBT");
@L2 A xRA_KEY_24X = A(24 ,"#RA-KEY-24X"); // ZWISCHENRAUM
@L2 A xRA_KEY_ABT = A(24 ,"#RA-KEY-ABT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_3 = A(8 ,"#RA-KEY-08X-3");// ZWISCHENRAUM
@L2 A xRA_KEY_VVK = A(16 ,"#RA-KEY-VVK");
@L2 A xRA_KEY_33X_1 = A(33 ,"#RA-KEY-33X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVK = A(16 ,"#RA-KEY-AVK");
@L2 A xRA_KEY_33X_2 = A(33 ,"#RA-KEY-33X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVK = A(16 ,"#RA-KEY-GVK");
@L1 A gSTV = A(250 ,"+STV");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_APRG = A(8 ,"#RA-STV-APRG");
@L2 A xRA_STV_VPRG = A(8 ,"#RA-STV-VPRG");
@L2 A xRA_STV_FUNK = A(4 ,"#RA-STV-FUNK");
@L2 Redefine xRA_STV_FUNK; // REDEF. BEGIN : #RA-STV-FUNK
@L3 N xRN_STV_FUNR = N(2,0 ,"#RN-STV-FUNR");
@L3 A xRA_STV_FUNR = A(2 ,"#RA-STV-FUNR");
@L2 N xRN_STV_AUSW = N(2,0 ,"#RN-STV-AUSW");
@L2 A xRA_STV_CODE = A(4 ,"#RA-STV-CODE"); // 30
@L2 N xRN_STV_OBNR = N(8,0 ,"#RN-STV-OBNR");
@L2 A xRA_STV_OART = A(2 ,"#RA-STV-OART");
@L2 N xRN_STV_HINR = N(3,0 ,"#RN-STV-HINR");
@L2 N xRN_STV_VINR = N(2,0 ,"#RN-STV-VINR");
@L2 N xRN_STV_BRAN = N(2,0 ,"#RN-STV-BRAN");
@L2 N xRN_STV_BRNR = N(1,0 ,"#RN-STV-BRNR");
@L2 N xRN_STV_GENR = N(3,0 ,"#RN-STV-GENR");
@L2 A xRA_STV_GMOD = A(1 ,"#RA-STV-GMOD");
@L2 A xRA_STV_AMOD = A(1 ,"#RA-STV-AMOD");
@L2 A xRA_STV_VIKZ = A(1 ,"#RA-STV-VIKZ");
@L2 A xRA_STV_DATI = A(8 ,"#RA-STV-DATI");
@L2 N xRN_STV_DATA = N(3,0 ,"#RN-STV-DATA");
@L2 A xRA_STV_OPID = A(3 ,"#RA-STV-OPID");
@L2 A xRA_STV_PFKY = A(96 ,"#RA-STV-PFKY");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PFF = A(4 ,"#RA-STV-PFF");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PF = A(4 ,"#RA-STV-PF").defineArray(range(1, 24));
@L2 N xRN_STV_DATN = N(6,0 ,"#RN-STV-DATN");
@L2 A xRA_STV_MMOD = A(3 ,"#RA-STV-MMOD");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_241X = A(241 ,"#RA-STV-241X");
@L2 A xRA_STV_APAR = A(1 ,"#RA-STV-APAR");
@L2 P xRN_STV_CURS = P(6,0 ,"#RN-STV-CURS");
@L2 P xRN_STV_MARK = P(3,0 ,"#RN-STV-MARK");
@L2 B xRN_STV_FNUM = B(2 ,"#RN-STV-FNUM");
@L1 A gKEY1 = A(250 ,"+KEY1");
@L1 Redefine gKEY1; // REDEF. BEGIN : +KEY1
@L2 A xRA_KEY1_ASSTART = A(4 ,"#RA-KEY1-ASSTART");
@L2 N xRN_KEY1_POSVI = N(2,0 ,"#RN-KEY1-POSVI");
@L2 N xRN_KEY1_POSVII = N(3,0 ,"#RN-KEY1-POSVII");
@L2 A xRA_KEY1_RVART = A(4 ,"#RA-KEY1-RVART");
@L2 N xRN_KEY1_RVVON = N(8,0 ,"#RN-KEY1-RVVON");
@L2 N xRN_KEY1_RVBHJAHR = N(4,0 ,"#RN-KEY1-RVBHJAHR");
@L2 N xRN_KEY1_RVBBNR = N(2,0 ,"#RN-KEY1-RVBBNR");
@L2 P xRN_KEY1_SBVANZ = P(1,0 ,"#RN-KEY1-SBVANZ");
@L2 P xRN_KEY1_SBVINR = P(2,0 ,"#RN-KEY1-SBVINR").defineArray(range(1, 2));
@L2 P xRN_KEY1_SBVII = P(2,0 ,"#RN-KEY1-SBVII").defineArray(range(1, 2));
@L2 N xRN_KEY1_BRA = N(2,0 ,"#RN-KEY1-BRA").defineArray(range(1, 3));
@L2 N xRN_KEY1_GEBDT = N(8,0 ,"#RN-KEY1-GEBDT");
@L2 A xRA_KEY1_2X = A(2 ,"#RA-KEY1-2X");
@L2 A xRA_KEY1_WAEHRUNG = A(3 ,"#RA-KEY1-WAEHRUNG");
@L2 A xRA_KEY1_SK_ALLE = A(1 ,"#RA-KEY1-SK-ALLE");
@L2 N xRN_KEY1_HNR = N(4,0 ,"#RN-KEY1-HNR");
@L2 A xRA_KEY1_MTVKZ = A(1 ,"#RA-KEY1-MTVKZ");
@L2 A xRA_KEY1_VIL = A(1 ,"#RA-KEY1-VIL");
@L2 A xRA_KEY1_OKZ = A(3 ,"#RA-KEY1-OKZ");
@L2 N xRN_KEY1_OPLZ = N(6,0 ,"#RN-KEY1-OPLZ");
@L2 N xRN_KEY1_OHNR = N(4,0 ,"#RN-KEY1-OHNR");
@L2 P xRN_KEY1_SUBVR = P(3,0 ,"#RN-KEY1-SUBVR");
@L2 P xRN_KEY1_SUBKTO = P(7,0 ,"#RN-KEY1-SUBKTO");
@L2 P xRN_KEY1_SUBZWST = P(3,0 ,"#RN-KEY1-SUBZWST");
@L2 A xRA_KEY1_SKART = A(3 ,"#RA-KEY1-SKART");
@L2 A xRA_KEY1_TCSART = A(1 ,"#RA-KEY1-TCSART");
@L2 A xRA_KEY1_TCZ1 = A(2 ,"#RA-KEY1-TCZ1");
@L2 Redefine xRA_KEY1_TCZ1; // REDEF. BEGIN : #RA-KEY1-TCZ1
@L3 N xRN_KEY1_TCZ1 = N(2,0 ,"#RN-KEY1-TCZ1");
@L2 P xRN_KEY1_TCODE = P(5,0 ,"#RN-KEY1-TCODE");
@L2 P xRN_KEY1_TCZ2 = P(2,0 ,"#RN-KEY1-TCZ2");
@L2 A xRA_KEY1_NATV = A(3 ,"#RA-KEY1-NATV");
@L2 A xRA_KEY1_NATO = A(3 ,"#RA-KEY1-NATO");
@L2 N xRN_KEY1_SNR = N(8,0 ,"#RN-KEY1-SNR");
@L2 N xRN_KEY1_LNR = N(5,0 ,"#RN-KEY1-LNR");
@L2 A xRA_KEY1_ZISB = A(3 ,"#RA-KEY1-ZISB");
@L2 N xRN_KEY1_STAT = N(5,0 ,"#RN-KEY1-STAT");
@L2 N xRN_KEY1_LZ = N(2,0 ,"#RN-KEY1-LZ");
@L2 A xRA_KEY1_GRDHA = A(2 ,"#RA-KEY1-GRDHA");
@L2 A xRA_KEY1_NATOV = A(3 ,"#RA-KEY1-NATOV");
@L2 N xRN_KEY1_AWDT = N(8,0 ,"#RN-KEY1-AWDT");
@L2 A xRA_KEY1_KOSP = A(1 ,"#RA-KEY1-KOSP");
@L2 N xRN_KEY1_EDSTAT = N(2,0 ,"#RN-KEY1-EDSTAT");
@L2 A xRA_KEY1_FREI = A(7 ,"#RA-KEY1-FREI");
@L2 N xRN_KEY1_KDAT = N(8,0 ,"#RN-KEY1-KDAT");
@L2 N xRN_KEY1_KGRD = N(2,0 ,"#RN-KEY1-KGRD");
@L2 N xRN_KEY1_PERSNR = N(5,0 ,"#RN-KEY1-PERSNR");
@L2 A xRA_KEY1_RPART = A(2 ,"#RA-KEY1-RPART");
@L2 N xRN_KEY1_RPDAT = N(8,0 ,"#RN-KEY1-RPDAT");
@L2 A xRA_KEY1_RVKZ = A(1 ,"#RA-KEY1-RVKZ");
@L2 A xRA_KEY1_RVKZ2 = A(1 ,"#RA-KEY1-RVKZ2");
@L2 A xRA_KEY1_LK = A(30 ,"#RA-KEY1-LK");
@L2 A xRA_KEY1_SCHEMA = A(2 ,"#RA-KEY1-SCHEMA");
@L2 N xRN_KEY1_RSOBNR = N(8,0 ,"#RN-KEY1-RSOBNR");
@L2 A xRA_KEY1_RSKZ = A(1 ,"#RA-KEY1-RSKZ");
@L2 A xRA_KEY1_BRFA = A(3 ,"#RA-KEY1-BRFA");
@L2 A xRA_KEY1_BUCHART = A(3 ,"#RA-KEY1-BUCHART");
@L2 N xRN_KEY1_BANKSCHL = N(3,0 ,"#RN-KEY1-BANKSCHL");
@L2 N xRN_KEY1_WERTEDAT = N(8,0 ,"#RN-KEY1-WERTEDAT");
@L2 A xRA_KEY1_VORPROG = A(8 ,"#RA-KEY1-VORPROG");
@L2 N xRN_KEY1_GILT_BIS = N(8,0 ,"#RN-KEY1-GILT-BIS");
@L2 P xRN_KEY1_FNR = P(3,0 ,"#RN-KEY1-FNR");
@L2 A xRA_KEY1_KO55MELD = A(1 ,"#RA-KEY1-KO55MELD");
@L1 A gKEY2 = A(250 ,"+KEY2");
@L1 Redefine gKEY2; // REDEF. BEGIN : +KEY2
@L2 A xRA_KEY2_TARIF = A(7 ,"#RA-KEY2-TARIF");
@L2 A xRA_KEY2_BANKNA = A(27 ,"#RA-KEY2-BANKNA");
@L2 A xRA_KEY2_USERGRUPPE = A(8 ,"#RA-KEY2-USERGRUPPE");
@L2 N xRN_KEY2_KOSTST = N(5,0 ,"#RN-KEY2-KOSTST");
@L2 A xRA_KEY2_AVISO_OP = A(3 ,"#RA-KEY2-AVISO-OP");
@L2 N xRN_KEY2_AVISO_DT = N(8,0 ,"#RN-KEY2-AVISO-DT");
@L2 N xRN_KEY2_AVISO_GD = N(3,0 ,"#RN-KEY2-AVISO-GD");
@L2 N xRN_KEY2_AVISO_DTB = N(8,0 ,"#RN-KEY2-AVISO-DTB");
@L2 A xRA_KEY2_SPRACH_KZ = A(2 ,"#RA-KEY2-SPRACH-KZ");
@L2 A xRA_KEY2_GEWINN_VB = A(4 ,"#RA-KEY2-GEWINN-VB");
@L2 N xRN_KEY2_GEWINN_JAHR = N(4,0 ,"#RN-KEY2-GEWINN-JAHR");
@L2 A xRA_KEY2_ABST_SA = A(2 ,"#RA-KEY2-ABST-SA");
@L2 A xRA_KEY2_ABST_ZA = A(1 ,"#RA-KEY2-ABST-ZA");
@L2 P xRN_KEY2_ABST_ZR = P(9,0 ,"#RN-KEY2-ABST-ZR");
@L2 A xRA_KEY2_ABST_NK = A(1 ,"#RA-KEY2-ABST-NK");
@L2 A xRA_KEY2_ABST_NA = A(8 ,"#RA-KEY2-ABST-NA");
@L2 A xRA_KEY2_FAB = A(3 ,"#RA-KEY2-FAB");
@L2 A xRA_KEY2_TYP = A(10 ,"#RA-KEY2-TYP");
@L2 A xRA_KEY2_FZA = A(1 ,"#RA-KEY2-FZA");
@L2 N xRN_KEY2_LST = N(1,0 ,"#RN-KEY2-LST");
@L2 N xRN_KEY2_KTBJ = N(4,0 ,"#RN-KEY2-KTBJ");
@L2 N xRN_KEY2_KW = N(3,0 ,"#RN-KEY2-KW");
@L2 P xRN_KEY2_UEBWNR = P(6,0 ,"#RN-KEY2-UEBWNR");
@L2 P xRN_KEY2_UNTNR = P(4,0 ,"#RN-KEY2-UNTNR");
@L2 A xRA_KEY2_SERIE = A(1 ,"#RA-KEY2-SERIE");
@L2 P xRN_KEY2_PAKNR = P(9,0 ,"#RN-KEY2-PAKNR");
@L2 A xRA_KEY2_WHG_VON = A(3 ,"#RA-KEY2-WHG-VON");
@L2 A xRA_KEY2_WHG_NACH = A(3 ,"#RA-KEY2-WHG-NACH");
@L2 A xRA_KEY2_EXTSA = A(3 ,"#RA-KEY2-EXTSA");
@L2 N xRN_KEY2_GKNR = N(8,0 ,"#RN-KEY2-GKNR");
@L1 C xCV_WHBS = C("#CV-WHBS"); // GW FUER WIEDERHOLTE BS-AUSG.
@L1 C xCV_OUTP = C("#CV-OUTP"); // PROTECT BEI A+B - PROGRAMMEN
@L1 C xCV_STV = C("#CV-STV"); // KEY-FELDER STEUERLEISTE
@L1 L xWL_KEYWECHS = L("#WL-KEYWECHS"); // SCHALTER, OB KEYWECHSEL
@L1 L xWL_INTERN = L("#WL-INTERN"); // SCHALTER, OB BLINDER DURCHL.
@L1 A xWA_HELP_TASTE = A(4 ,"#WA-HELP-TASTE"); // PF-TASTE FUER 'HELP'
@L1 A xWA_MENU_TASTE = A(4 ,"#WA-MENU-TASTE"); // PF-TASTE FUER 'MENU'
@L1 P xWN_SEL_ISN = P(9,0 ,"#WN-SEL-ISN").defineArray(range(1,24));
@L1 A xWA_FUNKTION = A(5 ,"#WA-FUNKTION").defineArray(range(1,100));// FUNKTIONSTABELLE
@L1 A xWA_VORWEG_AUFRUF = A(8 ,"#WA-VORWEG-AUFRUF");// VORWEG-AUFRUF (Z.B. SCHWEBE)
@L1 L xWL_TEST = L("#WL-TEST"); // KENNZEICHEN, OB TEST
@L1 N xWN_WARTEN_SUMME = N(5,0 ,"#WN-WARTEN-SUMME");// ZAEHLER FUER SUSPEND
@L1 A xWA_AUSWAHL_KZ = A(1 ,"#WA-AUSWAHL-KZ").defineArray(range(1,24));// MARKIERUNGSLEISTE
@L1 N xWN_AUSWAHL_ANZAHL = N(2,0 ,"#WN-AUSWAHL-ANZAHL");
@L1 A xWA_AUSWAHL_GUELTIG = A(20 ,"#WA-AUSWAHL-GUELTIG");
/*
* Platz für neue globale Variable
* Definitionen in:
* YLNEUGLO/YCNEUGLO
*/
@L1 A gNEUGLO_01 = A(250 ,"+NEUGLO-01");
@L1 Redefine gNEUGLO_01;
@L2 A xBTC_STEUERUNG = A(8 ,"#BTC-STEUERUNG");
@L2 A xUMS_MIS_BILANZKREIS = A(1 ,"#UMS-MIS-BILANZKREIS").defineArray(range(1, 7));
@L2 A xUMS_MIS_BRAN = A(2 ,"#UMS-MIS-BRAN").defineArray(range(1, 10));
@L2 P xKEY1_SUCH_BETRAG = P(9,2 ,"#KEY1-SUCH-BETRAG");
@L2 P xKEY2_ARCHIV_DB = P(3,0 ,"#KEY2-ARCHIV-DB");
@L2 A xKEY2_EXTKZ = A(1 ,"#KEY2-EXTKZ");
@L2 N xRN_KEY_JAHRV = N(4,0 ,"#RN-KEY-JAHRV");
@L2 N xRN_KEY_JAHRB = N(4,0 ,"#RN-KEY-JAHRB");
@L2 A xRA_KEY_EXT_GRUND_NAME = A(31 ,"#RA-KEY-EXT-GRUND-NAME");
@L2 P xRN_KEY_EXT_BB_VON = P(8,0 ,"#RN-KEY-EXT-BB-VON");
@L2 P xRN_KEY_EXT_BB_BIS = P(8,0 ,"#RN-KEY-EXT-BB-BIS");
@L2 P xUMS_MA_SUB_SATZART_1 = P(3,0 ,"#UMS-MA-SUB-SATZART-1");
@L2 A xRA_KEY_EXT_ANTRAGSART = A(3 ,"#RA-KEY-EXT-ANTRAGSART");
@L2 P xFLV_TRANS = P(7,0 ,"#FLV-TRANS");
@L2 A xFLV_ISIN = A(12 ,"#FLV-ISIN");
@L2 P xFLV_BUNR = P(7,0 ,"#FLV-BUNR");
@L2 P xFLV_GILT_BIS = P(8,0 ,"#FLV-GILT-BIS");
@L2 P xFLV_DATUM = P(8,0 ,"#FLV-DATUM");
@L2 P xFLV_VV_VINR = P(3,0 ,"#FLV-VV-VINR");
@L2 P xFLV_VV_BRAN = P(3,0 ,"#FLV-VV-BRAN");
@L2 P xFLV_VV_BRNR = P(3,0 ,"#FLV-VV-BRNR");
@L2 P xFLV_VV_HINR = P(3,0 ,"#FLV-VV-HINR");
@L2 A xVLK_EXVERARB = A(30 ,"#VLK-EXVERARB");
@L2 P xRN_KEY_IVKNR = P(8,0 ,"#RN-KEY-IVKNR");
@L2 A xRA_SC_MELDUNGS_ART = A(1 ,"#RA-SC-MELDUNGS-ART");
@L2 A xRA_OS_GFALL = A(2 ,"#RA-OS-GFALL");
@L2 A xRA_OS_DOKART = A(4 ,"#RA-OS-DOKART");
@L2 A xRA_OS_BR_ART = A(3 ,"#RA-OS-BR-ART");
@L2 N xRN_KEY_YU_PERSNR = N(13,0 ,"#RN-KEY-YU-PERSNR");
@L2 N xSCN_VERS_BEGINN = N(8,0 ,"#SCN-VERS-BEGINN");
@L2 A xDI_ABTEILUNG = A(8 ,"#DI-ABTEILUNG");
@L2 P xDI_STAT_DZT = P(3,0 ,"#DI-STAT-DZT");
@L2 P xDI_STAT_DZT_BIS = P(3,0 ,"#DI-STAT-DZT-BIS");
@L2 A xDI_SPARTE = A(2 ,"#DI-SPARTE");
@L2 A xDI_STATION = A(8 ,"#DI-STATION");
@L2 A xDI_WM_OPID = A(8 ,"#DI-WM-OPID");
@L2 A xDI_AW_POL_FIL = A(1 ,"#DI-AW-POL-FIL");
@L2 A xDI_VAE_ART = A(3 ,"#DI-VAE-ART");
@L2 A xDI_WK_VAE_ART = A(3 ,"#DI-WK-VAE-ART");
@L2 A xDI_AW_SUB_VR = A(1 ,"#DI-AW-SUB-VR");
@L2 P xDI_SUB_VR = P(3,0 ,"#DI-SUB-VR");
@L2 P xDI_POL_KREIS = P(2,0 ,"#DI-POL-KREIS");
@L2 P xDI_POL_BRAN = P(2,0 ,"#DI-POL-BRAN");
@L2 A xDI_VAE_HINW = A(3 ,"#DI-VAE-HINW");
@L1 A gNEUGLO_02 = A(250 ,"+NEUGLO-02");
@L1 Redefine gNEUGLO_02;
@L2 A xDI_BEARB_GRP = A(3 ,"#DI-BEARB-GRP");
@L2 A xDI_AW_DRF = A(1 ,"#DI-AW-DRF");
@L2 A xDI_AW_E_ANT = A(1 ,"#DI-AW-E-ANT");
@L2 P xDI_PC_ANT_VKTO = P(7,0 ,"#DI-PC-ANT-VKTO");
@L2 P xDI_PLZ_VON = P(7,0 ,"#DI-PLZ-VON");
@L2 P xDI_PLZ_BIS = P(7,0 ,"#DI-PLZ-BIS");
@L2 A xDI_POSTNATION = A(3 ,"#DI-POSTNATION");
@L2 P xDI_WM_DATUM = P(8,0 ,"#DI-WM-DATUM");
@L2 P xDI_POL_FILIALE = P(2,0 ,"#DI-POL-FILIALE");
@L2 A xDI_AW_VAE_HINW = A(1 ,"#DI-AW-VAE-HINW");
@L2 A xDI_AW_VKTO = A(1 ,"#DI-AW-VKTO");
@L2 A xDI_AW_BEARB_GRP = A(1 ,"#DI-AW-BEARB-GRP");
@L2 A xDI_AW_KR_BR = A(1 ,"#DI-AW-KR-BR");
@L2 A xDI_AW_VAE_ART = A(1 ,"#DI-AW-VAE-ART");
@L2 P xDI_VR_KEY = P(3,0 ,"#DI-VR-KEY");
@L2 A xDI_OART = A(2 ,"#DI-OART");
@L2 P xDI_WM_DATUM_VON = P(8,0 ,"#DI-WM-DATUM-VON");
@L2 A xDI_WM_AUSWAHL = A(1 ,"#DI-WM-AUSWAHL");
@L2 A xRA_KEY_OIB = A(20 ,"#RA-KEY-OIB");
}

View File

@@ -201,8 +201,10 @@ public final class CypherQueries {
*/
public static final String DELETE_STALE_NATURAL_TABLE_ACCESS_EDGES = """
UNWIND $files AS p
MATCH (src:AstNode {project: $project, sourceFile: p.f, language: 'natural'})-[r:READS|WRITES]->(t:AstNode {project: $project, sourceFile: ""})
MATCH (src:AstNode {project: $project, sourceFile: p.f})-[r:READS|WRITES]->(t:AstNode {project: $project, sourceFile: ""})
USING INDEX src:AstNode(project, sourceFile)
WHERE src.ownerModule IN p.os
AND src.language IN ['natural', 'natjav']
AND t.type IN ['DB_TABLE', 'WORKFILE']
DELETE r
""";
@@ -225,8 +227,9 @@ public final class CypherQueries {
*/
public static final String DELETE_STALE_NATURAL_USING_EDGES = """
UNWIND $files AS p
MATCH (src:AstNode {project: $project, sourceFile: p.f, language: 'natural'})-[r:INCLUDES]->(d:AstNode {project: $project, type: 'DATA_STRUCTURE'})
MATCH (src:AstNode {project: $project, sourceFile: p.f})-[r:INCLUDES]->(d:AstNode {project: $project, type: 'DATA_STRUCTURE'})
WHERE src.ownerModule IN p.os
AND src.language IN ['natural', 'natjav']
DELETE r
""";
@@ -1546,7 +1549,9 @@ public final class CypherQueries {
// one that used to answer 404 "no such module" for a name that exists twice. It is a
// deliberate record, not leftover scaffolding.
AND ph.duplicatePaths IS NULL
DELETE ph
// Batched: this is the node sweep that follows the two CONTAINS sweeps above, so its size
// follows theirs — on `korin` 313,088 placeholder VARIABLEs alone become edgeless here.
CALL { WITH ph DELETE ph } IN TRANSACTIONS OF $batchSize ROWS
""";
/**
* Scoped variant of {@link #LINK_ARGS_TO_PARAMS_JAVA_CROSS} for by-name deep ingest ({@code $names}).
@@ -2070,16 +2075,24 @@ public final class CypherQueries {
* final graph: a reference whose target file has since been ingested reads {@code unresolved =
* false} (a real twin exists), a genuinely dangling one (missing module, unresolvable dynamic
* {@code CALLNAT} variable) reads {@code true}. This is the resolution pass item 40 asks for —
* project-wide, cheap (bounded by placeholder count), idempotent, and safe in every ingest mode.
* project-wide, idempotent, and safe in every ingest mode.
*
* <p><b>{@code EXISTS}, not {@code count}.</b> The question is whether a real twin exists, and the
* earlier {@code OPTIONAL MATCH … count(real) = 0} answered it by enumerating <em>every</em> node
* sharing the placeholder's {@code (type, name)} -- for a field name that recurs across hundreds of
* files, thousands of rows per placeholder, all to compare against zero. Once NatJav dataflow
* (item E1) raised `korin`'s placeholder count to 340,667 that cost 274 s, the single most
* expensive step of the refresh. The existential form short-circuits at the first match and
* computes the identical flag: 274 s -> 15.7 s, same 6,973 unresolved.
* Reverse queries ({@code callers}, {@code callees}, {@code search_identifier}) surface the flag so
* dynamic/dangling call sites are visible yet distinguishable from resolved references.
*/
public static final String STAMP_UNRESOLVED_PLACEHOLDERS = """
MATCH (ph:AstNode {project: $project, sourceFile: ""})
OPTIONAL MATCH (real:AstNode {project: $project, type: ph.type, name: ph.name})
WHERE real.sourceFile <> ""
WITH ph, count(real) AS reals
SET ph.unresolved = (reals = 0)
SET ph.unresolved = NOT EXISTS {
MATCH (real:AstNode {project: $project, type: ph.type, name: ph.name})
WHERE real.sourceFile <> ""
}
""";
/**
* Scoped variant of {@link #DELETE_DYNAMIC_CALLNAT_PLACEHOLDER_EDGES}: only markers on callers in
@@ -2218,16 +2231,23 @@ public final class CypherQueries {
public static final String DELETE_RESOLVED_BARE_PLACEHOLDER_CONTAINS = """
MATCH (m:AstNode {project: $project, type: 'MODULE'})-[c:CONTAINS]->(phv:AstNode {project: $project, sourceFile: "", type: 'VARIABLE'})
WHERE m.sourceFile <> "" AND NOT EXISTS { (m)-[:CONTAINS*0..1]->()-[:READS|WRITES]->(phv) }
DELETE c
// Batched for the same reason as DELETE_RESOLVED_PLACEHOLDER_FIELD_CONTAINS: same shape,
// same growth with the project's placeholder count.
CALL { WITH c DELETE c } IN TRANSACTIONS OF $batchSize ROWS
""";
/**
* The {@code DATA_STRUCTURE}s a module references: external copybook areas pulled in via
* {@code DEFINE DATA ... USING} ({@code INCLUDES} edges) and inline {@code DEFINE DATA} groups
* ({@code CONTAINS} edges). Grouped to one row per (name, relationship); {@code fieldCount} is the
* size of the referenced structure's own subtree, {@code area} is derived from the defining file's
* extension. The precise {@code PARAMETER}/{@code LOCAL}/{@code GLOBAL} USING scope is not stored on
* the edge, so {@code area} (PDA/LDA/GDA) is the available proxy; an unresolved copybook (its file
* not ingested) reports {@code area = UNKNOWN}, {@code fieldCount = 0}, {@code sourceFile = null}.
* extension — or, where the {@code INCLUDES} edge carries a {@code scope} (item 186: every NatJav
* {@code @Using} does, 78,533 of 78,533 on korin; Natural edges do not), from that scope:
* {@code PARAMETER}/{@code LOCAL}/{@code GLOBAL} → PDA/LDA/GDA. The edge, not the node, is the
* right source: the same area is a PDA for one module and an LDA for another (3,112 of korin's
* 5,818), and this endpoint is per module, where the value is single — one edge, one scope per
* (module, area). The scope is applied only to a <em>resolved</em> definition, so an unresolved
* copybook (its file not ingested) still reports {@code area = UNKNOWN}, {@code fieldCount = 0},
* {@code sourceFile = null} — {@code UNKNOWN} keeps meaning exactly that and nothing else.
*/
public static final String MODULE_DATA_STRUCTURES = """
MATCH (m:MODULE {name: $name, project: $project})
@@ -2235,8 +2255,8 @@ public final class CypherQueries {
MATCH (m)-[r:INCLUDES|CONTAINS]->(d:AstNode {type: 'DATA_STRUCTURE'})
OPTIONAL MATCH (d)-[:CONTAINS*1..]->(f:AstNode)
WHERE f.type IN ['VARIABLE', 'CONSTANT', 'DATA_STRUCTURE']
WITH d, type(r) AS rel, count(DISTINCT f) AS fc
WITH d.name AS name, rel,
WITH d, type(r) AS rel, r.scope AS scope, count(DISTINCT f) AS fc
WITH d.name AS name, rel, scope,
CASE WHEN d.sourceFile = '' THEN null ELSE d.sourceFile END AS sourceFile,
max(fc) AS fieldCount
// Item 102: group by (name, sourceFile), never by name alone. The previous
@@ -2245,13 +2265,18 @@ public final class CypherQueries {
// `W-WIF-A2, 10 fields, old/W-WIF-A7.pda` — a combination true of neither definition), and
// `head(collect(...))` was order-dependent across ingests. A still-unresolved placeholder is
// only reported when no resolved definition exists for that (name, relationship).
WITH name, rel, collect({sourceFile: sourceFile, fieldCount: fieldCount}) AS defs
WITH name, rel, collect({sourceFile: sourceFile, fieldCount: fieldCount, scope: scope}) AS defs
WITH name, rel, defs, [x IN defs WHERE x.sourceFile IS NOT NULL] AS resolved
UNWIND (CASE WHEN size(resolved) > 0 THEN resolved ELSE defs END) AS def
WITH name, rel, def.sourceFile AS sourceFile, def.fieldCount AS fieldCount
WITH name, rel, def.sourceFile AS sourceFile, def.fieldCount AS fieldCount, def.scope AS scope
RETURN name,
CASE rel WHEN 'INCLUDES' THEN 'USING' ELSE 'INLINE' END AS relationship,
CASE
// Item 186: the edge's scope first, and only on a resolved definition — an
// unresolved area keeps UNKNOWN, which the API guide defines as "not resolved".
WHEN sourceFile IS NOT NULL AND scope = 'PARAMETER' THEN 'PDA'
WHEN sourceFile IS NOT NULL AND scope = 'LOCAL' THEN 'LDA'
WHEN sourceFile IS NOT NULL AND scope = 'GLOBAL' THEN 'GDA'
WHEN sourceFile ENDS WITH '.pda' THEN 'PDA'
WHEN sourceFile ENDS WITH '.lda' THEN 'LDA'
WHEN sourceFile ENDS WITH '.gda' THEN 'GDA'
@@ -2651,6 +2676,13 @@ public final class CypherQueries {
"CREATE INDEX ast_node_copysite IF NOT EXISTS FOR (n:AstNode) ON (n.project, n.sourceFile, n.type, n.name, n.copySite)",
"CREATE INDEX ast_node_project_sourcefile_paramposition IF NOT EXISTS FOR (n:AstNode) ON (n.project, n.sourceFile, n.paramPosition)",
"CREATE INDEX ast_node_project_sourcefile IF NOT EXISTS FOR (n:AstNode) ON (n.project, n.sourceFile)",
// NatJav placeholder re-keying joins a USING/CALLNAT placeholder to the real module on
// className (RESOLVE_NATJAV_PLACEHOLDER_NAMES) -- the one join in the pipeline that is not
// on name or sourceFile. Without this index it is a nested loop: measured on `korin`,
// 22,769 placeholders against 45,577 className-bearing nodes took 24 minutes, longer than
// the entire rest of the refresh. Narrow like item 164's: className is written only by the
// NatJav parser, so the index stays empty for Natural and Java projects.
"CREATE INDEX ast_node_project_classname IF NOT EXISTS FOR (n:AstNode) ON (n.project, n.className)",
// Item 82: manual dynamic-CALLNAT overrides are keyed by (project, originFile, lineNo);
// the apply/list/reset queries seek by project. Not on the :AstNode label on purpose, so
// DELETE_PROJECT_NODES (which matches :AstNode only) leaves overrides intact across a refresh.
@@ -2802,7 +2834,11 @@ public final class CypherQueries {
MATCH (ph)-[c:CONTAINS]->(phv:AstNode {project: $project, sourceFile: ""})
WHERE phv.type IN ['VARIABLE', 'CONSTANT']
AND NOT (phv)-[:READS|WRITES]-()
DELETE c
// Batched (item: NatJav/K3). By far the largest single write in the pipeline, because it
// scales with placeholder fields TIMES the areas that declare them: measured 4,438,644
// edges on `korin`, against the 674,708 the heaviest redirect step deletes. One
// transaction for that exceeds the limit whatever the heap is set to.
CALL { WITH c DELETE c } IN TRANSACTIONS OF $batchSize ROWS
""";
/**
* Item 130: the project's REST surface — one row per handler method, with the endpoint path
@@ -3717,10 +3753,17 @@ public final class CypherQueries {
WHERE size(matches) = 1
WITH m, ph, matches[0] AS realv
MATCH (m)-[:CONTAINS*0..1]->(src:AstNode)-[r:%s]->(ph)
MERGE (src)-[r2:%s {%s}]->(realv)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
DELETE r
// Batched (item: NatJav/K3), like the two placeholder-field passes before it - same
// shape, same growth with the project's reference count. No aggregation needed here:
// `size(matches) = 1` above already pins exactly one target per (m, ph), and a
// placeholder has one owning module, so every `r` appears in exactly one row.
CALL {
WITH src, r, realv
MERGE (src)-[r2:%s {%s}]->(realv)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
DELETE r
} IN TRANSACTIONS OF $batchSize ROWS
""".formatted(INCLUDE_FIELD_DEPTH, INCLUDE_FIELD_DEPTH, INCLUDE_FIELD_DEPTH, type.name(), type.name(),
edgeKey(type, "coalesce(r.lineNo, -1)", "r.originFile", "src")));
}
@@ -3785,6 +3828,77 @@ public final class CypherQueries {
return query;
}
/**
* NatJav placeholder keying (roadmap "Language support — NatJav", item B): rewrites a NatJav
* {@code USING} placeholder's {@code name} from the <b>Java class</b> it was parsed from to the
* <b>Natural</b> name the real data area carries, so that the ordinary
* {@link #resolvePlaceholderTargets} machinery — which joins on {@code name} — can redirect it
* like any other placeholder.
*
* <p>It exists because the two names cannot be bridged by a string rule. A NatJav
* {@code @Using KORIN...WSAIVP_DA} names the class; the node is {@code WSAIVP}. Stripping the
* {@code _DA} suffix yields the right Natural name for only 90.46 % of the 5,818 data areas in
* the reference corpus — {@code V_MIT03P_DA} is {@code V-MIT03P}, and which of its remaining
* underscores become hyphens is not derivable (Natural names may contain both). Joining on the
* {@code className} both sides recorded at parse time is exact.
*
* <p>Rewrites only when <b>exactly one</b> real node is identified. Two files in the corpus share
* a class name ({@code ACOMMON}, {@code COMMON_DA}, both duplicated across libraries); binding
* those arbitrarily would invent a dependency. Item 184: where the class name alone is ambiguous,
* the library the {@code @Using} named ({@code usingPackage}, see {@code NatJavUsings}) picks the
* candidate whose {@code package} matches; what is still ambiguous after that stays unresolved
* and surfaces as such.
*
* <p><b>Not idempotent across ingests by itself — item 183.</b> {@code name} is part of the node
* merge key. After the rewrite the placeholder is stored under its Natural name, so the next
* ingest's {@code MERGE} on the class name finds nothing and mints a second node, which this
* step then renames too: measured on {@code korin} as exactly +34 placeholder
* {@code DATA_STRUCTURE}s per deep refresh, 16 copies of each after 16 ingests, every copy wired
* to the same current child fields. The earlier javadoc here claimed idempotency; it held for
* the same node and not for the re-ingest that creates a new one.
*
* <p>So the rewrite now folds: if a placeholder of the same type already carries the target name
* <em>and the same class name</em> — the previous ingest's copy of this very area — it is deleted
* and the fresh one takes the name. Deleting the old rather than the new is safe because a
* renamed placeholder has no incoming {@code INCLUDES} left (they were redirected to the real
* area in the same run that renamed it; measured 0 on all 34) and its outgoing {@code CONTAINS}
* to placeholder fields are re-created by every parse (which is how one field came to have 16
* parents). The class-name guard keeps a homonym out of reach: two areas sharing a Natural name
* have different class names, so neither can ever delete the other. The {@code collect}/{@code
* UNWIND} is a deliberate barrier — every {@code stale} is read before any is deleted, so no row
* can touch a node a previous row removed.
*/
public static final String RESOLVE_NATJAV_PLACEHOLDER_NAMES = """
MATCH (ph:AstNode {project: $project, sourceFile: "", language: "natjav"})
WHERE ph.className IS NOT NULL
MATCH (real:AstNode {project: $project, type: ph.type, className: ph.className,
language: "natjav"})
WHERE real.sourceFile <> ""
WITH ph, collect(DISTINCT real) AS candidates
// Item 184: a class name two libraries share (COMMON_DA in KORIN and ABFRAGE) is told
// apart by the library the @Using named - or, unqualified, the including file's own
// package - which the parser recorded as usingPackage and the real area carries as
// package. Applied only where the class name alone is ambiguous, so the unique case is
// exactly what it was. A placeholder without a recorded package (one module including
// the same class name from two libraries) stays unresolved rather than being bound to
// either.
WITH ph, CASE WHEN size(candidates) = 1 THEN candidates
ELSE [c IN candidates WHERE ph.usingPackage IS NOT NULL
AND c.package = ph.usingPackage]
END AS candidates
WHERE size(candidates) = 1
WITH ph, head(candidates).name AS naturalName
WHERE ph.name <> naturalName
OPTIONAL MATCH (stale:AstNode {project: $project, sourceFile: "", type: ph.type,
name: naturalName, className: ph.className})
WHERE stale <> ph
WITH collect({ph: ph, naturalName: naturalName, stale: stale}) AS work
UNWIND work AS w
WITH w.ph AS ph, w.naturalName AS naturalName, w.stale AS stale
DETACH DELETE stale
SET ph.name = naturalName
""";
private static Map<EdgeType, String> buildResolvePlaceholderTargetQueries() {
Map<EdgeType, String> queries = new EnumMap<>(EdgeType.class);
for (EdgeType type : RESOLVABLE_EDGE_TYPES) {
@@ -3814,17 +3928,39 @@ public final class CypherQueries {
OR toLower(c.sourceFile) ENDS WITH '.gda')
AND toUpper(head(split(last(split(c.sourceFile, '/')), '.'))) = toUpper(c.name)
] AS memberRoots
UNWIND (CASE WHEN size(memberRoots) > 0 THEN memberRoots ELSE cands END) AS real
WITH ph, (CASE WHEN size(memberRoots) > 0 THEN memberRoots ELSE cands END) AS reals
MATCH (src:AstNode)-[r:%s]->(ph)
// coalesce only guards the MERGE key: a stale edge persisted before edge lineNo
// existed (commit fe6d24f) has no lineNo, and MERGE rejects a null key property.
// The trailing SET restores the true value (null stays null — Neo4j removes the
// property — so downstream `WHERE lineNo IS NOT NULL` collectors keep filtering it,
// and no -1 sentinel ever leaks into caller/callee output).
MERGE (src)-[r2:%s {%s}]->(real)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
DELETE r
// Batched (item: NatJav/K3): this redirects one edge per row, and the row count is
// the project's, not the query's. `korin` drives 6,424 rows here; `k3` — 27,687
// Java files, each call to an unresolved class minting a placeholder — drives
// 821,312 and exceeded the transaction memory limit at 1.4 GiB and again at
// 2.8 GiB after the heap was doubled. Batching bounds it by rows instead of by
// heap, the same way DELETE_PROJECT_NODES does. Requires an auto-commit
// transaction, which is what EnrichmentStep.batched arranges.
// One row per edge, never per (edge, candidate): the fan-out over `reals` happens
// INSIDE the batch. Unwinding it outside puts the same `r` in several rows, and once
// the batches are separate transactions the row that arrives after `r`'s DELETE has
// committed reads a relationship that no longer exists — `Relationship with id N has
// been deleted in this transaction`, which is how this failed on `korin` at step 3 of
// 52 (INCLUDES, where a `USING` with several member roots is the common case). The
// single transaction it ran in before hid the duplicate: Cypher's eager evaluation
// did every match before any delete.
CALL {
WITH src, r, reals
UNWIND reals AS real
// coalesce only guards the MERGE key: a stale edge persisted before edge lineNo
// existed (commit fe6d24f) has no lineNo, and MERGE rejects a null key property.
// The trailing SET restores the true value (null stays null - Neo4j removes the
// property - so downstream `WHERE lineNo IS NOT NULL` collectors keep filtering
// it, and no -1 sentinel ever leaks into caller/callee output).
MERGE (src)-[r2:%s {%s}]->(real)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
// Collapses the fan-out back to the one edge, so it is deleted once and only after
// every candidate has been merged.
WITH DISTINCT r
DELETE r
} IN TRANSACTIONS OF $batchSize ROWS
""".formatted(type.name(), type.name(),
edgeKey(type, "coalesce(r.lineNo, -1)", "r.originFile", "src")));
}
@@ -3924,10 +4060,30 @@ public final class CypherQueries {
// and builds an m x src cartesian; as a predicate it stays a bounded check on the
// module that already owns `src`.
WHERE m.sourceFile <> "" AND EXISTS { (m)-[:INCLUDES]->(real) }
MERGE (src)-[r2:%s {%s}]->(realv)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
DELETE r
// Batched (item: NatJav/K3). This is the heaviest write in the pipeline: on `korin`
// the READS pass alone set 2,335,704 properties across 434,372 created edges, and
// the WRITES pass — roughly twice the edges — exceeded the transaction limit at
// 2.8 GiB. The work scales with the project's field references, not with the query,
// so a bigger heap only moves the ceiling; batching bounds it by rows.
//
// The aggregation is what makes batching safe, and it is not optional: a placeholder
// name can match several real data areas (mean 1.45, up to 4 on upms), which put the
// SAME `r` in several rows. In one transaction Cypher's eager evaluation hid that —
// every match ran before any delete. Across batch boundaries the second row reads an
// `r` whose DELETE has already committed, which fails the whole step with
// `Relationship with id N has been deleted in this transaction`.
WITH src, r, collect(DISTINCT realv) AS realvs
CALL {
WITH src, r, realvs
UNWIND realvs AS realv
MERGE (src)-[r2:%s {%s}]->(realv)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
// Collapses the fan-out back to the one edge: deleted once, and only after every
// target has been merged.
WITH DISTINCT r
DELETE r
} IN TRANSACTIONS OF $batchSize ROWS
""".formatted(type.name(), type.name(),
edgeKey(type, "coalesce(r.lineNo, -1)", "r.originFile", "src")));
}
@@ -3948,10 +4104,20 @@ public final class CypherQueries {
MATCH (real)-[:CONTAINS*1..10]->(realv:AstNode {project: $project, name: phv.name})
WHERE realv.sourceFile <> "" AND realv.type IN ['VARIABLE', 'CONSTANT']
MATCH (src:AstNode)-[r:%s]->(phv)
MERGE (src)-[r2:%s {%s}]->(realv)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
DELETE r
// Batched, and aggregated for the same reason as the INCLUDES-scoped pass above:
// several real data areas can share a placeholder name, which duplicates `r` across
// rows and, once the rows are in separate transactions, makes the later row read a
// relationship whose DELETE has already committed.
WITH src, r, collect(DISTINCT realv) AS realvs
CALL {
WITH src, r, realvs
UNWIND realvs AS realv
MERGE (src)-[r2:%s {%s}]->(realv)
SET r2 += properties(r)
SET r2.lineNo = r.lineNo
WITH DISTINCT r
DELETE r
} IN TRANSACTIONS OF $batchSize ROWS
""".formatted(type.name(), type.name(),
edgeKey(type, "coalesce(r.lineNo, -1)", "r.originFile", "src")));
}

View File

@@ -123,8 +123,21 @@ public class GraphRepository {
// EXTENDS/IMPLEMENTS. Project-wide in both modes (no scoped variant). Note: when field
// resolution is skipped we deliberately do NOT delete placeholders, so a later (scoped) deep
// ingest can still resolve the field placeholders left behind.
// NatJav only: a `USING` placeholder was parsed from a Java class name, the real data area
// carries its Natural name. Re-key the placeholder on the className both sides recorded before
// the name-joining resolution below runs — no string rule can bridge the two (see the query).
// A no-op for Natural and Java projects: it matches only language "natjav" nodes.
statements.add(new EnrichmentStep("resolve-natjav-placeholder-names",
CypherQueries.RESOLVE_NATJAV_PLACEHOLDER_NAMES));
for (EdgeType type : CypherQueries.RESOLVABLE_EDGE_TYPES) {
statements.add(new EnrichmentStep("resolve-placeholder " + type, CypherQueries.resolvePlaceholderTargets(type)));
// Batched: this redirects every edge pointing at a placeholder, and that count follows the
// project, not the query. `korin` has 6,424 CALLS edges into placeholders and fit in one
// transaction; `k3` — a Java estate of 27,687 files, where every call to an unresolved
// class mints one — has 821,312 and did not, failing the refresh at 1.4 GiB and again at
// 2.8 GiB after the heap was doubled. Raising the heap only moves the ceiling; batching
// removes it.
statements.add(new EnrichmentStep("resolve-placeholder " + type,
CypherQueries.resolvePlaceholderTargets(type), true));
}
// J7: resolve Panache-ness inherited through a project base class (needs EXTENDS pointing at
// real nodes, i.e. after the placeholder loop above) by setting repositoryEntity on the
@@ -185,27 +198,36 @@ public class GraphRepository {
// project, dominated by the WRITES pass). Done globally only for a deep whole-root ingest;
// otherwise deferred to a scoped per-program deep ingest (scoped=true).
for (EdgeType type : CypherQueries.RESOLVABLE_FIELD_EDGE_TYPES) {
// Batched in the whole-root form only. This is where a deep refresh does its heaviest
// writing - on `korin` the READS pass set 2.3 M properties over 434 k created edges and
// the WRITES pass, about twice that, blew the 2.8 GiB transaction limit. The scoped form
// processes a handful of modules, where the row count is bounded by the caller's
// $names and batching would only add commit overhead. It goes through the same
// runEnrichment, so it COULD be batched if a scoped ingest ever grew large.
statements.add(new EnrichmentStep("resolve-field-placeholder" + sfx + " " + type,
scoped ? CypherQueries.resolvePlaceholderFieldTargetsScoped(type) : CypherQueries.resolvePlaceholderFieldTargets(type)));
scoped ? CypherQueries.resolvePlaceholderFieldTargetsScoped(type) : CypherQueries.resolvePlaceholderFieldTargets(type),
!scoped));
}
// Fallback by global name for the remainder whose module has no matching INCLUDES edge
// (GLOBAL USING / copycode). Runs after the INCLUDES-scoped pass deleted what it
// resolved, so it only sees the small leftover set.
for (EdgeType type : CypherQueries.RESOLVABLE_FIELD_EDGE_TYPES) {
statements.add(new EnrichmentStep("resolve-field-placeholder-by-name" + sfx + " " + type,
scoped ? CypherQueries.resolvePlaceholderFieldTargetsByNameScoped(type) : CypherQueries.resolvePlaceholderFieldTargetsByName(type)));
scoped ? CypherQueries.resolvePlaceholderFieldTargetsByNameScoped(type) : CypherQueries.resolvePlaceholderFieldTargetsByName(type),
!scoped));
}
statements.add(new EnrichmentStep("delete-resolved-field-contains", CypherQueries.DELETE_RESOLVED_PLACEHOLDER_FIELD_CONTAINS));
statements.add(new EnrichmentStep("delete-resolved-field-contains", CypherQueries.DELETE_RESOLVED_PLACEHOLDER_FIELD_CONTAINS, true));
// Item 18: redirect bare (unqualified) references to an included field onto the matching
// real field, when uniquely resolvable through the module's includes.
for (EdgeType type : CypherQueries.RESOLVABLE_FIELD_EDGE_TYPES) {
statements.add(new EnrichmentStep("resolve-bare-included" + sfx + " " + type,
scoped ? CypherQueries.resolveBareIncludedFieldTargetsScoped(type) : CypherQueries.resolveBareIncludedFieldTargets(type)));
scoped ? CypherQueries.resolveBareIncludedFieldTargetsScoped(type) : CypherQueries.resolveBareIncludedFieldTargets(type),
!scoped));
}
statements.add(new EnrichmentStep("delete-bare-placeholder-contains", CypherQueries.DELETE_RESOLVED_BARE_PLACEHOLDER_CONTAINS));
statements.add(new EnrichmentStep("delete-bare-placeholder-contains", CypherQueries.DELETE_RESOLVED_BARE_PLACEHOLDER_CONTAINS, true));
// Only safe once field placeholders are resolved — otherwise it would reap placeholder
// structures a later scoped deep ingest still needs to resolve their fields.
statements.add(new EnrichmentStep("delete-resolved-placeholders", CypherQueries.DELETE_RESOLVED_PLACEHOLDERS));
statements.add(new EnrichmentStep("delete-resolved-placeholders", CypherQueries.DELETE_RESOLVED_PLACEHOLDERS, true));
}
if (dataflow) {
// Dataflow: map CALLNAT arguments to callee parameters by position. Expensive on a whole
@@ -1845,7 +1867,8 @@ public class GraphRepository {
* idempotent).
*/
public Uni<Void> finalizeProjectScoped(String project, List<String> moduleNames) {
Map<String, Object> params = Map.of("project", project, "names", moduleNames);
Map<String, Object> params = Map.of("project", project, "names", moduleNames,
"batchSize", deleteBatchSize);
return runEnrichment(project, enrichmentSteps(true, true, true), params, "scoped-deep");
}
@@ -1864,7 +1887,8 @@ public class GraphRepository {
*/
public Uni<Void> finalizeProject(String project, EnrichmentLevel level, boolean profile) {
return runEnrichment(project, enrichmentSteps(level.dataflow(), level.resolveFields(), false),
Map.of("project", project), level.name().toLowerCase(java.util.Locale.ROOT), profile);
Map.of("project", project, "batchSize", deleteBatchSize),
level.name().toLowerCase(java.util.Locale.ROOT), profile);
}
/**
@@ -1894,7 +1918,11 @@ public class GraphRepository {
EnrichmentStep step = steps.get(i);
long start = System.nanoTime();
String cypher = profile ? "PROFILE " + step.cypher() : step.cypher();
ResultSummary summary = session.executeWrite(tx -> tx.run(cypher, params).consume());
// A batched step manages its own transactions; wrapping it in an explicit one
// would make Neo4j refuse the CALL { … } IN TRANSACTIONS inside it.
ResultSummary summary = step.batched()
? session.run(cypher, params).consume()
: session.executeWrite(tx -> tx.run(cypher, params).consume());
SummaryCounters c = summary.counters();
long ms = (System.nanoTime() - start) / 1_000_000;
if (profile && ms >= PROFILE_LOG_THRESHOLD_MS && summary.hasProfile()) {
@@ -2163,7 +2191,18 @@ public class GraphRepository {
return params;
}
private record EnrichmentStep(String label, String cypher) {
/**
* One enrichment statement.
*
* @param batched whether the query drives its own {@code CALL { … } IN TRANSACTIONS} and must
* therefore run in an <b>auto-commit</b> transaction. Neo4j rejects that construct
* inside an explicit one, which is what {@code session.executeWrite} opens.
*/
private record EnrichmentStep(String label, String cypher, boolean batched) {
EnrichmentStep(String label, String cypher) {
this(label, cypher, false);
}
}
public Uni<ProjectOpResult> createProject(String name, @Nullable String description,

View File

@@ -0,0 +1,127 @@
package com.agenticcode.parserjava;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.CommentBlocks;
import com.agenticcode.parsercore.ast.model.CommentProperties;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.comments.BlockComment;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.comments.JavadocComment;
import com.github.javaparser.ast.comments.LineComment;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
/**
* Comment blocks of a parsed Java compilation unit (roadmap item 141), shared by the Java and the
* NatJav parsers — the grouping and the DOCUMENTS attachment are identical for both, because NatJav
* <em>is</em> Java syntax.
*
* <p>What differs is the text: a transpiled file also carries the transpiler's own bookkeeping, which
* is why {@code retainedText} lets a caller drop parts of a block before it becomes a node. The Java
* parser passes the identity — a hand-written file has nothing to strip.
*/
public final class JavaComments {
private JavaComments() {
}
/**
* One {@link NodeType#COMMENT} node per contiguous comment block, each with a
* {@link EdgeType#DOCUMENTS} edge to the declaration it documents.
*
* <p>Comments are free here: JavaParser already attaches them to the {@link CompilationUnit}
* (the default {@code ParserConfiguration.setAttributeComments(true)}), so this collects what the
* parse produced rather than re-lexing. Adjacent {@code //} lines are folded into one block —
* a five-line {@code //} preamble is one comment, not five.
*/
public static void collect(CompilationUnit unit, String language, String sourceFile,
List<AstNode> nodes, List<AstEdge> edges, UnaryOperator<String> retainedText) {
List<Comment> comments = unit.getAllComments().stream()
.filter(c -> c.getBegin().isPresent() && c.getEnd().isPresent())
.sorted(Comparator.comparingInt(c -> c.getBegin().orElseThrow().line))
.toList();
if (comments.isEmpty()) {
return;
}
// Declaration nodes of THIS file only: placeholder modules for referenced classes carry
// sourceFile "" and are not a target a comment in this file could ever document.
List<AstNode> targets = nodes.stream()
.filter(n -> n.sourceFile().equals(sourceFile))
.filter(CommentBlocks::isDocumentable)
.toList();
if (targets.isEmpty()) {
return; // nothing parsed from this file — a comment with no target is not emitted
}
List<Comment> block = new ArrayList<>();
for (Comment comment : comments) {
boolean extendsBlock = !block.isEmpty()
&& comment instanceof LineComment
&& block.get(block.size() - 1) instanceof LineComment previous
&& previous.getEnd().orElseThrow().line + 1 == comment.getBegin().orElseThrow().line;
if (!extendsBlock && !block.isEmpty()) {
emitCommentBlock(block, language, sourceFile, targets, nodes, edges, retainedText);
block = new ArrayList<>();
}
block.add(comment);
}
emitCommentBlock(block, language, sourceFile, targets, nodes, edges, retainedText);
}
private static void emitCommentBlock(List<Comment> block, String language, String sourceFile,
List<AstNode> targets, List<AstNode> nodes, List<AstEdge> edges,
UnaryOperator<String> retainedText) {
if (block.isEmpty()) {
return;
}
int startLine = block.get(0).getBegin().orElseThrow().line;
int endLine = block.get(block.size() - 1).getEnd().orElseThrow().line;
String text = retainedText.apply(
block.stream().map(JavaComments::commentText).collect(Collectors.joining("\n")));
@Nullable AstNode commentNode = CommentBlocks.commentNode(language, sourceFile, startLine, endLine,
text, commentKind(block.get(0)));
if (commentNode == null) {
return; // an empty /**/ or a bare // documents nothing
}
nodes.add(commentNode);
edges.add(CommentBlocks.documentsEdge(commentNode,
CommentBlocks.documentedNode(targets, startLine, endLine)));
}
/**
* A comment's text with its decoration removed. {@code getContent()} hands back the inner text
* verbatim, which for a block/Javadoc comment still carries every line's indentation and leading
* {@code *} — so the stored text read {@code "* ServiceEndpoint: …\n\t * UpmsObject: …"}. Natural
* comments already lose their markers, and a reader (human or agent) wants the same thing here:
* the sentence, not the comment syntax.
*/
private static String commentText(Comment comment) {
if (comment instanceof LineComment) {
return comment.getContent().strip();
}
return comment.getContent().lines()
.map(line -> {
String stripped = line.strip();
return stripped.startsWith("*") ? stripped.substring(1).stripLeading() : stripped;
})
.collect(Collectors.joining("\n"))
.strip();
}
private static String commentKind(Comment comment) {
if (comment instanceof JavadocComment) {
return CommentProperties.KIND_JAVADOC;
}
return comment instanceof BlockComment ? CommentProperties.KIND_BLOCK : CommentProperties.KIND_LINE;
}
}

View File

@@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
/**
@@ -1539,100 +1540,11 @@ public final class JavaParser implements LanguageParser {
// Item 141: comments are file-level, so this runs once after the type loop — inside it, a file
// with nested types would emit every comment once per type.
addCommentNodes(unit, sourceFile, nodes, edges);
JavaComments.collect(unit, "java", sourceFile, nodes, edges, UnaryOperator.identity());
return new ParseResult(nodes, edges);
}
/**
* Item 141: one {@link NodeType#COMMENT} node per contiguous comment block, each with a
* {@link EdgeType#DOCUMENTS} edge to the declaration it documents.
*
* <p>Comments are free here: JavaParser already attaches them to the {@link CompilationUnit}
* (the default {@code ParserConfiguration.setAttributeComments(true)}), so this collects what the
* parse produced rather than re-lexing. Adjacent {@code //} lines are folded into one block —
* a five-line {@code //} preamble is one comment, not five.
*/
private static void addCommentNodes(CompilationUnit unit, String sourceFile,
List<AstNode> nodes, List<AstEdge> edges) {
List<Comment> comments = unit.getAllComments().stream()
.filter(c -> c.getBegin().isPresent() && c.getEnd().isPresent())
.sorted(Comparator.comparingInt(c -> c.getBegin().orElseThrow().line))
.toList();
if (comments.isEmpty()) {
return;
}
// Declaration nodes of THIS file only: placeholder modules for referenced classes carry
// sourceFile "" and are not a target a comment in this file could ever document.
List<AstNode> targets = nodes.stream()
.filter(n -> n.sourceFile().equals(sourceFile))
.filter(CommentBlocks::isDocumentable)
.toList();
if (targets.isEmpty()) {
return; // nothing parsed from this file — a comment with no target is not emitted
}
List<Comment> block = new ArrayList<>();
for (Comment comment : comments) {
boolean extendsBlock = !block.isEmpty()
&& comment instanceof LineComment
&& block.get(block.size() - 1) instanceof LineComment previous
&& previous.getEnd().orElseThrow().line + 1 == comment.getBegin().orElseThrow().line;
if (!extendsBlock && !block.isEmpty()) {
emitCommentBlock(block, sourceFile, targets, nodes, edges);
block = new ArrayList<>();
}
block.add(comment);
}
emitCommentBlock(block, sourceFile, targets, nodes, edges);
}
private static void emitCommentBlock(List<Comment> block, String sourceFile, List<AstNode> targets,
List<AstNode> nodes, List<AstEdge> edges) {
if (block.isEmpty()) {
return;
}
int startLine = block.get(0).getBegin().orElseThrow().line;
int endLine = block.get(block.size() - 1).getEnd().orElseThrow().line;
String text = block.stream().map(JavaParser::commentText).collect(Collectors.joining("\n"));
@Nullable AstNode commentNode = CommentBlocks.commentNode("java", sourceFile, startLine, endLine,
text, commentKind(block.get(0)));
if (commentNode == null) {
return; // an empty /**/ or a bare // documents nothing
}
nodes.add(commentNode);
edges.add(CommentBlocks.documentsEdge(commentNode,
CommentBlocks.documentedNode(targets, startLine, endLine)));
}
/**
* A comment's text with its decoration removed. {@code getContent()} hands back the inner text
* verbatim, which for a block/Javadoc comment still carries every line's indentation and leading
* {@code *} — so the stored text read {@code "* ServiceEndpoint: …\n\t * UpmsObject: …"}. Natural
* comments already lose their markers, and a reader (human or agent) wants the same thing here:
* the sentence, not the comment syntax.
*/
private static String commentText(Comment comment) {
if (comment instanceof LineComment) {
return comment.getContent().strip();
}
return comment.getContent().lines()
.map(line -> {
String stripped = line.strip();
return stripped.startsWith("*") ? stripped.substring(1).stripLeading() : stripped;
})
.collect(Collectors.joining("\n"))
.strip();
}
private static String commentKind(Comment comment) {
if (comment instanceof JavadocComment) {
return CommentProperties.KIND_JAVADOC;
}
return comment instanceof BlockComment ? CommentProperties.KIND_BLOCK : CommentProperties.KIND_LINE;
}
/**
* The handful of facts that differ between the four kinds of Java type declaration, resolved once
* so the ingest loop below can treat them uniformly.

63
ac-parser-natjav/pom.xml Normal file
View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.agenticcode</groupId>
<artifactId>agenticcode</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>ac-parser-natjav</artifactId>
<packaging>jar</packaging>
<name>ac-parser-natjav</name>
<description>Parser for NatJav — Software AG Natural transpiled to Java (innoWake maxenso)</description>
<dependencies>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-core</artifactId>
</dependency>
<!-- NatJav is syntactically Java, so the syntax layer is JavaParser; only the semantic
mapping (Natural module kinds, names, data areas) lives in this module. -->
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.agenticcode</groupId>
<artifactId>ac-mvn-plugins</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,182 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.CallKind;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* The call graph of a NatJav module: its internal subroutines and every {@code perform} /
* {@code callnat} / {@code fetch} / {@code fetchReturn} they make.
*
* <p>Resolution needs no name derivation, because both sides of a call use the same Java identifier:
* {@code perform("FX_LEEREN")} names the method {@code void FX_LEEREN()}, and
* {@code callnat("V_MIT04")} names the class {@code V_MIT04}. Measured over the corpus, 93.8 % of the
* 129,590 {@code perform}s hit a method in their own file — exact and file-local. The remaining
* 6.2 % are Natural <em>external</em> subroutines, which the transpiler made classes of their own
* ({@code NaturalSubroutine}); 98.4 % of them exist as such a class, so NatJav resolves a call the
* Natural parser explicitly cannot (see the roadmap's item-71 retraction).
*
* <p>A subroutine is a parameterless method that does <b>not</b> override a runtime hook.
* {@code run}, {@code atTopOfPage}, {@code atWriteTitle}, {@code onError}, {@code atEndOfPage} and
* {@code doBreak} are the transpiled forms of Natural's inline event blocks ({@code AT TOP OF PAGE},
* {@code ON ERROR}, …), which the Natural parser attributes to the module — only
* {@code DEFINE SUBROUTINE} makes a {@code FUNCTION} there. Treating them as subroutines would invent
* ~5,300 of them and add a hop to every call inside one.
*/
final class NatJavCalls {
/**
* The Natural statements the transpiler emits as bare API calls, mapped to the call kind the
* unified AST records.
*/
private static final Map<String, CallKind> CALL_STATEMENTS = Map.of(
"perform", CallKind.PERFORM,
"callnat", CallKind.CALLNAT,
"fetch", CallKind.CALLNAT,
"fetchReturn", CallKind.CALLNAT);
private NatJavCalls() {
}
/**
* Appends the module's {@code FUNCTION} nodes, its {@code CALLS} edges and the placeholders those
* edges point at.
*
* @return the subroutines by name, so a later pass can attribute its statements to the same
* owner this one used
*
* @param type the module's primary type
* @param module the file's shell node
* @param sourceFile the file's project-relative path
* @param copycode the file's expansion markers, used to attribute a call to the copycode it
* came from
*/
static Map<String, AstNode> collect(TypeDeclaration<?> type, AstNode module, String sourceFile,
NatJavCopycode copycode, List<AstNode> nodes, List<AstEdge> edges) {
List<MethodDeclaration> methods = type.getMethods();
// Subroutines first: a `perform` may call one declared further down the file.
Map<String, AstNode> subroutines = new LinkedHashMap<>();
for (MethodDeclaration method : methods) {
if (!isSubroutine(method)) {
continue;
}
AstNode function = new AstNode(UUID.randomUUID(), NodeType.FUNCTION, method.getNameAsString(),
sourceFile, NatJavParser.LANGUAGE, lineOf(method), endLineOf(method), null, null,
copycode.attribute(Map.of(), lineOf(method)));
nodes.add(function);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, module.id(), function.id(),
function.startLine(), null));
subroutines.put(function.name(), function);
}
Map<String, AstNode> placeholders = new HashMap<>();
for (MethodDeclaration method : methods) {
// Calls in an event block or in run() belong to the module, exactly as in Natural.
AstNode caller = subroutines.getOrDefault(method.getNameAsString(), module);
for (MethodCallExpr call : method.findAll(MethodCallExpr.class)) {
CallKind kind = CALL_STATEMENTS.get(call.getNameAsString());
// A bare call is the transpiled Natural statement; `x.perform(...)` would be something
// else entirely (a runtime object's own method).
if (kind == null || call.getScope().isPresent() || call.getArguments().isEmpty()) {
continue;
}
emitCall(call, kind, caller, subroutines, placeholders, copycode, nodes, edges);
}
}
return subroutines;
}
private static void emitCall(MethodCallExpr call, CallKind kind, AstNode caller,
Map<String, AstNode> subroutines, Map<String, AstNode> placeholders,
NatJavCopycode copycode, List<AstNode> nodes, List<AstEdge> edges) {
int lineNo = lineOf(call);
Map<String, String> properties = new LinkedHashMap<>();
Expression target = call.getArgument(0);
AstNode targetNode;
if (target instanceof StringLiteralExpr literal) {
String name = literal.asString().trim();
if (name.isEmpty()) {
return;
}
AstNode local = kind == CallKind.PERFORM ? subroutines.get(name) : null;
// A local subroutine wins over a same-named module: Natural resolves PERFORM against the
// module's own subroutines first, and 93.8 % of the corpus's targets are exactly that.
targetNode = local != null ? local : placeholder(placeholders, nodes, name, lineNo, true);
properties.put("callKind", kind.name());
} else {
// Dynamic dispatch: the target name lives in a variable. Recorded as a placeholder named
// after it plus the dynamicVar marker, the same shape the Natural parser uses so the
// constant-propagation enrichment can recover literal targets later.
String variable = target.toString().trim();
targetNode = placeholder(placeholders, nodes, variable, lineNo, false);
properties.put("callKind", CallKind.CALLNAT_DYNAMIC.name());
properties.put("dynamicVar", variable);
}
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CALLS, caller.id(), targetNode.id(), lineNo,
null, copycode.attribute(properties, lineNo)));
}
/**
* @return the deduped unresolved-module placeholder for {@code name}, carrying the class name the
* call site used so the NatJav placeholder resolution can key on it (the call names the Java
* class, the module node its Natural name)
*/
private static AstNode placeholder(Map<String, AstNode> placeholders, List<AstNode> nodes,
String name, int lineNo, boolean literalTarget) {
return placeholders.computeIfAbsent(name, n -> {
// className only where the name IS a class name. For a dynamic call it is the dispatch
// variable's text, and the NatJav placeholder re-keying matches any placeholder carrying
// one — so a variable that happens to share a module's identifier would have been wired
// to that module as a real call it never makes.
Map<String, String> properties = literalTarget
? Map.of(NatJavUsings.CLASS_NAME, n) : Map.of();
AstNode node = new AstNode(UUID.randomUUID(), NodeType.MODULE, n, "", NatJavParser.LANGUAGE,
lineNo, lineNo, null, null, properties);
nodes.add(node);
return node;
});
}
/**
* @return whether {@code method} is a Natural internal subroutine rather than an event block or
* the module body
*/
private static boolean isSubroutine(MethodDeclaration method) {
return method.getParameters().isEmpty()
&& !method.isAnnotationPresent("Override")
&& !RUNTIME_HOOKS.contains(method.getNameAsString());
}
/**
* The runtime hooks, checked by name as well as by {@code @Override}: a generated file may omit
* the annotation, and mistaking {@code run} for a subroutine would attach the whole module body
* to a function Natural does not have.
*/
private static final Set<String> RUNTIME_HOOKS = Set.of(
"run", "onError", "atWriteTitle", "atTopOfPage", "atEndOfPage", "doBreak",
"atStartOfData", "atEndOfData", "atBreak", "initialize");
private static int lineOf(com.github.javaparser.ast.Node node) {
return node.getBegin().map(p -> p.line).orElse(1);
}
private static int endLineOf(com.github.javaparser.ast.Node node) {
return node.getEnd().map(p -> p.line).orElse(1);
}
}

View File

@@ -0,0 +1,93 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.model.SourceHash;
import com.agenticcode.parsercore.ast.spi.CoarseScanner;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Tier-1 coarse scan of a NatJav file (roadmap item 36): the file's outline only — its
* {@code MODULE}/{@code DATA_STRUCTURE} shell, stamped with the content hash (item 41) and
* LoC/SLoC (item 46) — with the deep detail left to {@link NatJavParser}.
*
* <p>While the parser emits nothing beyond the shell, this scan is the same result plus the
* metrics; it filters rather than assumes, so it keeps producing the correct outline as the parser
* grows a call graph and data structures in later phases.
*/
public final class NatJavCoarseScanner implements CoarseScanner {
private final NatJavParser parser = new NatJavParser();
private final NatJavLineCounter lineCounter = new NatJavLineCounter();
/**
* @return {@code shell} with {@code sourceHash} and {@code loc}/{@code sloc} added, preserving
* every property the parser already set (naturalName identity, moduleKind, description, ...)
*/
private static AstNode withShellMetrics(AstNode shell, String hash, LocMetrics metrics) {
Map<String, String> properties = new HashMap<>();
if (shell.properties() != null) {
properties.putAll(shell.properties());
}
properties.put(SourceHash.PROPERTY, hash);
properties.put(LocMetrics.LOC, Integer.toString(metrics.loc()));
properties.put(LocMetrics.SLOC, Integer.toString(metrics.sloc()));
return new AstNode(shell.id(), shell.type(), shell.name(), shell.sourceFile(), shell.language(),
shell.startLine(), shell.endLine(), shell.dataType(), shell.value(), properties);
}
@Override
public String language() {
return NatJavParser.LANGUAGE;
}
@Override
public ParseResult scan(String sourceFile, String content) {
return scan(sourceFile, content, NatJavDdmResolver.NONE);
}
/**
* Tier-1 scan with the project's DDM index; see {@link NatJavParser#parse(String, String,
* NatJavDdmResolver)}.
*/
public ParseResult scan(String sourceFile, String content, NatJavDdmResolver ddms) {
NatJavParser.Parsed parsed = parser.parseWithShell(sourceFile, content, ddms);
String hash = SourceHash.of(content);
LocMetrics metrics = lineCounter.count(content);
// Kept: the file's shell, its FUNCTION shells (subroutines — the call graph is Tier-1) and
// the unresolved cross-file references (USING/CALLNAT placeholders, which carry an empty
// sourceFile). The field tree is deep detail a Tier-2 parse fills in — and it cannot be
// filtered by node type, because a data area's groups are DATA_STRUCTUREs as well, which is
// why the shell is recognised by identity rather than by type.
List<AstNode> coarseNodes = new ArrayList<>();
Set<UUID> kept = new HashSet<>();
for (AstNode node : parsed.result().nodes()) {
boolean shell = node.id().equals(parsed.shell().id());
// A placeholder is only outline material when it stands for another FILE — a USING or a
// CALLNAT target. Dataflow now also mints field placeholders and DB_TABLEs with an empty
// sourceFile, and keeping those made a Tier-1 scan persist the whole cross-file field
// population (340,667 on `korin`) plus tables with no access to hang off.
boolean crossFileReference = node.sourceFile().isEmpty()
&& (node.type() == NodeType.MODULE || node.type() == NodeType.DATA_STRUCTURE);
boolean outline = shell || node.type() == NodeType.FUNCTION || crossFileReference;
if (!outline) {
continue;
}
kept.add(node.id());
coarseNodes.add(shell ? withShellMetrics(node, hash, metrics) : node);
}
return new ParseResult(coarseNodes, parsed.result().edges().stream()
.filter(e -> kept.contains(e.sourceId()) && kept.contains(e.targetId()))
.toList());
}
}

View File

@@ -0,0 +1,126 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parserjava.JavaComments;
import com.github.javaparser.ast.CompilationUnit;
import java.util.List;
import java.util.regex.Pattern;
/**
* Comment blocks of a NatJav file. The grouping and the {@code DOCUMENTS} attachment are the Java
* parser's ({@link JavaComments}) — NatJav <em>is</em> Java syntax — and what this class adds is the
* one thing that differs: a transpiled file carries the transpiler's own bookkeeping alongside the
* comments a Natural developer wrote.
*
* <p>Measured over the corpus's 2,473,041 comment lines, 26 % are that bookkeeping, and every part of
* it is <b>already in the graph as structured data</b>: the {@code @@INCLUDE} markers as a call's
* {@code viaCopycode}, the header's {@code @FUNCTION}/{@code @OWNER} as the module's
* {@code description}/{@code owner}. Storing it again as comment text would bury the real comments —
* {@code search_by_value} with {@code includeComments} would answer a quarter transpiler markers.
*
* <p><b>The filtering is per line, not per block</b>, and that distinction is the whole point. The
* migration header looks like pure framing but is not: of the 14,749 files carrying one, <b>10,466
* (71 %)</b> have real prose inside it — often a fuller description of the program than the single
* {@code @FUNCTION} line. Dropping the block would delete the documentation of 10,466 modules.
*
* <pre>
* * *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01*** ← framing, dropped
* * * @FUNCTION LOESCHEN UMKONTIERUNGEN ← already the module's description
* * * @OWNER P36 ← already the module's owner
* * *
* * * Löschen Umkontierungen ← kept
* </pre>
*/
final class NatJavComments {
/**
* A block that is nothing but transpiler bookkeeping: the {@code @@USING}/{@code @@INCLUDE}
* expansion markers (245,024 lines) and the generation banner (21,603). Unlike the migration
* header these never carry prose, so they are dropped whole.
*/
private static final Pattern GENERATED_BLOCK = Pattern.compile(
"^\\s*(@@(USING|INCLUDE):(BEGIN|END)\\(|Generated by innoWake"
+ "|additional loop and database objects)");
/**
* One line of the migration header's frame, or a field of it already extracted into a module
* property. Everything else in the header is the developer's own text.
*/
private static final Pattern HEADER_FRAME = Pattern.compile(
"^[\\s*]*$"
+ "|LCM-HEADER\\s+(BEGIN|END)"
+ "|\\$\\$LCMHEADER"
// The corpus spells this both ways: 57,484 "migration" and 1,347 "migraion".
// Matching the correct spelling only let 442 blocks of pure framing through.
+ "|header created during migra[a-z]*ion process"
+ "|@(FUNCTION|OWNER|MERGE-SOURCE|NO-MERGE)\\b"
// The generator stamps per-line metadata wherever it likes, and the label is not
// one word: besides `versionRep2Str:` and `originalObjectName:` there is
// `Scanpunkte für manuelle Eingriffe:` (2,140 lines, also HTML-escaped as
// `f&uuml;r`). Any line that opens with the generator's name and carries a label
// is its stamp — a hand-written comment does not start that way.
+ "|^[\\s*]*innoWake\\b[^:]*:"
+ "|^\\s*</?pre>\\s*$");
private NatJavComments() {
}
/**
* Appends the file's comment nodes and their {@code DOCUMENTS} edges.
*/
static void collect(CompilationUnit unit, String sourceFile, List<AstNode> nodes, List<AstEdge> edges) {
JavaComments.collect(unit, NatJavParser.LANGUAGE, sourceFile, nodes, edges, NatJavComments::retained);
}
/**
* @return the part of a comment block worth keeping: {@code ""} for pure bookkeeping, otherwise
* the block without its framing lines
*/
static String retained(String text) {
if (GENERATED_BLOCK.matcher(text).find()) {
return "";
}
StringBuilder kept = new StringBuilder();
for (String line : text.split("\\R")) {
if (HEADER_FRAME.matcher(line).find() || isDecorationOnly(line)) {
continue;
}
if (kept.length() > 0) {
kept.append('\n');
}
kept.append(withoutNaturalMarker(line));
}
return kept.toString().strip();
}
/**
* @return the line without Natural's own comment marker. The Java layer already stripped the
* Javadoc {@code *}; underneath it sits the {@code *} that marked a full-line comment in the
* Natural source, and a Natural ingest does not store that either — the sentence is wanted, not
* the comment syntax.
*/
private static String withoutNaturalMarker(String line) {
String core = line.strip();
return core.startsWith("*") ? core.substring(1).strip() : core;
}
/**
* @return whether the line is only the comment box's own drawing — a row of asterisks, dashes or
* equals signs, which the Natural sources use to frame a header and which carries no statement
*/
private static boolean isDecorationOnly(String line) {
String core = line.strip();
if (core.isEmpty()) {
return true;
}
for (int i = 0; i < core.length(); i++) {
char c = core.charAt(i);
if (c != '*' && c != '-' && c != '=' && c != ' ' && c != '/') {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,287 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.stmt.DoStmt;
import com.github.javaparser.ast.stmt.ForEachStmt;
import com.github.javaparser.ast.stmt.ForStmt;
import com.github.javaparser.ast.stmt.IfStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.stmt.SwitchStmt;
import com.github.javaparser.ast.stmt.WhileStmt;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Control-flow blocks as {@code CONTROL_FLOW} nodes, nested by containment — the same shape the
* Natural parser builds for {@code IF}/{@code FOR}/{@code REPEAT}/{@code DECIDE}.
*
* <p>Two things this must <em>not</em> do, both measured over the reference corpus:
*
* <ul>
* <li><b>A database loop is not control flow.</b> 20,749 of the corpus's 23,696 {@code while}
* statements are {@code while (readUMK.hasNext())} — a transpiled {@code READ}/{@code FIND}
* loop. The Natural parser models {@code IF}, {@code FOR}, {@code REPEAT} and {@code DECIDE},
* never a {@code READ} loop, so taking these would invent 20,749 blocks the original has not.
* Only 2,947 {@code while}s are Natural {@code REPEAT}s.</li>
* <li><b>A {@code DECIDE} is one construct, not a chain of ifs.</b> The transpiler lowers Natural's
* {@code DECIDE} into {@code if}/{@code else if} around a counter variable, and emitting each
* arm separately would turn 9,340 dispatches into some 30,000 unrelated {@code IF}s and leave
* the dispatch views empty. The counter is what makes the chain recognisable.</li>
* </ul>
*/
final class NatJavControlFlow {
/**
* The method a transpiled read loop tests. Its receiver is the loop object a {@code read}/
* {@code find}/{@code get}/{@code histogram} assigned.
*/
private static final String LOOP_TEST = "hasNext";
/**
* Local the transpiler declares before a lowered {@code DECIDE} and increments in every branch;
* the trailing {@code if (decideCounterN == 0)} is the {@code NONE} arm.
*/
private static final String DECIDE_COUNTER = "decideCounter";
private NatJavControlFlow() {
}
/**
* Appends the module's control-flow blocks.
*
* @param subroutine method name → the {@code FUNCTION} owning its statements
*/
static void collect(TypeDeclaration<?> type, AstNode module, String sourceFile,
Map<String, AstNode> subroutine, NatJavCopycode copycode,
List<AstNode> nodes, List<AstEdge> edges) {
for (MethodDeclaration method : type.getMethods()) {
AstNode owner = subroutine.getOrDefault(method.getNameAsString(), module);
method.getBody().ifPresent(body -> walk(body, owner.id(), sourceFile, copycode, nodes, edges));
}
}
/**
* Walks {@code statement}'s children, emitting a node per block and recursing under it so the
* nesting in the graph is the nesting in the source.
*/
private static void walk(Node statement, UUID parent, String sourceFile, NatJavCopycode copycode,
List<AstNode> nodes, List<AstEdge> edges) {
for (Node child : statement.getChildNodes()) {
visit(child, parent, sourceFile, copycode, nodes, edges);
}
}
/**
* Processes one node: emits it if it opens a block, then descends. Splitting this from
* {@link #walk} is what makes an {@code else if} work — the else-branch of an {@code if} <b>is</b>
* an {@code IfStmt}, and handing it to a children-walk skipped it silently while its own
* else-branch surfaced one level too high. An {@code if}/{@code else if}/{@code else if} chain
* lost every second arm.
*/
private static void visit(Node child, UUID parent, String sourceFile, NatJavCopycode copycode,
List<AstNode> nodes, List<AstEdge> edges) {
Block block = blockOf(child);
if (block == null) {
walk(child, parent, sourceFile, copycode, nodes, edges);
return;
}
AstNode node = emit(block, sourceFile, copycode, nodes, edges, parent);
// The block's BODIES, never the statement itself: for a lowered DECIDE the bodies are the
// arms' then-branches, so the chain's own `else if` is not met again and folded into a
// second DECIDE. A body that is itself a block is emitted under this one.
for (Node body : block.bodies()) {
visit(body, node.id(), sourceFile, copycode, nodes, edges);
}
}
private static AstNode emit(Block block, String sourceFile, NatJavCopycode copycode,
List<AstNode> nodes, List<AstEdge> edges, UUID parent) {
int lineNo = block.node().getBegin().map(p -> p.line).orElse(1);
int endLine = block.node().getEnd().map(p -> p.line).orElse(lineNo);
Map<String, String> properties = new LinkedHashMap<>();
if (!block.branchValues().isEmpty()) {
properties.put("decideValues", String.join(",", block.branchValues()));
}
AstNode node = new AstNode(UUID.randomUUID(), NodeType.CONTROL_FLOW, block.kind(), sourceFile,
NatJavParser.LANGUAGE, lineNo, endLine, block.kind(), block.condition(),
copycode.attribute(properties, lineNo));
nodes.add(node);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, parent, node.id(), lineNo, null));
return node;
}
/**
* @return the block {@code child} opens, or {@code null} when it is not a control-flow statement
* (or is a database loop, which is not one)
*/
private static @Nullable Block blockOf(Node child) {
if (child instanceof IfStmt ifStatement) {
// Being a dispatch and having literal arms are two different questions. Conflating them
// classified every DECIDE whose arms compare numbers — not strings — as a plain IF.
if (!isLoweredDecide(ifStatement)) {
return new Block("IF", ifStatement.getCondition().toString(), ifStatement,
armBodies(ifStatement, false), List.of());
}
return new Block("DECIDE", ifStatement.getCondition().toString(), ifStatement,
armBodies(ifStatement, true), decideBranchValues(ifStatement));
}
if (child instanceof ForStmt statement) {
return new Block("FOR", conditionText(child), child, List.of(statement.getBody()), List.of());
}
if (child instanceof ForEachStmt statement) {
return new Block("FOR", conditionText(child), child, List.of(statement.getBody()), List.of());
}
if (child instanceof WhileStmt whileStatement) {
return isDatabaseLoop(whileStatement.getCondition())
? null
: new Block("REPEAT", whileStatement.getCondition().toString(), whileStatement,
List.of(whileStatement.getBody()), List.of());
}
if (child instanceof DoStmt doStatement) {
return new Block("REPEAT", doStatement.getCondition().toString(), doStatement,
List.of(doStatement.getBody()), List.of());
}
if (child instanceof SwitchStmt switchStatement) {
// The entries, not the statement: passing the statement itself would make `visit`
// recognise it again and recurse without end.
return new Block("DECIDE", switchStatement.getSelector().toString(), switchStatement,
List.copyOf(switchStatement.getEntries()), List.of());
}
return null;
}
/**
* @return the subtrees to recurse into. For a chain folded into one {@code DECIDE} these are the
* arms' bodies, so the chain's own {@code else if} statements are never met again; for a plain
* {@code if} the then- and else-branches, which nest as Java nests them.
*/
private static List<Node> armBodies(IfStmt ifStatement, boolean chain) {
List<Node> bodies = new ArrayList<>();
Statement branch = ifStatement;
while (branch instanceof IfStmt arm) {
bodies.add(arm.getThenStmt());
Statement next = arm.getElseStmt().orElse(null);
if (!chain) {
if (next != null) {
bodies.add(next);
}
break;
}
branch = next;
}
if (chain && branch != null) {
bodies.add(branch);
}
return bodies;
}
/**
* @return whether this {@code while} is a transpiled {@code READ}/{@code FIND} loop — its
* condition asks a loop object for the next record
*/
private static boolean isDatabaseLoop(Expression condition) {
return condition instanceof MethodCallExpr call
&& call.getNameAsString().equals(LOOP_TEST)
&& call.getScope().isPresent();
}
/**
* @return whether this {@code if} is a lowered {@code DECIDE} — every arm of the chain increments
* a {@code decideCounter} local. The counter's name is not always numbered
* ({@code decideCounterGWFMCHKC} occurs too), so it is matched by prefix.
*/
private static boolean isLoweredDecide(IfStmt ifStatement) {
Statement branch = ifStatement;
while (branch instanceof IfStmt arm) {
if (!incrementsDecideCounter(arm.getThenStmt())) {
return false;
}
branch = arm.getElseStmt().orElse(null);
}
return true;
}
/**
* @return the literal values the chain's arms compare against — what the Natural parser records
* as the dispatch's {@code VALUE} arms. Empty for a dispatch on numbers, which is a dispatch all
* the same.
*/
private static List<String> decideBranchValues(IfStmt ifStatement) {
List<String> values = new ArrayList<>();
Statement branch = ifStatement;
while (branch instanceof IfStmt arm) {
values.addAll(literalsOf(arm.getCondition()));
branch = arm.getElseStmt().orElse(null);
}
return values;
}
/**
* @return whether {@code body} contains the {@code decideCounterN++} the transpiler puts in every
* arm of a lowered {@code DECIDE}
*/
private static boolean incrementsDecideCounter(Statement body) {
// A DIRECT statement of the arm, not anywhere in its subtree. The transpiler puts the
// increment first in every arm; searching the subtree also found it through a nested `if`,
// so one dispatch was recognised again at each nesting level — 13,193 detections against the
// corpus's 9,340 counters, 1,917 of them a DECIDE inside its own DECIDE.
List<Statement> statements = body instanceof com.github.javaparser.ast.stmt.BlockStmt block
? block.getStatements()
: List.of(body);
return statements.stream()
.filter(statement -> statement instanceof com.github.javaparser.ast.stmt.ExpressionStmt)
.map(statement -> ((com.github.javaparser.ast.stmt.ExpressionStmt) statement).getExpression())
.anyMatch(expression -> expression instanceof com.github.javaparser.ast.expr.UnaryExpr unary
&& unary.getExpression().toString().startsWith(DECIDE_COUNTER));
}
/**
* @return the string literals a branch condition compares against — the {@code VALUE} arms of the
* original {@code DECIDE}
*/
private static List<String> literalsOf(Expression condition) {
return condition.findAll(StringLiteralExpr.class).stream()
.map(StringLiteralExpr::asString)
.map(String::trim)
.filter(value -> !value.isEmpty())
.toList();
}
private static String conditionText(Node loop) {
if (loop instanceof ForStmt statement) {
return statement.getCompare().map(Object::toString).orElse("");
}
if (loop instanceof ForEachStmt statement) {
return statement.getIterable().toString();
}
return "";
}
/**
* One control-flow block.
*
* @param kind the Natural construct: {@code IF}, {@code FOR}, {@code REPEAT} or
* {@code DECIDE}
* @param condition the condition text, stored as the node's value like the Natural parser does
* @param node the statement, for its line range
* @param bodies the subtrees to recurse into
* @param branchValues the arms of a dispatch, empty for anything else
*/
private record Block(String kind, String condition, Node node, List<Node> bodies,
List<String> branchValues) {
}
}

View File

@@ -0,0 +1,123 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.CopycodeProperties;
import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The copycode expansions the transpiler left behind as markers:
* {@code /* @@INCLUDE:BEGIN(YCISN) *}{@code /} … {@code /* @@INCLUDE:END(YCISN) *}{@code /}. Measured
* over the reference corpus: 47,313 blocks in 6,124 files, nested at most 3 deep, with no unbalanced
* block and no {@code BEGIN}/{@code END} name mismatch — so a plain stack is enough.
*
* <p><b>This is attribution, not the Natural copycode model.</b> On the Natural side a copycode is a
* separate {@code .cpy} file whose nodes live in it and are <em>shared</em> by every including module
* ({@code L4N-ENTER} is contained by 137 of them). The transpiler baked each expansion into its host
* file, so there is no second file and no shared node: the same copycode subroutine exists 137 times,
* once per host. What survives is where a statement came from, which is what
* {@link CopycodeProperties#VIA_COPYCODE} and {@link CopycodeProperties#INCLUDED_AT} record — 23.3 %
* of all calls in the corpus sit inside such a block, so it is the difference between attributing
* 50,336 calls to the program that happens to hold them and to the copycode they belong to.
*
* <p>{@link CopycodeProperties#INCLUDE_PATH} is deliberately <b>not</b> written. It identifies one
* expansion site because a Natural copycode node is shared between sites; here every node already
* lives in the host file being parsed, takes the ordinary merge path, and has no sibling to be told
* apart from.
*/
final class NatJavCopycode {
private static final Pattern BEGIN = Pattern.compile("@@INCLUDE:BEGIN\\(([^)]*)\\)");
private static final Pattern END = Pattern.compile("@@INCLUDE:END\\(([^)]*)\\)");
private final List<Block> blocks;
private NatJavCopycode(List<Block> blocks) {
this.blocks = blocks;
}
/**
* Scans {@code content} for the expansion markers.
*
* <p>Read off the raw text rather than the parsed comments on purpose: these markers are the
* transpiler's own bookkeeping, and a block opens and closes at a line, not at a syntax node —
* an expansion routinely starts mid-statement-list and is not a well-formed Java subtree.
*/
static NatJavCopycode of(String content) {
List<Block> blocks = new ArrayList<>();
Deque<Block> open = new ArrayDeque<>();
String[] lines = content.split("\\R", -1);
for (int i = 0; i < lines.length; i++) {
int lineNo = i + 1;
Matcher begin = BEGIN.matcher(lines[i]);
if (begin.find()) {
open.push(new Block(begin.group(1), lineNo));
}
Matcher end = END.matcher(lines[i]);
if (end.find() && !open.isEmpty()) {
Block block = open.pop();
block.endLine = lineNo;
blocks.add(block);
}
}
// An unterminated block (none in the corpus, but a truncated file could) still ends the file.
while (!open.isEmpty()) {
Block block = open.pop();
block.endLine = lines.length;
blocks.add(block);
}
return new NatJavCopycode(blocks);
}
/**
* @return {@code properties} plus the copycode origin of {@code lineNo}, or {@code properties}
* unchanged when the line is the host module's own code
*/
Map<String, String> attribute(Map<String, String> properties, int lineNo) {
Block innermost = innermost(lineNo);
if (innermost == null) {
return properties;
}
Map<String, String> merged = new LinkedHashMap<>(properties);
merged.put(CopycodeProperties.VIA_COPYCODE, innermost.member);
merged.put(CopycodeProperties.INCLUDED_AT, Integer.toString(innermost.beginLine));
return merged;
}
/**
* @return the innermost block containing {@code lineNo} — innermost, because a nested expansion
* is the one the statement actually came from
*/
private @Nullable Block innermost(int lineNo) {
Block innermost = null;
for (Block block : blocks) {
if (lineNo >= block.beginLine && lineNo <= block.endLine
&& (innermost == null || block.beginLine > innermost.beginLine)) {
innermost = block;
}
}
return innermost;
}
/**
* One expansion of one copycode member: the marker lines that bracket it.
*/
private static final class Block {
private final String member;
private final int beginLine;
private int endLine;
private Block(String member, int beginLine) {
this.member = member;
this.beginLine = beginLine;
this.endLine = beginLine;
}
}
}

View File

@@ -0,0 +1,250 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Statement-level dataflow: which variable a statement reads and which it writes, as
* {@code READS}/{@code WRITES} edges from the enclosing function (or the module) to the field —
* what makes {@code flow-forward}, {@code flow-backward} and {@code field-flow} answerable.
*
* <p>The corpus holds 1,414,872 such statements, and 77.6 % of their targets are qualified
* ({@code WSAIVP.INHALT}, {@code xWA_UMKONT_MENGE.xRN_UMKONT_MENGE}). Every operand names <b>Java</b>
* identifiers while the field nodes carry <b>Natural</b> names, so resolution is the crux:
*
* <ul>
* <li><b>File-local</b> — the symbol table the field walk built maps the Java name to the node
* exactly, with no guessing.</li>
* <li><b>A field of a {@code USING}'d data area</b> — the operand's prefix is an exposure field
* whose declared type names the area, and the pre-parse index supplies the field's Natural
* name from that area's own source. The placeholder is then emitted <em>under that Natural
* name</em>, so the existing placeholder-field resolution, which joins on name, redirects it
* like any Natural one. No new query, and in particular no join on a Java name — the shape
* that cost 24 minutes in item D3.</li>
* </ul>
*
* <p>An operand that resolves to neither is skipped rather than guessed at.
*/
final class NatJavDataflow {
/**
* Statements whose first operand is written and whose remaining operands are read.
*/
private static final Set<String> WRITE_FIRST = Set.of("assign", "compute");
/**
* Accumulating statements: the first operand is Natural's {@code ADD … TO x}, which reads the
* target as well as writing it — the old value is an input. The Natural parser makes the same
* distinction ({@code accumulate} for {@code TO}, plain write for {@code GIVING}); modelling
* these as write-only would break every backward flow that runs through an accumulator.
*/
private static final Set<String> ACCUMULATE_FIRST = Set.of(
"addTo", "subtractFrom", "multiplyBy", "divideInto");
/**
* Statements whose operands are all written (Natural {@code RESET}).
*/
private static final Set<String> WRITE_ALL = Set.of("reset", "resetInitial");
/**
* Statements whose operands are all read (Natural {@code IF}, {@code WRITE}, {@code DISPLAY} …
* are handled elsewhere; these are the value-producing forms whose target is named by
* {@code into(...)}).
*/
private static final Set<String> READ_ALL = Set.of(
"move", "compress", "add", "subtract", "multiply", "divide", "separate", "examine");
/**
* Wrapper naming the target of a {@code READ_ALL} statement: {@code compress(a, b, into(t))}.
*/
private static final Set<String> TARGET_WRAPPERS = Set.of("into", "giving", "to");
private NatJavDataflow() {
}
/**
* Appends the module's variable accesses.
*
* @param byJavaName the file's own fields, by Java identifier
* @param exposures Java field name → the data area it exposes, for cross-file operands
* @param subroutine method name → the {@code FUNCTION} owning its statements
*/
static void collect(TypeDeclaration<?> type, AstNode module, Map<String, AstNode> byJavaName,
Map<String, String> exposures, Map<String, AstNode> areaPlaceholders,
Map<String, AstNode> subroutine, NatJavDdmResolver ddms, NatJavCopycode copycode,
List<AstNode> nodes, List<AstEdge> edges) {
Map<String, AstNode> placeholderFields = new HashMap<>();
for (MethodDeclaration method : type.getMethods()) {
AstNode caller = subroutine.getOrDefault(method.getNameAsString(), module);
for (MethodCallExpr call : method.findAll(MethodCallExpr.class)) {
if (call.getScope().isPresent() || call.getArguments().isEmpty()) {
continue;
}
String statement = call.getNameAsString();
boolean accumulate = ACCUMULATE_FIRST.contains(statement);
boolean writeFirst = accumulate || WRITE_FIRST.contains(statement);
boolean writeAll = WRITE_ALL.contains(statement);
if (!writeFirst && !writeAll && !READ_ALL.contains(statement)) {
continue;
}
int lineNo = call.getBegin().map(p -> p.line).orElse(1);
List<Expression> arguments = call.getArguments();
for (int i = 0; i < arguments.size(); i++) {
Expression argument = arguments.get(i);
boolean write = writeAll || (writeFirst && i == 0) || isTargetWrapper(argument);
boolean read = !write || (accumulate && i == 0);
for (String operand : operands(argument)) {
AstNode field = resolve(operand, byJavaName, exposures, areaPlaceholders,
placeholderFields, ddms, lineNo, nodes, edges);
if (field == null) {
continue;
}
if (write) {
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.WRITES, caller.id(),
field.id(), lineNo, null, copycode.attribute(Map.of(), lineNo)));
}
if (read) {
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.READS, caller.id(),
field.id(), lineNo, null, copycode.attribute(Map.of(), lineNo)));
}
}
}
}
}
}
/**
* @return whether {@code argument} is {@code into(x)}/{@code giving(x)}/{@code to(x)}, which name
* the statement's target rather than one of its inputs
*/
private static boolean isTargetWrapper(Expression argument) {
return argument instanceof MethodCallExpr call
&& call.getScope().isEmpty()
&& TARGET_WRAPPERS.contains(call.getNameAsString());
}
/**
* @return the variable operands of one argument: the argument itself when it names a field, or
* the operands of a target wrapper. Literals, constants and runtime calls yield nothing.
*/
private static List<String> operands(Expression argument) {
if (argument instanceof MethodCallExpr call && call.getScope().isEmpty()
&& TARGET_WRAPPERS.contains(call.getNameAsString())) {
return call.getArguments().stream().map(NatJavDataflow::operandOf)
.filter(o -> !o.isEmpty()).toList();
}
String operand = operandOf(argument);
return operand.isEmpty() ? List.of() : List.of(operand);
}
/**
* @return the field path an expression names, with array subscripts removed
* ({@code MSG_INFO.xxMSG_DATA.get(1)} → {@code MSG_INFO.xxMSG_DATA}), or {@code ""} when it names
* no field (a literal, a system variable such as {@code $PROGRAM()}, a nested call)
*/
private static String operandOf(Expression expression) {
String text = withoutSubscripts(expression.toString().trim());
if (text.isEmpty() || !Character.isJavaIdentifierStart(text.charAt(0))
|| text.indexOf('(') >= 0 || text.indexOf('"') >= 0) {
return "";
}
return text;
}
/**
* Removes every {@code .get(...)} subscript from a field path, keeping what follows it.
*
* <p>Cutting the string at the first {@code .get(} instead — which is what this did — turned
* {@code ARR.get(1).FLD} into {@code ARR}: the access was attributed to the group and the field
* it actually names never saw it, so {@code field-flow} on an indexed group member missed every
* statement. The argument list nests ({@code .get(range(1, 4))}), so the closing parenthesis is
* found by counting, not by searching.
*/
private static String withoutSubscripts(String text) {
StringBuilder path = new StringBuilder();
int i = 0;
while (i < text.length()) {
int subscript = text.indexOf(".get(", i);
if (subscript < 0) {
path.append(text, i, text.length());
break;
}
path.append(text, i, subscript);
int depth = 0;
int j = subscript + ".get(".length() - 1;
for (; j < text.length(); j++) {
char c = text.charAt(j);
if (c == '(') {
depth++;
} else if (c == ')' && --depth == 0) {
break;
}
}
if (j >= text.length()) {
break; // unbalanced: keep what was collected rather than guess
}
i = j + 1;
}
return path.toString();
}
/**
* Resolves an operand to the field node it names, creating a cross-file placeholder when it
* belongs to a {@code USING}'d data area.
*/
private static @Nullable AstNode resolve(String operand, Map<String, AstNode> byJavaName,
Map<String, String> exposures,
Map<String, AstNode> areaPlaceholders,
Map<String, AstNode> placeholderFields,
NatJavDdmResolver ddms, int lineNo,
List<AstNode> nodes, List<AstEdge> edges) {
AstNode local = byJavaName.get(operand);
if (local != null) {
return local;
}
int dot = operand.indexOf('.');
if (dot < 0) {
// A bare name the file does not declare: nothing identifies which area it came from.
return byJavaName.get(operand);
}
String prefix = operand.substring(0, dot);
String field = operand.substring(operand.lastIndexOf('.') + 1);
AstNode nested = byJavaName.get(field);
if (nested != null && byJavaName.containsKey(prefix)) {
return nested; // both halves are this file's own — a group member
}
String area = exposures.get(prefix);
AstNode areaPlaceholder = area == null ? null : areaPlaceholders.get(area);
if (area == null || areaPlaceholder == null) {
return null;
}
String naturalName = ddms.resolveAreaField(area, field);
if (naturalName == null) {
return null;
}
// Emitted under the Natural name so the existing placeholder-field resolution, which joins on
// name, can redirect it to the real field in the data area's own file.
return placeholderFields.computeIfAbsent(area + "." + naturalName, key -> {
AstNode node = new AstNode(UUID.randomUUID(), NodeType.VARIABLE, naturalName, "",
NatJavParser.LANGUAGE, lineNo, lineNo, null, null, null);
nodes.add(node);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, areaPlaceholder.id(),
node.id(), lineNo, null));
return node;
});
}
}

View File

@@ -0,0 +1,316 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.AssignExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* The ADABAS and work-file statements of a NatJav module, mapped onto the same {@code DB_ACCESS} /
* {@code READS} / {@code WRITES} shape the Natural parser produces.
*
* <p>Only a call with no receiver is a Natural statement. That distinction is the whole game here:
* {@code UMK.SPARTEN.get(range(1, 4))} is an array subscript and occurs 435,129 times, against 9,904
* real {@code GET}s — reading {@code get(} without it would report a fortyfold phantom.
*
* <p><b>Writes name the loop, not the view.</b> {@code UPDATE} and {@code DELETE} operate on the
* record a preceding read is positioned on, and the transpiler expressed that with an object:
* {@code getDEL = get(MDUMKONT_DEL, withHold(), isn); … delete(getDEL);}. The table is only known at
* the read. These objects are <em>class fields</em>, so a file-wide map resolves them even when the
* read sits in {@code run()} and the write in a subroutine — measured over the corpus's 5,576
* {@code update}/{@code delete} statements, 98.7 % resolve, 1.3 % have no visible assignment, and 18
* (0.3 %) name an object assigned from two different views. Those 18 get an edge to each candidate
* and an {@code ambiguousLoopTarget} marker: for a write, a reported possibility beats a silent
* omission.
*/
final class NatJavDbAccess {
/**
* Statements whose first argument is the view being read. The {@code *Events} variants are the
* same Natural statements written with event blocks ({@code AT START OF DATA}, {@code AT BREAK});
* the corpus holds 876 of them, and omitting them lost both those reads and every
* {@code UPDATE}/{@code DELETE} positioned by one.
*/
private static final Set<String> READS = Set.of(
"read", "get", "find", "histogram",
"readEvents", "findEvents", "histogramEvents");
/**
* Statements whose first argument is the view being written.
*/
private static final Set<String> STORES = Set.of("store");
/**
* Statements whose first argument is a loop object, not a view.
*/
private static final Map<String, String> BY_LOOP = Map.of("update", "WRITE", "delete", "DELETE");
private static final Map<String, String> WORKFILE_STATEMENTS = Map.of(
"readWorkFile", "READ", "writeWorkFile", "WRITE");
private NatJavDbAccess() {
}
/**
* Appends the module's DB and work-file accesses.
*
* @param views the module's views, by Java field name
* @param subroutine resolves a method to the {@code FUNCTION} that owns its statements, falling
* back to the module for {@code run()} and the event blocks
*/
static void collect(TypeDeclaration<?> type, AstNode module, String sourceFile,
Map<String, NatJavFields.View> declaredViews, NatJavTables tables,
Map<String, AstNode> subroutine, NatJavCopycode copycode,
NatJavDdmResolver ddms, List<AstNode> nodes, List<AstEdge> edges) {
Map<String, NatJavFields.View> views = withOperandAliases(type, declaredViews, ddms, tables);
Map<String, Set<AstNode>> loopTables = loopTables(type, views);
Map<String, AstNode> workfiles = new HashMap<>();
for (MethodDeclaration method : type.getMethods()) {
AstNode caller = subroutine.getOrDefault(method.getNameAsString(), module);
for (MethodCallExpr call : method.findAll(MethodCallExpr.class)) {
if (call.getScope().isPresent() || call.getArguments().isEmpty()) {
continue;
}
String statement = call.getNameAsString();
if (WORKFILE_STATEMENTS.containsKey(statement)) {
workfileAccess(call, WORKFILE_STATEMENTS.get(statement), caller, sourceFile,
workfiles, copycode, nodes, edges);
} else if (READS.contains(statement)) {
tableAccess(call, "READ", EdgeType.READS, tableOf(call.getArgument(0), views),
caller, sourceFile, copycode, nodes, edges, false);
} else if (STORES.contains(statement)) {
tableAccess(call, "WRITE", EdgeType.WRITES, tableOf(call.getArgument(0), views),
caller, sourceFile, copycode, nodes, edges, false);
} else if (BY_LOOP.containsKey(statement)) {
Set<AstNode> candidates = loopTables.getOrDefault(name(call.getArgument(0)), Set.of());
boolean ambiguous = candidates.size() > 1;
if (candidates.isEmpty()) {
tableAccess(call, BY_LOOP.get(statement), EdgeType.WRITES, null, caller,
sourceFile, copycode, nodes, edges, false);
}
for (AstNode table : candidates) {
tableAccess(call, BY_LOOP.get(statement), EdgeType.WRITES, table, caller,
sourceFile, copycode, nodes, edges, ambiguous);
}
}
}
}
}
/**
* Extends the declared views with the operands that stand in for one. A statement rarely names
* the {@code @ViewOf} field directly:
*
* <pre>{@code
* VVSTICH.VT VTO = VT; // alias of a local view
* public final KORIN…YLPOSLV_DA.SWPOS SWPOS = YLPOSLV_DA.SWPOS; // view of a USING'd data area
* }</pre>
*
* <p>Both name their origin in the field's <b>declared type</b> — {@code <owner>.<view>} — which
* is a statement of fact, not a guess: the owner is either this class (an alias) or the data area
* that declares the view. Resolution follows the chain, since an alias may point at another
* alias, and stops at a small depth because a cycle cannot be ruled out from one file.
*/
/**
* @return Java field name → the data area its declared type names, for every field typed
* {@code <area>.<member>} where the area is a generated data-area class. This is what lets a
* dataflow operand such as {@code WSAIVP.INHALT} be traced to the file that declares the field.
*/
static Map<String, String> exposures(TypeDeclaration<?> type) {
Map<String, String> exposures = new LinkedHashMap<>();
for (FieldDeclaration declaration : type.getFields()) {
for (VariableDeclarator variable : declaration.getVariables()) {
String[] parts = ownerAndView(variable.getTypeAsString());
if (parts != null && parts[0].endsWith("_DA")) {
exposures.put(variable.getNameAsString(), parts[0]);
}
}
}
return exposures;
}
private static Map<String, NatJavFields.View> withOperandAliases(
TypeDeclaration<?> type, Map<String, NatJavFields.View> declared,
NatJavDdmResolver ddms, NatJavTables tables) {
// Java field name -> (owner class, view name) from the declared type.
Map<String, String[]> typed = new LinkedHashMap<>();
for (FieldDeclaration declaration : type.getFields()) {
for (VariableDeclarator variable : declaration.getVariables()) {
String[] parts = ownerAndView(variable.getTypeAsString());
if (parts != null) {
typed.put(variable.getNameAsString(), parts);
}
}
}
Map<String, NatJavFields.View> views = new LinkedHashMap<>(declared);
for (String operand : typed.keySet()) {
if (!views.containsKey(operand)) {
NatJavFields.View resolved = resolveOperand(operand, typed, views, ddms, tables, 0);
if (resolved != null) {
views.put(operand, resolved);
}
}
}
return views;
}
private static NatJavFields.@Nullable View resolveOperand(
String operand, Map<String, String[]> typed, Map<String, NatJavFields.View> views,
NatJavDdmResolver ddms, NatJavTables tables, int depth) {
if (depth > 4) {
return null;
}
NatJavFields.View known = views.get(operand);
if (known != null) {
return known;
}
String[] parts = typed.get(operand);
if (parts == null) {
return null;
}
String owner = parts[0];
String view = parts[1];
NatJavDdmResolver.Ddm ddm = ddms.resolveAreaView(owner, view);
if (ddm != null) {
return new NatJavFields.View(null, tables.get(ddm.tableName(), 1));
}
// Not a data-area view: an alias of another operand in this file.
return resolveOperand(view, typed, views, ddms, tables, depth + 1);
}
/**
* @return the last two segments of a declared type — {@code {owner, view}} — or, for an
* unqualified type naming a view in this same file ({@code VERTRAG_VD VD = VERTRAG_VD;}),
* {@code {"", view}}. {@code null} for a type that can name no view (a generic).
*/
private static String @Nullable [] ownerAndView(String declaredType) {
String type = declaredType.trim();
if (type.indexOf('<') >= 0 || type.isEmpty()) {
return null;
}
if (type.indexOf('.') < 0) {
// The transpiler also writes the alias with the view's own generated type, unqualified.
// There is no owner to look up then; the chain continues at the view itself.
return new String[]{"", type};
}
String[] segments = type.split("\\.");
if (segments.length < 2) {
return null;
}
return new String[]{segments[segments.length - 2], segments[segments.length - 1]};
}
/**
* @return loop-object Java name → the tables it may be positioned on. A {@link Set} because an
* object reassigned from two views has two, and picking one would be a guess.
*/
private static Map<String, Set<AstNode>> loopTables(TypeDeclaration<?> type,
Map<String, NatJavFields.View> views) {
Map<String, Set<AstNode>> loops = new LinkedHashMap<>();
for (AssignExpr assignment : type.findAll(AssignExpr.class)) {
if (!(assignment.getTarget() instanceof NameExpr target)
|| !(assignment.getValue() instanceof MethodCallExpr call)
|| call.getScope().isPresent()
// A STORE positions a record too, and an UPDATE may follow on it.
|| !(READS.contains(call.getNameAsString()) || STORES.contains(call.getNameAsString()))
|| call.getArguments().isEmpty()) {
continue;
}
AstNode table = tableOf(call.getArgument(0), views);
if (table != null) {
loops.computeIfAbsent(target.getNameAsString(), n -> new LinkedHashSet<>()).add(table);
}
}
return loops;
}
private static void tableAccess(MethodCallExpr call, String kind, EdgeType direction,
@Nullable AstNode table, AstNode caller, String sourceFile,
NatJavCopycode copycode, List<AstNode> nodes, List<AstEdge> edges,
boolean ambiguous) {
int lineNo = line(call);
Map<String, String> properties = new LinkedHashMap<>();
if (ambiguous) {
properties.put("ambiguousLoopTarget", "true");
}
AstNode access = new AstNode(UUID.randomUUID(), NodeType.DB_ACCESS,
table != null ? table.name() : "", sourceFile, NatJavParser.LANGUAGE, lineNo, lineNo,
kind, call.toString(), copycode.attribute(properties, lineNo));
nodes.add(access);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, caller.id(), access.id(), lineNo, null));
if (table != null) {
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.USES_TYPE, access.id(), table.id(), lineNo, null));
edges.add(new AstEdge(UUID.randomUUID(), direction, caller.id(), table.id(), lineNo, null,
copycode.attribute(Map.of(), lineNo)));
}
}
/**
* Emits a {@code WORKFILE} and its access. The work file is keyed by its number, exactly as in
* Natural ({@code READ/WRITE WORK FILE n}), and the remaining arguments are the record buffer.
*/
private static void workfileAccess(MethodCallExpr call, String kind, AstNode caller,
String sourceFile, Map<String, AstNode> workfiles,
NatJavCopycode copycode, List<AstNode> nodes, List<AstEdge> edges) {
int lineNo = line(call);
String number = call.getArgument(0).toString().trim();
AstNode workfile = workfiles.computeIfAbsent(number, n -> {
AstNode node = new AstNode(UUID.randomUUID(), NodeType.WORKFILE, n, sourceFile,
NatJavParser.LANGUAGE, lineNo, lineNo, null, null, null);
nodes.add(node);
return node;
});
Map<String, String> properties = new LinkedHashMap<>();
String buffer = call.getArguments().stream().skip(1).map(Object::toString)
.reduce((a, b) -> a + ", " + b).orElse("");
if (!buffer.isEmpty()) {
properties.put("recordBuffer", buffer);
}
AstNode access = new AstNode(UUID.randomUUID(), NodeType.WORKFILE_ACCESS, number, sourceFile,
NatJavParser.LANGUAGE, lineNo, lineNo, kind, call.toString(),
copycode.attribute(properties, lineNo));
nodes.add(access);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, caller.id(), access.id(), lineNo, null));
// The record buffer goes on the edge as well as the node: workfile-accesses aggregates it
// from the READS/WRITES edge (`collect(r.recordBuffer)`), so putting it only on the node
// leaves every buffer list empty. The Natural parser writes it to both for the same reason.
edges.add(new AstEdge(UUID.randomUUID(),
"WRITE".equals(kind) ? EdgeType.WRITES : EdgeType.READS,
caller.id(), workfile.id(), lineNo, null, copycode.attribute(properties, lineNo)));
}
/**
* @return the table a statement's first argument reads, or {@code null} when the operand is not a
* known view (a view declared in a {@code USING}'d data area, for instance)
*/
private static @Nullable AstNode tableOf(Expression operand, Map<String, NatJavFields.View> views) {
NatJavFields.View view = views.get(name(operand));
return view == null ? null : view.table();
}
private static String name(Expression expression) {
return expression instanceof NameExpr named ? named.getNameAsString() : expression.toString().trim();
}
private static int line(MethodCallExpr call) {
return call.getBegin().map(p -> p.line).orElse(1);
}
}

View File

@@ -0,0 +1,192 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ParserConfiguration.LanguageLevel;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* A data definition module — the description of one ADABAS file. The transpiler wrote each as a
* {@code NaturalDdm} class of constants:
*
* <pre>{@code
* public static final DbN VR_KEY = reference(FDT0050.AG, "VR-KEY", NUMERIC, 3, 0);
* public static final DbA OART = reference(FDT0050.AH, "OART");
* }</pre>
*
* <p>Two things come out of that, and both are needed elsewhere: the column's <b>Natural</b> name,
* which a view referencing it does not state (it declares columns by Java name only), and its format
* where the DDM gives one — 1,834 of the corpus's 17,982 references do; the other 16,148 keep their
* length in the FDT class, and it is left absent rather than guessed.
*
* <p>The DDM file itself stays a {@code MODULE}: a {@code DB_TABLE} carries no source file anywhere
* in this system — that is what makes every reference to a table merge onto one node — so the file
* and the table are two nodes joined by {@link EdgeType#MAPS_TO}, exactly as the Java parser joins a
* JPA entity to its table.
*/
public final class NatJavDdm {
private static final ParserConfiguration PARSER_CONFIGURATION =
new ParserConfiguration().setLanguageLevel(LanguageLevel.JAVA_21);
/**
* Declared field types of a DDM constant, mapped to the Natural format letter. {@code DbGroup} is
* a periodic/multiple-value group and carries no format of its own.
*/
private static final Map<String, String> FORMATS = Map.of(
"DbA", "A", "DbN", "N", "DbP", "P", "DbB", "B", "DbL", "L", "DbT", "T", "DbD", "D");
private NatJavDdm() {
}
/**
* Reads a DDM source file into the table description the view resolution needs.
*
* @param content the DDM class's source
* @param stem the file's name without extension, used as the identity fallback
* @return the DDM, or {@code null} if {@code content} does not parse
*/
public static NatJavDdmResolver.@Nullable Ddm parse(String content, String stem) {
CompilationUnit unit;
try {
unit = new com.github.javaparser.JavaParser(PARSER_CONFIGURATION).parse(content)
.getResult().orElse(null);
} catch (RuntimeException e) {
return null;
}
if (unit == null) {
return null;
}
TypeDeclaration<?> type = NatJavFacts.primaryType(unit, stem);
if (type == null) {
return null;
}
NatJavFacts facts = NatJavFacts.of(unit, stem);
Map<String, String> columns = new LinkedHashMap<>();
Map<String, String> formats = new LinkedHashMap<>();
for (FieldDeclaration declaration : type.getFields()) {
for (VariableDeclarator variable : declaration.getVariables()) {
Reference reference = referenceOf(variable);
if (reference == null) {
continue;
}
columns.put(variable.getNameAsString(), reference.naturalName);
if (reference.format != null) {
formats.put(variable.getNameAsString(), reference.format);
}
}
}
return new NatJavDdmResolver.Ddm(facts.naturalName(), columns, formats);
}
/**
* Emits the column nodes of a DDM file and the {@link EdgeType#MAPS_TO} edge to the table it
* describes. Called when the file being parsed <em>is</em> a DDM.
*/
static void collect(TypeDeclaration<?> type, AstNode module, String tableName, String sourceFile,
List<AstNode> nodes, List<AstEdge> edges) {
AstNode table = new AstNode(UUID.randomUUID(), NodeType.DB_TABLE, tableName, "",
NatJavParser.LANGUAGE, module.startLine(), module.startLine(), null, null, null);
nodes.add(table);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.MAPS_TO, module.id(), table.id(),
module.startLine(), null));
for (FieldDeclaration declaration : type.getFields()) {
for (VariableDeclarator variable : declaration.getVariables()) {
Reference reference = referenceOf(variable);
if (reference == null) {
continue;
}
int lineNo = declaration.getBegin().map(p -> p.line).orElse(module.startLine());
Map<String, String> properties = new LinkedHashMap<>();
properties.put(NatJavFields.JAVA_NAME, variable.getNameAsString());
if (reference.shortName != null) {
// The ADABAS two-character short name — the physical field key, which the Natural
// source never shows and only the DDM knows.
properties.put("adabasShortName", reference.shortName);
}
AstNode column = new AstNode(UUID.randomUUID(), NodeType.FIELD, reference.naturalName,
sourceFile, NatJavParser.LANGUAGE, lineNo, lineNo, reference.format, null,
properties);
nodes.add(column);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, module.id(), column.id(),
lineNo, null));
}
}
}
/**
* @return the {@code reference(...)} initializer's parts, or {@code null} if this field is not a
* column declaration
*/
private static @Nullable Reference referenceOf(VariableDeclarator variable) {
Expression initializer = variable.getInitializer().orElse(null);
if (!(initializer instanceof MethodCallExpr call) || !call.getNameAsString().equals("reference")
|| call.getArguments().isEmpty()) {
return null;
}
String naturalName = null;
List<String> trailing = new ArrayList<>();
String shortName = null;
for (Expression argument : call.getArguments()) {
if (argument instanceof StringLiteralExpr literal) {
if (naturalName == null) {
naturalName = literal.asString().trim();
}
} else if (naturalName == null) {
// The FDT constant, e.g. FDT0050.AG — its last segment is the ADABAS short name.
String text = argument.toString();
int dot = text.lastIndexOf('.');
shortName = dot >= 0 ? text.substring(dot + 1) : null;
} else {
trailing.add(argument.toString().trim());
}
}
if (naturalName == null || naturalName.isEmpty()) {
return null;
}
return new Reference(naturalName, shortName, format(variable.getTypeAsString(), trailing));
}
/**
* @return the Natural type spec — the declared {@code Db*} type's format letter plus the
* length/precision the DDM states, spelled as elsewhere ({@code N3}, not {@code N3,0}) — or
* {@code null} when the DDM names no format
*/
private static @Nullable String format(String declaredType, List<String> trailing) {
String letter = FORMATS.get(declaredType);
if (letter == null) {
return null;
}
List<String> numbers = trailing.stream().filter(t -> t.matches("\\d+")).toList();
if (numbers.isEmpty()) {
return letter;
}
if (numbers.size() >= 2 && "0".equals(numbers.get(1))) {
return letter + numbers.get(0);
}
return letter + String.join(",", numbers);
}
/**
* One column declaration's parts.
*/
private record Reference(String naturalName, @Nullable String shortName, @Nullable String format) {
}
}

View File

@@ -0,0 +1,80 @@
package com.agenticcode.parsernatjav;
import org.jspecify.annotations.Nullable;
import java.util.Map;
/**
* Resolves a DDM class name to the ADABAS file it describes. A NatJav view declares
* {@code @L1 @ViewOf SYSTEM.MDUMKONT_DDM MDUMKONT = view("MDUMKONT")}: the <em>class</em> is named
* here, the table name and every column name live in that other file. This is what carries them
* across.
*
* <p>Supplied by the ingest layer, which has filesystem access; the parser SPI itself stays
* file-agnostic — the same split as {@code CopycodeResolver} on the Natural side. {@link #NONE}
* knows no DDMs, in which case a view is emitted without its columns and without a table link.
*/
@FunctionalInterface
public interface NatJavDdmResolver {
/**
* A resolver that knows no DDM.
*/
NatJavDdmResolver NONE = name -> null;
/**
* @param ddmClassName the simple class name a view's {@code @ViewOf} refers to, e.g.
* {@code MDUMKONT_DDM}
* @return the DDM, or {@code null} if it is not among the project's files
*/
@Nullable Ddm resolve(String ddmClassName);
/**
* Resolves a view <em>declared in a data area</em>. A program that {@code USING}s an area re-declares
* its views as fields typed by the area:
* {@code public final KORIN…YLPOSLV_DA.SWPOS SWPOS = YLPOSLV_DA.SWPOS;} — and then reads
* {@code find(SWPOS, …)}. The view itself, and with it the DDM, lives in the area's file, so the
* statement's table is only knowable across files.
*
* @param areaClassName the data area's simple class name, e.g. {@code YLPOSLV_DA}
* @param viewName the view's Java field name inside it, e.g. {@code SWPOS}
* @return the DDM that view reads, or {@code null} if the area or the view is unknown
*/
default @Nullable Ddm resolveAreaView(String areaClassName, String viewName) {
return null;
}
/**
* Resolves a <em>field</em> of a data area to its Natural name. A dataflow operand names Java
* identifiers throughout ({@code WSAIVP.INHALT}), while the field's node carries the Natural name
* the transpiler preserved in the area's own file ({@code INHALT} may well be {@code #INHALT} or
* {@code IN-HALT}). Measured over the corpus, 77.6 % of assignment targets are qualified this
* way, so this is the bridge the whole of item E1 rests on.
*
* <p>Flat over the area rather than per group: the operand's prefix identifies the <em>area</em>
* (through the program's exposure field's declared type), and the field name within it is what
* has to be translated.
*
* @param areaClassName the data area's simple class name, e.g. {@code WSAIVP_DA}
* @param javaFieldName the field's Java identifier, e.g. {@code INHALT}
* @return the field's Natural name, or {@code null} if the area or the field is unknown
*/
default @Nullable String resolveAreaField(String areaClassName, String javaFieldName) {
return null;
}
/**
* One data definition module.
*
* @param tableName the ADABAS file's Natural name ({@code MDUMKONT}) — the name every
* {@code DB_TABLE} node in the graph is keyed by
* @param columns Java field name → the column's Natural name, e.g. {@code VR_KEY} →
* {@code VR-KEY}. A view declares its columns by Java name only, so this is the
* only way to give them the name the Natural original used.
* @param formats Java field name → the column's Natural type spec where the DDM states one
* ({@code N3}, {@code P8}); absent for the 16,148 of 17,982 references that
* name only the field, whose length lives in the FDT class
*/
record Ddm(String tableName, Map<String, String> columns, Map<String, String> formats) {
}
}

View File

@@ -0,0 +1,158 @@
package com.agenticcode.parsernatjav;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.MemberValuePair;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import org.jspecify.annotations.Nullable;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The module-level facts of one NatJav compilation unit, shared by {@link NatJavParser} and
* {@link NatJavCoarseScanner} so a coarse scan and a deep parse can never disagree about a
* module's identity.
*
* @param naturalName the <b>Natural</b> object name — the module's identity in the graph, so that a
* NatJav project answers the same queries as its Natural original
* ({@code V-MIT04}, not {@code V_MIT04}; the GDA {@code COMMON}, not
* {@code COMMON_DA})
* @param className the Java class name the transpiler generated
* @param packageName the Java package (the transpiled Natural library path), or {@code null}
* @param kind the Natural object type, or {@code null} for a class that is not NatJav at all
* @param description the migrated header's {@code @FUNCTION} text, or {@code null}
* @param owner the migrated header's {@code @OWNER} code, or {@code null}
*/
public record NatJavFacts(String naturalName, String className, @Nullable String packageName,
@Nullable NatJavModuleKind kind, @Nullable String description,
@Nullable String owner) {
/**
* {@code @FUNCTION <text>} of the {@code *LCM-HEADER} block the migration wrote into each class's
* Javadoc — the module's one-line purpose, and the closest equivalent to the {@code **SAG TITLE}
* line the Natural parser reads.
*/
private static final Pattern HEADER_FUNCTION = Pattern.compile("@FUNCTION\\s+(.+?)\\s*\\**\\s*$");
/**
* {@code @OWNER <code>} of the same header block: the owning developer/department key.
*/
private static final Pattern HEADER_OWNER = Pattern.compile("@OWNER\\s+(\\S+)");
/**
* Leading Javadoc decoration of one header line: {@code " * * "} — the comment's own asterisks
* plus the ones the original Natural comment box drew.
*/
private static final Pattern JAVADOC_DECORATION = Pattern.compile("^[\\s*]+");
/**
* Reads the facts of {@code unit}'s primary type.
*
* @param unit the parsed compilation unit
* @param stem the file's name without extension, used to pick the primary type among several
* top-level types and as the identity fallback for a unit with no type at all
* @return the facts, never {@code null}
*/
public static NatJavFacts of(CompilationUnit unit, String stem) {
TypeDeclaration<?> type = primaryType(unit, stem);
if (type == null) {
return new NatJavFacts(stem, stem, packageName(unit), null, null, null);
}
String className = type.getNameAsString();
NatJavModuleKind kind = NatJavModuleKind.ofBaseClass(baseClassName(type));
// The transpiler emits `@Natural(moduleName="...")` only where the Natural name cannot be
// spelled as a Java identifier — hyphens (V-MIT04), a leading '#' (#ABBRUCH), or the _DA/_DDM
// suffix it appends. Where the two agree it writes a bare `@Natural`, so the class name is
// the Natural name. Never derive the name from the class name by string surgery: measured
// over the reference corpus that rule is wrong for 72 of 7,093 annotated modules, because
// Natural names may legitimately contain '_' (V_MIT11, T-PRFZ11 vs T_PRFZ11).
String naturalName = moduleNameAnnotation(type).orElse(className);
return new NatJavFacts(naturalName, className, packageName(unit), kind,
headerValue(type, HEADER_FUNCTION), headerValue(type, HEADER_OWNER));
}
/**
* @return the type declaration that <em>is</em> the module: the top-level type named after the
* file, else the first top-level type. Nested types (a data area's inner group classes) are not
* modules of their own.
*/
static @Nullable TypeDeclaration<?> primaryType(CompilationUnit unit, String stem) {
TypeDeclaration<?> first = null;
for (TypeDeclaration<?> type : unit.getTypes()) {
if (type.getNameAsString().equals(stem)) {
return type;
}
if (first == null) {
first = type;
}
}
return first;
}
private static @Nullable String packageName(CompilationUnit unit) {
return unit.getPackageDeclaration().map(p -> p.getNameAsString()).orElse(null);
}
/**
* @return the simple name of {@code type}'s superclass, or {@code null} if it has none
*/
private static @Nullable String baseClassName(TypeDeclaration<?> type) {
if (!(type instanceof ClassOrInterfaceDeclaration declaration)) {
return null;
}
return declaration.getExtendedTypes().stream()
.findFirst()
.map(ClassOrInterfaceType::getNameAsString)
.orElse(null);
}
/**
* @return the {@code moduleName} member of the type's {@code @Natural} annotation, if it has one
*/
private static Optional<String> moduleNameAnnotation(TypeDeclaration<?> type) {
for (AnnotationExpr annotation : type.getAnnotations()) {
if (!annotation.getNameAsString().equals("Natural")
|| !(annotation instanceof NormalAnnotationExpr normal)) {
continue;
}
for (MemberValuePair pair : normal.getPairs()) {
if (pair.getNameAsString().equals("moduleName")
&& pair.getValue() instanceof StringLiteralExpr literal) {
String value = literal.asString().trim();
if (!value.isEmpty()) {
return Optional.of(value);
}
}
}
}
return Optional.empty();
}
/**
* @return the first match of {@code pattern} in the type's Javadoc (the migrated
* {@code *LCM-HEADER} block), stripped of the comment's own decoration, or {@code null}
*/
private static @Nullable String headerValue(TypeDeclaration<?> type, Pattern pattern) {
Optional<String> javadoc = type.getComment().map(c -> c.getContent());
if (javadoc.isEmpty()) {
return null;
}
for (String raw : javadoc.get().split("\\R")) {
String line = JAVADOC_DECORATION.matcher(raw).replaceFirst("");
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
String value = matcher.group(1).trim();
if (!value.isEmpty()) {
return value;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,433 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Builds the field tree of a NatJav class — the transpiled form of a Natural {@code DEFINE DATA}
* block, whether it is a standalone data area ({@code *_DA.java}) or the local data a program
* declares inline (15,775 non-data-area files in the reference corpus do).
*
* <p>The nesting comes from the {@code @L1}..{@code @L7} <b>annotations</b>, not from Java scoping:
* every field is a flat member of the class and only its level number says what contains it. That
* is the same shape the Natural parser walks, so this keeps a level stack exactly as it does.
*
* <p>Names are taken from the declaration's Natural-name literal ({@code A(8, "#WA-SPARTEN")}),
* never derived from the Java identifier. Measured over the corpus's 709,376 named fields, the
* obvious rule (leading {@code x} → {@code #}, {@code g} → {@code +}, {@code _} → {@code -}) is
* wrong for <b>4.81 %</b> of them, because Natural names may mix {@code _} and {@code -}
* ({@code #IP_EL_STV-CODE}). Where no literal exists, the Java name stands and
* {@link #NATURAL_NAME_KNOWN} records that it is not the Natural one — a wrong-but-plausible name
* is worse than an honest Java one.
*/
final class NatJavFields {
/**
* Property set to {@code "false"} on a field whose Natural name the source does not state, so a
* caller can tell an authoritative name from a Java identifier standing in for one.
*/
static final String NATURAL_NAME_KNOWN = "naturalNameKnown";
/**
* Property carrying the Java field identifier when it differs from the node's name.
*/
static final String JAVA_NAME = "javaName";
/**
* Marks the structural declaration {@code @Ln Redefine <field>} — Natural's
* {@code n REDEFINE <field>} — so a caller can tell a redefinition from a real group.
*/
static final String REDEFINES = "redefines";
/**
* The scope annotations, mapping to the {@code DEFINE DATA} section keyword the Natural parser
* stores as {@code scope}. Exactly one appears per data area in the corpus (4,654 PARAMETER,
* 1,157 LOCAL, 7 GLOBAL — summing to all 5,818), which is what the {@code .pda}/{@code .lda}/
* {@code .gda} extension says for Natural.
*/
private static final Map<String, String> SCOPES = Map.of(
"Parameter", "PARAMETER",
"Local", "LOCAL",
"Global", "GLOBAL",
"Independent", "INDEPENDENT");
private NatJavFields() {
}
/**
* Walks {@code type}'s field declarations and appends the resulting nodes and edges.
*
* @param type the module's primary type
* @param shell the file's {@code MODULE}/{@code DATA_STRUCTURE} node, which owns every
* top-level field
* @param moduleName the shell's Natural name, needed for the collapse rule below
* @param sourceFile the file's project-relative path
* @param nodes collects the created nodes
* @param edges collects the created {@code CONTAINS} edges
*/
static Declared collect(TypeDeclaration<?> type, AstNode shell, String moduleName,
String sourceFile, NatJavDdmResolver ddms, NatJavTables tables,
List<AstNode> nodes, List<AstEdge> edges) {
Map<String, View> views = new LinkedHashMap<>();
Deque<Level> stack = new ArrayDeque<>();
stack.push(new Level(0, shell));
// Java name -> the node it declared, so `Redefine xSYS_SCHALTER` and an array bound naming a
// variable resolve to the Natural name without guessing.
Map<String, AstNode> byJavaName = new HashMap<>();
String scope = "";
// A view's columns are declared without names or lengths — both live in the DDM class, which
// is another file. They are resolved with the DB access that reads them; until then the view
// itself is emitted and everything nested under it skipped, rather than inventing names.
int skipBelow = Integer.MAX_VALUE;
for (FieldDeclaration declaration : type.getFields()) {
String annotationScope = scopeOf(declaration);
if (annotationScope != null) {
scope = annotationScope;
}
Integer level = levelOf(declaration);
if (level == null || declaration.getVariables().isEmpty()) {
continue;
}
if (level > skipBelow) {
continue;
}
skipBelow = Integer.MAX_VALUE;
VariableDeclarator variable = declaration.getVariable(0);
String javaName = variable.getNameAsString();
String typeName = variable.getTypeAsString();
@Nullable MethodCallExpr baseCall = baseCall(variable);
@Nullable MethodCallExpr arrayCall = chainedCall(variable, "defineArray");
@Nullable MethodCallExpr constantCall = chainedCall(variable, "constant");
boolean isView = hasAnnotation(declaration, "ViewOf");
boolean isRedefine = typeName.equals("Redefine");
while (stack.size() > 1 && stack.peek().level >= level) {
stack.pop();
}
AstNode parent = stack.peek().node;
// Item-100 equivalent: the shell already carries the area's name, so a top-level group of
// that same name must not become a second node — 3,773 of the corpus's 5,818 data areas
// declare exactly that, and each would otherwise produce two identically named
// DATA_STRUCTUREs in one file.
String naturalName = naturalName(javaName, baseCall, isRedefine, byJavaName);
boolean isContainer = isRedefine || isView || !NatJavTypeSpec.isFormat(typeName);
if (level == 1 && !isView && isContainer && naturalName.equals(moduleName)) {
stack.push(new Level(level, shell));
byJavaName.put(javaName, shell);
continue;
}
AstNode node = fieldNode(naturalName, javaName, typeName, sourceFile, lineOf(declaration),
baseCall, arrayCall, constantCall, isView, isRedefine, scope, byJavaName);
nodes.add(node);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, parent.id(), node.id(),
node.startLine(), null));
// A REDEFINE re-declares the same Java identifier as the field it redefines. The symbol
// table must keep pointing at the field: an operand names the variable being written,
// not the redefinition group over it. The Natural parser keeps the same separation
// (`variables` vs `dataStructures`).
if (isRedefine) {
byJavaName.putIfAbsent(javaName, node);
} else {
byJavaName.put(javaName, node);
}
if (node.type() == NodeType.DATA_STRUCTURE) {
stack.push(new Level(level, node));
if (isView) {
// A view's columns are declared by Java name only; the DDM the @ViewOf names holds
// their Natural names and the table's. Everything nested under the view is taken
// from there instead of from the (nameless) declarations below it.
views.put(javaName, view(declaration, node, sourceFile, ddms, tables, nodes, edges));
skipBelow = level;
}
}
}
return new Declared(views, byJavaName);
}
/**
* What a file declares: its views (for DB statements) and its field symbol table (for dataflow
* operands, which name Java identifiers while the nodes carry Natural names).
*/
record Declared(Map<String, View> views, Map<String, AstNode> byJavaName) {
}
/**
* Links a view to the table it reads and gives its columns their Natural names, both out of the
* DDM class the {@code @ViewOf} annotation names.
*/
private static View view(FieldDeclaration declaration, AstNode viewNode, String sourceFile,
NatJavDdmResolver ddms, NatJavTables tables,
List<AstNode> nodes, List<AstEdge> edges) {
String ddmClass = ddmClassOf(declaration);
NatJavDdmResolver.Ddm ddm = ddmClass == null ? null : ddms.resolve(ddmClass);
if (ddm == null) {
// No DDM in this project (or none resolvable): the view stands, without columns and
// without a table — better than inventing either.
return new View(viewNode, null);
}
AstNode table = tables.get(ddm.tableName(), viewNode.startLine());
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.USES_TYPE, viewNode.id(), table.id(),
viewNode.startLine(), null));
for (VariableDeclarator column : columnsOf(declaration)) {
String javaName = column.getNameAsString();
String naturalName = ddm.columns().get(javaName);
if (naturalName == null) {
continue;
}
Map<String, String> properties = new LinkedHashMap<>();
properties.put(JAVA_NAME, javaName);
AstNode field = new AstNode(UUID.randomUUID(), NodeType.FIELD, naturalName, sourceFile,
NatJavParser.LANGUAGE, viewNode.startLine(), viewNode.startLine(),
ddm.formats().get(javaName), null, properties);
nodes.add(field);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, viewNode.id(), field.id(),
field.startLine(), null));
}
return new View(viewNode, table);
}
/**
* @return the simple class name of a view's {@code @ViewOf} target, or {@code null}
*/
private static @Nullable String ddmClassOf(FieldDeclaration declaration) {
for (AnnotationExpr annotation : declaration.getAnnotations()) {
if (!annotation.getNameAsString().equals("ViewOf")) {
continue;
}
String type = declaration.getVariable(0).getTypeAsString();
int dot = type.lastIndexOf('.');
return dot >= 0 ? type.substring(dot + 1) : type;
}
return null;
}
/**
* @return the column declarations nested under {@code view} — the field declarations that follow
* it at a deeper level, which the level walk skipped
*/
private static List<VariableDeclarator> columnsOf(FieldDeclaration view) {
List<VariableDeclarator> columns = new ArrayList<>();
Integer viewLevel = levelOf(view);
if (viewLevel == null || view.getParentNode().isEmpty()) {
return columns;
}
boolean seen = false;
for (com.github.javaparser.ast.Node sibling : view.getParentNode().get().getChildNodes()) {
if (sibling == view) {
seen = true;
continue;
}
if (!seen || !(sibling instanceof FieldDeclaration field)) {
continue;
}
Integer level = levelOf(field);
if (level == null || level <= viewLevel) {
break;
}
columns.addAll(field.getVariables());
}
return columns;
}
/**
* A view operand and the table it reads.
*
* @param node the view's own {@code DATA_STRUCTURE}, or {@code null} when the operand is an
* alias or a view declared in a {@code USING}'d data area — those have their node in
* the file that declares them, not here
* @param table the {@code DB_TABLE}, or {@code null} when the DDM could not be resolved
*/
record View(@Nullable AstNode node, @Nullable AstNode table) {
}
/**
* @return the field's Natural name: the declaration's name literal, the redefined field's name
* for a {@code Redefine}, else the Java identifier (flagged by the caller)
*/
private static String naturalName(String javaName, @Nullable MethodCallExpr baseCall,
boolean isRedefine, Map<String, AstNode> byJavaName) {
if (isRedefine) {
AstNode target = byJavaName.get(javaName);
return target != null ? target.name() : javaName;
}
String literal = nameLiteral(baseCall);
return literal != null ? literal : javaName;
}
private static AstNode fieldNode(String naturalName, String javaName, String typeName,
String sourceFile, int lineNo, @Nullable MethodCallExpr baseCall,
@Nullable MethodCallExpr arrayCall, @Nullable MethodCallExpr constantCall,
boolean isView, boolean isRedefine, String scope,
Map<String, AstNode> byJavaName) {
Map<String, String> properties = new LinkedHashMap<>();
if (!scope.isEmpty()) {
properties.put("scope", scope);
}
if (!naturalName.equals(javaName)) {
properties.put(JAVA_NAME, javaName);
} else if (!isRedefine && nameLiteral(baseCall) == null) {
properties.put(NATURAL_NAME_KNOWN, "false");
}
if (isRedefine) {
properties.put(REDEFINES, naturalName);
}
NodeType nodeType;
@Nullable String dataType = null;
@Nullable String value = null;
if (isRedefine || isView || !NatJavTypeSpec.isFormat(typeName)) {
// A Group, a Redefine and a view are all containers: no format, children nest under them.
nodeType = NodeType.DATA_STRUCTURE;
} else {
dataType = NatJavTypeSpec.render(typeName, baseCall, arrayCall,
name -> resolveBound(name, byJavaName));
if (constantCall != null) {
nodeType = NodeType.CONSTANT;
value = constantValue(constantCall, typeName);
} else {
nodeType = NodeType.VARIABLE;
}
}
return new AstNode(UUID.randomUUID(), nodeType, naturalName, sourceFile, NatJavParser.LANGUAGE,
lineNo, lineNo, dataType, value, properties);
}
/**
* @return the constant's value spelled as Natural's {@code CONST<...>} content — alphanumeric
* values quoted ({@code 'GET'}), numeric ones bare ({@code 0}), an array's values comma-joined
*/
private static String constantValue(MethodCallExpr constantCall, String format) {
List<String> values = new ArrayList<>();
for (Expression argument : constantCall.getArguments()) {
if (argument instanceof StringLiteralExpr literal) {
values.add(format.equals("A") ? "'" + literal.asString() + "'" : literal.asString());
} else if (argument instanceof MethodCallExpr call && call.getNameAsString().equals("val")
&& call.getArguments().isNonEmpty()
&& call.getArgument(0) instanceof StringLiteralExpr scaled) {
// val("0.83") carries a scaled decimal the transpiler could not write as a Java literal.
values.add(scaled.asString());
} else {
values.add(argument.toString().trim());
}
}
return String.join(",", values);
}
/**
* @return the Natural name of the variable used as an array bound, or the Java name if unknown
*/
private static String resolveBound(String javaName, Map<String, AstNode> byJavaName) {
AstNode bound = byJavaName.get(javaName);
return bound != null ? bound.name() : javaName;
}
/**
* @return the innermost call of the initializer chain — the format constructor {@code A(8, "X")},
* {@code group("X")} or {@code view("X")} — or {@code null} for a declaration with no initializer
*/
private static @Nullable MethodCallExpr baseCall(VariableDeclarator variable) {
Expression expression = variable.getInitializer().orElse(null);
while (expression instanceof MethodCallExpr call) {
Expression scope = call.getScope().orElse(null);
if (scope == null) {
return call;
}
expression = scope;
}
return null;
}
/**
* @return the chained call named {@code method} ({@code defineArray}, {@code constant}) anywhere
* in the initializer chain, or {@code null}
*/
private static @Nullable MethodCallExpr chainedCall(VariableDeclarator variable, String method) {
Expression expression = variable.getInitializer().orElse(null);
while (expression instanceof MethodCallExpr call) {
if (call.getNameAsString().equals(method)) {
return call;
}
expression = call.getScope().orElse(null);
}
return null;
}
/**
* @return the first string literal argument of {@code call} — the Natural name the transpiler
* preserved — or {@code null} if the declaration states none
*/
private static @Nullable String nameLiteral(@Nullable MethodCallExpr call) {
if (call == null) {
return null;
}
for (Expression argument : call.getArguments()) {
if (argument instanceof StringLiteralExpr literal) {
String value = literal.asString().trim();
return value.isEmpty() ? null : value;
}
}
return null;
}
/**
* @return the level from an {@code @L1}..{@code @L7} annotation, or {@code null} if the field
* carries none (a loop/database object, an event handler's helper — not data)
*/
private static @Nullable Integer levelOf(FieldDeclaration declaration) {
for (AnnotationExpr annotation : declaration.getAnnotations()) {
String name = annotation.getNameAsString();
if (name.length() == 2 && name.charAt(0) == 'L' && Character.isDigit(name.charAt(1))) {
return name.charAt(1) - '0';
}
}
return null;
}
private static @Nullable String scopeOf(FieldDeclaration declaration) {
for (AnnotationExpr annotation : declaration.getAnnotations()) {
String scope = SCOPES.get(annotation.getNameAsString());
if (scope != null) {
return scope;
}
}
return null;
}
private static boolean hasAnnotation(FieldDeclaration declaration, String name) {
return declaration.getAnnotations().stream().anyMatch(a -> a.getNameAsString().equals(name));
}
private static int lineOf(FieldDeclaration declaration) {
return declaration.getBegin().map(p -> p.line).orElse(1);
}
/**
* One entry of the level stack: the declared level and the node that contains what follows it.
*/
private record Level(int level, AstNode node) {
}
}

View File

@@ -0,0 +1,28 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.spi.LineCounter;
import com.agenticcode.parserjava.JavaLineCounter;
/**
* LoC/SLoC counter for NatJav. The comment and literal syntax is Java's, so the counting itself is
* delegated to {@link JavaLineCounter} rather than duplicated.
*
* <p>Delegation, deliberately not inheritance: {@link #language()} must answer {@code "natjav"},
* and an inherited {@code language()} would report these files as {@code "java"} and silently merge
* transpiled Natural into the hand-written-Java bucket of every per-language LoC rollup.
*/
public final class NatJavLineCounter implements LineCounter {
private final JavaLineCounter delegate = new JavaLineCounter();
@Override
public String language() {
return NatJavParser.LANGUAGE;
}
@Override
public LocMetrics count(String content) {
return delegate.count(content);
}
}

View File

@@ -0,0 +1,98 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.NodeType;
import org.jspecify.annotations.Nullable;
/**
* The Natural object type of a NatJav class, read off its {@code innowake.mee.natural.api} base
* class. Measured over the 22,808-file reference corpus, every generated class extends exactly one
* of these, so this is an <b>exact</b> classification — unlike the Natural parser, which can only
* guess {@code PROGRAM} vs {@code SUBPROGRAM} from the presence of a {@code PARAMETER} section
* (a flat {@code .nat} file carries no catalog metadata).
*
* <p>The {@link #nodeType()} mapping mirrors how the same object appears in a Natural ingest: a
* data area is the {@code DATA_STRUCTURE} a {@code .lda}/{@code .pda}/{@code .gda} produces,
* everything else is a {@code MODULE}.
*/
public enum NatJavModuleKind {
/**
* {@code NaturalProgram} — a Natural main program (8,393 of the reference corpus).
*/
PROGRAM("NaturalProgram", NodeType.MODULE),
/**
* {@code NaturalSubprogram} — a {@code CALLNAT} target (5,859).
*/
SUBPROGRAM("NaturalSubprogram", NodeType.MODULE),
/**
* {@code NaturalData} — an LDA/PDA/GDA data area (5,818; always named {@code *_DA.java}).
*/
DATA_AREA("NaturalData", NodeType.DATA_STRUCTURE),
/**
* {@code NaturalMap} — a Natural map (1,543).
*/
MAP("NaturalMap", NodeType.MODULE),
/**
* {@code NaturalForm} — a report/print form (354).
*/
FORM("NaturalForm", NodeType.MODULE),
/**
* {@code NaturalSubroutine} — an external subroutine (325).
*/
SUBROUTINE("NaturalSubroutine", NodeType.MODULE),
/**
* {@code NaturalDdm} — a data definition module, i.e. the schema of an ADABAS file (243; always
* named {@code *_DDM.java}). Kept as a {@code MODULE} in this phase: turning a DDM into the
* {@code DB_TABLE} the {@code db-accesses} endpoint reports is a data-access concern and is
* handled where views are resolved, not here.
*/
DDM("NaturalDdm", NodeType.MODULE),
/**
* {@code NaturalHelp} — a helproutine (142).
*/
HELP("NaturalHelp", NodeType.MODULE),
/**
* {@code NaturalFdt} — a field definition table (132).
*/
FDT("NaturalFdt", NodeType.MODULE);
private final String baseClass;
private final NodeType nodeType;
NatJavModuleKind(String baseClass, NodeType nodeType) {
this.baseClass = baseClass;
this.nodeType = nodeType;
}
/**
* @param baseClass the simple name of the class's {@code extends} target, e.g.
* {@code NaturalProgram}
* @return the matching kind, or {@code null} if {@code baseClass} is not a NatJav base type
* (a hand-written Java class in the same tree, which is not a NatJav module at all)
*/
public static @Nullable NatJavModuleKind ofBaseClass(@Nullable String baseClass) {
if (baseClass == null) {
return null;
}
for (NatJavModuleKind kind : values()) {
if (kind.baseClass.equals(baseClass)) {
return kind;
}
}
return null;
}
/**
* @return the simple name of the {@code innowake.mee.natural.api} base class this kind maps from
*/
public String baseClass() {
return baseClass;
}
/**
* @return the unified-AST node type this kind's file produces
*/
public NodeType nodeType() {
return nodeType;
}
}

View File

@@ -0,0 +1,178 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ParserConfiguration.LanguageLevel;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.TypeDeclaration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Parses <b>NatJav</b> — Software AG Natural transpiled to Java by the innoWake maxenso
* enterprise-engine — into the <em>same</em> unified AST the Natural parser produces, so a NatJav
* project answers the identical API as its Natural original.
*
* <p>The syntax layer is JavaParser (NatJav is valid Java); this class supplies the semantic
* mapping. Its advantage over the Natural parser is that the transpiler preserved the catalog
* metadata a flat {@code .nat} file loses: the base class states the Natural object type exactly
* ({@link NatJavModuleKind}) where {@code NaturalParser} can only guess PROGRAM vs SUBPROGRAM from
* a {@code PARAMETER} section.
*
* <p><b>Emitted so far:</b> the file's {@code MODULE} (or {@code DATA_STRUCTURE} for a data area)
* shell, its {@code DEFINE DATA} field tree ({@link NatJavFields}), its {@code USING} references
* ({@link NatJavUsings}) and its call graph ({@link NatJavCalls}, with copycode attribution from
* {@link NatJavCopycode}). DB access and dataflow follow in later phases; a view's columns come with
* the DB access that reads them.
*/
public final class NatJavParser implements LanguageParser {
/**
* The language identifier, distinct from {@code "java"} so a per-language rollup separates
* transpiled Natural from hand-written Java, and from {@code "natural"} so a mixed estate can
* tell an original from its migration.
*/
public static final String LANGUAGE = "natjav";
private static final ParserConfiguration PARSER_CONFIGURATION =
new ParserConfiguration().setLanguageLevel(LanguageLevel.JAVA_21);
/**
* @return the file's name without its extension — the transpiler names the file after the Java
* class, so this is the class name, not necessarily the Natural name
*/
static String stem(String sourceFile) {
String name = sourceFile;
int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
if (slash >= 0) {
name = name.substring(slash + 1);
}
int dot = name.lastIndexOf('.');
return dot >= 0 ? name.substring(0, dot) : name;
}
@Override
public String language() {
return LANGUAGE;
}
@Override
public ParseResult parse(String sourceFile, String content) {
return parse(sourceFile, content, NatJavDdmResolver.NONE);
}
/**
* Parses with the project's DDM index, which a view needs to name its table and its columns: a
* {@code @ViewOf} states only the DDM's class, and everything else about the ADABAS file lives in
* that other source. Without one ({@link NatJavDdmResolver#NONE}) views are still emitted, just
* without columns and without a table link.
*/
public ParseResult parse(String sourceFile, String content, NatJavDdmResolver ddms) {
return parseWithShell(sourceFile, content, ddms).result();
}
/**
* As {@link #parse}, but also names the file's shell node. The Tier-1 scan keeps the shell and
* drops the field tree, and must not have to <em>infer</em> which node the shell is: a data
* area's groups are {@code DATA_STRUCTURE}s too, so a type-based filter would keep all of them.
*/
Parsed parseWithShell(String sourceFile, String content, NatJavDdmResolver ddms) {
List<AstNode> nodes = new ArrayList<>();
List<AstEdge> edges = new ArrayList<>();
com.github.javaparser.ParseResult<CompilationUnit> parsed =
new com.github.javaparser.JavaParser(PARSER_CONFIGURATION).parse(content);
CompilationUnit unit = parsed.getResult()
.orElseThrow(() -> new ParseProblemException(parsed.getProblems()));
NatJavFacts facts = NatJavFacts.of(unit, stem(sourceFile));
AstNode module = moduleNode(facts, sourceFile, LocMetrics.physicalLineCount(content));
nodes.add(module);
TypeDeclaration<?> type = NatJavFacts.primaryType(unit, stem(sourceFile));
if (type != null) {
NatJavCopycode copycode = NatJavCopycode.of(content);
NatJavTables tables = new NatJavTables(nodes);
if (facts.kind() == NatJavModuleKind.DDM) {
// A DDM describes an ADABAS file rather than declaring data: its constants are the
// table's columns, and the table itself is a separate, file-less node.
NatJavDdm.collect(type, module, facts.naturalName(), sourceFile, nodes, edges);
} else {
NatJavFields.Declared declared = NatJavFields.collect(type, module,
facts.naturalName(), sourceFile, ddms, tables, nodes, edges);
Map<String, AstNode> areas = NatJavUsings.collect(type, module, nodes, edges);
Map<String, AstNode> subroutines =
NatJavCalls.collect(type, module, sourceFile, copycode, nodes, edges);
NatJavDbAccess.collect(type, module, sourceFile, declared.views(), tables, subroutines,
copycode, ddms, nodes, edges);
NatJavDataflow.collect(type, module, declared.byJavaName(),
NatJavDbAccess.exposures(type), areas, subroutines, ddms, copycode, nodes, edges);
NatJavControlFlow.collect(type, module, sourceFile, subroutines, copycode, nodes, edges);
if (NatJavPayload.collect(type, module, sourceFile, nodes, edges)) {
// Same marker the Natural parser sets, so the payload endpoint can find the
// wrappers without walking every module's nodes. Replaced rather than mutated:
// an AstNode is a record, and the copy keeps the id every edge already points at.
nodes.set(nodes.indexOf(module), withProperty(module, "xmlWrapper", "true"));
}
}
NatJavComments.collect(unit, sourceFile, nodes, edges);
}
return new Parsed(new ParseResult(nodes, edges), module);
}
/**
* @return a copy of {@code node} carrying one more property, with the same identity
*/
private static AstNode withProperty(AstNode node, String key, String value) {
Map<String, String> properties = new HashMap<>();
if (node.properties() != null) {
properties.putAll(node.properties());
}
properties.put(key, value);
return new AstNode(node.id(), node.type(), node.name(), node.sourceFile(), node.language(),
node.startLine(), node.endLine(), node.dataType(), node.value(), properties);
}
/**
* A parse result together with the {@code MODULE}/{@code DATA_STRUCTURE} node standing for the
* file itself.
*/
record Parsed(ParseResult result, AstNode shell) {
}
/**
* Builds the file's shell node. The line range is the whole file, not the type declaration's
* own range: in Natural a module <em>is</em> its source file, and the source/comment endpoints
* rely on that equivalence.
*/
private static AstNode moduleNode(NatJavFacts facts, String sourceFile, int lineCount) {
Map<String, String> properties = new HashMap<>();
properties.put(NatJavUsings.CLASS_NAME, facts.className());
if (facts.packageName() != null) {
properties.put("package", facts.packageName());
}
if (facts.kind() != null) {
// Same property name and PROGRAM/SUBPROGRAM vocabulary as the Natural parser, extended
// with the object types a .nat file cannot reveal (MAP, FORM, DDM, ...).
properties.put("moduleKind", facts.kind().name());
}
if (facts.description() != null) {
properties.put("description", facts.description());
}
if (facts.owner() != null) {
properties.put("owner", facts.owner());
}
NodeType type = facts.kind() != null ? facts.kind().nodeType() : NodeType.MODULE;
return new AstNode(UUID.randomUUID(), type, facts.naturalName(), sourceFile, LANGUAGE,
1, Math.max(1, lineCount), null, null, properties);
}
}

View File

@@ -0,0 +1,173 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import org.jspecify.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* The XML wire payload a module emits, as {@code PAYLOAD_FIELD} nodes — the same contract item 45
* models for Natural, reached through a different idiom.
*
* <p>None of the Natural patterns exist in the NatJav corpus: {@code COMPRESS '<' tag '>' value},
* {@code DEFINE SUBROUTINE ADD-XML-LINE} and {@code CALLNAT 'YFRAMN07'} occur zero times. This estate
* builds its XML through a framework subprogram instead, filling a parameter area line by line:
*
* <pre>{@code
* assign(XMLLINP.TYP, "TI");
* assign(XMLLINP.TAG, "ADRZEILE1");
* assign(XMLLINP.INHALT, YPADR2.ZEILE.get(1));
* callnat("XMLLIN", XMLLINP, ...);
* }</pre>
*
* <p>Two emitters share that shape — {@code XMLLIN} ("Ausgabe XML-Zeile", 7,086 calls) and
* {@code XMLDYN} ("… dynamische Länge", 1,818) — through their own parameter areas
* {@code XMLLINP}/{@code XMLDYNP}.
*
* <p><b>The direction is measured, not assumed.</b> Natural derives {@code REQUEST}/{@code RESPONSE}
* from {@code ADD-XML-LINE} vs {@code GET-XML-LINE}. Here the read counterpart {@code XMLREAD}
* ("Lesen 'abgeflachter' XML-Daten") exists as a program and carries the mirror-image
* {@code #TAG}/{@code #INHALT} outputs, but is called <b>zero</b> times in the corpus — so every one
* of the 10,238 tags is an emit, and {@code REQUEST} is a fact rather than a default.
*/
final class NatJavPayload {
/**
* The parameter areas the XML framework's emitters are driven through. Keyed by the Java field
* name a module exposes them under, which is also how the assignments address them.
*/
private static final Set<String> EMIT_AREAS = Set.of("XMLLINP", "XMLDYNP");
/**
* Field of the emit area holding the tag name.
*/
private static final String TAG = "TAG";
/**
* Fields holding the tag's content. {@code XMLDYNP} offers a fixed-length variant alongside the
* dynamic one.
*/
private static final Set<String> CONTENT = Set.of("INHALT", "INHALT_200");
/**
* Field holding the line type ({@code TI} text item, {@code ST}/{@code ET} element start/end, …).
*/
private static final String TYPE = "TYP";
/**
* Every tag in the corpus is written, never read — see the class comment.
*/
private static final String DIRECTION = "REQUEST";
private NatJavPayload() {
}
/**
* Appends the module's payload fields, and marks the module as an XML wrapper when it has any.
*
* @return whether this module emits XML at all
*/
static boolean collect(TypeDeclaration<?> type, AstNode module, String sourceFile,
List<AstNode> nodes, List<AstEdge> edges) {
boolean emits = false;
for (MethodDeclaration method : type.getMethods()) {
// Statement order carries the meaning: a TAG assignment is followed by the content
// assignment for that tag. Walking the calls in source order is what pairs them.
@Nullable String tag = null;
@Nullable String lineType = null;
int tagLine = 1;
for (MethodCallExpr call : method.findAll(MethodCallExpr.class)) {
if (!call.getNameAsString().equals("assign") || call.getScope().isPresent()
|| call.getArguments().size() < 2) {
continue;
}
Target target = targetOf(call.getArgument(0));
if (target == null) {
continue;
}
if (target.field.equals(TYPE)) {
lineType = literalOf(call.getArgument(1));
} else if (target.field.equals(TAG)) {
tag = literalOf(call.getArgument(1));
tagLine = call.getBegin().map(p -> p.line).orElse(1);
} else if (CONTENT.contains(target.field) && tag != null && !tag.isEmpty()) {
AstNode field = payload(tag, call.getArgument(1), lineType, sourceFile, tagLine);
if (field != null) {
nodes.add(field);
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.CONTAINS, module.id(),
field.id(), tagLine, null));
emits = true;
}
// One content assignment closes one tag; a following one belongs to the next.
tag = null;
}
}
}
return emits;
}
/**
* @return the payload field, or {@code null} when this line carries no content at all — a
* structural start/end tag, which the framework writes by assigning a blank. 1,200 of the
* corpus's 1,493 literal contents are that blank, and each would otherwise become a payload
* field whose "field" is a space.
*/
private static @Nullable AstNode payload(String tag, Expression content, @Nullable String lineType,
String sourceFile, int lineNo) {
@Nullable String literal = literalOf(content);
if (literal != null && literal.isEmpty()) {
return null;
}
Map<String, String> properties = new LinkedHashMap<>();
properties.put("tag", tag);
properties.put("field", literal != null ? literal : content.toString().trim());
properties.put("direction", DIRECTION);
if (literal != null) {
// A constant reaching the wire, not a field of the module's data. Marked rather than
// dropped: it is still part of the payload contract. The Natural side never sees these —
// its COMPRESS idiom matches an identifier — so this is a small, deliberate superset.
properties.put("constant", "true");
}
if (lineType != null) {
properties.put("lineType", lineType);
}
return new AstNode(UUID.randomUUID(), NodeType.PAYLOAD_FIELD, tag, sourceFile,
NatJavParser.LANGUAGE, lineNo, lineNo, null, null, properties);
}
/**
* @return the emit area and field an assignment targets ({@code XMLLINP.TAG}), or {@code null}
* when the assignment has nothing to do with the XML framework
*/
private static @Nullable Target targetOf(Expression operand) {
if (!(operand instanceof FieldAccessExpr access)) {
return null;
}
String area = access.getScope().toString();
int dot = area.lastIndexOf('.');
if (dot >= 0) {
area = area.substring(dot + 1);
}
return EMIT_AREAS.contains(area) ? new Target(area, access.getNameAsString()) : null;
}
private static @Nullable String literalOf(Expression expression) {
return expression instanceof StringLiteralExpr literal ? literal.asString().trim() : null;
}
private record Target(String area, String field) {
}
}

View File

@@ -0,0 +1,42 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.NodeType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* The {@code DB_TABLE} nodes of one parse, deduped by table name.
*
* <p>A {@code DB_TABLE} deliberately carries <b>no source file</b>. That is not an omission but the
* mechanism: the node merge key is {@code (type, name, sourceFile, project)}, so an empty
* {@code sourceFile} makes every module that touches a table converge on one node for it. Both the
* Natural and the Java parser build them that way, and all 72 tables of the {@code ac} project are
* file-less — a table with a file would sit <em>beside</em> the shared node instead of being it.
*/
final class NatJavTables {
private final Map<String, AstNode> byName = new HashMap<>();
private final List<AstNode> nodes;
NatJavTables(List<AstNode> nodes) {
this.nodes = nodes;
}
/**
* @param tableName the table's Natural name
* @param lineNo the line first referencing it, for the node's position
* @return the shared node for {@code tableName}, creating it on first reference
*/
AstNode get(String tableName, int lineNo) {
return byName.computeIfAbsent(tableName, name -> {
AstNode table = new AstNode(UUID.randomUUID(), NodeType.DB_TABLE, name, "",
NatJavParser.LANGUAGE, lineNo, lineNo, null, null, null);
nodes.add(table);
return table;
});
}
}

View File

@@ -0,0 +1,119 @@
package com.agenticcode.parsernatjav;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
/**
* Renders a NatJav field declaration's type into the <b>Natural</b> {@code dataType} spelling the
* {@link com.agenticcode.parsercore.ast.model.AstNode#dataType() unified AST} already uses for
* Natural sources — {@code A6}, {@code N15,3}, {@code A8/1:10} — so a query that filters on a data
* type finds a module and its migration alike.
*
* <p>Two conversions matter and are easy to get wrong:
* <ul>
* <li><b>A zero scale is dropped.</b> The transpiler always writes both digits ({@code N(3,0)}),
* Natural writes only the significant ones ({@code N3}). Keeping the {@code ,0} would give
* every integer field in the corpus a {@code dataType} its Natural original never had.</li>
* <li><b>Array bounds are appended with {@code /}</b>, not kept as a call: {@code A(200,"DATA")
* .defineArray(range(1, 9999))} is Natural's {@code (A200/1:9999)} → {@code A200/1:9999}.</li>
* </ul>
*/
final class NatJavTypeSpec {
/**
* The Natural format letters the transpiler emits as constructor names: alphanumeric, numeric,
* packed, binary, logical, date, time and control. A declared field type outside this set is not
* a Natural format (a {@code Group}, a {@code Redefine} marker, a view's DDM class).
*/
private static final List<String> FORMATS = List.of("A", "N", "P", "B", "L", "D", "T", "C");
private NatJavTypeSpec() {
}
/**
* @return whether {@code typeName} is a Natural format letter rather than a structural marker
*/
static boolean isFormat(String typeName) {
return FORMATS.contains(typeName);
}
/**
* Renders {@code A(8)}/{@code N(3,0)}/{@code A()} plus any {@code defineArray(...)} into the
* Natural spelling.
*
* @param format the format letter, e.g. {@code A}
* @param baseCall the format constructor call, or {@code null} for a declaration with no
* initializer (a view column, whose length lives in the DDM)
* @param arrayCall the {@code defineArray(...)} call, or {@code null} if the field is scalar
* @param resolveName maps a Java identifier used as an array bound to its Natural name
* @return e.g. {@code A8}, {@code N15,3}, {@code A200/1:9999}, or bare {@code A} for a dynamic
* or DDM-typed field
*/
static String render(String format, @Nullable MethodCallExpr baseCall,
@Nullable MethodCallExpr arrayCall, UnaryOperator<String> resolveName) {
StringBuilder spec = new StringBuilder(format);
if (baseCall != null) {
spec.append(lengthOf(baseCall));
}
if (arrayCall != null) {
String dims = dimensionsOf(arrayCall, resolveName);
if (!dims.isEmpty()) {
spec.append('/').append(dims);
}
}
return spec.toString();
}
/**
* @return the length/precision part of a format call: {@code ""} for {@code A()} (Natural
* {@code DYNAMIC}), {@code "8"} for {@code A(8)}, {@code "15,3"} for {@code N(15,3)} and
* {@code "3"} — not {@code "3,0"} — for {@code N(3,0)}
*/
private static String lengthOf(MethodCallExpr call) {
List<String> numbers = new ArrayList<>();
for (Expression argument : call.getArguments()) {
if (argument instanceof StringLiteralExpr) {
break; // the Natural-name literal ends the numeric part
}
numbers.add(argument.toString().trim());
}
if (numbers.isEmpty()) {
return "";
}
if (numbers.size() >= 2 && "0".equals(numbers.get(1))) {
return numbers.get(0);
}
return String.join(",", numbers);
}
/**
* @return the array bounds of {@code defineArray(range(a, b), ...)} as {@code a:b,c:d}. An
* unbounded {@code range(1, $)} becomes Natural's variable bound {@code 1:V}; a bound given as a
* variable is rendered under that variable's Natural name.
*/
private static String dimensionsOf(MethodCallExpr arrayCall, UnaryOperator<String> resolveName) {
List<String> dimensions = new ArrayList<>();
for (Expression argument : arrayCall.getArguments()) {
if (!(argument instanceof MethodCallExpr range) || !range.getNameAsString().equals("range")
|| range.getArguments().size() != 2) {
continue;
}
dimensions.add(bound(range.getArgument(0), resolveName) + ":" + bound(range.getArgument(1), resolveName));
}
return String.join(",", dimensions);
}
private static String bound(Expression expression, UnaryOperator<String> resolveName) {
String text = expression.toString().trim();
if (text.equals("$")) {
return "V";
}
return text.matches("\\d+") ? text : resolveName.apply(text);
}
}

View File

@@ -0,0 +1,210 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Emits the {@code INCLUDES} edges of a NatJav module's {@code @Using} declarations — the transpiled
* form of Natural's {@code LOCAL/PARAMETER/GLOBAL USING <data-area>} (72,145 of them in the
* reference corpus).
*
* <p>The target is an unresolved placeholder, exactly as in the Natural parser, but keyed
* <b>differently</b>: a {@code @Using} names the <em>Java class</em> ({@code WSAIVP_DA}) while the
* data area's node carries its <em>Natural</em> name ({@code WSAIVP}), and the two cannot be bridged
* by a string rule — stripping the {@code _DA} suffix produces the right Natural name for only
* 90.46 % of the corpus's 5,818 data areas (it misses {@code V_MIT03P_DA} → {@code V-MIT03P}, whose
* remaining underscores are ambiguous). The placeholder therefore carries the class name in
* {@link #CLASS_NAME} and is redirected after ingest by matching that against the real node's own
* {@code className}, which is exact.
*/
final class NatJavUsings {
/**
* Property holding the referenced (or declaring) Java class's simple name — the key the
* post-ingest NatJav placeholder resolution joins on.
*/
static final String CLASS_NAME = "className";
/**
* Property on the {@code INCLUDES} edge naming the {@code DEFINE DATA} section the area belongs
* to: {@code PARAMETER}, {@code LOCAL}, {@code GLOBAL} or {@code INDEPENDENT}.
*/
/**
* The Java package the {@code @Using} takes its area from — the qualifier of the declared type,
* or, for an unqualified type, the including file's own package (Java's same-package rule;
* measured 1,193 of 1,193 unqualified {@code @Using} in the corpus name a class in the same
* directory, none an import). Item 184: this is what tells {@code COMMON_DA} in {@code KORIN}
* from {@code COMMON_DA} in {@code ABFRAGE}, which the class name alone cannot. The real area
* carries the same string as {@code package}, so the resolution is a plain equality. Absent when
* one module includes the same class name from two libraries — then no single library is right
* and the placeholder is left for the resolution to refuse, as it does today.
*/
static final String USING_PACKAGE = "usingPackage";
static final String SCOPE = "scope";
/**
* Property on a {@code PARAMETER} {@code INCLUDES} edge: the area's 0-based position in the
* subprogram's argument list.
*/
static final String PARAM_POSITION = "paramPosition";
private static final String PARAMETER = "PARAMETER";
private NatJavUsings() {
}
/**
* Appends one placeholder plus {@code INCLUDES} edge per distinct {@code @Using} declaration.
*
* @param type the module's primary type
* @param module the file's shell node, the source of every {@code INCLUDES} edge
* @param nodes collects the placeholder nodes
* @param edges collects the {@code INCLUDES} edges
* @return the placeholders by data-area class name, so a dataflow operand naming a field of one
* can be hung underneath it
*/
static Map<String, AstNode> collect(TypeDeclaration<?> type, AstNode module, List<AstNode> nodes, List<AstEdge> edges) {
// First pass: which library each area comes from, before any node exists — a node's
// properties are fixed at construction, and the second @Using of the same class name may
// contradict the first.
String ownPackage = type.findCompilationUnit()
.flatMap(CompilationUnit::getPackageDeclaration)
.map(pd -> pd.getNameAsString())
.orElse(null);
Map<String, String> packages = new HashMap<>();
Set<String> ambiguous = new HashSet<>();
for (FieldDeclaration declaration : type.getFields()) {
if (!isUsingDeclaration(declaration)) {
continue;
}
String typeName = declaration.getVariable(0).getTypeAsString();
String className = simpleName(typeName);
String pkg = packageOf(typeName, ownPackage);
if (className.isEmpty() || pkg == null) {
continue;
}
String previous = packages.putIfAbsent(className, pkg);
if (previous != null && !previous.equals(pkg)) {
ambiguous.add(className);
}
}
Map<String, AstNode> placeholders = new HashMap<>();
int parameterSlot = 0;
for (FieldDeclaration declaration : type.getFields()) {
if (!isUsingDeclaration(declaration)) {
continue;
}
String className = simpleName(declaration.getVariable(0).getTypeAsString());
if (className.isEmpty()) {
continue;
}
int lineNo = declaration.getBegin().map(p -> p.line).orElse(1);
// Deduped by class name: a repeated @Using must not create duplicate nodes that then
// collide on the (type, name, sourceFile, project) merge key. The key stays the simple
// class name because NatJavDbAccess.exposures() looks areas up by it.
AstNode placeholder = placeholders.computeIfAbsent(className, name -> {
Map<String, String> nodeProperties = new LinkedHashMap<>();
nodeProperties.put(CLASS_NAME, name);
String pkg = packages.get(name);
if (pkg != null && !ambiguous.contains(name)) {
nodeProperties.put(USING_PACKAGE, pkg);
}
AstNode node = new AstNode(UUID.randomUUID(), NodeType.DATA_STRUCTURE, name, "",
NatJavParser.LANGUAGE, lineNo, lineNo, null, null, nodeProperties);
nodes.add(node);
return node;
});
Map<String, String> properties = new LinkedHashMap<>();
String scope = scopeOf(declaration);
properties.put(SCOPE, scope);
if (PARAMETER.equals(scope)) {
// The position this area occupies in the subprogram's CALLNAT argument list. On the
// EDGE, never on the node: a data area is shared, and the very same one is parameter 0
// of one subprogram and parameter 2 of another — stamping the node would let whichever
// module was ingested last decide the answer for all of them.
properties.put(PARAM_POSITION, Integer.toString(parameterSlot++));
}
edges.add(new AstEdge(UUID.randomUUID(), EdgeType.INCLUDES, module.id(), placeholder.id(),
lineNo, null, properties));
}
return placeholders;
}
/**
* @return the {@code DEFINE DATA} section this {@code @Using} stands for, read off the factory
* the transpiler called: {@code parameterUsing} is {@code PARAMETER USING}, {@code localUsing} is
* {@code LOCAL USING}, {@code globalUsing} is {@code GLOBAL USING}. The corpus holds 13,696 /
* 59,895 / 1,698 of them, and without this the three are indistinguishable — a subprogram's
* interface would look exactly like its scratch data.
*/
private static String scopeOf(FieldDeclaration declaration) {
Expression initializer = declaration.getVariable(0).getInitializer().orElse(null);
if (initializer instanceof MethodCallExpr call) {
String factory = call.getNameAsString();
if (factory.equals("parameterUsing")) {
return PARAMETER;
}
if (factory.equals("globalUsing")) {
return "GLOBAL";
}
if (factory.equals("independentUsing")) {
return "INDEPENDENT";
}
}
return "LOCAL";
}
private static boolean isUsing(AnnotationExpr annotation) {
return annotation.getNameAsString().equals("Using");
}
/**
* @return the last segment of a possibly fully-qualified type name, without generic arguments
*/
private static boolean isUsingDeclaration(FieldDeclaration declaration) {
return declaration.getAnnotations().stream().anyMatch(NatJavUsings::isUsing)
&& !declaration.getVariables().isEmpty();
}
private static String withoutGenerics(String typeName) {
String name = typeName.trim();
int generics = name.indexOf('<');
return generics >= 0 ? name.substring(0, generics) : name;
}
private static String simpleName(String typeName) {
String name = withoutGenerics(typeName);
int dot = name.lastIndexOf('.');
return dot >= 0 ? name.substring(dot + 1) : name;
}
/**
* @return the package a {@code @Using}'s declared type lives in: its qualifier, or for an
* unqualified type the including file's own package ({@code null} in the default package)
*/
private static @Nullable String packageOf(String typeName, @Nullable String ownPackage) {
String name = withoutGenerics(typeName);
int dot = name.lastIndexOf('.');
return dot >= 0 ? name.substring(0, dot) : ownPackage;
}
}

View File

@@ -0,0 +1,13 @@
/**
* Parser for <b>NatJav</b> — Software AG Natural transpiled to Java by the innoWake maxenso
* enterprise-engine. NatJav is syntactically Java but semantically Natural: a module is a class
* annotated {@code @Natural} extending one of the {@code innowake.mee.natural.api} base types, a
* data area is a {@code NaturalData} subclass, and Natural statements survive as API calls
* ({@code perform}, {@code callnat}, {@code read}, {@code store}, ...).
*
* <p>The parser therefore maps onto the <em>same</em> unified AST as
* {@code com.agenticcode.parsernatural}, so a NatJav project answers the identical API as its
* Natural original.
*/
@org.jspecify.annotations.NullMarked
package com.agenticcode.parsernatjav;

View File

@@ -0,0 +1,202 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.CopycodeProperties;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
/**
* The call graph. {@code CDDCINP.java} is an unmodified corpus file that happens to hold every shape
* at once: three subroutines, calls from {@code run()} and from a subroutine, and two copycode
* expansions, one of which contains a call.
*/
class NatJavCallsTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
private ParseResult parse(String name) {
return parser.parse("fixtures/natjav/" + name, fixture(name));
}
private static AstNode node(ParseResult result, NodeType type, String name) {
return result.nodes().stream()
.filter(n -> n.type() == type && n.name().equals(name))
.findFirst()
.orElseThrow(() -> new AssertionError("no " + type + " named '" + name + "'"));
}
/**
* @return the {@code CALLS} edge from {@code source} to the node named {@code target}
*/
private static AstEdge call(ParseResult result, AstNode source, String target) {
return result.edges().stream()
.filter(e -> e.type() == EdgeType.CALLS && e.sourceId().equals(source.id())
&& result.nodes().stream().anyMatch(n -> n.id().equals(e.targetId())
&& n.name().equals(target)))
.findFirst()
.orElseThrow(() -> new AssertionError("no CALLS " + source.name() + " -> " + target));
}
private static @Nullable String prop(AstEdge edge, String key) {
Map<String, String> properties = edge.properties();
assertNotNull(properties, "edge has no properties");
return properties.get(key);
}
@Test
void subroutinesBecomeFunctions() {
ParseResult result = parse("CDDCINP.java");
AstNode module = node(result, NodeType.MODULE, "CDDCINP");
for (String name : List.of("CAPTURE_RESTORE_ENVIRONMENT", "FINAL_PROCESSING", "SET_USER_MSG")) {
AstNode function = node(result, NodeType.FUNCTION, name);
assertTrue(result.edges().stream().anyMatch(e -> e.type() == EdgeType.CONTAINS
&& e.sourceId().equals(module.id()) && e.targetId().equals(function.id())),
name + " must be contained by its module");
}
}
/**
* {@code run()} is the module body, not a subroutine — as in Natural, where only
* {@code DEFINE SUBROUTINE} makes a function. A {@code FUNCTION} here would add a module hop to
* every call the program makes.
*/
@Test
void runIsNotAFunction() {
ParseResult result = parse("CDDCINP.java");
assertTrue(result.nodes().stream()
.noneMatch(n -> n.type() == NodeType.FUNCTION && n.name().equals("run")),
"run() is the module body");
}
@Test
void performFromRunIsAttributedToTheModule() {
ParseResult result = parse("CDDCINP.java");
AstNode module = node(result, NodeType.MODULE, "CDDCINP");
AstNode target = node(result, NodeType.FUNCTION, "CAPTURE_RESTORE_ENVIRONMENT");
AstEdge edge = call(result, module, "CAPTURE_RESTORE_ENVIRONMENT");
assertEquals(target.id(), edge.targetId(), "a local PERFORM resolves inside the file");
assertEquals("PERFORM", prop(edge, "callKind"));
}
/**
* A {@code CALLNAT} names another module, which lives in another file — an unresolved placeholder
* keyed on the class name, because the target module's node carries its Natural name.
*/
@Test
void callnatBecomesAPlaceholderKeyedOnTheClassName() {
ParseResult result = parse("CDDCINP.java");
AstNode module = node(result, NodeType.MODULE, "CDDCINP");
AstNode target = node(result, NodeType.MODULE, "CDUTRANS");
assertEquals("", target.sourceFile());
assertNotNull(target.properties());
assertEquals("CDUTRANS", target.properties().get(NatJavUsings.CLASS_NAME));
assertEquals("CALLNAT", prop(call(result, module, "CDUTRANS"), "callKind"));
}
/**
* A call inside a subroutine belongs to that subroutine, not to the module — otherwise every
* caller list would report the program where it should report the routine.
*/
@Test
void callInsideASubroutineIsAttributedToIt() {
ParseResult result = parse("CDDCINP.java");
AstNode subroutine = node(result, NodeType.FUNCTION, "SET_USER_MSG");
assertEquals("CALLNAT", prop(call(result, subroutine, "CDUMSG"), "callKind"));
}
/**
* The transpiler inlined the copycode, so the call physically sits in this file — but it came
* from {@code CCENVIR}, and a caller list that says otherwise misattributes 23.3 % of the
* corpus's calls.
*/
@Test
void callInsideACopycodeExpansionCarriesItsOrigin() {
ParseResult result = parse("CDDCINP.java");
AstNode subroutine = node(result, NodeType.FUNCTION, "CAPTURE_RESTORE_ENVIRONMENT");
AstEdge edge = call(result, subroutine, "CDENVIR");
assertEquals("CCENVIR", prop(edge, CopycodeProperties.VIA_COPYCODE));
assertNotNull(prop(edge, CopycodeProperties.INCLUDED_AT), "the host's expansion line");
assertTrue(Integer.parseInt(requireProp(edge, CopycodeProperties.INCLUDED_AT)) < edge.lineNo(),
"the expansion starts before the call it contains");
}
/**
* {@link CopycodeProperties#INCLUDE_PATH} identifies one expansion site among the several that
* share a Natural copycode node. NatJav has no shared node — every node lives in the host file —
* so writing it would move nodes onto the copycode-resident merge key for no reason.
*/
@Test
void includePathIsNeverWritten() {
ParseResult result = parse("CDDCINP.java");
assertTrue(result.edges().stream().allMatch(e -> e.properties() == null
|| !e.properties().containsKey(CopycodeProperties.INCLUDE_PATH)),
"no edge carries includePath");
}
@Test
void callOutsideAnExpansionCarriesNoOrigin() {
ParseResult result = parse("CDDCINP.java");
AstNode module = node(result, NodeType.MODULE, "CDDCINP");
AstEdge edge = call(result, module, "CDUTRANS");
assertNull(prop(edge, CopycodeProperties.VIA_COPYCODE),
"this call is the program's own code");
}
/**
* The trailing comment on a subroutine looks like the Natural name and is not one — {@code
* FINAL_PROCESSING} is followed by "Required by CCDCOUT". The node keeps the Java method name,
* which is also what {@code perform} names.
*/
@Test
void subroutineKeepsTheJavaMethodName() {
ParseResult result = parse("CDDCINP.java");
assertEquals("FINAL_PROCESSING", node(result, NodeType.FUNCTION, "FINAL_PROCESSING").name());
}
/**
* A file with no subroutine at all still yields its calls, attributed to the module.
*/
@Test
void moduleWithoutSubroutinesStillEmitsCalls() {
ParseResult result = parse("ACOMMON.java");
AstNode module = node(result, NodeType.MODULE, "ACOMMON");
assertEquals("CALLNAT", prop(call(result, module, "WSAIV"), "callKind"));
}
private static String requireProp(AstEdge edge, String key) {
return Optional.ofNullable(prop(edge, key)).orElseThrow(() -> new AssertionError("no " + key));
}
}

View File

@@ -0,0 +1,90 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.model.SourceHash;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class NatJavCoarseScannerTest {
private final NatJavCoarseScanner scanner = new NatJavCoarseScanner();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
private static @Nullable String prop(AstNode node, String key) {
Map<String, String> properties = node.properties();
assertNotNull(properties, "shell " + node.name() + " has no properties");
return properties.get(key);
}
@Test
void languageIsNatjav() {
assertEquals("natjav", scanner.language());
}
@Test
void shellCarriesSourceHashAndLineMetrics() {
String content = fixture("MDBT74B.java");
ParseResult result = scanner.scan("fixtures/natjav/MDBT74B.java", content);
assertEquals(1, result.nodes().size());
AstNode shell = result.nodes().get(0);
assertEquals(NodeType.MODULE, shell.type());
assertEquals(SourceHash.of(content), prop(shell, SourceHash.PROPERTY));
assertEquals(Integer.toString(LocMetrics.physicalLineCount(content)),
prop(shell, LocMetrics.LOC));
assertNotNull(prop(shell, LocMetrics.SLOC));
}
/**
* Item 41 relies on the digest being a pure function of the file text: an unchanged file must
* reproduce it, or every refresh looks like a change.
*/
@Test
void sourceHashIsStableAcrossScans() {
String content = fixture("V_MIT04.java");
assertEquals(prop(scanner.scan("a/V_MIT04.java", content).nodes().get(0), SourceHash.PROPERTY),
prop(scanner.scan("a/V_MIT04.java", content).nodes().get(0), SourceHash.PROPERTY));
}
/**
* The coarse scan must not lose the identity the parser established — a Tier-1 project would
* otherwise key its modules differently from a Tier-2 one.
*/
@Test
void coarseScanKeepsTheNaturalNameAndKind() {
AstNode shell = scanner.scan("fixtures/natjav/V_MIT04.java", fixture("V_MIT04.java")).nodes().get(0);
assertEquals("V-MIT04", shell.name());
assertEquals("SUBPROGRAM", prop(shell, "moduleKind"));
assertEquals("V_MIT04", prop(shell, "className"));
}
@Test
void dataAreaShellIsStampedToo() {
AstNode shell = scanner.scan("fixtures/natjav/YPUMPRUE_DA.java", fixture("YPUMPRUE_DA.java")).nodes().get(0);
assertEquals(NodeType.DATA_STRUCTURE, shell.type());
assertEquals("YPUMPRUE", shell.name());
assertNotNull(prop(shell, SourceHash.PROPERTY));
}
}

View File

@@ -0,0 +1,170 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comment blocks. The grouping is the Java parser's; what is tested here is the one thing that
* differs — separating a Natural developer's comments from the transpiler's bookkeeping.
*/
class NatJavCommentsTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
private static List<AstNode> comments(ParseResult result) {
return result.nodes().stream().filter(n -> n.type() == NodeType.COMMENT).toList();
}
/**
* The migration header looks like pure framing and is not: 10,466 of the corpus's 14,749 files
* carry real prose inside it, often a fuller description than the single {@code @FUNCTION} line.
* Dropping the block would delete the documentation of all of them.
*/
@Test
void headerFramingGoesButItsProseStays() {
String retained = NatJavComments.retained("""
<pre>
*LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* header created during migration process *
**************************************************************
* @FUNCTION LOESCHEN UMKONTIERUNGEN
* @OWNER P36
**************************************************************
*
* Löschen Umkontierungen
*
</pre>
""");
assertEquals("Löschen Umkontierungen", retained);
}
/**
* The expansion markers carry nothing a reader wants — their content is already a call's
* {@code viaCopycode} — so they are dropped whole.
*/
@Test
void transpilerBookkeepingIsDroppedWhole() {
assertEquals("", NatJavComments.retained("@@INCLUDE:BEGIN(YCISN)"));
assertEquals("", NatJavComments.retained("@@USING:END(KORIN.Diverse.WSAIVP_DA)"));
assertEquals("", NatJavComments.retained("Generated by innoWake maxenso enterprise-engine."));
assertEquals("", NatJavComments.retained("additional loop and database objects"));
}
/**
* Two gaps the first corpus run exposed, both pure framing that reached the graph: the generator
* stamps per-line metadata into the header, and the corpus spells "migration" two ways —
* 57,484 times correctly and 1,347 times as "migraion".
*/
@Test
void generatorStampsAndTheMisspelledBoilerplateAreDroppedToo() {
assertEquals("", NatJavComments.retained("""
*innoWake versionRep2Str: 1.4.0211
*innoWake datumUmsetzung: 2014-02-11
*innoWake originalObjectName: MDBT74B
"""));
assertEquals("", NatJavComments.retained("header created during migraion process"));
// The label is not one word, and the corpus also carries it HTML-escaped.
assertEquals("", NatJavComments.retained("*innoWake Scanpunkte für manuelle Eingriffe: #iw#"));
assertEquals("", NatJavComments.retained("*innoWake Scanpunkte f&uuml;r manuelle Eingriffe: #iw#"));
assertEquals("", NatJavComments.retained("header created during migration process"));
}
/**
* A block mixing a generator stamp with a real comment must keep the comment — which is the whole
* reason the filter works per line rather than per block.
*/
@Test
void aStampNextToARealCommentLosesOnlyTheStamp() {
assertEquals("Setzen Attribute vor Input-Statement", NatJavComments.retained("""
*innoWake Scanpunkte für manuelle Eingriffe: #iw#
Setzen Attribute vor Input-Statement
"""));
}
@Test
void anOrdinaryCommentSurvivesUntouched() {
assertEquals("Steuerungsprogramm für GDA-Initialisierungen",
NatJavComments.retained("Steuerungsprogramm für GDA-Initialisierungen"));
}
/**
* A row of asterisks is the comment box's drawing, not a statement.
*/
@Test
void decorationOnlyLinesAreDropped() {
assertEquals("", NatJavComments.retained("*****************************"));
assertEquals("", NatJavComments.retained("-----------------------------"));
assertEquals("Text", NatJavComments.retained("=====\nText\n====="));
}
@Test
void commentsAreAttachedToWhatTheyDocument() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
@Override
public void run() { perform("SUB"); }
// Prüft die Eingabe
void SUB() { assign(A, 1); }
}
""");
AstNode comment = comments(result).stream()
.filter(n -> "Prüft die Eingabe".equals(n.value()))
.findFirst().orElseThrow(() -> new AssertionError("comment not kept: "
+ comments(result).stream().map(AstNode::value).toList()));
AstNode function = result.nodes().stream()
.filter(n -> n.type() == NodeType.FUNCTION && n.name().equals("SUB"))
.findFirst().orElseThrow();
assertTrue(result.edges().stream().anyMatch(e -> e.type() == EdgeType.DOCUMENTS
&& e.sourceId().equals(comment.id()) && e.targetId().equals(function.id())),
"the comment documents the subroutine that follows it");
}
/**
* A real corpus file: its comments must survive and its markers must not.
*/
@Test
void aCorpusFileKeepsItsCommentsAndNotItsMarkers() {
ParseResult result = parser.parse("fixtures/natjav/ACOMMON.java", fixture("ACOMMON.java"));
List<String> texts = comments(result).stream().map(AstNode::value).toList();
assertTrue(texts.stream().anyMatch(t -> t != null && t.contains("Steuerungsprogramm")),
"the developer's comment survives: " + texts);
assertTrue(texts.stream().noneMatch(t -> t != null && t.contains("@@USING")),
"no expansion marker became a comment");
assertTrue(texts.stream().noneMatch(t -> t != null && t.contains("Generated by innoWake")),
"no generation banner became a comment");
}
@Test
void languageIsNatjav() {
ParseResult result = parser.parse("fixtures/natjav/ACOMMON.java", fixture("ACOMMON.java"));
assertTrue(comments(result).stream().allMatch(n -> n.language().equals("natjav")));
}
}

View File

@@ -0,0 +1,232 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Control-flow blocks. The two things that matter here are what is <em>not</em> emitted: a database
* loop is not control flow, and a lowered {@code DECIDE} is one construct rather than a chain of ifs.
*/
class NatJavControlFlowTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
private static List<AstNode> blocks(ParseResult result, String kind) {
return result.nodes().stream()
.filter(n -> n.type() == NodeType.CONTROL_FLOW && kind.equals(n.name()))
.toList();
}
private ParseResult parse(String body) {
return parser.parse("W.java", "package X;\nimport innowake.mee.natural.api.*;\n@Natural\n"
+ "public class W extends NaturalProgram {\n"
+ " @Override\n public void run() {\n" + body + "\n }\n}\n");
}
@Test
void ifForAndWhileBecomeBlocks() {
ParseResult result = parse("""
if (EQ(A, "X")) { assign(B, 1); }
for (int i = 0; i < 3; i++) { assign(C, 2); }
while (LT(D, 9)) { addTo(D, 1); }
""");
assertEquals(1, blocks(result, "IF").size());
assertEquals(1, blocks(result, "FOR").size());
assertEquals(1, blocks(result, "REPEAT").size(), "a real while is Natural's REPEAT");
}
/**
* 20,749 of the corpus's 23,696 {@code while}s are read loops. The Natural parser models
* {@code IF}/{@code FOR}/{@code REPEAT}/{@code DECIDE} and never a {@code READ} loop, so these
* would be 20,749 blocks the original does not have.
*/
@Test
void aDatabaseLoopIsNotControlFlow() {
ParseResult result = parse("""
readUMK = read(MDUMKONT);
while (readUMK.hasNext()) { assign(A, 1); }
""");
assertTrue(blocks(result, "REPEAT").isEmpty(), "while (x.hasNext()) is a READ loop");
}
/**
* The transpiler lowers {@code DECIDE} into an if-chain around a counter. Emitting each arm
* separately would turn 9,340 dispatches into some 30,000 unrelated IFs.
*/
@Test
void aLoweredDecideIsOneBlockWithItsBranchValues() {
ParseResult result = parse("""
int decideCounter1 = 0;
if (EQ(xAPP, "KORIN")) { decideCounter1++; fetch("ACC_KR"); }
else if (EQ(xAPP, "HAGEL")) { decideCounter1++; fetch("ACC_HA"); }
""");
List<AstNode> decides = blocks(result, "DECIDE");
assertEquals(1, decides.size(), "one dispatch, not two ifs");
assertTrue(blocks(result, "IF").isEmpty(), "the arms are not separate IF blocks");
Map<String, String> properties = decides.get(0).properties();
assertNotNull(properties);
assertEquals("KORIN,HAGEL", properties.get("decideValues"));
}
/**
* An ordinary if-chain has no counter and must stay a plain {@code IF} — the counter is the only
* thing that distinguishes the two.
*/
@Test
void anOrdinaryIfChainIsNotADecide() {
ParseResult result = parse("""
if (EQ(A, "X")) { assign(B, 1); }
else if (EQ(A, "Y")) { assign(B, 2); }
""");
assertTrue(blocks(result, "DECIDE").isEmpty());
assertEquals(2, blocks(result, "IF").size(), "both arms are blocks");
}
/**
* Every arm of a chain must appear. The earlier assertion only demanded that <em>some</em> IF
* existed, and under it the walk silently dropped every second arm: the else-branch of an
* {@code if} <b>is</b> an {@code IfStmt}, and it was handed to a children-walk instead of being
* treated as a block.
*/
@Test
void everyArmOfAnIfChainBecomesABlock() {
ParseResult result = parse("""
if (EQ(A, "1")) { assign(B, 1); }
else if (EQ(A, "2")) { assign(B, 2); }
else if (EQ(A, "3")) { assign(B, 3); }
else { assign(B, 4); }
""");
List<String> conditions = blocks(result, "IF").stream()
.map(AstNode::value).sorted().toList();
assertEquals(List.of("EQ(A, \"1\")", "EQ(A, \"2\")", "EQ(A, \"3\")"), conditions);
}
/**
* A {@code for} and a {@code switch} must not hand themselves back as their own body — that would
* make the walk recognise them again without end.
*/
@Test
void loopsAndSwitchesTerminate() {
ParseResult result = parse("""
for (int i = 0; i < 3; i++) { assign(A, 1); }
switch (X) { case 1: assign(A, 2); break; default: break; }
""");
assertEquals(1, blocks(result, "FOR").size());
assertEquals(1, blocks(result, "DECIDE").size());
}
@Test
void blocksNestByContainment() {
ParseResult result = parse("""
if (EQ(A, "X")) {
for (int i = 0; i < 3; i++) { assign(B, 1); }
}
""");
AstNode outer = blocks(result, "IF").get(0);
AstNode inner = blocks(result, "FOR").get(0);
assertTrue(result.edges().stream().anyMatch(e -> e.type() == EdgeType.CONTAINS
&& e.sourceId().equals(outer.id()) && e.targetId().equals(inner.id())),
"the FOR nests inside the IF, not beside it");
}
/**
* A block inside a subroutine belongs to that subroutine, as its statements do.
*/
@Test
void aBlockInASubroutineBelongsToIt() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
@Override
public void run() { perform("SUB"); }
void SUB() {
if (EQ(A, "X")) { assign(B, 1); }
}
}
""");
AstNode function = result.nodes().stream()
.filter(n -> n.type() == NodeType.FUNCTION && n.name().equals("SUB"))
.findFirst().orElseThrow();
AstNode block = blocks(result, "IF").get(0);
assertTrue(result.edges().stream().anyMatch(e -> e.type() == EdgeType.CONTAINS
&& e.sourceId().equals(function.id()) && e.targetId().equals(block.id())));
}
/**
* The counter increment must be a <em>direct</em> statement of the arm. Searching the arm's whole
* subtree also found it through a nested {@code if}, so one dispatch was recognised again at each
* nesting level — 1,917 DECIDEs inside their own DECIDE in the corpus.
*/
@Test
void aNestedIfInsideADecideArmIsNotASecondDecide() {
ParseResult result = parse("""
int decideCounter1 = 0;
if (EQ(A, "X")) {
decideCounter1++;
if (EQ(B, "Y")) { assign(C, 1); }
}
else if (EQ(A, "Z")) { decideCounter1++; assign(C, 2); }
""");
assertEquals(1, blocks(result, "DECIDE").size(), "one dispatch, not one per nesting level");
assertEquals(1, blocks(result, "IF").size(), "the nested if stays a plain IF");
}
/**
* A dispatch on numbers has no string literals in its arms — it is a dispatch nonetheless.
* Deciding "is this a DECIDE?" by "does it have literal values?" classified those as plain ifs.
* The counter's name is not always numbered either ({@code decideCounterGWFMCHKC}).
*/
@Test
void aDispatchWithoutStringLiteralsIsStillADecide() {
ParseResult result = parse("""
int decideCounterGWFMCHKC = 0;
if (EQ(xIDX, 57)) { decideCounterGWFMCHKC++; assign(A, 1); }
else if (EQ(xIDX, 58)) { decideCounterGWFMCHKC++; assign(A, 2); }
""");
assertEquals(1, blocks(result, "DECIDE").size());
assertTrue(blocks(result, "IF").isEmpty());
}
@Test
void aRealCorpusFileYieldsBlocks() {
ParseResult result = parser.parse("fixtures/natjav/MDBT74B.java", fixture("MDBT74B.java"));
assertFalse(blocks(result, "IF").isEmpty(), "MDBT74B has conditionals");
assertTrue(blocks(result, "REPEAT").isEmpty(),
"its two while loops are both read/sort loops");
}
}

View File

@@ -0,0 +1,151 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* Statement-level dataflow. The operands name Java identifiers throughout while the field nodes
* carry Natural names, so every test here is really about that translation.
*/
class NatJavDataflowTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
private static AstNode node(ParseResult result, NodeType type, String name) {
return result.nodes().stream()
.filter(n -> n.type() == type && n.name().equals(name))
.findFirst()
.orElseThrow(() -> new AssertionError("no " + type + " named '" + name + "'"));
}
private static boolean hasEdge(ParseResult result, EdgeType type, AstNode from, AstNode to) {
return result.edges().stream().anyMatch(e -> e.type() == type
&& e.sourceId().equals(from.id()) && e.targetId().equals(to.id()));
}
/**
* {@code assign(target, source)} writes its first operand and reads the rest — and both are
* found through the file's own symbol table, under their Natural names.
*/
@Test
void assignWritesTheTargetAndReadsTheSource() {
ParseResult result = parser.parse("fixtures/natjav/MDBT74B.java", fixture("MDBT74B.java"),
NatJavDdmResolver.NONE);
AstNode module = node(result, NodeType.MODULE, "MDBT74B");
AstNode target = node(result, NodeType.VARIABLE, "#WN-DATN");
assertTrue(hasEdge(result, EdgeType.WRITES, module, target),
"assign(xWN_DATN, $DATN()) writes #WN-DATN");
}
/**
* A subscript is not a member: {@code xWA_UMKONT_MENGE.xRN_UMKONT_MENGE.get($)} names the field,
* not a field called {@code get}.
*/
@Test
void arraySubscriptIsStrippedFromTheOperand() {
ParseResult result = parser.parse("fixtures/natjav/MDBT74B.java", fixture("MDBT74B.java"),
NatJavDdmResolver.NONE);
AstNode field = node(result, NodeType.VARIABLE, "#RN-UMKONT-MENGE");
AstNode module = node(result, NodeType.MODULE, "MDBT74B");
assertTrue(hasEdge(result, EdgeType.WRITES, module, field)
|| hasEdge(result, EdgeType.READS, module, field),
"the subscripted field is still the operand");
}
/**
* The cross-file case, and the reason item E1 needs an index: {@code WSAIVP.INHALT} names a field
* of a {@code USING}'d data area. The placeholder must carry the field's <em>Natural</em> name,
* so the existing placeholder-field resolution — which joins on name — can redirect it.
*/
@Test
void fieldOfAUsedDataAreaBecomesAPlaceholderUnderItsNaturalName() {
NatJavDdmResolver ddms = new NatJavDdmResolver() {
@Override
public NatJavDdmResolver.@Nullable Ddm resolve(String ddmClassName) {
return null;
}
@Override
public @Nullable String resolveAreaField(String areaClassName, String javaFieldName) {
return areaClassName.equals("WSAIVP_DA") && javaFieldName.equals("INHALT")
? "IN-HALT" : null;
}
};
ParseResult result = parser.parse("fixtures/natjav/ACOMMON.java", fixture("ACOMMON.java"), ddms);
AstNode placeholder = node(result, NodeType.VARIABLE, "IN-HALT");
assertEquals("", placeholder.sourceFile(), "an unresolved field reference carries no file");
AstNode area = node(result, NodeType.DATA_STRUCTURE, "WSAIVP_DA");
assertTrue(hasEdge(result, EdgeType.CONTAINS, area, placeholder),
"it hangs under the data area it came from");
}
/**
* A subscript is not the end of the path. Cutting at the first {@code .get(} attributed
* {@code ARR.get(1).FLD} to the group {@code ARR}, so the field it names never saw the access.
*/
@Test
void anIndexedGroupMemberResolvesToTheMemberNotTheGroup() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
@Local
@L1 Group ARR = group("ARR");
@L2 A FLD = A(8 ,"#FLD");
@L1 A ZIEL = A(8 ,"#ZIEL");
@Override
public void run() {
move(ARR.get(1).FLD, into(ZIEL));
}
}
""", NatJavDdmResolver.NONE);
AstNode module = node(result, NodeType.MODULE, "W");
assertTrue(hasEdge(result, EdgeType.READS, module, node(result, NodeType.VARIABLE, "#FLD")),
"the indexed member is read");
assertFalse(hasEdge(result, EdgeType.READS, module, node(result, NodeType.DATA_STRUCTURE, "ARR")),
"the group is not");
}
/**
* Without the index nothing is invented: an unresolvable operand yields no edge rather than a
* plausible-looking wrong one.
*/
@Test
void unresolvableOperandYieldsNothing() {
ParseResult result = parser.parse("fixtures/natjav/ACOMMON.java", fixture("ACOMMON.java"),
NatJavDdmResolver.NONE);
assertTrue(result.nodes().stream().noneMatch(n -> n.type() == NodeType.VARIABLE
&& n.sourceFile().isEmpty()),
"no placeholder field without a known Natural name");
}
}

View File

@@ -0,0 +1,364 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* DB access, views and the DDM index they resolve through. {@code MDBT74B.java} and
* {@code MDUMKONT_DDM.java} are unmodified corpus files: the program reads a view, positions a
* second one and deletes through the loop object it kept.
*/
class NatJavDbAccessTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
/**
* @return a resolver holding just the corpus DDM this test needs
*/
private static NatJavDdmResolver ddms() {
NatJavDdmResolver.Ddm ddm = NatJavDdm.parse(fixture("MDUMKONT_DDM.java"), "MDUMKONT_DDM");
assertNotNull(ddm, "the DDM fixture must parse");
return name -> name.equals("MDUMKONT_DDM") ? ddm : null;
}
private ParseResult parse(String name, NatJavDdmResolver ddms) {
return parser.parse("fixtures/natjav/" + name, fixture(name), ddms);
}
private static AstNode node(ParseResult result, NodeType type, String name) {
return result.nodes().stream()
.filter(n -> n.type() == type && n.name().equals(name))
.findFirst()
.orElseThrow(() -> new AssertionError("no " + type + " named '" + name + "'"));
}
private static List<AstNode> nodes(ParseResult result, NodeType type) {
return result.nodes().stream().filter(n -> n.type() == type).toList();
}
private static boolean hasEdge(ParseResult result, EdgeType type, AstNode from, AstNode to) {
return result.edges().stream().anyMatch(e -> e.type() == type
&& e.sourceId().equals(from.id()) && e.targetId().equals(to.id()));
}
@Test
void ddmYieldsTableNameAndNaturalColumnNames() {
NatJavDdmResolver.Ddm ddm = NatJavDdm.parse(fixture("MDUMKONT_DDM.java"), "MDUMKONT_DDM");
assertNotNull(ddm);
assertEquals("MDUMKONT", ddm.tableName());
assertEquals("V-FREIGABE", ddm.columns().get("V_FREIGABE"),
"the view declares V_FREIGABE; only the DDM knows it is V-FREIGABE");
assertEquals("AUSLEER-DATUM", ddm.columns().get("AUSLEER_DATUM"));
}
/**
* A {@code DB_TABLE} carries no source file anywhere in this system — that is what makes every
* reference to a table converge on one node. The DDM file stays a module and is joined to the
* table by {@code MAPS_TO}, as the Java parser joins a JPA entity to its table.
*/
@Test
void ddmFileIsAModuleMappedToAFilelessTable() {
ParseResult result = parse("MDUMKONT_DDM.java", NatJavDdmResolver.NONE);
AstNode module = node(result, NodeType.MODULE, "MDUMKONT");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertEquals("DDM", properties(module).get("moduleKind"));
assertEquals("", table.sourceFile(), "a DB_TABLE is never file-backed");
assertTrue(hasEdge(result, EdgeType.MAPS_TO, module, table));
}
@Test
void ddmColumnsBecomeFieldsOfTheDdmModule() {
ParseResult result = parse("MDUMKONT_DDM.java", NatJavDdmResolver.NONE);
AstNode module = node(result, NodeType.MODULE, "MDUMKONT");
AstNode column = node(result, NodeType.FIELD, "AUSLEER-DATUM");
assertTrue(hasEdge(result, EdgeType.CONTAINS, module, column));
assertEquals("AUSLEER_DATUM", properties(column).get(NatJavFields.JAVA_NAME));
assertEquals("AF", properties(column).get("adabasShortName"),
"the ADABAS short name is only ever visible in the DDM");
}
@Test
void viewIsLinkedToItsTable() {
ParseResult result = parse("MDBT74B.java", ddms());
AstNode view = node(result, NodeType.DATA_STRUCTURE, "MDUMKONT");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.USES_TYPE, view, table));
}
/**
* The columns deferred in item B: a view declares them by Java name with no length, and the DDM
* supplies both the Natural name and, where it states one, the format.
*/
@Test
void viewColumnsGetTheirNaturalNamesFromTheDdm() {
ParseResult result = parse("MDBT74B.java", ddms());
AstNode view = node(result, NodeType.DATA_STRUCTURE, "MDUMKONT");
AstNode column = node(result, NodeType.FIELD, "AUSLEER-DATUM");
assertTrue(hasEdge(result, EdgeType.CONTAINS, view, column));
assertEquals("AUSLEER_DATUM", properties(column).get(NatJavFields.JAVA_NAME));
}
@Test
void readEmitsAnAccessAndAReadsEdge() {
ParseResult result = parse("MDBT74B.java", ddms());
AstNode module = node(result, NodeType.MODULE, "MDBT74B");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.READS, module, table));
assertTrue(nodes(result, NodeType.DB_ACCESS).stream()
.anyMatch(n -> "READ".equals(n.dataType()) && n.name().equals("MDUMKONT")),
"a READ access on MDUMKONT");
}
/**
* {@code delete(getDEL)} names the loop object, not the view. The table is only known at the
* {@code get} that positioned it, twelve lines earlier and in another block.
*/
@Test
void deleteResolvesThroughTheLoopObject() {
ParseResult result = parse("MDBT74B.java", ddms());
AstNode module = node(result, NodeType.MODULE, "MDBT74B");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.WRITES, module, table),
"the DELETE must reach the table the loop object was positioned on");
assertTrue(nodes(result, NodeType.DB_ACCESS).stream()
.anyMatch(n -> "DELETE".equals(n.dataType())),
"a DELETE access");
}
/**
* An array subscript is not a database statement. {@code UMK.SPARTEN.get(range(1, 4))} occurs
* 435,129 times in the corpus against 9,904 real {@code GET}s — counting it would report a
* fortyfold phantom.
*/
@Test
void arraySubscriptIsNotADatabaseGet() {
ParseResult result = parse("MDBT74B.java", ddms());
long reads = nodes(result, NodeType.DB_ACCESS).stream()
.filter(n -> "READ".equals(n.dataType())).count();
assertEquals(2, reads, "exactly the read(MDUMKONT) and the get(MDUMKONT_DEL, ...)");
}
/**
* Without a DDM index a view still exists — it is declared in the file — but nothing about the
* table is invented.
*/
@Test
void viewWithoutADdmIndexHasNoTable() {
ParseResult result = parse("MDBT74B.java", NatJavDdmResolver.NONE);
assertNotNull(node(result, NodeType.DATA_STRUCTURE, "MDUMKONT"));
assertTrue(nodes(result, NodeType.DB_TABLE).isEmpty(), "no table is guessed");
}
/**
* {@code workfile-accesses} aggregates the record buffer from the {@code READS}/{@code WRITES}
* edge, not from the access node — putting it on the node alone left every buffer list empty.
*/
@Test
void workfileRecordBufferIsOnTheEdgeAsWellAsTheNode() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
@Override
public void run() {
writeWorkFile(1, REC);
}
}
""", NatJavDdmResolver.NONE);
AstNode workfile = node(result, NodeType.WORKFILE, "1");
AstEdge write = result.edges().stream()
.filter(e -> e.type() == EdgeType.WRITES && e.targetId().equals(workfile.id()))
.findFirst().orElseThrow(() -> new AssertionError("no WRITES to the work file"));
assertNotNull(write.properties());
assertEquals("REC", write.properties().get("recordBuffer"));
assertEquals("REC", properties(node(result, NodeType.WORKFILE_ACCESS, "1")).get("recordBuffer"));
}
/**
* A statement rarely names the {@code @ViewOf} field itself. {@code VVSTICH.VT VTO = VT;} makes
* {@code VTO} an alias, and the declared type says so — 1,390 accesses in the corpus reach their
* table only this way.
*/
@Test
void aliasOfALocalViewResolvesToTheSameTable() {
ParseResult result = parser.parse("A.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class A extends NaturalProgram {
@Local
@L1 @ViewOf SYSTEM.MDUMKONT_DDM MDUMKONT = view("MDUMKONT");
@L2 A SATZART;
A.MDUMKONT ALIAS = MDUMKONT;
@Override
public void run() {
read(ALIAS);
}
}
""", ddms());
AstNode module = node(result, NodeType.MODULE, "A");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.READS, module, table),
"the alias must reach the table its view reads");
}
/**
* A view declared in a {@code USING}'d data area: the program re-declares it as a field typed by
* the area, and only that other file knows which DDM it reads.
*/
@Test
void viewOfAUsedDataAreaResolvesThroughTheIndex() {
NatJavDdmResolver.Ddm ddm = NatJavDdm.parse(fixture("MDUMKONT_DDM.java"), "MDUMKONT_DDM");
assertNotNull(ddm);
NatJavDdmResolver ddms = new NatJavDdmResolver() {
@Override
public NatJavDdmResolver.@Nullable Ddm resolve(String ddmClassName) {
return ddmClassName.equals("MDUMKONT_DDM") ? ddm : null;
}
@Override
public NatJavDdmResolver.@Nullable Ddm resolveAreaView(String areaClassName, String viewName) {
return areaClassName.equals("YLX_DA") && viewName.equals("SWPOS") ? ddm : null;
}
};
ParseResult result = parser.parse("B.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class B extends NaturalProgram {
@Using KORIN.YLX_DA YLX_DA = localUsing(KORIN.YLX_DA.class);
public final KORIN.deep.YLX_DA.SWPOS SWPOS = YLX_DA.SWPOS;
@Override
public void run() {
find(SWPOS, withLimit(1));
}
}
""", ddms);
AstNode module = node(result, NodeType.MODULE, "B");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.READS, module, table),
"a fully qualified data-area type must still resolve");
}
/**
* {@code findEvents} is {@code FIND} written with event blocks. Leaving it out lost 695 reads and
* left every {@code UPDATE} positioned by one without a table — 73 of them in the corpus.
*/
@Test
void eventFormOfAReadIsADatabaseStatementToo() {
ParseResult result = parser.parse("C.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class C extends NaturalProgram {
@Local
@L1 @ViewOf SYSTEM.MDUMKONT_DDM MDUMKONT = view("MDUMKONT");
@L2 A SATZART;
FindEvents findU;
@Override
public void run() {
findU = findEvents(MDUMKONT, withHold());
update(findU);
}
}
""", ddms());
AstNode module = node(result, NodeType.MODULE, "C");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.READS, module, table), "findEvents is a read");
assertTrue(hasEdge(result, EdgeType.WRITES, module, table),
"and the update it positions reaches the same table");
}
/**
* The alias can also be written with the view's own generated type, unqualified —
* {@code VERTRAG_VD VD = VERTRAG_VD;} — and a {@code STORE} positions a record an
* {@code UPDATE} may follow on. Both were the last four unresolved accesses in the corpus.
*/
@Test
void unqualifiedAliasAndStorePositionedUpdateResolve() {
ParseResult result = parser.parse("D.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class D extends NaturalProgram {
@Local
@L1 @ViewOf SYSTEM.MDUMKONT_DDM MDUMKONT = view("MDUMKONT");
@L2 A SATZART;
MDUMKONT VD = MDUMKONT;
Store storeS1;
@Override
public void run() {
read(VD);
storeS1 = store(MDUMKONT);
update(storeS1);
}
}
""", ddms());
AstNode module = node(result, NodeType.MODULE, "D");
AstNode table = node(result, NodeType.DB_TABLE, "MDUMKONT");
assertTrue(hasEdge(result, EdgeType.READS, module, table), "the unqualified alias resolves");
assertTrue(nodes(result, NodeType.DB_ACCESS).stream()
.allMatch(a -> !a.name().isEmpty()),
"no access is left without a table: " + nodes(result, NodeType.DB_ACCESS).stream()
.filter(a -> a.name().isEmpty()).map(AstNode::value).toList());
}
@Test
void everyEdgeConnectsEmittedNodes() {
for (String name : List.of("MDBT74B.java", "MDUMKONT_DDM.java")) {
ParseResult result = parse(name, ddms());
for (AstEdge edge : result.edges()) {
assertTrue(result.nodes().stream().anyMatch(n -> n.id().equals(edge.sourceId())),
name + ": edge source missing");
assertTrue(result.nodes().stream().anyMatch(n -> n.id().equals(edge.targetId())),
name + ": edge target missing");
}
}
}
private static Map<String, String> properties(AstNode node) {
Map<String, String> properties = node.properties();
assertNotNull(properties, node.name() + " has no properties");
return properties;
}
}

View File

@@ -0,0 +1,343 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstEdge;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* The {@code DEFINE DATA} field tree. Fixtures are unmodified corpus files.
*/
class NatJavFieldsTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
try {
return Files.readString(Path.of("src/test/resources/fixtures/natjav", name), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
private static AstNode node(ParseResult result, NodeType type, String name) {
return result.nodes().stream()
.filter(n -> n.type() == type && n.name().equals(name))
.findFirst()
.orElseThrow(() -> new AssertionError("no " + type + " named '" + name + "' among "
+ result.nodes().stream().map(AstNode::name).toList()));
}
private static @Nullable String prop(AstNode node, String key) {
Map<String, String> properties = node.properties();
assertNotNull(properties, "node " + node.name() + " has no properties");
return properties.get(key);
}
private static boolean contains(ParseResult result, AstNode parent, AstNode child) {
return result.edges().stream().anyMatch(e -> e.type() == EdgeType.CONTAINS
&& e.sourceId().equals(parent.id()) && e.targetId().equals(child.id()));
}
private ParseResult parse(String name) {
return parser.parse("fixtures/natjav/" + name, fixture(name));
}
@Test
void groupAndItsFieldsUseTheNaturalNames() {
ParseResult result = parse("YPMA71VK_DA.java");
AstNode area = node(result, NodeType.DATA_STRUCTURE, "YPMA71VK");
AstNode taste = node(result, NodeType.VARIABLE, "MENU-TASTE");
assertEquals("A4", taste.dataType());
assertEquals("MENU_TASTE", prop(taste, NatJavFields.JAVA_NAME));
assertTrue(contains(result, area, taste), "the field hangs off the data area");
}
/**
* 3,773 of the corpus's 5,818 data areas open with a top-level group named exactly like the area.
* That group is the area, not a member of it — emitting both would put two identically named
* {@code DATA_STRUCTURE}s in one file.
*/
@Test
void topLevelGroupNamedLikeTheAreaIsNotASecondNode() {
ParseResult result = parse("YPMA71VK_DA.java");
assertEquals(1, result.nodes().stream()
.filter(n -> n.name().equals("YPMA71VK")).count(), "exactly one node carries the area name");
}
/**
* The transpiler always writes a scale ({@code N(9,0)}); Natural writes only significant digits
* ({@code N9}). Without this the migration's every integer field would carry a {@code dataType}
* its original never had.
*/
@Test
void zeroScaleIsDroppedFromNumericTypes() {
ParseResult result = parse("COMMON_DA.java");
assertEquals("N9", node(result, NodeType.VARIABLE, "+BBST-POLNR").dataType());
assertEquals("A17", node(result, NodeType.VARIABLE, "+BPOL-PARM").dataType());
}
/**
* {@code Redefine xSYS_SCHALTER} names a Java field, and its Natural name is only known from that
* field's own declaration — resolved through the class's symbol table, not by a string rule.
*/
@Test
void redefineTakesTheNameOfTheFieldItRedefines() {
ParseResult result = parse("COMMON_DA.java");
AstNode redefine = result.nodes().stream()
.filter(n -> n.type() == NodeType.DATA_STRUCTURE && "#SYS-SCHALTER".equals(n.name()))
.findFirst().orElseThrow(() -> new AssertionError("no redefine of #SYS-SCHALTER"));
assertEquals("#SYS-SCHALTER", prop(redefine, NatJavFields.REDEFINES));
AstNode nested = node(result, NodeType.VARIABLE, "#ERROR-OPTN");
assertTrue(contains(result, redefine, nested), "redefined subfields nest under the redefine");
}
@Test
void scopeComesFromTheAnnotation() {
assertEquals("GLOBAL", prop(node(parse("COMMON_DA.java"), NodeType.VARIABLE, "+PROGRAMM"), "scope"));
assertEquals("PARAMETER", prop(node(parse("YPMA71VK_DA.java"), NodeType.VARIABLE, "VR"), "scope"));
}
@Test
void arrayBoundsAreAppendedTheNaturalWay() {
ParseResult result = parse("YPUMKLOG_DA.java");
assertEquals("A200/1:9999", node(result, NodeType.VARIABLE, "DATA").dataType());
}
/**
* {@code A()} with no length is Natural's {@code A DYNAMIC} — a length must not be invented.
*/
@Test
void dynamicLengthYieldsTheBareFormat() {
assertEquals("A", node(parse("YPUMKLOG_DA.java"), NodeType.VARIABLE, "OPID").dataType());
}
@Test
void nestedGroupsNestByLevelAnnotation() {
ParseResult result = parse("YPUMKLOG_DA.java");
AstNode area = node(result, NodeType.DATA_STRUCTURE, "YPUMKLOG");
AstNode in = node(result, NodeType.DATA_STRUCTURE, "IN");
AstNode vrKey = node(result, NodeType.VARIABLE, "VR-KEY");
assertTrue(contains(result, area, in), "IN nests under the area");
assertTrue(contains(result, in, vrKey), "the field nests under IN, not under the area");
}
/**
* A field whose Natural name the source does not state keeps its Java name and says so, rather
* than carrying a derived name that looks authoritative and is wrong 4.81 % of the time.
*/
@Test
void fieldWithoutANameLiteralKeepsTheJavaNameAndIsFlagged() {
AstNode field = node(parse("YPUMKLOG_DA.java"), NodeType.VARIABLE, "TRACE_JN");
assertEquals("false", prop(field, NatJavFields.NATURAL_NAME_KNOWN));
assertNull(prop(field, NatJavFields.JAVA_NAME), "name and Java name are the same here");
}
/**
* A view's columns carry neither name nor length — both live in the DDM, another file — so the
* view is emitted and its columns are left to the DB-access phase rather than invented.
*/
@Test
void viewIsAStructureWithoutColumnsForNow() {
ParseResult result = parse("MDBT74B.java");
AstNode view = node(result, NodeType.DATA_STRUCTURE, "MDUMKONT");
assertTrue(result.edges().stream().noneMatch(e -> e.type() == EdgeType.CONTAINS
&& e.sourceId().equals(view.id())), "no column nodes under the view yet");
}
/**
* A program declares local data inline, exactly as Natural's {@code DEFINE DATA LOCAL} does —
* 15,775 non-data-area files in the corpus do.
*/
@Test
void programLocalDataIsParsedToo() {
ParseResult result = parse("MDBT74B.java");
AstNode module = node(result, NodeType.MODULE, "MDBT74B");
AstNode local = node(result, NodeType.VARIABLE, "#WN-ISN");
assertEquals("P9", local.dataType());
assertEquals("LOCAL", prop(local, "scope"));
assertTrue(contains(result, module, local));
}
@Test
void usingBecomesAnIncludesEdgeKeyedOnTheClassName() {
ParseResult result = parse("ACOMMON.java");
AstNode module = node(result, NodeType.MODULE, "ACOMMON");
AstNode placeholder = node(result, NodeType.DATA_STRUCTURE, "WSAIVP_DA");
assertEquals("", placeholder.sourceFile(), "an unresolved reference carries no source file");
assertEquals("WSAIVP_DA", prop(placeholder, NatJavUsings.CLASS_NAME));
assertTrue(result.edges().stream().anyMatch(e -> e.type() == EdgeType.INCLUDES
&& e.sourceId().equals(module.id()) && e.targetId().equals(placeholder.id())));
}
@Test
void repeatedUsingOfTheSameAreaYieldsOnePlaceholder() {
ParseResult result = parse("ACOMMON.java");
List<AstNode> placeholders = result.nodes().stream()
.filter(n -> n.sourceFile().isEmpty() && n.name().equals("GWLOGNAM_DA"))
.toList();
assertEquals(1, placeholders.size());
}
/**
* The transpiler distinguishes {@code parameterUsing} from {@code localUsing}, which is Natural's
* {@code PARAMETER USING} vs {@code LOCAL USING} — without it a subprogram's interface is
* indistinguishable from its scratch data.
*/
@Test
void usingScopeAndParameterSlotAreRecordedOnTheEdge() {
ParseResult result = parser.parse("V.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class V extends NaturalSubprogram {
@Using KORIN.A_DA A_DA = parameterUsing(KORIN.A_DA.class);
@Using KORIN.B_DA B_DA = parameterUsing(KORIN.B_DA.class);
@Using KORIN.C_DA C_DA = localUsing(KORIN.C_DA.class);
@Override
public void run() {
}
}
""");
assertEquals("0", includeProp(result, "A_DA", NatJavUsings.PARAM_POSITION));
assertEquals("1", includeProp(result, "B_DA", NatJavUsings.PARAM_POSITION));
assertEquals("PARAMETER", includeProp(result, "A_DA", NatJavUsings.SCOPE));
assertEquals("LOCAL", includeProp(result, "C_DA", NatJavUsings.SCOPE));
assertNull(includeProp(result, "C_DA", NatJavUsings.PARAM_POSITION),
"a LOCAL area occupies no argument slot");
}
/**
* @return a property of the {@code INCLUDES} edge pointing at the area named {@code area}
*/
/**
* Item 184: {@code COMMON_DA} exists in {@code KORIN} and in {@code ABFRAGE}. The class name
* cannot tell them apart; the qualifier the transpiler wrote can, and it is the full Java
* package the real area carries as {@code package}.
*/
@Test
void qualifiedUsingRecordsTheLibraryItNamesOnThePlaceholder() {
ParseResult result = parser.parse("V.java", """
package CSTENTW;
import innowake.mee.natural.api.*;
@Natural
public class V extends NaturalProgram {
@Using KORIN.Vertrag.COMMON_DA COMMON_DA = globalUsing(KORIN.Vertrag.COMMON_DA.class);
@Override
public void run() {
}
}
""");
AstNode placeholder = node(result, NodeType.DATA_STRUCTURE, "COMMON_DA");
assertEquals("KORIN.Vertrag", prop(placeholder, NatJavUsings.USING_PACKAGE),
"the qualifier, not the including module's own package");
assertEquals("COMMON_DA", placeholder.name(), "the node keeps the class name - the merge key must not move");
}
/**
* An unqualified type resolves to the file's own package (Java's same-package rule; in the corpus
* 1,193 of 1,193 unqualified {@code @Using} name a class in the same directory).
*/
@Test
void unqualifiedUsingRecordsTheIncludingFilesOwnPackage() {
ParseResult result = parser.parse("V.java", """
package KORIN.Vertrag.Beteiligung;
import innowake.mee.natural.api.*;
@Natural
public class V extends NaturalProgram {
@Using ABGB02P_DA ABGB02P_DA = localUsing(ABGB02P_DA.class);
@Override
public void run() {
}
}
""");
assertEquals("KORIN.Vertrag.Beteiligung",
prop(node(result, NodeType.DATA_STRUCTURE, "ABGB02P_DA"), NatJavUsings.USING_PACKAGE));
}
/**
* The same class name from two libraries in one module has no single right library. The
* placeholder stays one node (the merge key is the class name) and records no package, so the
* resolution refuses instead of binding to whichever came last.
*/
@Test
void usingTheSameClassNameFromTwoLibrariesRecordsNoPackage() {
ParseResult result = parser.parse("V.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class V extends NaturalProgram {
@Using KORIN.COMMON_DA A = globalUsing(KORIN.COMMON_DA.class);
@Using ABFRAGE.COMMON_DA B = globalUsing(ABFRAGE.COMMON_DA.class);
@Override
public void run() {
}
}
""");
List<AstNode> placeholders = result.nodes().stream()
.filter(n -> n.sourceFile().isEmpty() && n.name().equals("COMMON_DA"))
.toList();
assertEquals(1, placeholders.size());
assertNull(prop(placeholders.get(0), NatJavUsings.USING_PACKAGE));
assertEquals("COMMON_DA", prop(placeholders.get(0), NatJavUsings.CLASS_NAME));
}
private static @Nullable String includeProp(ParseResult result, String area, String key) {
AstNode target = node(result, NodeType.DATA_STRUCTURE, area);
AstEdge edge = result.edges().stream()
.filter(e -> e.type() == EdgeType.INCLUDES && e.targetId().equals(target.id()))
.findFirst().orElseThrow(() -> new AssertionError("no INCLUDES to " + area));
assertNotNull(edge.properties(), "the INCLUDES edge carries no properties");
return edge.properties().get(key);
}
/**
* Every emitted edge must connect two nodes of this parse result — a dangling endpoint would be
* persisted as an edge to nothing.
*/
@Test
void everyEdgeConnectsEmittedNodes() {
for (String name : List.of("COMMON_DA.java", "YPUMKLOG_DA.java", "MDBT74B.java", "ACOMMON.java")) {
ParseResult result = parse(name);
for (AstEdge edge : result.edges()) {
assertTrue(result.nodes().stream().anyMatch(n -> n.id().equals(edge.sourceId())),
name + ": edge source missing");
assertTrue(result.nodes().stream().anyMatch(n -> n.id().equals(edge.targetId())),
name + ": edge target missing");
}
}
}
}

View File

@@ -0,0 +1,45 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parserjava.JavaLineCounter;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class NatJavLineCounterTest {
private final NatJavLineCounter counter = new NatJavLineCounter();
/**
* The reason this class exists rather than a reused {@link JavaLineCounter}: reporting
* {@code "java"} here would merge transpiled Natural into the hand-written-Java bucket of every
* per-language LoC rollup.
*/
@Test
void languageIsNatjavNotJava() {
assertEquals("natjav", counter.language());
assertEquals("java", new JavaLineCounter().language());
}
@Test
void countsWithJavaCommentRules() {
String content = """
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
@Natural
public class X extends NaturalProgram {
// a comment
public void run() {
callnat("Y"); /* trailing */
}
}
""";
LocMetrics metrics = counter.count(content);
assertEquals(12, metrics.loc());
assertEquals(new JavaLineCounter().count(content).sloc(), metrics.sloc());
}
}

View File

@@ -0,0 +1,178 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.LocMetrics;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Fixtures are unmodified files from the reference corpus (innoWake-transpiled Natural), one per
* shape the naming and classification rules have to survive.
*/
class NatJavParserTest {
private final NatJavParser parser = new NatJavParser();
private static String fixture(String name) {
Path path = Path.of("src/test/resources/fixtures/natjav", name);
try {
return Files.readString(path, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException("cannot read fixture " + name, e);
}
}
/**
* The file's shell: the one node whose line range spans the whole file. Since the field tree
* landed, a parse result also holds the module's fields and its USING placeholders.
*/
private static AstNode only(LanguageParser.ParseResult result) {
List<AstNode> shells = result.nodes().stream()
.filter(n -> !n.sourceFile().isEmpty() && n.startLine() == 1
&& (n.type() == NodeType.MODULE || n.type() == NodeType.DATA_STRUCTURE)
&& n.properties() != null && n.properties().containsKey("className"))
.toList();
assertEquals(1, shells.size(), "exactly one file shell");
return shells.get(0);
}
private static @Nullable String rawProp(AstNode node, String key) {
Map<String, String> properties = node.properties();
return properties == null ? null : properties.get(key);
}
private static String prop(AstNode node, String key) {
Map<String, String> properties = node.properties();
assertNotNull(properties, "node " + node.name() + " has no properties");
return String.valueOf(properties.get(key));
}
private AstNode parseFixture(String name) {
return only(parser.parse("fixtures/natjav/" + name, fixture(name)));
}
@Test
void languageIsNatjav() {
assertEquals("natjav", parser.language());
}
@Test
void plainNaturalAnnotationTakesTheClassNameAsModuleName() {
AstNode module = parseFixture("ACOMMON.java");
assertEquals(NodeType.MODULE, module.type());
assertEquals("ACOMMON", module.name());
assertEquals("natjav", module.language());
assertEquals("PROGRAM", prop(module, "moduleKind"));
assertEquals("KORIN", prop(module, "package"));
assertEquals("ACOMMON", prop(module, "className"));
}
/**
* The whole point of reading {@code @Natural(moduleName=...)}: the graph must key this module as
* the Natural name {@code V-MIT04}, or a query written against the Natural original misses it.
*/
@Test
void hyphenatedNaturalNameWinsOverTheJavaClassName() {
AstNode module = parseFixture("V_MIT04.java");
assertEquals("V-MIT04", module.name());
assertEquals("V_MIT04", prop(module, "className"));
assertEquals("SUBPROGRAM", prop(module, "moduleKind"));
}
/**
* The transpiler spells a leading {@code #} as {@code X}, which no string rule could invert
* (plenty of modules legitimately start with X) — only the annotation carries the truth.
*/
@Test
void hashPrefixedNaturalNameIsRecovered() {
AstNode module = parseFixture("XABBRUCH.java");
assertEquals("#ABBRUCH", module.name());
assertEquals("XABBRUCH", prop(module, "className"));
}
@Test
void dataAreaIsADataStructureNamedWithoutTheDaSuffix() {
AstNode area = parseFixture("YPUMPRUE_DA.java");
assertEquals(NodeType.DATA_STRUCTURE, area.type());
assertEquals("YPUMPRUE", area.name());
assertEquals("YPUMPRUE_DA", prop(area, "className"));
assertEquals("DATA_AREA", prop(area, "moduleKind"));
}
@Test
void ddmIsRecognisedAndNamedWithoutTheDdmSuffix() {
AstNode ddm = parseFixture("BANKEZ_DDM.java");
assertEquals(NodeType.MODULE, ddm.type());
assertEquals("BANKEZ", ddm.name());
assertEquals("DDM", prop(ddm, "moduleKind"));
}
@Test
void migratedHeaderSuppliesDescriptionAndOwner() {
AstNode module = parseFixture("MDBT74B.java");
assertEquals("LOESCHEN UMKONTIERUNGEN", prop(module, "description"));
assertEquals("P36", prop(module, "owner"));
assertEquals("KORIN.Mitarbeiter.Verwaltung", prop(module, "package"));
}
/**
* {@code XABBRUCH} carries a second {@code @FUNCTION} line further down, inside an inlined
* copycode block. The header read must come from the class's own Javadoc, not from the first
* {@code @FUNCTION} anywhere in the file.
*/
@Test
void descriptionComesFromTheClassHeaderNotFromAnInlinedCopycode() {
assertEquals("Zentrale Abbruch-Routinen", prop(parseFixture("XABBRUCH.java"), "description"));
}
/**
* A module is its source file, as in Natural — the source and comment endpoints depend on the
* shell spanning the whole file rather than just the type declaration.
*/
@Test
void shellSpansTheWholeFile() {
String content = fixture("MDBT74B.java");
AstNode module = only(parser.parse("fixtures/natjav/MDBT74B.java", content));
assertEquals(1, module.startLine());
assertEquals(LocMetrics.physicalLineCount(content), module.endLine());
}
/**
* A hand-written Java class that slipped into a NatJav tree still gets a node — dropping it
* would make the file invisible rather than merely unclassified — but claims no Natural kind.
*/
@Test
void nonNatJavClassYieldsAModuleWithoutAKind() {
AstNode module = only(parser.parse("Helper.java",
"package util;\npublic class Helper { void run() {} }\n"));
assertEquals(NodeType.MODULE, module.type());
assertEquals("Helper", module.name());
assertNull(rawProp(module, "moduleKind"));
}
@Test
void stemStripsPathAndExtension() {
assertEquals("V_MIT04", NatJavParser.stem("KORIN/Mitarbeiter/V_MIT04.java"));
assertEquals("ACOMMON", NatJavParser.stem("ACOMMON.java"));
}
}

View File

@@ -0,0 +1,240 @@
package com.agenticcode.parsernatjav;
import com.agenticcode.parsercore.ast.model.AstNode;
import com.agenticcode.parsercore.ast.model.EdgeType;
import com.agenticcode.parsercore.ast.model.NodeType;
import com.agenticcode.parsercore.ast.spi.LanguageParser.ParseResult;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* The XML wire payload. The Natural idioms ({@code COMPRESS '<' tag '>'}, {@code ADD-XML-LINE},
* {@code YFRAMN07}) occur zero times in this corpus; the estate drives a framework subprogram
* through a parameter area instead.
*/
class NatJavPayloadTest {
private final NatJavParser parser = new NatJavParser();
private static List<AstNode> payloads(ParseResult result) {
return result.nodes().stream().filter(n -> n.type() == NodeType.PAYLOAD_FIELD).toList();
}
private static Map<String, String> properties(AstNode node) {
Map<String, String> properties = node.properties();
assertNotNull(properties, node.name() + " has no properties");
return properties;
}
@Test
void tagAndContentBecomeAPayloadField() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
@Using KORIN.XMLLINP_DA XMLLINP_DA = localUsing(KORIN.XMLLINP_DA.class);
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TYP, "TI");
assign(XMLLINP.TAG, "ADRZEILE1");
assign(XMLLINP.INHALT, YPADR2.ZEILE);
callnat("XMLLIN", XMLLINP);
}
}
""");
List<AstNode> fields = payloads(result);
assertEquals(1, fields.size());
Map<String, String> properties = properties(fields.get(0));
assertEquals("ADRZEILE1", properties.get("tag"));
assertEquals("YPADR2.ZEILE", properties.get("field"));
assertEquals("TI", properties.get("lineType"));
}
/**
* The read counterpart {@code XMLREAD} exists but is called zero times in the corpus, so every
* tag is an emit — {@code REQUEST} is measured, not defaulted.
*/
@Test
void directionIsRequest() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TAG, "T");
assign(XMLLINP.INHALT, F);
}
}
""");
assertEquals("REQUEST", properties(payloads(result).get(0)).get("direction"));
}
/**
* {@code XMLDYN} is the dynamic-length emitter — the same shape through its own parameter area,
* and 1,793 of the corpus's tags go through it.
*/
@Test
void theDynamicEmitterCountsToo() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLDYNP_DA.XMLDYNP XMLDYNP = XMLDYNP_DA.XMLDYNP;
@Override
public void run() {
assign(XMLDYNP.TAG, "BETRAG");
assign(XMLDYNP.INHALT_200, xWA_BETRAG);
}
}
""");
assertEquals("BETRAG", properties(payloads(result).get(0)).get("tag"));
assertEquals("xWA_BETRAG", properties(payloads(result).get(0)).get("field"));
}
@Test
void moduleThatEmitsIsMarkedAsAnXmlWrapper() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TAG, "T");
assign(XMLLINP.INHALT, F);
}
}
""");
AstNode module = result.nodes().stream()
.filter(n -> n.type() == NodeType.MODULE && n.name().equals("W"))
.findFirst().orElseThrow();
assertEquals("true", properties(module).get("xmlWrapper"));
assertTrue(result.edges().stream().anyMatch(e -> e.type() == EdgeType.CONTAINS
&& e.sourceId().equals(module.id())
&& payloads(result).stream().anyMatch(p -> p.id().equals(e.targetId()))),
"the payload field hangs off its module");
}
/**
* A structural tag (element start/end) has no content assignment and must not invent one.
*/
@Test
void tagWithoutContentYieldsNoPayloadField() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TYP, "ST");
assign(XMLLINP.TAG, "EMPFAENGER");
callnat("XMLLIN", XMLLINP);
}
}
""");
assertTrue(payloads(result).isEmpty());
}
/**
* A structural start/end tag is written with a blank content — 1,200 of the corpus's 1,493
* literal contents. Each would otherwise become a payload field whose "field" is a space.
*/
@Test
void structuralTagWithBlankContentIsNotAPayloadField() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TYP, "ST");
assign(XMLLINP.TAG, "EMPFAENGER");
assign(XMLLINP.INHALT, " ");
}
}
""");
assertTrue(payloads(result).isEmpty(), "a blank is the absence of content, not content");
}
/**
* A non-blank literal <em>is</em> part of the payload contract — 293 in the corpus — and is kept,
* marked so it is distinguishable from a field of the module's own data.
*/
@Test
void constantContentIsKeptAndMarked() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TAG, "KENNZEICHEN");
assign(XMLLINP.INHALT, "J");
}
}
""");
Map<String, String> properties = properties(payloads(result).get(0));
assertEquals("J", properties.get("field"));
assertEquals("true", properties.get("constant"));
}
@Test
void emptyTagYieldsNoPayloadField() {
ParseResult result = parser.parse("W.java", """
package X;
import innowake.mee.natural.api.*;
@Natural
public class W extends NaturalProgram {
public final KORIN.XMLLINP_DA.XMLLINP XMLLINP = XMLLINP_DA.XMLLINP;
@Override
public void run() {
assign(XMLLINP.TAG, "");
assign(XMLLINP.INHALT, F);
}
}
""");
assertTrue(payloads(result).isEmpty());
}
@Test
void aModuleWithoutXmlIsNotAWrapper() {
ParseResult result = parser.parse("fixtures/natjav/MDBT74B.java",
NatJavPayloadTest.class.getClassLoader() == null ? "" : readFixture());
assertTrue(payloads(result).isEmpty());
}
private static String readFixture() {
try {
return java.nio.file.Files.readString(
java.nio.file.Path.of("src/test/resources/fixtures/natjav/MDBT74B.java"));
} catch (java.io.IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * header created during migration process *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION Steuerung Initialisierungen der Libspezifischen GDA's
* * @OWNER P35
* **************************************************************
* **************************************************************
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* </pre>
*/
@Natural
public class ACOMMON extends NaturalProgram {
//
// Steuerungsprogramm für GDA-Initialisierungen
//
@Using KORIN.Diverse.Webservices.WSAIVP_DA WSAIVP_DA = localUsing(KORIN.Diverse.Webservices.WSAIVP_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Webservices.WSAIVP_DA) */
public final KORIN.Diverse.Webservices.WSAIVP_DA.WSAIVP WSAIVP = WSAIVP_DA.WSAIVP;
/* @@USING:END(KORIN.Diverse.Webservices.WSAIVP_DA) */
@Using KORIN.Diverse.Webservices.WSAIVL_DA WSAIVL_DA = localUsing(KORIN.Diverse.Webservices.WSAIVL_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Webservices.WSAIVL_DA) */
public final KORIN.Diverse.Webservices.WSAIVL_DA.WSAIVL WSAIVL = WSAIVL_DA.WSAIVL;
/* @@USING:END(KORIN.Diverse.Webservices.WSAIVL_DA) */
@Using KORIN.Diverse.Framework.GWPDA_M_DA GWPDA_M_DA = localUsing(KORIN.Diverse.Framework.GWPDA_M_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWPDA_M_DA) */
public final KORIN.Diverse.Framework.GWPDA_M_DA.MSG_INFO MSG_INFO = GWPDA_M_DA.MSG_INFO;
/* @@USING:END(KORIN.Diverse.Framework.GWPDA_M_DA) */
@Using KORIN.Diverse.Framework.GWLOGNAM_DA GWLOGNAM_DA = localUsing(KORIN.Diverse.Framework.GWLOGNAM_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWLOGNAM_DA) */
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_RETCODE xSTD_RETCODE = GWLOGNAM_DA.xSTD_RETCODE;
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_LOOPCODE xSTD_LOOPCODE = GWLOGNAM_DA.xSTD_LOOPCODE;
/* @@USING:END(KORIN.Diverse.Framework.GWLOGNAM_DA) */
@Local
@L1 A xGRAWE_APPLIKATION = A(8 ,"#GRAWE-APPLIKATION");
@Override
public void run() {
//
reset(WSAIVP);
assign(WSAIVP.AUFRUFCODE, WSAIVL.xC_GET_ITEM);
assign(WSAIVP.BEZEICHNER, WSAIVL.xB_GRAWE_APPLIKATION);
callnat("WSAIV", WSAIVP, MSG_INFO);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
fetchReturn("XABBRUCH",
cp("ACOMMON: Fehler WSAIV-Aufruf:"),
MSG_INFO.xxMSG_NR,
MSG_INFO.xxMSG_DATA.get(1),
"NO");
}
assign(xGRAWE_APPLIKATION, WSAIVP.INHALT);
//
if (EQ($LIBRARY_ID(), "KORIN ")
|| EQ($LIBRARY_ID(), "TEST")) {
int decideCounter1 = 0;
if (EQ(xGRAWE_APPLIKATION, "KORIN ")) {
decideCounter1++;
fetch("ACC_KR");
}
/* * VALUE 'KARTEI ' FETCH 'ACC-KA'
* * VALUE 'SCHADEN ' FETCH 'ACC-SB' */
else if (EQ(xGRAWE_APPLIKATION, "HAGEL ")) {
decideCounter1++;
fetch("ACC_HA");
}
/* * VALUE 'RVRETRO ' FETCH 'ACC-RV'
* * VALUE 'SCHDPRF ' FETCH 'ACC-SP' */
else if (EQ(xGRAWE_APPLIKATION, "ABFRAGE")) {
decideCounter1++;
fetch("SYSMENU2");
}
if (decideCounter1 == 0) {
// Wenn noch nicht gesetzt -> Default Korin
fetch("ACC_KR");
}
}
else if (EQ($LIBRARY_ID(), "ABFRAGE")) {
fetch("SYSMENU2");
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package SYSTEM;
import innowake.mee.natural.api.*;
import database.FDT.*;
@Natural(moduleName="BANKEZ")
public class BANKEZ_DDM extends NaturalDdm {
public static final DbN HIST_NR = reference(FDT0050.AF, "HIST-NR", NUMERIC, 3, 0);
public static final DbGroup ORDNR = reference(FDT0050.DO, "ORDNR");
public static final DbN VR_KEY = reference(FDT0050.AG, "VR-KEY", NUMERIC, 3, 0);
public static final DbA OART = reference(FDT0050.AH, "OART");
public static final DbN OBNR = reference(FDT0050.AI, "OBNR", NUMERIC, 8, 0);
public static final DbGroup STATUS_INFO = reference(FDT0050.AK, "STATUS-INFO");
public static final DbA STATUS_BYTE = reference(FDT0050.AJ, "STATUS-BYTE");
public static final DbN STATUS_DERZEIT = reference(FDT0050.AN, "STATUS-DERZEIT", NUMERIC, 3, 0);
public static final DbN ZAHLUNGSWEISE = reference(FDT0050.AO, "ZAHLUNGSWEISE", NUMERIC, 1, 0);
public static final DbN INKASSOWEG = reference(FDT0050.AP, "INKASSOWEG", NUMERIC, 1, 0);
public static final DbN FOLGEDATUM = reference(FDT0050.AT, "FOLGEDATUM", NUMERIC, 8, 0);
public static final DbA BV_NUMMER = reference(FDT0050.AV, "BV-NUMMER");
public static final DbGroup OPID_DATEN = reference(FDT0050.CK, "OPID-DATEN");
public static final DbA OPID_VAE = reference(FDT0050.BR, "OPID-VAE");
public static final DbN OPID_AM = reference(FDT0050.BS, "OPID-AM", NUMERIC, 8, 0);
public static final DbN OPID_TIMN = reference(FDT0050.BT, "OPID-TIMN");
public static final DbGroup BANKINKASSO = reference(FDT0050.CR, "BANKINKASSO");
public static final DbN BANKLEITZAHL = reference(FDT0050.CS, "BANKLEITZAHL", NUMERIC, 15, 0);
public static final DbA BANKKONTONUMMER = reference(FDT0050.CT, "BANKKONTONUMMER");
public static final DbN BANKEINZUG_STORNO = reference(FDT0050.ID, "BANKEINZUG-STORNO", NUMERIC, 8, 0);
public static final DbA BANK_OPID = reference(FDT0050.IE, "BANK-OPID");
public static final DbN BANK_AM = reference(FDT0050.IF, "BANK-AM", NUMERIC, 8, 0);
public static final DbN BANK_UM = reference(FDT0050.IG, "BANK-UM");
public static final DbN PNR_STORNO_AB = reference(FDT0050.CX, "PNR-STORNO-AB", NUMERIC, 8, 0);
public static final DbGroup GRUND_ADR = reference(FDT0050.DL, "GRUND-ADR");
public static final DbN GRUND_PLZ = reference(FDT0050.DN, "GRUND-PLZ", NUMERIC, 6, 0);
public static final DbA GRUND_NAME = reference(FDT0050.DQ, "GRUND-NAME");
public static final DbA GRUND_ADRESSE = reference(FDT0050.DR, "GRUND-ADRESSE");
public static final DbA GRUND_ERGAENZUNG = reference(FDT0050.DS, "GRUND-ERGAENZUNG");
public static final DbA GRUND_POSTNATION = reference(FDT0050.G6, "GRUND-POSTNATION");
public static final DbA GRUND_POSTORT = reference(FDT0050.H8, "GRUND-POSTORT");
public static final DbA INFO_EXTERN = reference(FDT0050.H9, "INFO-EXTERN");
public static final DbA ORDNUNGSBEGRIFF = reference(FDT0050.DE, "ORDNUNGSBEGRIFF");
public static final DbGroup AS_TAB = reference(FDT0050.CJ, "AS-TAB");
public static final DbA VTAB = reference(FDT0050.CQ, "VTAB");
public static final DbA ABZUG_STATUS = reference(FDT0050.FU, "ABZUG-STATUS");
public static final DbA KZ_FELD = reference(FDT0050.D8, "KZ-FELD");
public static final DbP SUB_VR_KEY = reference(FDT0050.IM, "SUB-VR-KEY");
public static final DbB TIMESTMP = reference(FDT0050.JJ, "TIMESTMP");
public static final DbA BANK_BIC = reference(FDT0050.KD, "BANK-BIC");
public static final DbA BANK_IBAN = reference(FDT0050.KE, "BANK-IBAN");
public static final DbA NAME_KONTOINHABER = reference(FDT0050.KF, "NAME-KONTOINHABER");
public static final DbB SPBANKEZ_VR = reference(FDT0050.JS, "SPBANKEZ-VR", BINARY, 10);
public static final DbA SPBANK_BIC_VR = reference(FDT0050.KX, "SPBANK-BIC-VR");
public static final DbA SPOPID = reference(FDT0050.MB, "SPOPID");
public static final DbA DSGVO_STATUS = reference(FDT0050.MG, "DSGVO-STATUS");
{
setDbId(0);
setFileName(50);
setFdt(FDT0050.class);
addDescriptor(BANKLEITZAHL);
addDescriptor(ABZUG_STATUS);
addDescriptor(BANK_BIC);
addDescriptor(BANK_IBAN);
addSuppression(ORDNUNGSBEGRIFF, Suppression.NullValueSuppression);
addSuppression(SPBANKEZ_VR, Suppression.NullValueSuppression);
addSuppression(SPBANK_BIC_VR, Suppression.NullValueSuppression);
addSuppression(SPOPID, Suppression.NullValueSuppression);
addSuppression(DSGVO_STATUS, Suppression.NullValueSuppression);
addHeader(HIST_NR, "HIST/-NR");
addHeader(VR_KEY, "VR");
addHeader(OART, "ART");
addHeader(OBNR, "OBNR");
addHeader(STATUS_INFO, "STATUS/INFO");
addHeader(STATUS_BYTE, "STAT");
addHeader(STATUS_DERZEIT, "STATUS/DERZEIT");
addHeader(ZAHLUNGSWEISE, "ZW");
addHeader(INKASSOWEG, "INK/WEG");
addHeader(FOLGEDATUM, "FOLGE");
addHeader(BV_NUMMER, "BV");
addHeader(OPID_AM, "VAE-/AM");
addHeader(OPID_TIMN, "VAE-/TIME");
addHeader(BANKEINZUG_STORNO, "BKEZ-STO");
addHeader(BANK_OPID, "BKEZ/OPID");
addHeader(ABZUG_STATUS, "ABZ/STA");
}
}

View File

@@ -0,0 +1,143 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package CSTENTW.HELP;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ********************************************
* * *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION HELP-Routinen &amp; Diverses
* * @OWNER P01
* **************************************************************
* **************************************************************
* *LCM-HEADER END ********************************************
* ***********************************************************************
* Program : CDDCINP /* Input new direct command
* System : NATURAL-CONSTRUCT
* Application: All
* Written on : 27 Jan. 1993
* Description: This subprogram is normally associated with an
* : extended PF-Key that is used to pop-up a direct
* : command line.
* :
* History
* ***********************************************************************
* </pre>
*/
@Natural
public class CDDCINP extends NaturalSubprogram {
@Using CDGETDCA_DA CDGETDCA_DA = parameterUsing(CDGETDCA_DA.class);
/* @@USING:BEGIN(CDGETDCA_DA) */
public final CDGETDCA_DA.CDGETDCA CDGETDCA = CDGETDCA_DA.CDGETDCA;
/* @@USING:END(CDGETDCA_DA) */
@Using CDPDA_D_DA CDPDA_D_DA = parameterUsing(CDPDA_D_DA.class);
/* @@USING:BEGIN(CDPDA_D_DA) */
public final CDPDA_D_DA.DIALOG_INFO DIALOG_INFO = CDPDA_D_DA.DIALOG_INFO;
/* @@USING:END(CDPDA_D_DA) */
@Using CDPDA_M_DA CDPDA_M_DA = parameterUsing(CDPDA_M_DA.class);
/* @@USING:BEGIN(CDPDA_M_DA) */
public final CDPDA_M_DA.MSG_INFO MSG_INFO = CDPDA_M_DA.MSG_INFO;
/* @@USING:END(CDPDA_M_DA) */
@Using CDGLBTXL_DA CDGLBTXL_DA = localUsing(CDGLBTXL_DA.class);
/* @@USING:BEGIN(CDGLBTXL_DA) */
public final CDGLBTXL_DA.CDGLBTXL CDGLBTXL = CDGLBTXL_DA.CDGLBTXL;
public final A CDGLBTXL_NAME = CDGLBTXL_DA.CDGLBTXL_NAME;
/* @@USING:END(CDGLBTXL_DA) */
@Using CDENVIRA_DA CDENVIRA_DA = localUsing(CDENVIRA_DA.class);
/* @@USING:BEGIN(CDENVIRA_DA) */
public final CDENVIRA_DA.CDENVIRA CDENVIRA = CDENVIRA_DA.CDENVIRA;
/* @@USING:END(CDENVIRA_DA) */
@Override
public void run() {
format(0, KD(false));
setKey(all());
/* @INCLUDE CCSETHLP */
/* @@INCLUDE:BEGIN(CCSETHLP) */
/* *LCM-HEADER BEGIN ******************************************** */
/* * *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION HELP-Routinen & Diverses
* * @OWNER P01
* **************************************************************
* **************************************************************
* *LCM-HEADER END ********************************************
* Help key must be hardcoded. Change PF-KEY value to change default
* help key. */
setKey(PF1, HELP);
/* @@INCLUDE:END(CCSETHLP) */
perform("CAPTURE_RESTORE_ENVIRONMENT");
setControl(cp("WBL79C4FB18/0"));
callnat("CDUTRANS", CDGLBTXL.CDUTRANS.get($), CDGLBTXL_NAME, MSG_INFO);
input(withText(MSG_INFO.xxMSG_NR, MSG_INFO.xxMSG_DATA.get(1), MSG_INFO.xxMSG_DATA.get(2), MSG_INFO.xxMSG_DATA.get(3)), AD("MI'_'"), IP(false), CDGLBTXL.xDIRECT_CMD_TXT, AD("O"),
CDGETDCA.xDIRECT_COMMAND, AL(58), AD("M'_'"), HE("CDDCHELP"));
reset(MSG_INFO.xxMSG);
if (NE($PF_KEY(), "ENTR")) {
reset(CDGETDCA.xDIRECT_COMMAND);
assign(MSG_INFO.xxMSG_NR, 8040); // Function canceled using:1:
assign(MSG_INFO.xxMSG_DATA.get(1), $PF_KEY());
perform("SET_USER_MSG");
setControl("K0"); // Set *PF-KEY = ENTR
perform("CAPTURE_RESTORE_ENVIRONMENT");
escapeRoutineImmediate();
}
perform("CAPTURE_RESTORE_ENVIRONMENT");
// SET-USER-MSG
}
/*
* *********************************************************************** */
void CAPTURE_RESTORE_ENVIRONMENT() { // CAPTURE-RESTORE-ENVIRONMENT
/* ***********************************************************************
*/
/* @INCLUDE CCENVIR */
/* @@INCLUDE:BEGIN(CCENVIR) */
/* *LCM-HEADER BEGIN ******************************************** */
/* * *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION HELP-Routinen & Diverses
* * @OWNER P01
* **************************************************************
* **************************************************************
* *LCM-HEADER END ********************************************
*
* This subprogram is used to capture and restore the current window
* setting, pagesize etc. in a online dialog environment. The first
* call of CDENVIR captures the current environment, the second call
* restores it to what it was. */
if (GT($LEVEL(), 1)) {
callnat("CDENVIR", CDENVIRA);
}
/* @@INCLUDE:END(CCENVIR) */
}
/*
* *********************************************************************** */
void FINAL_PROCESSING() { // Required by CCDCOUT
/* ***********************************************************************
*/
escapeRoutine();
}
// CAPTURE-RESTORE-ENVIRONMENT
/*
* *********************************************************************** */
void SET_USER_MSG() {
/* ***********************************************************************
*/
callnat("CDUMSG", MSG_INFO);
}
}

View File

@@ -0,0 +1,652 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN;
import innowake.mee.natural.api.*;
@Natural(moduleName="COMMON")
public class COMMON_DA extends NaturalData {
@Global
/*
* GDA für LIBRARY KORIN INCLUSIVE
* REDEFINITIONEN
*/
@L1 A gPROGRAMM = A(8 ,"+PROGRAMM");
@L1 A gBPOL_PARM = A(17 ,"+BPOL-PARM");
@L1 N gBBST_POLNR = N(9,0 ,"+BBST-POLNR");
@L1 A gBBST_ART = A(9 ,"+BBST-ART");
@L1 A gABTEILUNG = A(5 ,"+ABTEILUNG");
@L1 A gPASSWORD = A(8 ,"+PASSWORD");
@L1 A gGETMAIL = A(1 ,"+GETMAIL");
@L1 A gERROR_PROGRAM = A(8 ,"+ERROR-PROGRAM");
@L1 A gERROR_STAT = A(1 ,"+ERROR-STAT");
@L1 N gERROR_LINE = N(4,0 ,"+ERROR-LINE");
@L1 N gERROR_NR = N(4,0 ,"+ERROR-NR");
@L1 A gERGEBNIS = A(79 ,"+ERGEBNIS");
@L1 A gSPOOL_PARM = A(80 ,"+SPOOL-PARM");
@L1 Redefine gSPOOL_PARM;
@L2 A xSPUSER = A(8 ,"#SPUSER");
@L2 A xSPDEST = A(8 ,"#SPDEST");
@L2 A xSPFORM = A(1 ,"#SPFORM");
@L1 A gINIT_VAR = A(240 ,"+INIT-VAR");
@L1 Redefine gINIT_VAR; // REDEF. BEGIN : +INIT-VAR
@L2 A xRA_INITVAR_GETMAIL = A(1 ,"#RA-INITVAR-GETMAIL");// GETMAIL duchgeführt Y/N
@L2 N xINIT_DATIME = N(15,0 ,"#INIT-DATIME");// STARTUP Datum/Zeitder
@L2 Redefine xINIT_DATIME; // letzten Startup-Durchführung
@L3 N xINIT_DATE = N(8,0 ,"#INIT-DATE");// "YYYYMMDDHHMMSSZ"
@L3 Redefine xINIT_DATE;
@L4 A xINIT_DATE_ALPHA = A(8 ,"#INIT-DATE-ALPHA");
@L3 N xINIT_TIME = N(7,0 ,"#INIT-TIME");
@L3 Redefine xINIT_TIME;
@L4 A xINIT_TIME_ALPHA = A(7 ,"#INIT-TIME-ALPHA");
@L2 A xINIT_USER = A(8 ,"#INIT-USER"); // STARTUP U-ID d. Current LOGON
@L2 A xINIT_ID = A(8 ,"#INIT-ID"); // STARTUP Terminal-ID
@L2 A xINIT_GROUP = A(8 ,"#INIT-GROUP"); // STARTUP GROUP-ID, über die
/* /* gelinkt */
@L2 A xSTARTUP_PROGRAM = A(8 ,"#STARTUP-PROGRAM");// STARTUP Startup-Programm
@L2 A xRESTART_PROGRAM = A(8 ,"#RESTART-PROGRAM");// STARTUP Restart-Programm
@L2 A xERROR_PROGRAM = A(8 ,"#ERROR-PROGRAM");// #ERR letztes fehlerhaftes PRG
/* /* STARTUP Error-Programm */
@L2 A xRA_INITVAR_CICS_NAME = A(8 ,"#RA-INITVAR-CICS-NAME");// STARTUP und APPLID
@L2 Redefine xRA_INITVAR_CICS_NAME; // "CSWVCICS"/"CSWVCICT"
@L3 A xCICSNAME = A(8 ,"#CICSNAME"); // Redef. aus Namensgründen
@L2 Redefine xRA_INITVAR_CICS_NAME;
@L3 A xCICS_NAME = A(8 ,"#CICS-NAME"); // Redef. aus Namensgründen
@L2 A FILLER = A(8 ,"FILLER");
@L2 A xUSER_NAME = A(32 ,"#USER-NAME"); // STARTUP User-Name
@L2 A xAPPL_NAME = A(32 ,"#APPL-NAME"); // STARTUP Anwendungs-Name
@L2 A xSYS_SCHALTER = A(8 ,"#SYS-SCHALTER");// DIV SYSTEM-Schalter
@L2 Redefine xSYS_SCHALTER;
@L3 A xSYS_RESTART = A(1 ,"#SYS-RESTART");// #ERR bei Absturz Restart Y/N
@L3 A xSYS_CANCEL = A(1 ,"#SYS-CANCEL");// #CAN BATCH CANCELNin #ERROR
@L3 A xERROR_STAT = A(1 ,"#ERROR-STAT");// #ERR letzter Fehler-Status
@L3 A xERROR_OPTN = A(1 ,"#ERROR-OPTN");// #ERR RETRY ON ERRORS Y/N
@L3 A xERROR_SAME = A(1 ,"#ERROR-SAME");// #ERR same ERROR wie zuvor Y/N
/* 0=NEIN 1=PROG 2=NUMMER 3=BEIDE */
@L3 A xERROR_LOG = A(1 ,"#ERROR-LOG"); // #ERR N=kein ERROR-Logging
@L3 A xMONITOR = A(1 ,"#MONITOR"); // STARTUP B=BATCH, C=CICS
/* /* P=COMPLETE */
@L3 A FILLER2 = A(1 ,"FILLER2");
@L2 A xERROR_PROG = A(8 ,"#ERROR-PROG");
@L2 N xERROR_NR = N(4,0 ,"#ERROR-NR"); // #ERR letzte Fehlernummer
@L2 Redefine xERROR_NR;
@L3 A xERRAX = A(4 ,"#ERRAX");
@L2 N xERROR_LINE = N(4,0 ,"#ERROR-LINE"); // #ERR letzte fehlerhafte Zeile
@L2 A xMESSAGE = A(72 ,"#MESSAGE"); // #ERR Fehlermeldung
@L2 Redefine xMESSAGE;
@L3 A xMANF = A(1 ,"#MANF");
@L3 A STAR = A(1 ,"STAR");
@L3 A RESTMSG = A(70 ,"RESTMSG");
@L1 A gBERECHTIGUNGEN = A("+BERECHTIGUNGEN");
@L1 A gVSU = A(250 ,"+VSU");
@L1 A gKIS = A(250 ,"+KIS");
@L1 Redefine gKIS; // REDEF. BEGIN : +KIS
@L2 N xRN_KIS_ZAHL = N(3,0 ,"#RN-KIS-ZAHL");
@L2 B xRA_KIS_KUISNT = B(3 ,"#RA-KIS-KUISNT").defineArray(range(1, 20));
@L2 Redefine xRA_KIS_KUISNT; // REDEF. BEGIN : #RA-KIS-KUISNT
@L3 B xRA_KIS_KUISN = B(3 ,"#RA-KIS-KUISN");
@L2 N xRN_KIS_OVZL = N(3,0 ,"#RN-KIS-OVZL");
@L2 B xRA_KIS_OVISN = B(3 ,"#RA-KIS-OVISN").defineArray(range(1, 60));
@L1 A gUMS = A(250 ,"+UMS");
@L1 Redefine gUMS; // REDEF. BEGIN : +UMS
@L2 A xRA_UMS_CODE = A(8 ,"#RA-UMS-CODE");
@L2 P xRN_UMS_LAST_VR = P(3,0 ,"#RN-UMS-LAST-VR");
@L2 A xRA_UMS_MD_KZ1_BKLZ_KZ = A(1 ,"#RA-UMS-MD-KZ1-BKLZ-KZ");
@L2 A xRA_UMS_MD_KZ1_TARIF = A(1 ,"#RA-UMS-MD-KZ1-TARIF");
@L2 A xRA_UMS_WAEHRUNG_MD = A(3 ,"#RA-UMS-WAEHRUNG-MD");
@L2 N xRN_UMS_LAST_LOAD = N(6,0 ,"#RN-UMS-LAST-LOAD");
@L2 A xUMS_VORG_OPID = A(3 ,"#UMS-VORG-OPID");
@L2 A xUMS_FORCE = A(1 ,"#UMS-FORCE"); // Beharrung auf eigene
/* * /* PF-Tasten-Belegung */
@L2 N xUMS_SNR_FIL = N(2,0 ,"#UMS-SNR-FIL");// Schaden-Filial-Nummer
@L2 A xUMS_SNR_KREIS = A(2 ,"#UMS-SNR-KREIS");// Schaden-Kreis zB.:'SA'
@L2 N xUMS_SNR_JAHR = N(4,0 ,"#UMS-SNR-JAHR");// Schaden-Jahr
@L2 N xUMS_SNR_LFD_NR = N(6,0 ,"#UMS-SNR-LFD-NR");// Schaden-Laufende-Nummer
@L2 A xUMS_SNR_STATUS = A(1 ,"#UMS-SNR-STATUS");// Status-Byte 'O','S','H'
@L2 A xUMS_ART = A(2 ,"#UMS-ART"); // Schadenart
@L2 A xUMS_REFERENT = A(8 ,"#UMS-REFERENT");// Referent
@L2 A xUMS_EVI_TYP = A(1 ,"#UMS-EVI-TYP"); // eigen oder fremd-Vi
@L2 P xUMS_IDENTCOUNTER = P(5,0 ,"#UMS-IDENTCOUNTER");
@L2 P xUMS_OUTPUTNR = P(5,0 ,"#UMS-OUTPUTNR");
@L2 P xUMS_EREIGNIS_DT = P(8,0 ,"#UMS-EREIGNIS-DT");// nur für LV (= Stornodatum)
@L2 P xUMS_SCN_HIST = P(3,0 ,"#UMS-SCN-HIST");// merken Hist-Lesestand
@L2 B xUMS_ANZEIGEDAT_KPL = B(3 ,"#UMS-ANZEIGEDAT-KPL");// für Aufruf SK10
@L2 N xUMS_SKT_FIL = N(2,0 ,"#UMS-SKT-FIL");// Filial-Nummer Kontostamm
@L2 A xUMS_SKT_KREIS = A(2 ,"#UMS-SKT-KREIS");// Schaden-Kreis Kontostamm
@L2 N xUMS_SKT_JAHR = N(4,0 ,"#UMS-SKT-JAHR");// Schaden-Jahr Kontostamm
@L2 N xUMS_SKT_LFD_NR = N(6,0 ,"#UMS-SKT-LFD-NR");// Schaden-Lfd-Nr Kontostamm
@L2 N xUMS_SKT_BRAN = N(2,0 ,"#UMS-SKT-BRAN");// Schaden-Branche Kontostamm
@L2 A xUMS_WORKSTATION_ID = A(8 ,"#UMS-WORKSTATION-ID");// Workstation (OD) *** frei ***
@L2 A xRA_UMS_BRGR = A(1 ,"#RA-UMS-BRGR").defineArray(range(1,3));
@L2 A xUMS_WF_ABT = A(8 ,"#UMS-WF-ABT"); // Workflow-Abteilung(für OD)
@L2 A xUMS_WF_STAT = A(8 ,"#UMS-WF-STAT"); // Workflow-Station(für OD)
@L2 A xUMS_WF_OPID = A(8 ,"#UMS-WF-OPID"); // Workflow-Opid(für OD)
@L2 P xUMS_WF_SORT_DOKU_ART = P(3,0 ,"#UMS-WF-SORT-DOKU-ART").defineArray(range(1,5));// WF-Dokument(für OD)
@L2 P xUMS_KEYCOUNTER = P(5,0 ,"#UMS-KEYCOUNTER").defineArray(range(1,10));// Identcounter aus File111
@L2 A xUMS_SKT_BELEGTYP = A(1 ,"#UMS-SKT-BELEGTYP");// Schaden-Konto-Belegtyp
@L2 P xUMS_SKT_POS_IDC = P(5,0 ,"#UMS-SKT-POS-IDC");// Identcounter Pos.Buchung-SKT
@L2 P xUMS_SKT_BRAN_IDC = P(5,0 ,"#UMS-SKT-BRAN-IDC");// Identcounter Bran.Buchung-SKT
@L2 P xUMS_SKT_ABST_IDC = P(5,0 ,"#UMS-SKT-ABST-IDC");// Identcounter Abst.Buchung-SKT
@L2 A xUMS_TEXT_FN95 = A(1 ,"#UMS-TEXT-FN95");// Text-Option im FN95
@L2 A xUMS_REGRESS_RATE = A(2 ,"#UMS-REGRESS-RATE");// Schaden-Regreß oder -Rate
@L2 P xUMS_REGRESS_NR = P(6,0 ,"#UMS-REGRESS-NR");// Schaden-Regreß-Nr.
@L2 A xUMS_DAT2_FN95 = A(1 ,"#UMS-DAT2-FN95");// 2. Datumszeile FN95 optional
@L2 N xUMS_PC_DAT = N(8,0 ,"#UMS-PC-DAT"); // Pc-Antragsdatum
@L2 N xUMS_PC_NUM = N(4,0 ,"#UMS-PC-NUM"); // Pc-Antragsnummer
@L2 A xUMS_DRUCKER_ID = A(10 ,"#UMS-DRUCKER-ID");// Drucker-Id (OD)
@L2 A xUMS_KEY_STAT_DI32 = A(8 ,"#UMS-KEY-STAT-DI32");// Station für AufrufDI32 (OD)
@L2 B xUMS_SKT_ANZEIGEDAT_KPL = B(3 ,"#UMS-SKT-ANZEIGEDAT-KPL");// Anzeigedatum-KPL für Sammelb.
@L2 P xUMS_WINDOW_NUM = P(2,0 ,"#UMS-WINDOW-NUM");// Image-Display (OD)
@L2 P xUMS_STORNO_GRUND = P(2,0 ,"#UMS-STORNO-GRUND");// stornogrund (nur LV)
@L2 A xUMS_DOI_KEY_ART = A(2 ,"#UMS-DOI-KEY-ART");// Kz f. Einschränkung Di30 (OD)
@L2 P xUMS_SKT_WERTEDATUM = P(8,0 ,"#UMS-SKT-WERTEDATUM");// Schaden-Konto-Wertedatum
@L2 P xUMS_SKT_BANKSCHLUESSEL = P(2,0 ,"#UMS-SKT-BANKSCHLUESSEL");// Schaden-Konto-Bankschlüssel
@L2 A xUMS_REST = A(28 ,"#UMS-REST"); // Bytes frei
@L1 A gPEL = A(250 ,"+PEL");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 A xRA_PEL_ABT = A(1 ,"#RA-PEL-ABT");
@L2 N xRN_PEL_VPOL = N(8,0 ,"#RN-PEL-VPOL");
@L2 N xRN_PEL_KPNR = N(8,0 ,"#RN-PEL-KPNR");
@L2 N xRN_PEL_GT = N(8,0 ,"#RN-PEL-GT");
@L2 N xRN_PEL_BR1 = N(8,0 ,"#RN-PEL-BR1");
@L2 N xRN_PEL_BR2 = N(8,0 ,"#RN-PEL-BR2");
@L2 N xRN_PEL_BUCH = N(8,0 ,"#RN-PEL-BUCH");
@L2 N xRN_PEL_EINL = N(6,0 ,"#RN-PEL-EINL");
@L2 A xRA_PEL_FREI = A(2 ,"#RA-PEL-FREI");
@L2 N xRN_PEL_TEXT = N(8,0 ,"#RN-PEL-TEXT");
@L2 N xRN_PEL_STAT = N(8,0 ,"#RN-PEL-STAT");
@L2 N xRN_PEL_AGT = N(8,0 ,"#RN-PEL-AGT");
@L2 N xRN_PEL_ABR1 = N(8,0 ,"#RN-PEL-ABR1");
@L2 N xRN_PEL_ABR2 = N(8,0 ,"#RN-PEL-ABR2");
@L2 N xRN_PEL_ATXT = N(8,0 ,"#RN-PEL-ATXT");
@L2 N xRN_PEL_INDX = N(2,0 ,"#RN-PEL-INDX");
@L2 N xRN_PEL_VDAT = N(6,0 ,"#RN-PEL-VDAT");
@L2 A xRA_PEL_EFKZ = A(1 ,"#RA-PEL-EFKZ");
@L2 A xRA_PEL_DRUC = A(4 ,"#RA-PEL-DRUC");
@L2 A xRA_PEL_VART = A(3 ,"#RA-PEL-VART");
@L2 N xRN_PEL_KREI = N(3,0 ,"#RN-PEL-KREI");
@L2 A xRA_PEL_VAE = A(3 ,"#RA-PEL-VAE");
@L2 A xRA_PEL_BNEU = A(3 ,"#RA-PEL-BNEU");
@L2 A xRA_PEL_FILL118 = A(118 ,"#RA-PEL-FILL118");
@L2 A xRA_PEL_MENU = A(2 ,"#RA-PEL-MENU");
@L1 Redefine gPEL; // REDEF. BEGIN : +PEL
@L2 N xRN_KFZ_MENR = N(1,0 ,"#RN-KFZ-MENR");
@L2 N xRN_KFZ_IEVI = N(3,0 ,"#RN-KFZ-IEVI");
@L2 N xRN_KFZ_IVV = N(3,0 ,"#RN-KFZ-IVV");
@L2 N xRN_KFZ_HIST = N(3,0 ,"#RN-KFZ-HIST");
@L2 N xRN_KFZ_STND = N(3,0 ,"#RN-KFZ-STND");
@L2 A xRA_KFZ_EVBR = A(30 ,"#RA-KFZ-EVBR");
@L2 Redefine xRA_KFZ_EVBR;
@L3 A xRA_KFZ_BRA = A(3 ,"#RA-KFZ-BRA").defineArray(range(1, 10));
@L2 Redefine xRA_KFZ_EVBR;
@L3 N xRN_KFZ_BRA1 = N(3,0 ,"#RN-KFZ-BRA1");
@L3 N xRN_KFZ_BRA2 = N(3,0 ,"#RN-KFZ-BRA2");
@L3 N xRN_KFZ_BRA3 = N(3,0 ,"#RN-KFZ-BRA3");
@L3 N xRN_KFZ_BRA4 = N(3,0 ,"#RN-KFZ-BRA4");
@L3 N xRN_KFZ_BRA5 = N(3,0 ,"#RN-KFZ-BRA5");
@L2 N xRN_KFZ_VDAT = N(8,0 ,"#RN-KFZ-VDAT");
@L2 A xRA_KFZ_AREV = A(1 ,"#RA-KFZ-AREV");
@L2 A xRA_KFZ_SABE = A(3 ,"#RA-KFZ-SABE");
@L2 A xRA_KFZ_RAG = A(1 ,"#RA-KFZ-RAG");
@L2 P xRN_KFZ_OZW = P(3,0 ,"#RN-KFZ-OZW");
@L2 N xRN_KFZ_VVAE = N(8,0 ,"#RN-KFZ-VVAE");
@L2 A xRA_KFZ_VEF = A(1 ,"#RA-KFZ-VEF");
@L2 A xRA_KFZ_VVAR = A(3 ,"#RA-KFZ-VVAR");
@L2 N xRN_KFZ_VEGD = N(8,0 ,"#RN-KFZ-VEGD");
@L2 N xRN_KFZ_VELD = N(8,0 ,"#RN-KFZ-VELD");
@L2 A xRA_KFZ_VDRU = A(4 ,"#RA-KFZ-VDRU");
@L2 A xRA_KFZ_PZSG = A(3 ,"#RA-KFZ-PZSG");
@L2 N xRN_KFZ_LESE = N(3,0 ,"#RN-KFZ-LESE");
@L2 N xRN_KFZ_FZW = N(2,0 ,"#RN-KFZ-FZW");
@L2 N xRN_KFZ_VI10 = N(2,0 ,"#RN-KFZ-VI10");
@L2 N xRN_KFZ_VD = N(8,0 ,"#RN-KFZ-VD");
@L2 N xRN_KFZ_VB = N(8,0 ,"#RN-KFZ-VB");
@L2 A xRA_KFZ_KFKZ = A(11 ,"#RA-KFZ-KFKZ");
@L2 N xRN_KFZ_AWDT = N(8,0 ,"#RN-KFZ-AWDT");
@L2 A xRA_KFZ_AWKZ = A(1 ,"#RA-KFZ-AWKZ");
@L2 N xRN_KFZ_AEND = N(8,0 ,"#RN-KFZ-AEND");
@L2 A xRA_KFZ_VABT = A(3 ,"#RA-KFZ-VABT");
@L2 A xRA_KFZ_OPID = A(3 ,"#RA-KFZ-OPID");
@L2 N xRN_KFZ_DATV = N(8,0 ,"#RN-KFZ-DATV");
@L2 N xRN_KFZ_DATB = N(8,0 ,"#RN-KFZ-DATB");
@L2 A xRA_KFZ_FIL = A(2 ,"#RA-KFZ-FIL");
@L2 N xRN_KFZ_KRBR = N(4,0 ,"#RN-KFZ-KRBR");
@L2 Redefine xRN_KFZ_KRBR; // REDEF. BEGIN : #RN-KFZ-KRBR
@L3 N xRN_KFZ_KR = N(2,0 ,"#RN-KFZ-KR");
@L3 N xRN_KFZ_BR = N(2,0 ,"#RN-KFZ-BR");
@L2 A xRA_KFZ_VAE1 = A(3 ,"#RA-KFZ-VAE1");
@L2 A xRA_KFZ_VAE2 = A(3 ,"#RA-KFZ-VAE2");
@L2 A xRA_KFZ_SUMM = A(1 ,"#RA-KFZ-SUMM");
@L2 N xRN_KFZ_STAT = N(2,0 ,"#RN-KFZ-STAT");
@L2 A xRA_KFZ_GSNA = A(10 ,"#RA-KFZ-GSNA");
@L2 A xRA_KFZ_BVNR = A(7 ,"#RA-KFZ-BVNR");
@L2 N xRN_KFZ_ANTR = N(8,0 ,"#RN-KFZ-ANTR");
@L2 N xRN_KFZ_VINK = N(8,0 ,"#RN-KFZ-VINK");
@L2 N xRN_KFZ_FPNR = N(3,0 ,"#RN-KFZ-FPNR");
@L2 A xRA_KFZ_MAPA = A(1 ,"#RA-KFZ-MAPA");
@L2 L xRL_KFZ_PRVV = L("#RL-KFZ-PRVV");
@L2 L xRL_KFZ_ST01 = L("#RL-KFZ-ST01");
@L2 L xRL_KFZ_AUTO = L("#RL-KFZ-AUTO");
@L2 A xRA_KFZ_OPI2 = A(3 ,"#RA-KFZ-OPI2");
@L2 L xRL_KFZ_BLND = L("#RL-KFZ-BLND");
@L2 C xRC_KFZ_BLND = C("#RC-KFZ-BLND");
@L2 A xRA_KFZ_ATXT = A(10 ,"#RA-KFZ-ATXT");
@L2 A xRA_KFZ_KTXT = A(3 ,"#RA-KFZ-KTXT");
@L2 A xRA_KFZ_AMNU = A(8 ,"#RA-KFZ-AMNU");
@L2 A xRA_KFZ_MENU = A(2 ,"#RA-KFZ-MENU");
@L1 A gISN = A(250 ,"+ISN");
@L1 Redefine gISN; // REDEF. BEGIN : +ISN
@L2 A xRA_ISN_ISN = A(5 ,"#RA-ISN-ISN");
@L2 Redefine xRA_ISN_ISN; // REDEF. BEGIN : #RA-ISN-ISN
@L3 P xRN_ISN_AKUN = P(9,0 ,"#RN-ISN-AKUN");
@L2 P xRN_ISN_HKUN = P(9,0 ,"#RN-ISN-HKUN");
@L2 P xRN_ISN_VKUN = P(9,0 ,"#RN-ISN-VKUN");
@L2 P xRN_ISN_ORTE = P(9,0 ,"#RN-ISN-ORTE");
@L2 P xRN_ISN_KORP = P(9,0 ,"#RN-ISN-KORP");
@L2 P xRN_ISN_KTAB = P(9,0 ,"#RN-ISN-KTAB");
@L2 P xRN_ISN_ABKU = P(9,0 ,"#RN-ISN-ABKU");
@L2 P xRN_ISN_OPOL = P(9,0 ,"#RN-ISN-OPOL");
@L2 P xRN_ISN_SPOL = P(9,0 ,"#RN-ISN-SPOL");
@L2 P xRN_ISN_OEVI = P(9,0 ,"#RN-ISN-OEVI");
@L2 P xRN_ISN_SEVI = P(9,0 ,"#RN-ISN-SEVI");
@L2 P xRN_ISN_OVV = P(9,0 ,"#RN-ISN-OVV");
@L2 P xRN_ISN_SVV = P(9,0 ,"#RN-ISN-SVV");
@L2 P xRN_ISN_OBU = P(9,0 ,"#RN-ISN-OBU");
@L2 P xRN_ISN_SBU = P(9,0 ,"#RN-ISN-SBU");
@L2 P xRN_ISN_BTS = P(9,0 ,"#RN-ISN-BTS");
@L2 P xRN_ISN_BTK = P(9,0 ,"#RN-ISN-BTK");
@L2 P xRN_ISN_EKPO = P(9,0 ,"#RN-ISN-EKPO");
@L2 P xRN_ISN_EKVV = P(9,0 ,"#RN-ISN-EKVV");
@L2 P xRN_ISN_EKK1 = P(9,0 ,"#RN-ISN-EKK1");
@L2 P xRN_ISN_OPOS = P(9,0 ,"#RN-ISN-OPOS");
@L2 P xRN_ISN_SPOS = P(9,0 ,"#RN-ISN-SPOS");
@L2 P xRN_ISN_EKG1 = P(9,0 ,"#RN-ISN-EKG1");
@L2 P xRN_ISN_HGPO = P(9,0 ,"#RN-ISN-HGPO");
@L2 P xRN_ISN_HGKO = P(9,0 ,"#RN-ISN-HGKO");
@L2 P xRN_ISN_OLS = P(9,0 ,"#RN-ISN-OLS");
@L2 P xRN_ISN_SLS = P(9,0 ,"#RN-ISN-SLS");
@L2 P xRN_ISN_HLS = P(9,0 ,"#RN-ISN-HLS");
@L2 P xRN_ISN_VDNR = P(9,0 ,"#RN-ISN-VDNR");
@L2 P xRN_ISN_MDST = P(9,0 ,"#RN-ISN-MDST");
@L2 P xRN_ISN_KTOS = P(9,0 ,"#RN-ISN-KTOS");
@L2 P xRN_ISN_KTOO = P(9,0 ,"#RN-ISN-KTOO");
@L2 P xRN_ISN_ORTS = P(9,0 ,"#RN-ISN-ORTS");
@L2 P xRN_ISN_FPNR = P(9,0 ,"#RN-ISN-FPNR");
@L2 P xRN_ISN_WA49 = P(9,0 ,"#RN-ISN-WA49");
@L2 P xRN_ISN_SD34 = P(8,0 ,"#RN-ISN-SD34");
@L2 P xRN_ISN_SPER = P(9,0 ,"#RN-ISN-SPER");
@L2 P xRN_ISN_MDS = P(9,0 ,"#RN-ISN-MDS");
@L2 P xRN_ISN_SUCH = P(9,0 ,"#RN-ISN-SUCH");
@L2 P xRN_ISN_ABST = P(9,0 ,"#RN-ISN-ABST");
@L2 P xRN_ISN_UMKON = P(9,0 ,"#RN-ISN-UMKON");
@L2 P xRN_ISN_MUBU = P(9,0 ,"#RN-ISN-MUBU");// MUTTERBUCHUNG (SK)
@L2 P xRN_ISN_RESERVE = P(9,0 ,"#RN-ISN-RESERVE").defineArray(range(1, 5));
@L1 A gKUN = A(250 ,"+KUN");
@L1 Redefine gKUN; // REDEF. BEGIN : +KUN
@L2 A xRA_KUN_KUNA = A(27 ,"#RA-KUN-KUNA");
@L2 A xRA_KUN_GHKZ = A(1 ,"#RA-KUN-GHKZ");
@L2 N xRN_KUN_GHK1 = N(8,0 ,"#RN-KUN-GHK1");
@L2 N xRN_KUN_GHK2 = N(8,0 ,"#RN-KUN-GHK2");
@L2 N xRN_KUN_DATV = N(8,0 ,"#RN-KUN-DATV");
@L2 N xRN_KUN_DATB = N(8,0 ,"#RN-KUN-DATB");
@L2 A xRA_KUN_BRAX = A(30 ,"#RA-KUN-BRAX");
@L2 N xRN_KUN_FILV = N(2,0 ,"#RN-KUN-FILV");
@L2 N xRN_KUN_FILB = N(2,0 ,"#RN-KUN-FILB");
@L2 N xRN_KUN_ANZEI = N(2,0 ,"#RN-KUN-ANZEI");
@L2 N xRN_KUN_BILKR = N(2,0 ,"#RN-KUN-BILKR");
@L2 N xRN_KUN_POLK = N(2,0 ,"#RN-KUN-POLK");
@L2 N xRN_KUN_POLB = N(2,0 ,"#RN-KUN-POLB");
@L2 A xRA_KUN_BANKKU = A(5 ,"#RA-KUN-BANKKU");
@L2 A xRA_KUN_BANKOR = A(27 ,"#RA-KUN-BANKOR");
@L2 N xRN_KUN_BANKLE = N(5,0 ,"#RN-KUN-BANKLE");
@L2 N xRN_KUN_TDAT = N(8,0 ,"#RN-KUN-TDAT");
@L2 N xRN_KUN_VR = N(3,0 ,"#RN-KUN-VR");
@L2 N xRN_KUN_VGVR = N(3,0 ,"#RN-KUN-VGVR");
@L2 N xRN_KUN_STAT = N(3,0 ,"#RN-KUN-STAT");
@L2 N xRN_KUN_HGV = N(2,0 ,"#RN-KUN-HGV");
@L2 A xRA_KUN_UG1V = A(2 ,"#RA-KUN-UG1V");
@L2 A xRA_KUN_UG2V = A(2 ,"#RA-KUN-UG2V");
@L2 A xRA_KUN_UG3V = A(2 ,"#RA-KUN-UG3V");
@L2 A xRA_KUN_SPV = A(1 ,"#RA-KUN-SPV");
@L2 N xRN_KUN_HGB = N(2,0 ,"#RN-KUN-HGB");
@L2 A xRA_KUN_UG1B = A(2 ,"#RA-KUN-UG1B");
@L2 A xRA_KUN_UG2B = A(2 ,"#RA-KUN-UG2B");
@L2 A xRA_KUN_UG3B = A(2 ,"#RA-KUN-UG3B");
@L2 A xRA_KUN_SPB = A(1 ,"#RA-KUN-SPB");
@L2 A xRA_KUN_LEER1 = A(38 ,"#RA-KUN-LEER1");
@L2 N xRN_KUN_OVGEM = N(5,0 ,"#RN-KUN-OVGEM");
@L2 N xRN_KUN_OVOKZ = N(6,0 ,"#RN-KUN-OVOKZ");
@L2 N xRN_KUN_OVOBK = N(4,0 ,"#RN-KUN-OVOBK");
@L2 A xRA_KUN_OVNAM = A(15 ,"#RA-KUN-OVNAM");
@L2 A xRA_KUN_LEER2 = A(4 ,"#RA-KUN-LEER2");
@L2 A xRA_KUN_OVFIL = A(2 ,"#RA-KUN-OVFIL");
@L2 A xRA_KUN_OVSA = A(1 ,"#RA-KUN-OVSA");
@L1 A gKEY = A(250 ,"+KEY");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 N xRN_KEY_VKKZ = N(8,0 ,"#RN-KEY-VKKZ");
@L2 A xRA_KEY_VVA = A(2 ,"#RA-KEY-VVA");
@L2 N xRN_KEY_VVNR = N(8,0 ,"#RN-KEY-VVNR");
@L2 N xRN_KEY_VHNR = N(3,0 ,"#RN-KEY-VHNR");
@L2 N xRN_KEY_VGES = N(3,0 ,"#RN-KEY-VGES");
@L2 N xRN_KEY_VRES = N(1,0 ,"#RN-KEY-VRES");
@L2 N xRN_KEY_VVIN = N(2,0 ,"#RN-KEY-VVIN");
@L2 N xRN_KEY_VBRA = N(2,0 ,"#RN-KEY-VBRA");
@L2 N xRN_KEY_VBRN = N(1,0 ,"#RN-KEY-VBRN");
@L2 A xRA_KEY_VBTART = A(1 ,"#RA-KEY-VBTART");
@L2 A xRA_KEY_VBSTAT = A(1 ,"#RA-KEY-VBSTAT");
@L2 N xRN_KEY_VUGES = N(3,0 ,"#RN-KEY-VUGES");
@L2 N xRN_KEY_VBJAHR = N(2,0 ,"#RN-KEY-VBJAHR");
@L2 N xRN_KEY_AKTONR = N(7,0 ,"#RN-KEY-AKTONR");
@L2 Redefine xRN_KEY_AKTONR; // REDEF. BEGIN : #RN-KEY-AKTONR
@L3 A xRA_KEY_VKTO = A(7 ,"#RA-KEY-VKTO");
@L2 A xRA_KEY_KOSEL = A(1 ,"#RA-KEY-KOSEL");
@L2 A xRA_KEY_LEER1 = A(4 ,"#RA-KEY-LEER1");
@L2 N xRN_KEY_AKKZ = N(8,0 ,"#RN-KEY-AKKZ");
@L2 A xRA_KEY_AVA = A(2 ,"#RA-KEY-AVA");
@L2 N xRN_KEY_AVNR = N(8,0 ,"#RN-KEY-AVNR");
@L2 N xRN_KEY_AHNR = N(3,0 ,"#RN-KEY-AHNR");
@L2 N xRN_KEY_AGES = N(3,0 ,"#RN-KEY-AGES");
@L2 N xRN_KEY_ARES = N(1,0 ,"#RN-KEY-ARES");
@L2 N xRN_KEY_AVIN = N(2,0 ,"#RN-KEY-AVIN");
@L2 N xRN_KEY_ABRA = N(2,0 ,"#RN-KEY-ABRA");
@L2 N xRN_KEY_ABRN = N(1,0 ,"#RN-KEY-ABRN");
@L2 A xRA_KEY_ABTART = A(1 ,"#RA-KEY-ABTART");
@L2 A xRA_KEY_ABSTAT = A(1 ,"#RA-KEY-ABSTAT");
@L2 N xRN_KEY_AUGES = N(3,0 ,"#RN-KEY-AUGES");
@L2 N xRN_KEY_ABJAHR = N(2,0 ,"#RN-KEY-ABJAHR");
@L2 N xRN_KEY_KTONR = N(7,0 ,"#RN-KEY-KTONR");
@L2 N xRN_KEY_PROV = N(2,0 ,"#RN-KEY-PROV");
@L2 N xRN_KEY_PROD = N(2,0 ,"#RN-KEY-PROD");
@L2 A xRA_KEY_PRKZ = A(1 ,"#RA-KEY-PRKZ");
@L2 N xRN_KEY_AFGS = N(3,0 ,"#RN-KEY-AFGS");
@L2 A xRA_KEY_AFPO = A(14 ,"#RA-KEY-AFPO");
@L2 N xRN_KEY_VFGS = N(3,0 ,"#RN-KEY-VFGS");
@L2 A xRA_KEY_VFPO = A(14 ,"#RA-KEY-VFPO");
@L2 N xRN_KEY_FBRA = N(2,0 ,"#RN-KEY-FBRA");
@L2 N xRN_KEY_NOTD = N(6,0 ,"#RN-KEY-NOTD");
@L2 Redefine xRN_KEY_NOTD;
@L3 N xRN_KEY_NOTD_YYYY = N(4,0 ,"#RN-KEY-NOTD-YYYY");
@L3 N xRN_KEY_NOTD_MM = N(2,0 ,"#RN-KEY-NOTD-MM");
@L2 N xRN_KEY_LSNR = N(2,0 ,"#RN-KEY-LSNR");
@L2 N xRN_KEY_KBJ = N(4,0 ,"#RN-KEY-KBJ");
@L2 N xRN_KEY_KBJNR = N(1,0 ,"#RN-KEY-KBJNR");
@L2 A xRA_KEY_ZNAM = A(31 ,"#RA-KEY-ZNAM");
@L2 A xRA_KEY_VNAM = A(2 ,"#RA-KEY-VNAM");
@L2 A xRA_KEY_STRA = A(1 ,"#RA-KEY-STRA");
@L2 N xRN_KEY_HAUS = N(4,0 ,"#RN-KEY-HAUS");
@L2 N xRN_KEY_POST = N(6,0 ,"#RN-KEY-POST");
@L2 A xRA_KEY_KZEX = A(1 ,"#RA-KEY-KZEX");
@L2 A xRA_KEY_ACHT = A(1 ,"#RA-KEY-ACHT");
@L2 N xRN_KEY_ALFA = N(2,0 ,"#RN-KEY-ALFA");
@L2 N xRN_KEY_BERU = N(5,0 ,"#RN-KEY-BERU");
@L2 A xRA_KEY_LEER2 = A(4 ,"#RA-KEY-LEER2");
@L2 A xRA_KEY_BRSL = A(1 ,"#RA-KEY-BRSL");
@L2 A xRA_KEY_PSTA = A(1 ,"#RA-KEY-PSTA");
@L2 A xRA_KEY_BRAW = A(1 ,"#RA-KEY-BRAW");
@L2 N xRN_KEY_BRVO = N(2,0 ,"#RN-KEY-BRVO");
@L2 N xRN_KEY_BRBI = N(2,0 ,"#RN-KEY-BRBI");
@L2 N xRN_KEY_JRVO = N(2,0 ,"#RN-KEY-JRVO");
@L2 N xRN_KEY_JRBI = N(2,0 ,"#RN-KEY-JRBI");
@L2 A xRA_KEY_VNAW = A(1 ,"#RA-KEY-VNAW");
@L2 A xRA_KEY_KEIG = A(1 ,"#RA-KEY-KEIG");
@L2 A xRA_KEY_SBID = A(3 ,"#RA-KEY-SBID");
@L2 A xRA_KEY_KURO = A(2 ,"#RA-KEY-KURO");
@L2 A xRA_KEY_PRAE = A(1 ,"#RA-KEY-PRAE");
@L2 A xRA_KEY_PROV = A(1 ,"#RA-KEY-PROV");
@L2 A xRA_KEY_OFFE = A(1 ,"#RA-KEY-OFFE");
@L2 A xRA_KEY_AKTU = A(1 ,"#RA-KEY-AKTU");
@L2 A xRA_KEY_ALLE = A(1 ,"#RA-KEY-ALLE");
@L2 A xRA_KEY_AOFA = A(1 ,"#RA-KEY-AOFA");
@L2 N xRN_KEY_BUDT = N(8,0 ,"#RN-KEY-BUDT");
@L2 P xRN_KEY_BUNR = P(9,0 ,"#RN-KEY-BUNR");
@L2 A xRA_KEY_VKZ = A(1 ,"#RA-KEY-VKZ");
@L2 A xRA_KEY_ABGA = A(1 ,"#RA-KEY-ABGA");
@L2 A xRA_KEY_SART = A(2 ,"#RA-KEY-SART");
@L2 A xRA_KEY_PFIL = A(2 ,"#RA-KEY-PFIL");
@L2 L xRL_KEY_UHR = L("#RL-KEY-UHR");
@L2 A xRA_KEY_STAT = A(1 ,"#RA-KEY-STAT");
@L2 A xRA_KEY_GSMT = A(1 ,"#RA-KEY-GSMT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_1 = A(8 ,"#RA-KEY-08X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_VVO = A(22 ,"#RA-KEY-VVO");
@L2 A xRA_KEY_27X_1 = A(27 ,"#RA-KEY-27X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVO = A(22 ,"#RA-KEY-AVO");
@L2 A xRA_KEY_27X_2 = A(27 ,"#RA-KEY-27X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVO = A(22 ,"#RA-KEY-GVO");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_2 = A(8 ,"#RA-KEY-08X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_VBT = A(24 ,"#RA-KEY-VBT");
@L2 A xRA_KEY_24X = A(24 ,"#RA-KEY-24X"); // ZWISCHENRAUM
@L2 A xRA_KEY_ABT = A(24 ,"#RA-KEY-ABT");
@L1 Redefine gKEY; // REDEF. BEGIN : +KEY
@L2 A xRA_KEY_08X_3 = A(8 ,"#RA-KEY-08X-3");// ZWISCHENRAUM
@L2 A xRA_KEY_VVK = A(16 ,"#RA-KEY-VVK");
@L2 A xRA_KEY_33X_1 = A(33 ,"#RA-KEY-33X-1");// ZWISCHENRAUM
@L2 A xRA_KEY_AVK = A(16 ,"#RA-KEY-AVK");
@L2 A xRA_KEY_33X_2 = A(33 ,"#RA-KEY-33X-2");// ZWISCHENRAUM
@L2 A xRA_KEY_GVK = A(16 ,"#RA-KEY-GVK");
@L1 A gSTV = A(250 ,"+STV");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_APRG = A(8 ,"#RA-STV-APRG");
@L2 A xRA_STV_VPRG = A(8 ,"#RA-STV-VPRG");
@L2 A xRA_STV_FUNK = A(4 ,"#RA-STV-FUNK");
@L2 Redefine xRA_STV_FUNK; // REDEF. BEGIN : #RA-STV-FUNK
@L3 N xRN_STV_FUNR = N(2,0 ,"#RN-STV-FUNR");
@L3 A xRA_STV_FUNR = A(2 ,"#RA-STV-FUNR");
@L2 N xRN_STV_AUSW = N(2,0 ,"#RN-STV-AUSW");
@L2 A xRA_STV_CODE = A(4 ,"#RA-STV-CODE"); // 30
@L2 N xRN_STV_OBNR = N(8,0 ,"#RN-STV-OBNR");
@L2 A xRA_STV_OART = A(2 ,"#RA-STV-OART");
@L2 N xRN_STV_HINR = N(3,0 ,"#RN-STV-HINR");
@L2 N xRN_STV_VINR = N(2,0 ,"#RN-STV-VINR");
@L2 N xRN_STV_BRAN = N(2,0 ,"#RN-STV-BRAN");
@L2 N xRN_STV_BRNR = N(1,0 ,"#RN-STV-BRNR");
@L2 N xRN_STV_GENR = N(3,0 ,"#RN-STV-GENR");
@L2 A xRA_STV_GMOD = A(1 ,"#RA-STV-GMOD");
@L2 A xRA_STV_AMOD = A(1 ,"#RA-STV-AMOD");
@L2 A xRA_STV_VIKZ = A(1 ,"#RA-STV-VIKZ");
@L2 A xRA_STV_DATI = A(8 ,"#RA-STV-DATI");
@L2 N xRN_STV_DATA = N(3,0 ,"#RN-STV-DATA");
@L2 A xRA_STV_OPID = A(3 ,"#RA-STV-OPID");
@L2 A xRA_STV_PFKY = A(96 ,"#RA-STV-PFKY");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PFF = A(4 ,"#RA-STV-PFF");
@L2 Redefine xRA_STV_PFKY; // REDEF. BEGIN : #RA-STV-PFKY
@L3 A xRA_STV_PF = A(4 ,"#RA-STV-PF").defineArray(range(1, 24));
@L2 N xRN_STV_DATN = N(6,0 ,"#RN-STV-DATN");
@L2 A xRA_STV_MMOD = A(3 ,"#RA-STV-MMOD");
@L1 Redefine gSTV; // REDEF. BEGIN : +STV
@L2 A xRA_STV_241X = A(241 ,"#RA-STV-241X");
@L2 A xRA_STV_APAR = A(1 ,"#RA-STV-APAR");
@L2 P xRN_STV_CURS = P(6,0 ,"#RN-STV-CURS");
@L2 P xRN_STV_MARK = P(3,0 ,"#RN-STV-MARK");
@L2 B xRN_STV_FNUM = B(2 ,"#RN-STV-FNUM");
@L1 A gKEY1 = A(250 ,"+KEY1");
@L1 Redefine gKEY1; // REDEF. BEGIN : +KEY1
@L2 A xRA_KEY1_ASSTART = A(4 ,"#RA-KEY1-ASSTART");
@L2 N xRN_KEY1_POSVI = N(2,0 ,"#RN-KEY1-POSVI");
@L2 N xRN_KEY1_POSVII = N(3,0 ,"#RN-KEY1-POSVII");
@L2 A xRA_KEY1_RVART = A(4 ,"#RA-KEY1-RVART");
@L2 N xRN_KEY1_RVVON = N(8,0 ,"#RN-KEY1-RVVON");
@L2 N xRN_KEY1_RVBHJAHR = N(4,0 ,"#RN-KEY1-RVBHJAHR");
@L2 N xRN_KEY1_RVBBNR = N(2,0 ,"#RN-KEY1-RVBBNR");
@L2 P xRN_KEY1_SBVANZ = P(1,0 ,"#RN-KEY1-SBVANZ");
@L2 P xRN_KEY1_SBVINR = P(2,0 ,"#RN-KEY1-SBVINR").defineArray(range(1, 2));
@L2 P xRN_KEY1_SBVII = P(2,0 ,"#RN-KEY1-SBVII").defineArray(range(1, 2));
@L2 N xRN_KEY1_BRA = N(2,0 ,"#RN-KEY1-BRA").defineArray(range(1, 3));
@L2 N xRN_KEY1_GEBDT = N(8,0 ,"#RN-KEY1-GEBDT");
@L2 A xRA_KEY1_2X = A(2 ,"#RA-KEY1-2X");
@L2 A xRA_KEY1_WAEHRUNG = A(3 ,"#RA-KEY1-WAEHRUNG");
@L2 A xRA_KEY1_SK_ALLE = A(1 ,"#RA-KEY1-SK-ALLE");
@L2 N xRN_KEY1_HNR = N(4,0 ,"#RN-KEY1-HNR");
@L2 A xRA_KEY1_MTVKZ = A(1 ,"#RA-KEY1-MTVKZ");
@L2 A xRA_KEY1_VIL = A(1 ,"#RA-KEY1-VIL");
@L2 A xRA_KEY1_OKZ = A(3 ,"#RA-KEY1-OKZ");
@L2 N xRN_KEY1_OPLZ = N(6,0 ,"#RN-KEY1-OPLZ");
@L2 N xRN_KEY1_OHNR = N(4,0 ,"#RN-KEY1-OHNR");
@L2 P xRN_KEY1_SUBVR = P(3,0 ,"#RN-KEY1-SUBVR");
@L2 P xRN_KEY1_SUBKTO = P(7,0 ,"#RN-KEY1-SUBKTO");
@L2 P xRN_KEY1_SUBZWST = P(3,0 ,"#RN-KEY1-SUBZWST");
@L2 A xRA_KEY1_SKART = A(3 ,"#RA-KEY1-SKART");
@L2 A xRA_KEY1_TCSART = A(1 ,"#RA-KEY1-TCSART");
@L2 A xRA_KEY1_TCZ1 = A(2 ,"#RA-KEY1-TCZ1");
@L2 Redefine xRA_KEY1_TCZ1; // REDEF. BEGIN : #RA-KEY1-TCZ1
@L3 N xRN_KEY1_TCZ1 = N(2,0 ,"#RN-KEY1-TCZ1");
@L2 P xRN_KEY1_TCODE = P(5,0 ,"#RN-KEY1-TCODE");
@L2 P xRN_KEY1_TCZ2 = P(2,0 ,"#RN-KEY1-TCZ2");
@L2 A xRA_KEY1_NATV = A(3 ,"#RA-KEY1-NATV");
@L2 A xRA_KEY1_NATO = A(3 ,"#RA-KEY1-NATO");
@L2 N xRN_KEY1_SNR = N(8,0 ,"#RN-KEY1-SNR");
@L2 N xRN_KEY1_LNR = N(5,0 ,"#RN-KEY1-LNR");
@L2 A xRA_KEY1_ZISB = A(3 ,"#RA-KEY1-ZISB");
@L2 N xRN_KEY1_STAT = N(5,0 ,"#RN-KEY1-STAT");
@L2 N xRN_KEY1_LZ = N(2,0 ,"#RN-KEY1-LZ");
@L2 A xRA_KEY1_GRDHA = A(2 ,"#RA-KEY1-GRDHA");
@L2 A xRA_KEY1_NATOV = A(3 ,"#RA-KEY1-NATOV");
@L2 N xRN_KEY1_AWDT = N(8,0 ,"#RN-KEY1-AWDT");
@L2 A xRA_KEY1_KOSP = A(1 ,"#RA-KEY1-KOSP");
@L2 N xRN_KEY1_EDSTAT = N(2,0 ,"#RN-KEY1-EDSTAT");
@L2 A xRA_KEY1_FREI = A(7 ,"#RA-KEY1-FREI");
@L2 N xRN_KEY1_KDAT = N(8,0 ,"#RN-KEY1-KDAT");
@L2 N xRN_KEY1_KGRD = N(2,0 ,"#RN-KEY1-KGRD");
@L2 N xRN_KEY1_PERSNR = N(5,0 ,"#RN-KEY1-PERSNR");
@L2 A xRA_KEY1_RPART = A(2 ,"#RA-KEY1-RPART");
@L2 N xRN_KEY1_RPDAT = N(8,0 ,"#RN-KEY1-RPDAT");
@L2 A xRA_KEY1_RVKZ = A(1 ,"#RA-KEY1-RVKZ");
@L2 A xRA_KEY1_RVKZ2 = A(1 ,"#RA-KEY1-RVKZ2");
@L2 A xRA_KEY1_LK = A(30 ,"#RA-KEY1-LK");
@L2 A xRA_KEY1_SCHEMA = A(2 ,"#RA-KEY1-SCHEMA");
@L2 N xRN_KEY1_RSOBNR = N(8,0 ,"#RN-KEY1-RSOBNR");
@L2 A xRA_KEY1_RSKZ = A(1 ,"#RA-KEY1-RSKZ");
@L2 A xRA_KEY1_BRFA = A(3 ,"#RA-KEY1-BRFA");
@L2 A xRA_KEY1_BUCHART = A(3 ,"#RA-KEY1-BUCHART");
@L2 N xRN_KEY1_BANKSCHL = N(3,0 ,"#RN-KEY1-BANKSCHL");
@L2 N xRN_KEY1_WERTEDAT = N(8,0 ,"#RN-KEY1-WERTEDAT");
@L2 A xRA_KEY1_VORPROG = A(8 ,"#RA-KEY1-VORPROG");
@L2 N xRN_KEY1_GILT_BIS = N(8,0 ,"#RN-KEY1-GILT-BIS");
@L2 P xRN_KEY1_FNR = P(3,0 ,"#RN-KEY1-FNR");
@L2 A xRA_KEY1_KO55MELD = A(1 ,"#RA-KEY1-KO55MELD");
@L1 A gKEY2 = A(250 ,"+KEY2");
@L1 Redefine gKEY2; // REDEF. BEGIN : +KEY2
@L2 A xRA_KEY2_TARIF = A(7 ,"#RA-KEY2-TARIF");
@L2 A xRA_KEY2_BANKNA = A(27 ,"#RA-KEY2-BANKNA");
@L2 A xRA_KEY2_USERGRUPPE = A(8 ,"#RA-KEY2-USERGRUPPE");
@L2 N xRN_KEY2_KOSTST = N(5,0 ,"#RN-KEY2-KOSTST");
@L2 A xRA_KEY2_AVISO_OP = A(3 ,"#RA-KEY2-AVISO-OP");
@L2 N xRN_KEY2_AVISO_DT = N(8,0 ,"#RN-KEY2-AVISO-DT");
@L2 N xRN_KEY2_AVISO_GD = N(3,0 ,"#RN-KEY2-AVISO-GD");
@L2 N xRN_KEY2_AVISO_DTB = N(8,0 ,"#RN-KEY2-AVISO-DTB");
@L2 A xRA_KEY2_SPRACH_KZ = A(2 ,"#RA-KEY2-SPRACH-KZ");
@L2 A xRA_KEY2_GEWINN_VB = A(4 ,"#RA-KEY2-GEWINN-VB");
@L2 N xRN_KEY2_GEWINN_JAHR = N(4,0 ,"#RN-KEY2-GEWINN-JAHR");
@L2 A xRA_KEY2_ABST_SA = A(2 ,"#RA-KEY2-ABST-SA");
@L2 A xRA_KEY2_ABST_ZA = A(1 ,"#RA-KEY2-ABST-ZA");
@L2 P xRN_KEY2_ABST_ZR = P(9,0 ,"#RN-KEY2-ABST-ZR");
@L2 A xRA_KEY2_ABST_NK = A(1 ,"#RA-KEY2-ABST-NK");
@L2 A xRA_KEY2_ABST_NA = A(8 ,"#RA-KEY2-ABST-NA");
@L2 A xRA_KEY2_FAB = A(3 ,"#RA-KEY2-FAB");
@L2 A xRA_KEY2_TYP = A(10 ,"#RA-KEY2-TYP");
@L2 A xRA_KEY2_FZA = A(1 ,"#RA-KEY2-FZA");
@L2 N xRN_KEY2_LST = N(1,0 ,"#RN-KEY2-LST");
@L2 N xRN_KEY2_KTBJ = N(4,0 ,"#RN-KEY2-KTBJ");
@L2 N xRN_KEY2_KW = N(3,0 ,"#RN-KEY2-KW");
@L2 P xRN_KEY2_UEBWNR = P(6,0 ,"#RN-KEY2-UEBWNR");
@L2 P xRN_KEY2_UNTNR = P(4,0 ,"#RN-KEY2-UNTNR");
@L2 A xRA_KEY2_SERIE = A(1 ,"#RA-KEY2-SERIE");
@L2 P xRN_KEY2_PAKNR = P(9,0 ,"#RN-KEY2-PAKNR");
@L2 A xRA_KEY2_WHG_VON = A(3 ,"#RA-KEY2-WHG-VON");
@L2 A xRA_KEY2_WHG_NACH = A(3 ,"#RA-KEY2-WHG-NACH");
@L2 A xRA_KEY2_EXTSA = A(3 ,"#RA-KEY2-EXTSA");
@L2 N xRN_KEY2_GKNR = N(8,0 ,"#RN-KEY2-GKNR");
@L1 C xCV_WHBS = C("#CV-WHBS"); // GW FUER WIEDERHOLTE BS-AUSG.
@L1 C xCV_OUTP = C("#CV-OUTP"); // PROTECT BEI A+B - PROGRAMMEN
@L1 C xCV_STV = C("#CV-STV"); // KEY-FELDER STEUERLEISTE
@L1 L xWL_KEYWECHS = L("#WL-KEYWECHS"); // SCHALTER, OB KEYWECHSEL
@L1 L xWL_INTERN = L("#WL-INTERN"); // SCHALTER, OB BLINDER DURCHL.
@L1 A xWA_HELP_TASTE = A(4 ,"#WA-HELP-TASTE"); // PF-TASTE FUER 'HELP'
@L1 A xWA_MENU_TASTE = A(4 ,"#WA-MENU-TASTE"); // PF-TASTE FUER 'MENU'
@L1 P xWN_SEL_ISN = P(9,0 ,"#WN-SEL-ISN").defineArray(range(1,24));
@L1 A xWA_FUNKTION = A(5 ,"#WA-FUNKTION").defineArray(range(1,100));// FUNKTIONSTABELLE
@L1 A xWA_VORWEG_AUFRUF = A(8 ,"#WA-VORWEG-AUFRUF");// VORWEG-AUFRUF (Z.B. SCHWEBE)
@L1 L xWL_TEST = L("#WL-TEST"); // KENNZEICHEN, OB TEST
@L1 N xWN_WARTEN_SUMME = N(5,0 ,"#WN-WARTEN-SUMME");// ZAEHLER FUER SUSPEND
@L1 A xWA_AUSWAHL_KZ = A(1 ,"#WA-AUSWAHL-KZ").defineArray(range(1,24));// MARKIERUNGSLEISTE
@L1 N xWN_AUSWAHL_ANZAHL = N(2,0 ,"#WN-AUSWAHL-ANZAHL");
@L1 A xWA_AUSWAHL_GUELTIG = A(20 ,"#WA-AUSWAHL-GUELTIG");
/*
* Platz für neue globale Variable
* Definitionen in:
* YLNEUGLO/YCNEUGLO
*/
@L1 A gNEUGLO_01 = A(250 ,"+NEUGLO-01");
@L1 Redefine gNEUGLO_01;
@L2 A xBTC_STEUERUNG = A(8 ,"#BTC-STEUERUNG");
@L2 A xUMS_MIS_BILANZKREIS = A(1 ,"#UMS-MIS-BILANZKREIS").defineArray(range(1, 7));
@L2 A xUMS_MIS_BRAN = A(2 ,"#UMS-MIS-BRAN").defineArray(range(1, 10));
@L2 P xKEY1_SUCH_BETRAG = P(9,2 ,"#KEY1-SUCH-BETRAG");
@L2 P xKEY2_ARCHIV_DB = P(3,0 ,"#KEY2-ARCHIV-DB");
@L2 A xKEY2_EXTKZ = A(1 ,"#KEY2-EXTKZ");
@L2 N xRN_KEY_JAHRV = N(4,0 ,"#RN-KEY-JAHRV");
@L2 N xRN_KEY_JAHRB = N(4,0 ,"#RN-KEY-JAHRB");
@L2 A xRA_KEY_EXT_GRUND_NAME = A(31 ,"#RA-KEY-EXT-GRUND-NAME");
@L2 P xRN_KEY_EXT_BB_VON = P(8,0 ,"#RN-KEY-EXT-BB-VON");
@L2 P xRN_KEY_EXT_BB_BIS = P(8,0 ,"#RN-KEY-EXT-BB-BIS");
@L2 P xUMS_MA_SUB_SATZART_1 = P(3,0 ,"#UMS-MA-SUB-SATZART-1");
@L2 A xRA_KEY_EXT_ANTRAGSART = A(3 ,"#RA-KEY-EXT-ANTRAGSART");
@L2 P xFLV_TRANS = P(7,0 ,"#FLV-TRANS");
@L2 A xFLV_ISIN = A(12 ,"#FLV-ISIN");
@L2 P xFLV_BUNR = P(7,0 ,"#FLV-BUNR");
@L2 P xFLV_GILT_BIS = P(8,0 ,"#FLV-GILT-BIS");
@L2 P xFLV_DATUM = P(8,0 ,"#FLV-DATUM");
@L2 P xFLV_VV_VINR = P(3,0 ,"#FLV-VV-VINR");
@L2 P xFLV_VV_BRAN = P(3,0 ,"#FLV-VV-BRAN");
@L2 P xFLV_VV_BRNR = P(3,0 ,"#FLV-VV-BRNR");
@L2 P xFLV_VV_HINR = P(3,0 ,"#FLV-VV-HINR");
@L2 A xVLK_EXVERARB = A(30 ,"#VLK-EXVERARB");
@L2 P xRN_KEY_IVKNR = P(8,0 ,"#RN-KEY-IVKNR");
@L2 A xRA_SC_MELDUNGS_ART = A(1 ,"#RA-SC-MELDUNGS-ART");
@L2 A xRA_OS_GFALL = A(2 ,"#RA-OS-GFALL");
@L2 A xRA_OS_DOKART = A(4 ,"#RA-OS-DOKART");
@L2 A xRA_OS_BR_ART = A(3 ,"#RA-OS-BR-ART");
@L2 N xRN_KEY_YU_PERSNR = N(13,0 ,"#RN-KEY-YU-PERSNR");
@L2 N xSCN_VERS_BEGINN = N(8,0 ,"#SCN-VERS-BEGINN");
@L2 A xDI_ABTEILUNG = A(8 ,"#DI-ABTEILUNG");
@L2 P xDI_STAT_DZT = P(3,0 ,"#DI-STAT-DZT");
@L2 P xDI_STAT_DZT_BIS = P(3,0 ,"#DI-STAT-DZT-BIS");
@L2 A xDI_SPARTE = A(2 ,"#DI-SPARTE");
@L2 A xDI_STATION = A(8 ,"#DI-STATION");
@L2 A xDI_WM_OPID = A(8 ,"#DI-WM-OPID");
@L2 A xDI_AW_POL_FIL = A(1 ,"#DI-AW-POL-FIL");
@L2 A xDI_VAE_ART = A(3 ,"#DI-VAE-ART");
@L2 A xDI_WK_VAE_ART = A(3 ,"#DI-WK-VAE-ART");
@L2 A xDI_AW_SUB_VR = A(1 ,"#DI-AW-SUB-VR");
@L2 P xDI_SUB_VR = P(3,0 ,"#DI-SUB-VR");
@L2 P xDI_POL_KREIS = P(2,0 ,"#DI-POL-KREIS");
@L2 P xDI_POL_BRAN = P(2,0 ,"#DI-POL-BRAN");
@L2 A xDI_VAE_HINW = A(3 ,"#DI-VAE-HINW");
@L1 A gNEUGLO_02 = A(250 ,"+NEUGLO-02");
@L1 Redefine gNEUGLO_02;
@L2 A xDI_BEARB_GRP = A(3 ,"#DI-BEARB-GRP");
@L2 A xDI_AW_DRF = A(1 ,"#DI-AW-DRF");
@L2 A xDI_AW_E_ANT = A(1 ,"#DI-AW-E-ANT");
@L2 P xDI_PC_ANT_VKTO = P(7,0 ,"#DI-PC-ANT-VKTO");
@L2 P xDI_PLZ_VON = P(7,0 ,"#DI-PLZ-VON");
@L2 P xDI_PLZ_BIS = P(7,0 ,"#DI-PLZ-BIS");
@L2 A xDI_POSTNATION = A(3 ,"#DI-POSTNATION");
@L2 P xDI_WM_DATUM = P(8,0 ,"#DI-WM-DATUM");
@L2 P xDI_POL_FILIALE = P(2,0 ,"#DI-POL-FILIALE");
@L2 A xDI_AW_VAE_HINW = A(1 ,"#DI-AW-VAE-HINW");
@L2 A xDI_AW_VKTO = A(1 ,"#DI-AW-VKTO");
@L2 A xDI_AW_BEARB_GRP = A(1 ,"#DI-AW-BEARB-GRP");
@L2 A xDI_AW_KR_BR = A(1 ,"#DI-AW-KR-BR");
@L2 A xDI_AW_VAE_ART = A(1 ,"#DI-AW-VAE-ART");
@L2 P xDI_VR_KEY = P(3,0 ,"#DI-VR-KEY");
@L2 A xDI_OART = A(2 ,"#DI-OART");
@L2 P xDI_WM_DATUM_VON = P(8,0 ,"#DI-WM-DATUM-VON");
@L2 A xDI_WM_AUSWAHL = A(1 ,"#DI-WM-AUSWAHL");
@L2 A xRA_KEY_OIB = A(20 ,"#RA-KEY-OIB");
}

View File

@@ -0,0 +1,158 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * header created during migration process *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION LOESCHEN UMKONTIERUNGEN
* * @OWNER P36
* **************************************************************
* **************************************************************
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* *
* * L&ouml;schen Umkontierungen
* *
* </pre>
*/
@Natural
public class MDBT74B extends NaturalProgram {
@Local
@L1 N xWN_DATN = N(8,0 ,"#WN-DATN");
@L1 Redefine xWN_DATN;
@L2 N xRN_DATN_YY = N(4,0 ,"#RN-DATN-YY");
@L2 N xRN_DATN_MM = N(2,0 ,"#RN-DATN-MM");
@L2 N xRN_DATN_TT = N(2,0 ,"#RN-DATN-TT");
@L1 P xWN_ISN = P(9,0 ,"#WN-ISN");
@L1 P xWN_Z1 = P(9,0 ,"#WN-Z1");
@L1 A xWA_SPARTEN = A(14 ,"#WA-SPARTEN");
@L1 A xWA_PRV_ART = A(3 ,"#WA-PRV-ART");
@L1 A xWA_UMKONT_MENGE = A(31 ,"#WA-UMKONT-MENGE");
@L1 Redefine xWA_UMKONT_MENGE;
@L2 P xRN_UMKONT_MENGE = P(7,0 ,"#RN-UMKONT-MENGE").defineArray(range(1, 4));
@L1 @ViewOf SYSTEM.MDUMKONT_DDM MDUMKONT = view("MDUMKONT");
@L2 A SPARTEN = A().defineArray(range(1, 4));
@L2 A PRV_ART = A().defineArray(range(1, 2));
@L2 A SATZART;
@L2 N AUSLEER_DATUM;
@L2 N VR_KEY;
@L2 N VKTO;
@L2 N VKTO_ZIEL;
@L2 N UMKONT_PER;
@L2 A UMKONT_ART;
@L2 N OBNR;
@L1 @ViewOf SYSTEM.MDUMKONT_DDM MDUMKONT_DEL = view("MDUMKONT-DEL");
@L2 A SPARTEN = A().defineArray(range(1, 4));
@L2 A PRV_ART = A().defineArray(range(1, 2));
@L2 A OVKEY = A().defineArray(range(1, 4));
@L2 A UMKONT_MENGE;
@L2 N VR_KEY;
@L2 N VKTO;
@L2 N VKTO_ZIEL;
@L2 N UMKONT_PER;
@L2 A UMKONT_ART;
@L2 N OBNR;
@L2 A ANLAGE_VON;
@L2 N ANLAGE_AM;
@L2 N AUSLEER_DATUM;
/* additional loop and database objects */
Read readUMK;
Get getDEL;
MDBT74B.MDUMKONT UMK = MDUMKONT;
MDBT74B.MDUMKONT_DEL DEL = MDUMKONT_DEL;
@Override
public void run() {
/* * */
format(1, PS(68), LS(132));
//
displayDefinition(1, ES(true), DEL.VR_KEY, DEL.VKTO, EM("99^99999"), DEL.VKTO_ZIEL, EM("99^99999"), DEL.UMKONT_PER, DEL.UMKONT_ART, "SPARTEN", xWA_SPARTEN, newline(), "PRV-ART", xWA_PRV_ART, DEL.OVKEY, EM("XXXXXXXXXXXXXXX"), DEL.OBNR, DEL.ANLAGE_VON, DEL.ANLAGE_AM, DEL.AUSLEER_DATUM, "KUNDEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE, EM("Z^ZZZ^ZZ9"), newline(), "POLIZZEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE, EM("Z^ZZZ^ZZ9"), "SCHAEDEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE, EM("Z^ZZZ^ZZ9"), newline(), "BRANCHEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE, EM("Z^ZZZ^ZZ9"));
assign(xWN_DATN, $DATN());
subtractFrom(xWN_DATN.xRN_DATN_YY, 3);
assign(xWN_DATN.xRN_DATN_MM, 1);
assign(xWN_DATN.xRN_DATN_TT, 1);
/* * */
readUMK = read(MDUMKONT);
readUMK.registerForSort(UMK.VR_KEY, UMK.VKTO, UMK.VKTO_ZIEL, UMK.UMKONT_PER, UMK.UMKONT_ART, descending(), xWA_SPARTEN, xWA_PRV_ART, UMK.OBNR, using(xWN_ISN));
UMK:
while (readUMK.hasNext()) {
UMK = MDUMKONT;
if ( ! ((EQ(UMK.SATZART, "M")
&& EQ(UMK.AUSLEER_DATUM, thru(1, xWN_DATN))))) { /* ACCEPT */
reject();
}
compress(UMK.SPARTEN.get(range(1, 4)), into(xWA_SPARTEN));
compress(UMK.PRV_ART.get(range(1, 2)), into(xWA_PRV_ART));
assign(xWN_ISN, $ISN(readUMK));
readUMK.addForSort(UMK.VR_KEY, UMK.VKTO, UMK.VKTO_ZIEL, UMK.UMKONT_PER, UMK.UMKONT_ART, xWA_SPARTEN, xWA_PRV_ART, UMK.OBNR, xWN_ISN);
}
SortEvents sort1 = readUMK.sortEvents(UMK.VR_KEY, UMK.VKTO, UMK.VKTO_ZIEL, UMK.UMKONT_PER, UMK.UMKONT_ART, descending(), xWA_SPARTEN, xWA_PRV_ART, UMK.OBNR, using(xWN_ISN));
SRT:
while(sort1.hasNext()) {
if(sort1.isAtBreak(UMK.VKTO_ZIEL)) {
skip(1, 1);
}
if(sort1.isAtBreak(UMK.VKTO)) {
skip(1, 3);
}
if(sort1.isAtBreak(UMK.VKTO, 2)) {
skip(1, 5);
}
if(sort1.isAtBreak(UMK.VKTO, 1)) {
newpage(1);
}
if(sort1.isAtBreak(UMK.VR_KEY)) {
newpage(1);
}
if (sort1.mustProcess()) {
//
//
getDEL = get(MDUMKONT_DEL, withHold(), xWN_ISN);
DEL = MDUMKONT_DEL;
if (EQ(DEL.UMKONT_MENGE, " ")) {
reset(xWA_UMKONT_MENGE.xRN_UMKONT_MENGE.get($));
} else {
assign(xWA_UMKONT_MENGE, DEL.UMKONT_MENGE);
}
compress(DEL.SPARTEN.get(range(1, 4)), into(xWA_SPARTEN));
compress(DEL.PRV_ART.get(range(1, 2)), into(xWA_PRV_ART));
display(1, ES(true), DEL.VR_KEY, DEL.VKTO, EM("99^99999"), DEL.VKTO_ZIEL, EM("99^99999"), DEL.UMKONT_PER, DEL.UMKONT_ART, "SPARTEN", xWA_SPARTEN, newline(), "PRV-ART", xWA_PRV_ART, DEL.OVKEY.get($), EM("XXXXXXXXXXXXXXX"), DEL.OBNR, DEL.ANLAGE_VON, DEL.ANLAGE_AM, DEL.AUSLEER_DATUM, "KUNDEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE.get(1), EM("Z^ZZZ^ZZ9"), newline(), "POLIZZEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE.get(2), EM("Z^ZZZ^ZZ9"), "SCHAEDEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE.get(3), EM("Z^ZZZ^ZZ9"), newline(), "BRANCHEN", xWA_UMKONT_MENGE.xRN_UMKONT_MENGE.get(4), EM("Z^ZZZ^ZZ9"));
skip(1, 1);
delete(getDEL);
addTo(xWN_Z1, 1);
if (EQ(xWN_Z1, mask(cp(".......00")))) {
endTransaction();
}
}
}
// (SRT.)
endTransaction();
/* * */
}
/* Event handling methods */
@Override
protected void atWriteTitle(Number report) {
boolean done = false;
if (EQ(report, 1)) {
done = true;
write(1, $PROGRAM(), X(5), cp("GELOESCHTE UMKONTIERUNGEN STICHTAG:"), xWN_DATN, EM("9999-99-99"), T(80), "DATUM", $DATN(), AD("L"), T(100), "SEITE", $PAGE_NUMBER(1), AD("L"), newline(), newline());
}
if (done == false) {
super.atWriteTitle(report);
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package SYSTEM;
import innowake.mee.natural.api.*;
import database.FDT.*;
@Natural(moduleName="MDUMKONT")
public class MDUMKONT_DDM extends NaturalDdm {
public static final DbA SATZART = reference(FDT0022.AA, "SATZART");
public static final DbA V_FREIGABE = reference(FDT0022.CB, "V-FREIGABE");
public static final DbA ANLAGE_VON = reference(FDT0022.AC, "ANLAGE-VON");
public static final DbN ANLAGE_AM = reference(FDT0022.AD, "ANLAGE-AM");
public static final DbN AUSLEER_DATUM = reference(FDT0022.AF, "AUSLEER-DATUM");
public static final DbN VR_KEY = reference(FDT0022.CC, "VR-KEY", NUMERIC, 3, 0);
public static final DbN VKTO = reference(FDT0022.AY, "VKTO");
public static final DbN VKTO_ZIEL = reference(FDT0022.BC, "VKTO-ZIEL");
public static final DbA UMKONT_ART = reference(FDT0022.BD, "UMKONT-ART", 2);
public static final DbN UMKONT_PER = reference(FDT0022.BG, "UMKONT-PER");
public static final DbN OBNR = reference(FDT0022.AB, "OBNR");
public static final DbGroup SPARTEN_PE = reference(FDT0022.BH, "SPARTEN-PE");
public static final DbA SPARTEN = reference(FDT0022.BI, "SPARTEN");
public static final DbGroup PRV_ART_PE = reference(FDT0022.BJ, "PRV-ART-PE");
public static final DbA PRV_ART = reference(FDT0022.BK, "PRV-ART");
public static final DbGroup OVKEY_PE = reference(FDT0022.BU, "OVKEY-PE");
public static final DbA OVKEY = reference(FDT0022.BV, "OVKEY", 15);
public static final DbA KZ_PR_UMK_ABWEICH_KVA = reference(FDT0022.BZ, "KZ-PR-UMK-ABWEICH-KVA");
public static final DbA KZ_PR_UMK_AN_TL = reference(FDT0022.CA, "KZ-PR-UMK-AN-TL");
public static final DbN GT_ZO_VKTO_VON = reference(FDT0022.DA, "GT-ZO-VKTO-VON");
public static final DbB SPUMZIEL = reference(FDT0022.CX, "SPUMZIEL", BINARY, 19);
public static final DbB SPUMVKTO = reference(FDT0022.CY, "SPUMVKTO", BINARY, 19);
public static final DbA SPUMVA = reference(FDT0022.CZ, "SPUMVA");
public static final DbA UMKONT_MENGE = reference(FDT0022.AZ, "UMKONT-MENGE");
public static final DbB TIMESTMP = reference(FDT0022.BW, "TIMESTMP");
{
setDbId(0);
setFileName(22);
setFdt(FDT0022.class);
addSuppression(SPUMZIEL, Suppression.NullValueSuppression);
addSuppression(SPUMVKTO, Suppression.NullValueSuppression);
addSuppression(SPUMVA, Suppression.NullValueSuppression);
addHeader(SATZART, "SA");
addHeader(V_FREIGABE, "V-/Frei-/gabe");
addHeader(ANLAGE_VON, "Anlage/Opid");
addHeader(ANLAGE_AM, "Anlage/Datum");
addHeader(AUSLEER_DATUM, "Ausleer-/datum");
addHeader(UMKONT_ART, "Umkont-/Art");
addHeader(UMKONT_PER, "Umkont-/per");
addHeader(SPARTEN, "Umkont-/Sparten");
addHeader(PRV_ART, "Umkont-/PRV-Art");
addHeader(OVKEY, "Umkont-/OV-Key");
addHeader(TIMESTMP, "Timestamp");
}
}

View File

@@ -0,0 +1,113 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * header created during migration process *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION Rule : Vkto-Daten
* * @OWNER P07
* **************************************************************
* **************************************************************
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* </pre>
*/
@Natural(moduleName="V-MIT04")
public class V_MIT04 extends NaturalSubprogram {
//
// VKTO-Daten zu Original od. Schwebe
//
@Using KORIN.Diverse.Framework.GWSTGP_DA GWSTGP_DA = parameterUsing(KORIN.Diverse.Framework.GWSTGP_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWSTGP_DA) */
public final KORIN.Diverse.Framework.GWSTGP_DA.GWSTGP GWSTGP = GWSTGP_DA.GWSTGP;
/* @@USING:END(KORIN.Diverse.Framework.GWSTGP_DA) */
@Using KORIN.Diverse.Framework.GWPDA_M_DA GWPDA_M_DA = parameterUsing(KORIN.Diverse.Framework.GWPDA_M_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWPDA_M_DA) */
public final KORIN.Diverse.Framework.GWPDA_M_DA.MSG_INFO MSG_INFO = GWPDA_M_DA.MSG_INFO;
/* @@USING:END(KORIN.Diverse.Framework.GWPDA_M_DA) */
@Using V_MIT04P_DA V_MIT04P_DA = parameterUsing(V_MIT04P_DA.class);
/* @@USING:BEGIN(V_MIT04P_DA) */
public final V_MIT04P_DA.V_MIT04P V_MIT04P = V_MIT04P_DA.V_MIT04P;
/* @@USING:END(V_MIT04P_DA) */
@Using KORIN.Diverse.Framework.GWLOGNAM_DA GWLOGNAM_DA = localUsing(KORIN.Diverse.Framework.GWLOGNAM_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Framework.GWLOGNAM_DA) */
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_RETCODE xSTD_RETCODE = GWLOGNAM_DA.xSTD_RETCODE;
public final KORIN.Diverse.Framework.GWLOGNAM_DA.xSTD_LOOPCODE xSTD_LOOPCODE = GWLOGNAM_DA.xSTD_LOOPCODE;
/* @@USING:END(KORIN.Diverse.Framework.GWLOGNAM_DA) */
/* * */
// Daten für: MITZ1R
@Using MITZ1RP_DA MITZ1RP_DA = localUsing(MITZ1RP_DA.class);
/* @@USING:BEGIN(MITZ1RP_DA) */
public final MITZ1RP_DA.MITZ1RR MITZ1RR = MITZ1RP_DA.MITZ1RR;
public final MITZ1RP_DA.MITZ1RP MITZ1RP = MITZ1RP_DA.MITZ1RP;
/* @@USING:END(MITZ1RP_DA) */
@Local
@L1 Group xVON_MITZ1R = group("#VON-MITZ1R"); // <PRIME-KEY-VON>
@L2 A xGESAMT = A(7 ,"#GESAMT");
@L2 Redefine xGESAMT;
@L3 A SVKTO = A(7 ,"SVKTO");
@L3 Redefine SVKTO;
@L4 P VR_KEY = P(3,0 ,"VR-KEY");
@L4 P VKTO = P(7,0 ,"VKTO");
@L4 A STATUS_BYTE = A(1 ,"STATUS-BYTE");
/* * */
@Override
public void run() {
/* * */
reset(V_MIT04P.OUTPUTS);
/* * */
if (((NE(V_MIT04P.xVR_KEY, 0)
&& NE(V_MIT04P.xVKTO, 0))
&& NE(V_MIT04P.xSTATUS_BYTE, " "))) {
assign(xVON_MITZ1R.VR_KEY, V_MIT04P.xVR_KEY);
assign(xVON_MITZ1R.VKTO, V_MIT04P.xVKTO);
assign(xVON_MITZ1R.STATUS_BYTE, V_MIT04P.xSTATUS_BYTE);
//
//
assign(MITZ1RR.xVON_KEY, xVON_MITZ1R.xGESAMT);
assign(MITZ1RR.xFUNCTION, "Read");
callnat("MITZ1R", GWSTGP, MSG_INFO, MITZ1RR, MITZ1RP);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
escapeRoutine();
}
if ((NE(MITZ1RR.xSW_FOUND, "J")
&& EQ(V_MIT04P.xSCHWEBE_GILT, "J"))) {
assign(xVON_MITZ1R.STATUS_BYTE, "S");
assign(MITZ1RR.xVON_KEY, xVON_MITZ1R.xGESAMT);
assign(MITZ1RR.xFUNCTION, "Read");
callnat("MITZ1R", GWSTGP, MSG_INFO, MITZ1RR, MITZ1RP);
if (NE(MSG_INFO.xxRETURN_CODE, xSTD_RETCODE.OK)) {
escapeRoutine();
}
}
if (EQ(MITZ1RR.xSW_FOUND, "J")) {
assign(V_MIT04P.xZS_NAME, MITZ1RP.ZS_NAME.get(1));
assign(V_MIT04P.xVKTO_ORGBEZEICHNUNG, MITZ1RP.VKTO_ORGBEZEICHNUNG);
assign(V_MIT04P.xVKTO_STATUS_TEXT, MITZ1RP.VKTO_STATUS_TEXT);
assign(V_MIT04P.xVKTO_VERWENDUNG, MITZ1RP.VKTO_VERWENDUNG);
assign(V_MIT04P.xVKTO_VERWENDUNG_TEXT, MITZ1RP.VKTO_VERWENDUNG_TEXT);
if (GT(V_MIT04P.xVKTO_ORGBEZEICHNUNG, " ")) {
assign(V_MIT04P.xMAP_NAME, V_MIT04P.xVKTO_ORGBEZEICHNUNG);
} else {
assign(V_MIT04P.xMAP_NAME, V_MIT04P.xZS_NAME);
}
} else {
// KONTONUMMER :1: NICHT IN MITARBEITER-DATEI GESPEICHERT
assign(MSG_INFO.xxMSG_NR, 684);
assign(MSG_INFO.xxMSG_DATA.get(1), V_MIT04P.xVKTO);
escapeRoutine();
}
}
/* * */
}
}

View File

@@ -0,0 +1,148 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Diverse.Framework;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ****** $$LCMHEADER:20140410-V00.01**********
* * header created during migration process *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION Zentrale Abbruch-Routinen
* * @OWNER P05
* **************************************************************
* **************************************************************
* *LCM-HEADER END ****** $$LCMHEADER:20140410-V00.01**********
* </pre>
*/
@Natural(moduleName="#ABBRUCH")
public class XABBRUCH extends NaturalProgram {
//
// Programm-Abbruch-Routinen
//
// Schritt 1 bei Webservices:
//
// Protkollieren Msg
// Wegsichern Msg in AIV-Variablen
// Simulieren Fehler 954
// damit dieser in einer ON-ERROR-ROUTINE (z.B. Aussenfassade)
// gecatched werden kann, um die gesicherten Infos auszuwerten
//
@Using KORIN.Diverse.Webservices.WSAIVP_DA WSAIVP_DA = localUsing(KORIN.Diverse.Webservices.WSAIVP_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Webservices.WSAIVP_DA) */
public final KORIN.Diverse.Webservices.WSAIVP_DA.WSAIVP WSAIVP = WSAIVP_DA.WSAIVP;
/* @@USING:END(KORIN.Diverse.Webservices.WSAIVP_DA) */
@Using KORIN.Diverse.Webservices.WSAIVL_DA WSAIVL_DA = localUsing(KORIN.Diverse.Webservices.WSAIVL_DA.class);
/* @@USING:BEGIN(KORIN.Diverse.Webservices.WSAIVL_DA) */
public final KORIN.Diverse.Webservices.WSAIVL_DA.WSAIVL WSAIVL = WSAIVL_DA.WSAIVL;
/* @@USING:END(KORIN.Diverse.Webservices.WSAIVL_DA) */
@Using GWPDA_M_DA GWPDA_M_DA = localUsing(GWPDA_M_DA.class);
/* @@USING:BEGIN(GWPDA_M_DA) */
public final GWPDA_M_DA.MSG_INFO MSG_INFO = GWPDA_M_DA.MSG_INFO;
/* @@USING:END(GWPDA_M_DA) */
@Using GWLOGNAM_DA GWLOGNAM_DA = localUsing(GWLOGNAM_DA.class);
/* @@USING:BEGIN(GWLOGNAM_DA) */
public final GWLOGNAM_DA.xSTD_RETCODE xSTD_RETCODE = GWLOGNAM_DA.xSTD_RETCODE;
public final GWLOGNAM_DA.xSTD_LOOPCODE xSTD_LOOPCODE = GWLOGNAM_DA.xSTD_LOOPCODE;
/* @@USING:END(GWLOGNAM_DA) */
@Using CSTENTW.GWPRTSTP_DA GWPRTSTP_DA = localUsing(CSTENTW.GWPRTSTP_DA.class);
/* @@USING:BEGIN(CSTENTW.GWPRTSTP_DA) */
public final CSTENTW.GWPRTSTP_DA.GWPRTSTP GWPRTSTP = GWPRTSTP_DA.GWPRTSTP;
/* @@USING:END(CSTENTW.GWPRTSTP_DA) */
@Using CSTENTW.XTERM_A_DA XTERM_A_DA = localUsing(CSTENTW.XTERM_A_DA.class);
/* @@USING:BEGIN(CSTENTW.XTERM_A_DA) */
public final CSTENTW.XTERM_A_DA.xTERM_A xTERM_A = XTERM_A_DA.xTERM_A;
/* @@USING:END(CSTENTW.XTERM_A_DA) */
@Local
//
@L1 A xWA_MELDUNG = A(80 ,"#WA-MELDUNG").defineArray(range(1, 30));
@L1 A xWA_MESSAGE = A(72 ,"#WA-MESSAGE");
//
@L1 P xWN_DATA = P(3,0 ,"#WN-DATA");
//
@L1 A xERR_954 = A(2 ,"#ERR-954");
@L1 Redefine xERR_954;
@L2 P FORMAT_P = P(2,0 ,"FORMAT-P");
@L1 A xRUNTIME = A(32 ,"#RUNTIME");
@Override
public void run() {
//
// Umwandeln Stack in eine String
format(0, LS(81), IP(false));
//
// Kein Webservice:
// --> Aufruf Abbruch-Routine für 'Nicht-Webservices'
if (NE($APPLIC_ID(), "SYSRPC")) {
fetchReturn("ABBRUCH1");
/* @INCLUDE XTERM_C "0" */
/* @@INCLUDE:BEGIN(XTERM_C) */
/* *LCM-HEADER BEGIN ******************************************** */
/* * *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION CC für Kapsel Aufruf TERMINATE
* * @OWNER P01
* **************************************************************
* **************************************************************
* *LCM-HEADER END ******************************************** */
reset(xTERM_A);
assign(xTERM_A.xTERM_CODE, 0);
callnat("#TERM-N", xTERM_A);
/* @@INCLUDE:END(XTERM_C) */
}
assign(xWN_DATA, $DATA());
if (NE(xWN_DATA, 0)) {
input(PS(200), xWA_MELDUNG.get($));
compress(xWA_MELDUNG.get($), into(xWA_MESSAGE));
} else {
assign(xWA_MESSAGE, cp("*** Programm - Abbruch ***"));
}
//
// Protokoll
//
releaseStack();
backoutTransaction();
//
// Ermitteln runtime
reset(WSAIVP);
assign(WSAIVP.AUFRUFCODE, WSAIVL.xC_GET_ITEM);
assign(WSAIVP.BEZEICHNER, WSAIVL.xB_RUNTIME);
callnat("WSAIV", WSAIVP, MSG_INFO);
assign(xRUNTIME, WSAIVP.INHALT);
//
// Merken Infos in AIV
if (EQ(xRUNTIME, WSAIVL.xR_WS_CALL)) { // Aufruf aus Aussenfassade
reset(MSG_INFO);
assign(WSAIVP.AUFRUFCODE, WSAIVL.xC_PUT_ITEM);
assign(WSAIVP.BEZEICHNER, WSAIVL.xB_REINPUT_FNR);
assign(WSAIVP.INHALT, 1381);
callnat("WSAIV", WSAIVP, MSG_INFO);
assign(WSAIVP.AUFRUFCODE, WSAIVL.xC_PUT_ITEM);
assign(WSAIVP.BEZEICHNER, WSAIVL.xB_REINPUT_FVAR);
assign(WSAIVP.INHALT, xWA_MELDUNG.get(1));
callnat("WSAIV", WSAIVP, MSG_INFO);
assign(WSAIVP.AUFRUFCODE, WSAIVL.xC_PUT_ITEM);
assign(WSAIVP.BEZEICHNER, WSAIVL.xB_REINPUT_PGM);
assign(WSAIVP.INHALT, $PROGRAM());
callnat("WSAIV", WSAIVP, MSG_INFO);
//
assign(GWPRTSTP.AUFRUFENDES_PROGRAMM, $PROGRAM());
callnat("GWPRTST", GWPRTSTP);
//
// Produktion Fehler 954
addTo(xERR_954.FORMAT_P, 1);
//
} else {
// Sonstiger RPC (Host-Host, ...)
stop();
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
@Natural(moduleName="YPMA71VK")
public class YPMA71VK_DA extends NaturalData {
@Parameter
@L1 Group YPMA71VK = group("YPMA71VK");
@L2 A MARKIERUNG = A(1 ,"MARKIERUNG");
@L2 A MENU_TASTE = A(4 ,"MENU-TASTE");
@L2 N VR = N(3,0 ,"VR");
@L2 N VKTO = N(7,0 ,"VKTO");
@L2 N DATUM = N(6,0 ,"DATUM");
@L2 Redefine DATUM;
@L3 N JAHR = N(4,0 ,"JAHR");
@L3 N MONAT = N(2,0 ,"MONAT");
@L2 A SA = A(2 ,"SA");
}

View File

@@ -0,0 +1,33 @@
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
/**
* <pre>
* *LCM-HEADER BEGIN ********************************************
* * *
* **************************************************************
* * @MERGE-SOURCE N
* * @NO-MERGE N
* * @FUNCTION Parameter YNUMKLOG
* * @OWNER P36
* **************************************************************
* **************************************************************
* *LCM-HEADER END ********************************************
* </pre>
*/
@Natural(moduleName="YPUMKLOG")
public class YPUMKLOG_DA extends NaturalData {
@Parameter
@L1 Group YPUMKLOG = group("YPUMKLOG");
@L2 Group IN = group("IN");
@L3 A OPID = A("OPID");
@L3 B TIMESTMP = B(8 ,"TIMESTMP");
@L3 N VR_KEY = N(3,0 ,"VR-KEY");
@L3 A DATA = A(200 ,"DATA").defineArray(range(1, 9999));
@L3 A TRACE_JN = A(1);
@L2 Group OUT = group("OUT");
@L3 A JOBNAME = A(8 ,"JOBNAME");
@L3 A PAR_ASYNID = A("PAR-ASYNID");
@L3 A PAR_UNIQUE_ID = A("PAR-UNIQUE-ID");
}

View File

@@ -0,0 +1,24 @@
/*
* Generated by innoWake maxenso enterprise-engine.
*/
package KORIN.Mitarbeiter.Verwaltung;
import innowake.mee.natural.api.*;
@Natural(moduleName="YPUMPRUE")
public class YPUMPRUE_DA extends NaturalData {
@Parameter
@L1 Group YPUMPRUE = group("YPUMPRUE");
@L2 Group INPUTS = group("INPUTS");
@L3 N VR_KEY = N(3,0 ,"VR-KEY");
@L3 N VKTO = N(7,0 ,"VKTO");
@L3 N VKTO_ZIEL = N(7,0 ,"VKTO-ZIEL");
@L3 A KZ_PR_UMK_ABWEICH_KVA = A(1 ,"KZ-PR-UMK-ABWEICH-KVA");
@L2 Group OUTPUTS = group("OUTPUTS");
@L3 A VKTO_NAME = A(31 ,"VKTO-NAME");
@L3 N FNUM = N(4,0 ,"FNUM");
@L3 A FEHLER = A(78 ,"FEHLER");
}

View File

@@ -11,7 +11,13 @@ services:
# (~7.8 GiB here) and never gives it back: measured 6.17 GiB still held one hour after a
# deep refresh of upms had finished, at <1% CPU. The cap is what forces the JVM to
# collect rather than commit.
NEO4J_server_memory_heap_max__size: "2G"
# 2G -> 4G. The 2G was measured against `upms` alone; the database now also holds the NatJav
# reference corpus (2.64 M nodes) and K3 (531 k), i.e. 3.19 M nodes total. Neo4j derives the
# per-transaction memory limit from the heap, and at 2G that limit is 1.4 GiB — which the
# generic placeholder resolution exceeded on K3, failing the refresh at step 2 of 38 with
# "allocation of an extra 2.0 MiB would use more than the limit 1.4 GiB". The parse and persist
# phases were unaffected; it is one enrichment transaction that outgrew the cap.
NEO4J_server_memory_heap_max__size: "4G"
NEO4J_server_memory_heap_initial__size: "512M"
# 512M -> 1G -> 3G -> 2G. The store has grown to 2.4 GB (1.2 GB of it range indexes), so 1G
# covered only ~42% of it. 2G covers nearly all of it; the 2.5 GB of transaction logs are
@@ -37,7 +43,10 @@ services:
# spare GB. This follows the page cache: Neo4j's own rule of thumb is heap + page cache + ~1G, so
# 2G of cache means 6g here — 1G of reserve, not generosity. Do not raise the page cache without
# raising this too.
mem_limit: 6g
# 6g -> 8g, following the heap: Neo4j's rule of thumb is heap + page cache + ~1G, so 4G + 2G + 1G
# with a gigabyte of reserve. An OOM-kill mid-refresh leaves the graph half-updated, which is far
# worse than a spare GB. Do not raise the heap or the page cache without raising this too.
mem_limit: 8g
volumes:
- neo4j-data:/data
healthcheck:
@@ -81,7 +90,11 @@ services:
# the API (which store absolute host paths) resolve inside the container too.
# One entry per registered project root (GET /api/projects) — add a line
# here whenever a new project root is registered outside this repo.
- /home/ingo/deve/agenticCode:/home/ingo/deve/agenticCode:ro
- /home/ingo/deve/agenticcode:/home/ingo/deve/agenticcode:ro
# NatJav reference corpus (innoWake-transpiled Natural), reached via the
# repo-local symlink korin-natjav -> /home/ingo/deve/korin-natjav.
- /home/ingo/deve/korin-natjav:/home/ingo/deve/korin-natjav:ro
- /home/ingo/deve/K3:/home/ingo/deve/K3:ro
- /home/ingo/deve/uniqa/uniqa-upms-app:/home/ingo/deve/uniqa/uniqa-upms-app:ro
- /home/ingo/deve/uniqa/pur-sources/backend:/home/ingo/deve/uniqa/pur-sources/backend:ro
- /home/ingo/deve/tools/conqat/system/src/250401_UMPS/src/pur-analysis/pur-legacy:/home/ingo/deve/tools/conqat/system/src/250401_UMPS/src/pur-analysis/pur-legacy:ro

View File

@@ -18,6 +18,7 @@
<module>ac-parser-core</module>
<module>ac-parser-natural</module>
<module>ac-parser-java</module>
<module>ac-parser-natjav</module>
<module>ac-neo4j-store</module>
<module>ac-code-server</module>
<module>ac-cli</module>
@@ -74,6 +75,11 @@
<artifactId>ac-parser-java</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-parser-natjav</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.agenticcode</groupId>
<artifactId>ac-neo4j-store</artifactId>

View File

@@ -897,6 +897,230 @@ or `CALLNAT` that only exists in a `.cpy` shows up in the host's `db-accesses`/`
<gda>` resolves to them (the `INCLUDE`/`USING` recogniser now accepts `GLOBAL`, not just
`PARAMETER`/`LOCAL`).
## NatJav: Natural transpiled to Java (2026-09-07)
A project may declare `language: "natjav"` alongside `natural` and `java`. NatJav is Software AG
Natural transpiled to Java by the innoWake maxenso engine: the files are `.java`, so the **project's**
language — not the extension — selects the parser. Consequence: a `natjav` project reads *every*
`.java` file under its root as transpiled Natural, so point its `root` at the generated tree
(`src-natural-java`), not at a directory that also holds hand-written Java.
Create one exactly like any other project:
```bash
ac project create korin -r /path/to/src-natural-java -l natjav
curl -X POST localhost:8787/api/projects/korin \
-H 'Content-Type: application/json' \
-d '{"root":"/path/to/src-natural-java","language":"natjav"}'
```
**Query it with Natural names, not Java class names.** A module's identity in the graph is its
Natural object name, recovered from the generated `@Natural(moduleName=...)`:
| Ask for | Not | File on disk |
|---|---|---|
| `V-MIT04` | `V_MIT04` | `V_MIT04.java` |
| `#ABBRUCH` | `XABBRUCH` | `XABBRUCH.java` (a leading `#` transpiles to `X`) |
| `COMMON` (a GDA) | `COMMON_DA` | `COMMON_DA.java` |
| `BANKEZ` (a DDM) | `BANKEZ_DDM` | `BANKEZ_DDM.java` |
That is the whole point: a query written against the Natural original also answers against its
migration. The Java class name is kept as the `className` property, and the Java package (the
transpiled library path) as `package`.
`moduleKind` is **exact** here, not the PROGRAM/SUBPROGRAM heuristic a flat `.nat` file forces —
the transpiler kept the catalog type in the base class:
| `moduleKind` | base class | node type |
|---|---|---|
| `PROGRAM` | `NaturalProgram` | `MODULE` |
| `SUBPROGRAM` | `NaturalSubprogram` | `MODULE` |
| `DATA_AREA` | `NaturalData` | `DATA_STRUCTURE` |
| `MAP` / `FORM` / `SUBROUTINE` / `DDM` / `HELP` / `FDT` | `NaturalMap` / `NaturalForm` / `NaturalSubroutine` / `NaturalDdm` / `NaturalHelp` / `NaturalFdt` | `MODULE` |
`GET /loc` reports `natjav` as its own language row, so a mixed estate can separate transpiled
Natural from hand-written Java.
### Fields and data areas
`GET /data-structures/{name}/fields` and the identifier search answer for `natjav`. The field tree is
built from the `@L1`..`@L7` **annotations**, not from Java nesting, and looks like the Natural one:
`Group`/`Redefine`/view → `DATA_STRUCTURE`, `.constant(...)` → `CONSTANT`, the rest → `VARIABLE`,
linked by `CONTAINS`. A program's inline local data is parsed too, not just `*_DA.java` files.
`GET /modules/{name}/data-structures` reports `area` for a NatJav `USING` from the edge's `scope`
(see the `INCLUDES` edge below): `PDA`/`LDA`/`GDA` for `parameterUsing`/`localUsing`/`globalUsing`.
It is a property of the *use*, not of the area — `YPKUNWE` is a PDA to the subprogram that takes it
and an LDA to the program that declares it locally — which is why it lives on the edge and why this
per-module endpoint can state it. An area whose file is not in the project stays `area: UNKNOWN`
with `sourceFile: null` (item 186).
`dataType` uses the Natural spelling, so it compares directly with a Natural ingest:
| NatJav declaration | `dataType` | Natural |
|---|---|---|
| `A(8 ,"#WA-SPARTEN")` | `A8` | `(A8)` |
| `N(9,0 ,"+BBST-POLNR")` | `N9` — the `,0` is dropped | `(N9)` |
| `N(15,3 ,"BETRAG")` | `N15,3` | `(N15,3)` |
| `A(200,"DATA").defineArray(range(1, 9999))` | `A200/1:9999` | `(A200/1:9999)` |
| `A("OPID")` | `A` | `(A) DYNAMIC` |
`scope` (`PARAMETER`/`LOCAL`/`GLOBAL`) comes from the class's `@Parameter`/`@Local`/`@Global`
annotation — the equivalent of Natural's `.pda`/`.lda`/`.gda` extension, which a `_DA.java` filename
cannot express.
**Trust `naturalNameKnown`.** A field node's name is the Natural name whenever the source states it
(63.5 % of declarations) or it can be resolved from the class's own symbol table (every `Redefine`).
Otherwise the node keeps the **Java** field name and carries `naturalNameKnown=false`; nothing is
derived, because the obvious transformation is wrong for 4.81 % of the corpus's named fields. When a
node's name and Java name differ, the latter is in `javaName`.
`USING` shows up as `INCLUDES` to the data area, resolved by class name rather than by the (underivable)
Natural name.
### When two files claim one module name
Natural module names are unique **per library**, not per estate, and the migration occasionally
copied a header without updating it. Both show up as a NatJav name claimed twice, and the ingest
resolves them differently:
- **Both files legitimately own the name** (the same module name in two libraries): each is qualified
with its library — `KORIN.ACOMMON`, `ABFRAGE.ACOMMON`. Only the colliding names are qualified, so
everything else stays addressable by its bare Natural name. The library is also on every node as
`package`.
- **One file's declaration is wrong** (`ZB80BM6.java` declaring `moduleName="ZB80BM5"`): the file whose
class name matches keeps the name; the other is ingested under its own class name and carries
`naturalNameConflict` naming the declaration it lost. Six files in the reference corpus are like
this — treat `naturalNameConflict` as a defect report about the migrated source.
- **Nothing distinguishes them**: neither file is ingested, and the endpoints answer
`409 DUPLICATE_IDENTITY` with both paths, as for any other language.
### Call graph
`callers`, `callees`, `call-tree` and `ego-graph` answer for `natjav`. A `CALLS` edge's `callKind` is
`PERFORM`, `CALLNAT` (also used for `fetch`/`fetchReturn`) or `CALLNAT_DYNAMIC` for a target held in a
variable, which additionally carries `dynamicVar`.
**Subroutines are named by their Java method name** (`FX_LEEREN`, not `FX-LEEREN`) — unlike modules,
whose Natural name the transpiler preserved in an annotation. Nothing states a subroutine's Natural
name: the comment that trails the method looks like one but is a remark in 1,852 cases
(`GWFMCHK // FORMAT-CHECK`) and is absent from half of them. `perform("FX_LEEREN")` names the same
identifier, so the graph is internally exact.
`run()` and the event blocks (`atTopOfPage`, `atWriteTitle`, `onError`, …) are **not** functions —
they are Natural's inline event blocks, so their calls are attributed to the module, exactly as in a
Natural ingest.
**Copycode: origin only.** A call inside an expanded copycode carries `viaCopycode` and `includedAt`,
so `callers` reports which copycode a call came from. It does **not** carry `originFile`, and there is
no node for the copycode itself: the transpiler inlined every expansion into its host file, so unlike
a Natural ingest there is no shared copycode node and no separate `.cpy` to point at. Treat
`viaCopycode` as the answer to "where did this call come from", not as a file you can read.
### DB access, views and DDMs
`db-accesses`, `module-tables` and `workfile-accesses` answer for `natjav`, in the same shape as for
Natural: a `DB_ACCESS` whose `dataType` is `READ`/`WRITE`/`DELETE`, `READS`/`WRITES` from the caller
to a `DB_TABLE`, and `USES_TYPE` from the access to the table and the view.
A `DB_TABLE` is **file-less**, as in every other language here — that is what makes all references to
a table converge on one node. The DDM file that describes it is a separate `MODULE`
(`moduleKind=DDM`) holding the columns, joined by `MODULE -[MAPS_TO]-> DB_TABLE`, exactly as a Java
JPA entity is joined to its table. So "which file defines this table" is
`MATCH (m)-[:MAPS_TO]->(t:DB_TABLE {name: …})`, not a `sourceFile` on the table.
DDM column nodes carry `adabasShortName` (the two-character physical field key, e.g. `AF`), which no
Natural source shows — it exists only in the DDM.
**A statement's operand is often not the view.** `read(VTO, …)` where `VVSTICH.VT VTO = VT;`, or
`find(SWPOS, …)` where `SWPOS` is a view of a `USING`'d data area — the field's declared type names
the origin (`<owner>.<view>`), and the ingest follows it, across files where the view belongs to a
data area. So `db-accesses` reports the table even when the statement never mentions a view by name.
**A write's table comes from its read.** `UPDATE`/`DELETE` name the loop object the read positioned
(`delete(getDEL)`), not the view, and the parser resolves that file-wide. Where an object was
assigned from two different views the access carries `ambiguousLoopTarget=true` and points at both
candidates; treat those 18 corpus cases as "one of these", not as two writes.
### Dataflow
`flow-forward`, `flow-backward` and `field-flow` answer for `natjav`: every assignment statement emits
`READS`/`WRITES` from the enclosing function (or the module) to the field.
Accumulating statements read their target as well as writing it — `addTo(x, 1)` is `ADD 1 TO x`, whose
old value is an input — matching how the Natural parser treats `TO` versus `GIVING`. A backward flow
therefore runs through a counter rather than stopping at it.
A field of a `USING`'d data area resolves across files: the operand names Java identifiers
(`WSAIVP.INHALT`) while the node carries the Natural name, and the ingest bridges the two through the
data areas' own sources. An operand that resolves to neither the file's own fields nor a known area
yields **no** edge — nothing is guessed.
### `USING` scope
An `INCLUDES` edge carries `scope` (`PARAMETER`/`LOCAL`/`GLOBAL`) and, for a parameter area,
`paramPosition` — its 0-based slot in the subprogram's argument list. That is a subprogram's
interface: `MATCH (m)-[i:INCLUDES]->(a) WHERE i.scope = 'PARAMETER' RETURN a.name ORDER BY i.paramPosition`.
**Read those from the edge, not the node.** A data area is shared, and the same one is parameter 0 of
one subprogram and parameter 2 of another.
There are no `ARG_TO_PARAM` edges for `natjav`, by design: the transpiler passes shared data-area
objects rather than the caller's own variables, so caller and callee reference the *same* node and the
edge would be a self-loop. The relationship it would express is already there — both modules
`INCLUDE` the same area.
### XML payload
`/modules/{name}/payload` answers, and an emitting module carries `xmlWrapper=true`. A
`PAYLOAD_FIELD` has `tag`, `field`, `direction` and (NatJav-only) `lineType`.
The idiom differs from Natural's: this estate assigns `TAG`/`INHALT` into a framework parameter area
and calls `XMLLIN` or `XMLDYN`, rather than using `ADD-XML-LINE`/`COMPRESS '<' tag '>'`, none of which
occur here. `direction` is always `REQUEST` — not a default but a measurement: the read counterpart
`XMLREAD` exists and is called zero times in the corpus.
### Control flow
`CONTROL_FLOW` nodes nest by containment, as for Natural. A block's `name` and `dataType` are the
Natural construct — `IF`, `FOR`, `REPEAT` or `DECIDE` — and its `value` is the condition text.
Two rules are worth knowing before reading the numbers:
- **A database loop is not a control-flow block.** `while (readUMK.hasNext())` is a transpiled
`READ`/`FIND` loop and yields no node, exactly as `READ` yields none in a Natural ingest. 20,749 of
the corpus's `while` statements are of that kind; the `REPEAT` blocks you see are Natural `REPEAT`
loops (`hasNextWhile`).
- **A `DECIDE` is one block, not a chain of ifs.** The transpiler lowers Natural's `DECIDE` into
`if`/`else if` around a counter local; the ingest folds the chain back into a single `DECIDE`, whose
`decideValues` lists the arms' literal values (comma-separated). A dispatch on numbers has no such
values and is a `DECIDE` all the same — do not use `decideValues IS NOT NULL` as the test for "is
this a dispatch".
A `switch` statement is also a `DECIDE`, without `decideValues`.
### Comments
`/modules/{name}/comments` answers, and `search_by_value` with `includeComments` searches them.
Blocks, kinds and the `DOCUMENTS` attachment are the Java parser's — NatJav is Java syntax — so a
comment's shape is the same as in a Java project.
**Transpiler bookkeeping is not stored.** A generated file carries roughly a quarter of its comment
lines as machine output — `@@USING`/`@@INCLUDE` expansion markers, the generation banner, the
migration header's frame and its per-line generator stamps. All of it is already in the graph as
structured data (`viaCopycode` on a call, `description`/`owner` on the module), and keeping it as text
as well would make a full-text search answer a quarter markers.
What is *not* dropped is the prose inside the migration header: 71 % of the files carrying one have a
real description there, often fuller than the `description` property taken from its `@FUNCTION` line.
Filtering happens per line, so that text survives while the frame around it does not.
Comment text carries neither the Javadoc `*` nor Natural's own full-line comment marker — the
sentence is stored, not the comment syntax, as for a Natural ingest.
`agenticcode.ingest.comments.enabled=false` drops comment nodes at the persist seam for every
language, NatJav included.
## Deep-ingest: now automatic (lazy Tier-2)
Field-level endpoints (`flow-forward`, `flow-backward`, `field-flow`) and
@@ -1009,7 +1233,7 @@ origins (`http://localhost:5173`, `http://localhost:4173`) — extend the
| `GET /modules/{name}/graph?direction=&depth=&limit=` | Ego graph (item 49): bounded module-level call neighbourhood as **nodes + edges** (unlike call-tree). `direction` = `out`/`in`/`both`; `limit` caps nodes (BFS order) and sets `truncated`; unresolved targets carry `unresolved=true` + empty `sourceFile`. CLI `ac ego-graph` |
| `GET /modules/{name}/db-accesses` \| `/sql-statements` | DB tables + mode, raw statement text (pass `?depth=` for Natural). **`db-accesses`/`workfile-accesses` return every row when no `limit` is given (item 103)** — they used to default to 50, and since the response is a bare array with no total and no `truncated` flag the cut was invisible: `WGEAGB0S?depth=10` returned 50 of 64 rows and hid 7 tables outright. An explicit `limit` is still honoured exactly. `db-accesses` items carry **`sites: [{lineNo, sourceFile, viaCopycode, includedAt}]`** (+ kept `lineNos`); `sql-statements` items carry **`sourceFile`** + **`viaCopycode`** — so a copycode-sourced access (e.g. `SELECT … FROM SYSIBM-SYSDUMMY1` in `USIX043C.cpy`) reports the `.cpy` line, not a bare number that reads as a host-file line |
| `GET /modules/{name}/workfile-accesses` | Natural **work files** (sequential/flat-file I/O — `READ`/`WRITE WORK FILE n`), the work-file analogue of `db-accesses` (item 84): `[{workFile, physicalName, mode: READS\|WRITES, recordBuffers, lineNos, sites}]`, aggregated per work-file number + mode. `sites: [{lineNo, sourceFile, viaCopycode, includedAt}]` gives each access its file context (copycode-aware), like `db-accesses`. `physicalName` comes from a `DEFINE WORK FILE n '<name>'`, else `null`. **Kept separate from `db-accesses`** — a work file is not an ADABAS/SQL table (fixes a former bug where `READ WORK FILE` created a phantom `DB_TABLE 'WORK'`). CLI `ac workfile-accesses <module>` |
| `GET /modules/{name}/data-structures` | Which copybooks/inline groups a module uses. A `USING <member>` binds by **member (file) name**, never by a level-1 record inside the file (item 100) — before that, `WGEAGB0S USING W-WIF-A2` reported `old/W-WIF-A7.pda` (whose level-1 record is a copy-pasted `1W-WIF-A2`), and a data area with several level-1 records and none named after the member (`VLAYERLA.lda`, `USIX020L.lda`) resolved to nothing at all (`sourceFile: null`, `area: UNKNOWN`, `fieldCount: 0`) although the file was ingested. One row per resolved definition, `(name, sourceFile)` (item 102) — never one row blending an arbitrary file with another definition's `fieldCount` |
| `GET /modules/{name}/data-structures` | Which copybooks/inline groups a module uses. A `USING <member>` binds by **member (file) name**, never by a level-1 record inside the file (item 100) — before that, `WGEAGB0S USING W-WIF-A2` reported `old/W-WIF-A7.pda` (whose level-1 record is a copy-pasted `1W-WIF-A2`), and a data area with several level-1 records and none named after the member (`VLAYERLA.lda`, `USIX020L.lda`) resolved to nothing at all (`sourceFile: null`, `area: UNKNOWN`, `fieldCount: 0`) although the file was ingested. Item 186: for NatJav, `area` comes from the `INCLUDES` edge's `scope` (`PARAMETER`→`PDA`, `LOCAL`→`LDA`, `GLOBAL`→`GDA`), not from the file extension — a `.java` area has none, and the same area is a PDA for one module and an LDA for another (3,112 of korin's 5,818), so the value is per module by nature. The scope is applied only to resolved rows, so `UNKNOWN` again means exactly one thing: not resolved. One row per resolved definition, `(name, sourceFile)` (item 102) — never one row blending an arbitrary file with another definition's `fieldCount` |
| `GET /modules/{name}/payload` | Natural XML wire-payload contract: `{tag, field, direction, source, lineNo, sourceFile}` — static `ADD-XML-LINE` idiom (`source=IDIOM`, item 45) or derived from the wrapper's interface PDA (`source=PDA`, item 46b). `sourceFile` is the file `lineNo` refers to (module for IDIOM, PDA for PDA) |
| `GET /modules/{name}/comments?kind=&limit=&offset=` (`ac comments`) | **Item 141:** the module's **comment blocks** — `{text, kind, sourceFile, startLine, endLine, target, targetType, truncated}`, one row per contiguous block, ordered by line. `target`/`targetType` name the declaration the block documents: the declaration immediately below it, else the one enclosing it (so a file header banner documents the `MODULE`, a `/*` comment on a field's own line documents that field). `kind` is `JAVADOC`\|`LINE`\|`BLOCK` (Java) or `NATURAL_BANNER`\|`NATURAL_INLINE`\|`SAG` (Natural); `?kind=` filters to one. **`SAG` is excluded by default** — `**SAG` directives are generator metadata, not human notes, and would otherwise be most of the answer for every generated Natural module. Text is cut at 4 000 chars (`truncated:true`); read the file for the rest. Natural copycode comments belong to the **copycode's own module**, not to each includer. **Deep-gated:** comments come from the full parse, not the Tier-1 coarse scan, so the module is deep-ingested on demand and a still-shallow module answers `409 NOT_DEEPLY_INGESTED` rather than a misleading `[]` |
| `GET /modules/{name}/dispatch-table` | Natural `DECIDE ON VALUE OF` routing table |

View File

@@ -104,6 +104,343 @@ Item **141** — the one that made the API return a *wrong* answer rather than a
"no `pur` module claims this program" from "a module claims it in a form the parser did not
recognise", or the backlog list silently pads itself with the parser's own gaps.
## Language support — NatJav (2026-09-07)
**NatJav** is Software AG Natural transpiled to Java by the innoWake maxenso enterprise-engine. The
files are syntactically Java (`@Natural class X extends NaturalProgram`, `perform("SUB")`,
`callnat("Y")`, `read(VIEW)`), but the constructs are Natural's, so they map onto the *same* unified
AST as `.nat`/`.lda` sources. Goal: a NatJav project answers the **identical API** as its Natural
original. Reference corpus: `korin-natjav` (22,808 files, ingested as project `korin`).
- [x] **A. Foundation: module, language wiring, module shells** (2026-09-07)
New Maven module `ac-parser-natjav` (`NatJavParser`/`NatJavCoarseScanner`/`NatJavLineCounter`,
JavaParser as the syntax layer), `SourceFiles.Language.NATJAV` selected by the *project's*
`language` (the extension cannot decide — NatJav files are `.java`), and `natjav` accepted by
`POST /api/projects/{name}` and `ac project create -l`.
A module's identity is its **Natural** name, read from `@Natural(moduleName=...)`: `V-MIT04` not
`V_MIT04`, the GDA `COMMON` not `COMMON_DA`, `#ABBRUCH` not `XABBRUCH`. Deriving it from the class
name by string surgery was measured and rejected — the rule "strip `_DA`/`_DDM`, `_` → `-`" is
wrong for **72 of 7,093** annotated modules, because Natural names may themselves contain `_`
(`V_MIT11`) and a leading `#` transpiles to `X`.
`moduleKind` comes from the base class and is therefore **exact**, where the Natural parser can
only guess PROGRAM vs SUBPROGRAM from a `PARAMETER` section: PROGRAM 8,391 / SUBPROGRAM 5,856 /
MAP 1,539 / FORM 354 / SUBROUTINE 325 / DDM 243 / HELP 142 / FDT 132, plus 5,816 `DATA_STRUCTURE`
data areas — matching a base-class census of the corpus.
Verified end-to-end: 22,798 of 22,808 files ingested, 0 failures, 5 duplicate names reported. Two
of those are genuine cross-library collisions (`ACOMMON`, `COMMON`); the other three are **defects
in the migrated source** — `ZB80BM6.java` declares `moduleName="ZB80BM5"`, likewise
`YNSCK3D`/`YNSCK3C` and `DRFDK412`/`DRFDK312` — i.e. copy-pasted headers, correctly surfaced
rather than silently merged.
- [x] **B. Data structures** (2026-09-07)
The `DEFINE DATA` field tree — for data areas *and* for the local data a program declares inline
(15,775 non-`_DA` files do). Levels come from the `@L1`..`@L7` annotations, not from Java nesting:
`Group`/`Redefine`/view → `DATA_STRUCTURE`, `.constant(...)` → `CONSTANT`, everything else →
`VARIABLE`, wired by `CONTAINS` exactly as the Natural parser does. `scope` comes from
`@Parameter`/`@Local`/`@Global` (one per data area: 4,654 + 1,157 + 7 = all 5,818), which is what
the `.pda`/`.lda`/`.gda` extension says for Natural.
`dataType` is normalised to the Natural spelling: a zero scale is dropped (`N(9,0)` → `N9`, since
Natural writes `N9` and a mismatch would make every integer field of the migration unfindable by
type), array bounds are appended with `/` (`A(200).defineArray(range(1, 9999))` → `A200/1:9999`),
`range(1, $)` is Natural's `1:V`, and `A()` stays bare `A` (Natural `A DYNAMIC`).
**Names are never derived.** 63.5 % of the corpus's 1,163,879 declarations state the Natural name
as a literal; a `Redefine` names a Java field whose Natural name is already known from the class's
own symbol table (78,030 of them). For the rest the Java name stands, flagged
`naturalNameKnown=false`. The tempting rule (`x` → `#`, `g` → `+`, `_` → `-`) was measured against
the 709,376 named fields and is **wrong for 4.81 %** — Natural names may mix `_` and `-`
(`#IP_EL_STV-CODE`), the same trap as item A's module names.
Two structural rules that are not obvious from the source:
- A top-level group named like the data area **is** the area, not a member of it (3,773 of 5,818
areas open that way); emitting both would put two identically named `DATA_STRUCTURE`s in one
file. Same purpose as item 100's wrapper rule on the Natural side.
- `@Using` names the *Java class* while the data area's node carries its *Natural* name, and no
string rule bridges them (stripping `_DA` is right for only 90.46 % of areas). The placeholder
therefore carries `className`, and a new NatJav-only enrichment step
(`resolve-natjav-placeholder-names`) re-keys it before the existing name-joining resolution runs
— a no-op for Natural and Java projects.
Deferred on purpose: a view's columns (name and length live in the DDM, another file — they come
with D), and `paramPosition` (it feeds `ARG_TO_PARAM`, and how NatJav maps `callnat` arguments onto
parameters is C's question; Natural's own data-area path sets neither).
- [x] **B2. Two files claiming one module name** (2026-09-07)
Five names were claimed twice in the corpus, and three more collided *silently* — found only by
querying the graph, because the duplicate guard keyed a data-area file on its filename stem
(`WSK386P_DA`) while the graph names it by its Natural name (`WSK385P`), so `WSK385P`, `WSSC31P`
and `YLRY401A` each sat in the graph as one ambiguous pair. The guard now uses the shell node's own
name for NatJav.
The eight split into two causes, told apart by which file *owns* the name (its class name, less the
`_DA`/`_DDM` suffix, is the declared name):
- **Library homonyms** — Natural names are unique per library, not globally, so `KORIN` and
`ABFRAGE` each hold an `ACOMMON` and each its own GDA `COMMON`. Both are real: they are qualified
as `KORIN.ACOMMON` / `ABFRAGE.ACOMMON`, and *only where the collision occurs*, so the other
22,790 modules keep answering to their bare Natural name.
- **A copied header the migration never updated** — `ZB80BM6.java` declares `moduleName="ZB80BM5"`
though `ZB80BM5.java` exists and the two differ in 178 of ~570 lines; likewise `YNSCK3D`→
`YNSCK3C`, `DRFDK412`→`DRFDK312`, and the three data-area pairs. The owner keeps the name, the
impostor is ingested under its own class name and carries `naturalNameConflict` with the
declaration it lost, so the defect stays visible instead of being quietly corrected.
Same-library claims with no owner are still left to the duplicate detection: nothing distinguishes
them, and inventing a distinction would be worse than reporting the collision.
Cross-file by necessity (one file cannot know its name is contested), so it runs once over the
whole walk before parsing, reading only each file's head. A no-op for Natural and Java projects.
- [x] **C. Call graph** (2026-09-08)
`FUNCTION` nodes from the parameterless, non-overriding methods; `CALLS` edges for
`perform` (129,590), `callnat` (66,687), `fetchReturn` (14,617) and `fetch` (1,839), plus 3,106
non-literal targets as `CALLNAT_DYNAMIC` placeholders carrying `dynamicVar`, the same shape the
Natural parser uses so constant-propagation enrichment can recover them later.
Resolution needs no derived names: both sides of a call use the same Java identifier.
`perform("FX_LEEREN")` names the method `void FX_LEEREN()` — **93.8 %** of targets resolve inside
their own file, exactly and locally — and `callnat("V_MIT04")` names the class, resolved by the
className step built in B.
**Better than the Natural side here:** the 6.2 % of `perform`s that call an *external* subroutine
resolve too, because the transpiler made each one a class of its own (`NaturalSubroutine`) and
**98.4 %** of those targets exist as such. The roadmap's item-71 retraction records this as an
open gap for Natural ("a `PERFORM` to another module's subroutine stays an unresolved placeholder").
`run()` and the event blocks (`atTopOfPage`, `atWriteTitle`, `onError`, `atEndOfPage`, `doBreak` —
~5,300 methods) are **not** functions. They are Natural's inline event blocks, which the Natural
parser attributes to the module; making them functions would invent subroutines and add a module
hop to every call inside one.
**Copycode is attribution, not equivalence — and cannot be more.** A Natural copycode is a separate
`.cpy` file whose nodes are *shared* by every including module (`L4N-ENTER` is contained by 137).
The transpiler baked each expansion into its host file: no second file, no shared node, the same
routine duplicated 137 times. What survives is where a statement came from, and the 50,336 calls
(23.3 % of all) inside an expansion carry `viaCopycode` + `includedAt`. No `originFile` (there is no
file to point at) and no `includePath` (it identifies one expansion site among several sharing a
node — here every node lives in the host file and takes the ordinary merge path). Markers are
reliable: 47,313 blocks in 6,124 files, nesting at most 3, zero unbalanced, zero name mismatches.
- [x] **D. DB access** (2026-09-08)
`DB_ACCESS` nodes with `READS`/`WRITES` to a `DB_TABLE`, in the same shape the Natural parser
produces: 27,602 reads (`read` 9,939, `get` 9,904, `find` 6,481, `histogram` 1,278), 7,409 writes
(`update` 4,462, `delete` 1,710, `store` 1,237) and 9,294 work-file statements as
`WORKFILE`/`WORKFILE_ACCESS`.
**Only a call with no receiver is a statement.** `UMK.SPARTEN.get(range(1, 4))` is an array
subscript and occurs 435,129 times against 9,904 real `GET`s — reading `get(` without that
distinction would have reported a fortyfold phantom. This was caught while measuring, not after.
**Writes name the loop, not the view.** `getDEL = get(MDUMKONT_DEL, withHold(), isn); … delete(getDEL);`
— the table is known only at the read. The loop objects are class fields, so a file-wide map
resolves them across method boundaries: of 5,576 `update`/`delete` statements **98.7 %** resolve,
1.3 % have no visible assignment, and 18 (0.3 %) name an object assigned from two views and get an
edge to each plus `ambiguousLoopTarget` — for a write, a reported possibility beats a silent
omission.
**DDM handling revised a decision from item A.** A DDM was going to become the `DB_TABLE` itself,
until measurement showed that *every* `DB_TABLE` in this system is file-less — the Natural parser,
the Java parser and all 72 tables of the `ac` project — because an empty `sourceFile` is what makes
the merge key converge every reference to a table onto one node. So the DDM file stays a `MODULE`
with its columns, and `MODULE -[MAPS_TO]-> DB_TABLE` joins the two, exactly as the Java parser
joins a JPA entity to its table. Column nodes carry the ADABAS short name, which the Natural source
never shows.
The view columns deferred in B arrive here: a view declares them by Java name only, and
`NatJavDdmLibrary` — a pre-parse index over the 243 DDM files, the same split as
`CopycodeResolver`/`CopycodeLibrary` on the Natural side — supplies the Natural name always and the
format for the 1,834 of 17,982 references that state one. The remaining 16,148 keep their length in
the FDT classes; it is left absent rather than guessed.
- [x] **D2. Statements whose operand is not the view** (2026-09-08)
1,390 accesses (4.1 %) reached no table after D, because a statement rarely names the `@ViewOf`
field itself. Two forms, both stating their origin in the field's **declared type**:
- `VVSTICH.VT VTO = VT;` — an alias of a local view, resolved file-locally, transitively (an alias
may point at another).
- `public final KORIN.Programmierer.P40.YLPOSLV_DA.SWPOS SWPOS = YLPOSLV_DA.SWPOS;` — a view
declared in a `USING`'d data area, whose DDM only that other file knows. The pre-parse index from
D now also carries the data areas' `@ViewOf` declarations: **243 DDMs and 173 data-area views**.
The declared type is a statement of fact, not a heuristic, so the resolution guesses nothing.
*A note on estimating this before implementing it:* the line-regex measurement I used to size the
work was wrong twice — it missed fully qualified types (`KORIN.…P40.YLPOSLV_DA.SWPOS`) and counted
locally declared views as unresolved — and moved from 47 % to 82 % as I fixed my own pattern. The
implementation works on the JavaParser AST, where a field's type is read rather than matched; the
coverage that matters was measured on the graph afterwards, not predicted.
- [x] **D3. A 30-minute refresh was two unindexed joins** (2026-09-08)
`korin` took 30:00 to refresh; it now takes **4:36**. Both causes were the same shape — a join the
planner could not support with an index — and both only became expensive once the graph passed a
million nodes.
- **`reap-table-access-edges` (item 86, existing code): 97 % of every persist batch, deleting 0
rows.** The planner started at the target, because `sourceFile: ""` looks selective and is not
(`korin` holds 23,012 file-less nodes), and expanded *every* `READS`/`WRITES` edge in the project
(35,103) for *each* unwound file before filtering on the file at all — ~11.6 M expansions per
200-file batch. A `USING INDEX src:AstNode(project, sourceFile)` applies the file filter first:
**13.1 s → 0.3 s** per batch, persist **28.4 min → 2.5 min**. Measured against a Natural project
(`ac`) as well: no regression.
- **`RESOLVE_NATJAV_PLACEHOLDER_NAMES` (item B, mine): 24 minutes.** It joins a placeholder to the
real module on `className` — the one join in the pipeline that is on neither `name` nor
`sourceFile` — and there was no index for it: 22,769 placeholders against 45,577 className-bearing
nodes, as a nested loop. New index `ast_node_project_classname`: **24 min → 1.3 s**. Narrow like
item 164's, and empty for Natural and Java projects, which never write `className`.
A correctness gap came out of the same query: item 86 and item 106 both filtered
`language: 'natural'`, so a NatJav file's `READS`/`WRITES` and `INCLUDES` edges were **never**
reaped on re-ingest — the parser re-emits them exactly as the Natural parser does, so an edited
access target would have left its old edge behind forever. Both now match `natural` *and* `natjav`.
Fixing that is also what exposed the second slow query: while natjav placeholders were never reaped,
they survived from the first ingest and there was almost nothing left to re-key.
- [x] **E1. Statement-level dataflow** (2026-09-09)
`READS`/`WRITES` from the enclosing function (or the module) to the field, for the corpus's
1,414,872 assignment statements — what makes `flow-forward`, `flow-backward` and `field-flow`
answerable. Result: 1,258,601 `WRITES` and 607,822 `READS`.
**Operands name Java identifiers, field nodes carry Natural names**, and 77.6 % of assignment
targets are qualified (`WSAIVP.INHALT`). File-local operands resolve through the symbol table from
item B, exactly. For a field of a `USING`'d data area the pre-parse index (extended here to the
areas' 739,346 named fields) supplies the Natural name, and the placeholder is emitted **under that
name** — so the existing placeholder-field resolution, which joins on `name`, redirects it. No new
query, and deliberately no join on a Java name: that is the shape that cost 24 minutes in D3.
Two defects the verification run caught, neither visible in unit tests alone:
- **A `REDEFINE` re-declares its field's Java identifier.** `@L1 N xWN_DATN = N(8,0,"#WN-DATN")`
followed by `@L1 Redefine xWN_DATN` overwrote the variable in the symbol table with the
redefinition group, so assignments wrote to the wrong node type. The Natural parser keeps
`variables` and `dataStructures` separate for exactly this reason.
- **Accumulators read their target.** `addTo(x, 1)` is `ADD 1 TO x`: the old value is an input. The
Natural parser distinguishes `TO` (accumulate) from `GIVING` (plain write); modelling these
write-only broke every backward flow through a counter. Visible in the ratio — 1.26 M writes
against 540 k reads before, +45,390 reads after.
- [x] **E1b. `stamp-unresolved-placeholders`: 274 s → 7.6 s** (2026-09-09)
E1 raised `korin`'s placeholder count to 340,667, and the step that flags them became the single
most expensive of the refresh. It asked "does a real twin exist?" as
`OPTIONAL MATCH … count(real) = 0`, enumerating **every** node sharing the placeholder's
`(type, name)` — thousands of rows per placeholder for a field name that recurs across files — only
to compare against zero. `NOT EXISTS { … }` short-circuits at the first match and computes the
identical flag (same 6,973 unresolved). The whole refresh went 13:16 → **6:47**.
Third finding of the same family after D3, and the first that helps all three languages rather than
NatJav alone.
- [x] **E2. `USING` scope and parameter slots** (2026-09-09)
The transpiler distinguishes `parameterUsing` (13,696), `localUsing` (59,895) and `globalUsing`
(1,698) — Natural's `PARAMETER`/`LOCAL`/`GLOBAL USING`. Item B treated all three alike, so a
subprogram's interface was indistinguishable from its scratch data. Now recorded as `scope` on the
`INCLUDES` edge, with `paramPosition` for the parameter slots in declaration order.
**On the edge, never on the node.** A data area is shared: the same one is parameter 0 of one
subprogram and parameter 2 of another, so stamping the node would let whichever module was ingested
last decide the answer for every caller.
*`ARG_TO_PARAM` itself is not built, and should not be.* NatJav passes shared data-area objects, not
the caller's own variables: `V_MIT04` declares `parameterUsing(V_MIT04P_DA)` while its caller
declares `localUsing(V_MIT04P_DA)` — the same area, hence the same node, hence a self-loop. Natural
has the same property for `PARAMETER USING`, which is why it assigns `paramPosition` only to
*inline* parameters; NatJav has none — all 13,696 parameter bindings go through `parameterUsing`.
What the edge would express is already in the graph: both modules `INCLUDE` the same area.
- [x] **E3. XML wire payload** (2026-09-09)
`PAYLOAD_FIELD` nodes and the `xmlWrapper` module marker, so `/modules/{name}/payload` answers.
**None of item 45's Natural idioms exist here**: `COMPRESS '<' tag '>' value`,
`DEFINE SUBROUTINE ADD-XML-LINE` and `CALLNAT 'YFRAMN07'` occur **zero** times. (An early estimate
of "195 files with XML emit" was a grep artefact — the pattern matched `XMLLINP`, an unrelated data
area.) The estate drives a framework subprogram through a parameter area instead:
`assign(XMLLINP.TAG, "ADRZEILE1"); assign(XMLLINP.INHALT, …); callnat("XMLLIN", …)`. Two emitters
share the shape — `XMLLIN` (7,086 calls) and `XMLDYN` (1,818) — for 10,238 tags in 176 files.
**The direction is measured, not defaulted.** Natural derives `REQUEST`/`RESPONSE` from
`ADD-XML-LINE` vs `GET-XML-LINE`. Here the read counterpart `XMLREAD` exists, carries the
mirror-image `#TAG`/`#INHALT` outputs — and is called **zero** times in the corpus. So every tag is
an emit and `REQUEST` is a fact.
- [x] **E4a. Control flow** (2026-09-09)
`CONTROL_FLOW` blocks nested by containment, the same shape the Natural parser builds:
`IF` 461,646 / `DECIDE` 28,589 / `FOR` 26,396 / `REPEAT` 10,257. The graph grew from 1.40 M to
1.96 M nodes and the refresh from 6:19 to **7:58** — +558,000 nodes for +99 s, with none of the
index surprises that D3 and E1b had at a third of the size.
Two things it deliberately does **not** emit:
- **A database loop is not control flow.** 20,749 of the corpus's `while` statements are
`while (readUMK.hasNext())` — a transpiled `READ`/`FIND` loop. Natural models `IF`/`FOR`/`REPEAT`/
`DECIDE` and never a `READ` loop. The 10,257 `REPEAT`s that remain are `hasNextWhile` loops,
matching the corpus's 10,240 `RepeatLoop` declarations.
- **A `DECIDE` is one construct, not a chain of ifs.** The transpiler lowers it into `if`/`else if`
around a counter local, and the counter is what makes the chain recognisable.
*Three rounds, each fault hidden by the previous one, and all three found by comparing two
independent numbers — never by the node count, which looked plausible every time:*
1. Recursing into the chain's `else` branch found the same pattern again → every dispatch doubled.
2. Searching the arm's whole subtree for the counter matched it through a nested `if` → a dispatch
re-recognised at each nesting level (1,917 DECIDEs inside their own DECIDE).
3. Deciding "is this a dispatch?" by "does it have literal values?" classified every dispatch on
numbers as a plain `if`.
*A residual that is not pinned down:* 17,607 lowered dispatches against 15,361 `decideCounter`
declarations. Double emission is ruled out (no two nodes share a position) and nesting was verified
against the source; a counter reused across chains explains the rest plausibly but is not proven.
- [x] **E4b. Comments** (2026-09-09)
666,637 `COMMENT` nodes with 670,619 `DOCUMENTS` edges. The graph went from 1.96 M to 2.63 M nodes
and the refresh from 7:58 to **9:39** — the last and largest step, and still no index surprise.
**The grouping is now shared, not copied.** NatJav *is* Java syntax, so the block folding and the
`DOCUMENTS` attachment were already in `JavaParser`, privately. They moved to a public
`JavaComments` in `ac-parser-java`, parameterised by language and by a text filter; both parsers
call it. The 24 `JavaParserTest` cases stayed green unchanged — that was the regression proof for
touching tested foreign code.
**What differs is the text.** 26 % of the corpus's 2,473,041 comment lines are transpiler
bookkeeping whose content is *already* structured in the graph: `@@INCLUDE` markers as a call's
`viaCopycode`, the header's `@FUNCTION`/`@OWNER` as the module's `description`/`owner`. Stored
again as comment text it would bury the real comments — `search_by_value` with `includeComments`
would answer a quarter transpiler markers.
**Filtered per line, not per block**, and that distinction was the finding of the validation pass:
the migration header looks like pure framing but of the 14,749 files carrying one, **10,466 (71 %)**
have real prose inside it, often a fuller description than the single `@FUNCTION` line. Dropping the
block would have deleted the documentation of 10,466 modules.
Three things only the corpus run showed, none of them visible in unit tests:
- **Two comment markers sit on top of each other.** The Java layer strips the Javadoc `*`; beneath
it is the `*` with which Natural marked a full-line comment. A Natural ingest stores neither.
- **The generator stamps per-line metadata** (`*innoWake versionRep2Str:`, `datumUmsetzung`,
`originalObjectName`, ~5,400 each) that the `Generated by innoWake` pattern did not match.
- **The corpus spells "migration" two ways:** 57,484 `migration process` and 1,347
`migraion process` — a typo in the 2014 migration template, inherited by 1,347 files. Matching
only the correct spelling let 442 blocks of framing through.
Together those three were 5,434 blocks, 0.8 % — a share at which the node count still looks
perfectly plausible. They surfaced only by searching the result for residues of the very patterns
the filter is meant to remove.
## Known bugs
- [ ] **152. `NaturalLines.stripInlineComment` is not quote-aware — a `/*` inside a string literal
@@ -415,6 +752,306 @@ after probing and are recorded at the end, so nobody re-files them.
## Ingest performance
- [x] **181. Six enrichment steps ran in one transaction each, sized by the project rather than by the query** (2026-09-09)
Found by ingesting `k3` (27,687 Java files) and then deep-refreshing `korin` (22,808 NatJav files).
Both failed, four times between them, with `dbms.memory.transaction.total.max threshold reached`.
The Neo4j heap was raised 2G -> 4G on the way, which lifted the per-transaction limit from 1.4 GiB
to 2.8 GiB and bought exactly one more failing step. **Raising the heap only moves the ceiling.**
The pattern is one query shape: `MATCH ... MERGE ... DELETE r` (or a bare bulk `DELETE`) over every
edge that points at a placeholder. The row count follows the corpus, not the query, and nothing
bounded it. Measured on `korin`:
| step | work in one transaction |
|---|---:|
| `resolve-placeholder INCLUDES` | 73,519 edges redirected |
| `resolve-field-placeholder READS` | 434,372 created / 279,746 deleted, 2.34 M properties |
| `resolve-field-placeholder WRITES` | 1,054,093 created / 674,708 deleted, 5.68 M properties |
| `delete-resolved-field-contains` | **4,780,020 edges deleted** |
| `delete-resolved-placeholders` | 402,217 nodes deleted |
All six now run as `CALL { ... } IN TRANSACTIONS OF $batchSize ROWS`, which needs an auto-commit
transaction — that is what `EnrichmentStep.batched` arranges. Batched, the 4.78 M-edge sweep takes
**48.8 s at 383 MB of server heap**, against a step that previously could not finish at any heap
size. Whole-root form only; the scoped form is bounded by the caller's `$names`.
**Batching is not a drop-in wrapper, and that is the part worth remembering.** Three of these
queries fan out over several resolution candidates, and the fan-out sat *outside* the write, so the
same edge appeared in several rows. In one transaction Cypher's eager evaluation hid it — every
match ran before any delete. Across batch boundaries the second row reads an edge whose `DELETE`
has already committed, and the step dies with `Relationship with id N has been deleted in this
transaction`. That is a real regression this change first *introduced* and then fixed: on `korin`
1,147 of 63,519 `INCLUDES` edges into placeholders have more than one candidate. The fix is to
aggregate the candidates per edge and move the `UNWIND` inside the batch, ending with
`WITH DISTINCT r DELETE r` so the edge is deleted once, after every target is merged. Verified with
`EXPLAIN` that item 159's planner hoist survives the added aggregation.
Effect on the whole run, `korin` deep: **1,856 s, 52/52 steps, 0 failures**, and the placeholder
population falls from 388,439 to 15,134 (27 % -> 96 % resolved). `k3` deep: **1,094 s, 27,687
files, 0 failures**, adding 79,168 `ARG_TO_PARAM` edges the call-graph mode never built.
**Second run, and it changes what the first one means.** Repeated with no code change:
`korin` **1,856 s -> 712 s**, `k3` **1,094 s -> 1,075 s**. `k3` is simply reproducible (every step
within noise, node count identical at 643,936). `korin` is not, and the counters say why: step 29
deleted **4,780,020** CONTAINS edges in run 1 against **342,132** in run 2, because run 1 was
clearing a backlog the pre-fix 08:38 ingest had left in the graph. Steps 25/26 then did *identical*
work in both runs (279,744 / 674,708 edges deleted) at **9x and 5x** the speed, because the
`(real)-[:CONTAINS*1..10]->(realv)` expansion walks a graph carrying 3.4 M fewer CONTAINS edges.
So **712 s is the steady-state cost of a `korin` deep refresh**; 1,856 s was a one-off catch-up.
This also settles why the 08:38 run finished in 626 s: it did not resolve the fields, it deferred
the bill.
**The second and third runs exposed a separate bug — see item 183.** `korin` gains exactly +34
nodes per deep refresh (2,289,657 -> 2,289,691 -> 2,289,725), all placeholder `DATA_STRUCTURE`s.
Not non-determinism and not an artefact of run 1's backlog, both of which this item claimed before
the third run settled it: a monotonic leak. Timing itself is reproducible (712 s, 723 s).
- [x] **183. Ownerless placeholders are re-created every ingest and swept by nothing** (found 2026-09-09, fixed 2026-09-10)
A `korin` deep refresh adds **+34 nodes and +102 edges every time it runs**, on unchanged input and
unchanged code. Found by running it three times: 477 -> 511 -> 545 placeholder `DATA_STRUCTURE`s,
16 copies of each of 34 names, one per ingest since the project was created. Every other
project/type combination in the graph is clean (`ac`, `k3`, and `korin`'s own MODULE, VARIABLE,
CONSTANT, DB_TABLE placeholders all show zero duplicate identities), so this is specific to
ownerless placeholders, which today means NatJav data areas.
**They fall through all three cleanup mechanisms**, and the third condition is the one that makes
this invisible:
| sweep | why it misses them |
|---|---|
| `DELETE_STALE_FILE_NODES` | keys on `sourceFile`; a placeholder's is `""` |
| `DELETE_STALE_PLACEHOLDER_NODES` | filters `(n:VARIABLE OR n:CONSTANT)` **and** `n.ownerModule IN $owners` - these are `DATA_STRUCTURE` **and** their `ownerModule` is the empty string, so they fail both halves |
| `delete-resolved-placeholders` | needs an edgeless node; each copy carries `CONTAINS` edges to placeholder fields |
**Not just a node count.** Every copy, old and new, points at the *same current-generation* child
fields - the name-driven field resolution attaches new children to all of them. So a traversal
asking which data area contains a given field got **16 answers where one is true**. Query results
were affected, not only `count(*)`.
`ownerModule` being empty is deliberate and correct: a data area is shared between modules, which
is the same reason `SCOPE` and `PARAM_POSITION` live on the edge rather than the node. What follows
from "shared" is that the placeholder must keep **one identity across ingests** instead of being
minted fresh each time - so the fix is a deterministic `nid` derived from (type, name), letting the
re-ingest MERGE onto the existing node. The alternative, a project-wide generation sweep for
ownerless placeholders gated to whole-root refreshes, treats the symptom and leaves the identity
question open. Either touches the persist path and wants its own proposal/validate pass.
**The 510 stale copies were deleted by hand on 2026-09-09**; the leak was closed on 2026-09-10.
**Root cause, found while fixing: it was not the sweeps, it was the rename.** The parser emits the
`USING` placeholder under the Java class name (`K3VT000P_DA`), and `resolve-natjav-placeholder-names`
— step 1 of the pipeline, added for NatJav — rewrites `name` to the Natural name (`K3VT000P`).
`name` is part of the node merge key, so the next ingest's `MERGE` on `K3VT000P_DA` finds nothing
and mints a second node, which step 1 renames again. Deterministic, one copy per ingest, exactly the
34 areas that get renamed; the 35th, `COMMON_DA`, is never renamed (two real candidates, see 184)
and is the one that stayed stable. The step's own javadoc claimed idempotency — true for the same
node, false for the re-ingest.
**Fix:** the rename folds. If a same-type placeholder already carries the target name **and the
same `className`**, it is deleted and the fresh one takes the name. Deleting the old copy loses
nothing: a renamed placeholder has 0 incoming `INCLUDES` (all 1,678 in the graph sit on the
unrenamed `COMMON_DA`) and its `CONTAINS` edges are re-created by every parse. The `className`
guard makes a homonym unreachable. A `collect`/`UNWIND` barrier reads every stale node before any
is deleted. Cypher only, no Java, no index, no batching (~34 rows). No test references the step;
the verification is the test that found the bug — two consecutive deep refreshes with identical
node counts.
**Verified 2026-09-10, two consecutive `korin` deep refreshes on the deployed fix.** Step 1 logged
`nodes +0/-34, rels +0/-380, props 5855` in **both** runs — the fresh parse minted 34 class-name
placeholders, the fold deleted the 34 previous-generation copies and their 380 `CONTAINS` edges,
and renamed the fresh ones. All 13 node-type counts, all 4 placeholder-type counts and
`dup-copies = 0` were identical before, after run A (738 s) and after run B (727 s). Before the
fix the same comparison showed `DATA_STRUCTURE +34` three times in a row.
**Rejected alternative:** resolving the Natural name at parse time through the DDM library, which
already reads every `_DA.java`. Right in principle — an index, not a string rule — but it walks into
two traps the class-name join handles today: a copied header (item NatJav fix #8, `WSK386P_DA`
declaring `WSK385P`) would yield the *wrong* name, and library homonyms (`COMMON_DA` in two
libraries) need the referencing module's library to disambiguate. That is a feature (184), not a
fix.
*Method note, because it nearly went unrecorded:* a single refresh looked clean, two looked like
non-determinism worth tolerating, and only the third showed the constant +34 that names it a leak.
Two of the three explanations offered along the way were wrong.
- [x] **184. Library-aware resolution of homonymous data areas** (found 2026-09-10, implemented 2026-09-10)
`COMMON_DA` exists in `KORIN` and `ABFRAGE`. Its placeholder is never renamed (two candidates) and
so never resolved, and everything referenced through it stays unresolved: **1,678 `INCLUDES` and
13,311 field placeholders** — 97 % of all unresolved field references in `korin` hang under this one
name. The refusal is correct today: binding arbitrarily would invent a dependency. But the
information to bind *correctly* exists — `NatJavIdentities` already qualifies the real nodes as
`KORIN.COMMON` / `ABFRAGE.COMMON` by top-level directory, and a referencing module's own
`sourceFile` names its library. Resolving a placeholder to the candidate in the module's library
would close the single largest unresolved mass in the corpus. Natural-side check needed first:
does `upms` have the same shape, and is "same top-level directory" the right notion of library there.
*Recorded because it was misread once:* a count of "unresolved children whose real area has a
same-named field" reported 13,311 as a resolution bug. It matched `real` by class name and so hit
both homonyms; the field exists in one of them. Every non-homonym area had 0 such children.
**Implemented.** The source of the library is not the including module's directory — 6 `CSTENTW`
modules include `KORIN.COMMON_DA`, and their directory would have bound them wrong — but the
`@Using` itself: 70 % (52,908) are qualified with the **full Java package**
(`KORIN.Vertrag.Beteiligung.ABGB02P_DA`), which the transpiler wrote after resolving the steplib
chain; the other 30 % (22,381) are unqualified and resolve by Java's same-package rule — 1,193 of
1,193 checked name a class in the same directory, none an import. `NatJavUsings` records that
package as `usingPackage` on the placeholder (node name and merge key unchanged; the placeholder
map stays keyed by simple class name because `NatJavDbAccess.exposures()` looks areas up by it).
`resolve-natjav-placeholder-names` applies it only where the class name alone yields more than one
candidate: `c.package = ph.usingPackage`, a plain string equality — `package` is present on all
5,818 real areas. One module including the same class name from two libraries (0 in the corpus)
gets no `usingPackage` and is refused as before. Three tests in `NatJavFieldsTest`.
**Verified 2026-09-10, two consecutive `korin` deep refreshes (755 s, 742 s), all 18 count rows
identical between them.** Against the pre-fix graph: `INCLUDES` on the `COMMON_DA` placeholder
1,678 -> 0, on `KORIN.COMMON` 0 -> **1,678**, on `ABFRAGE.COMMON` 0 -> 0; placeholder `VARIABLE`
13,691 -> **380** (exactly the fields the real area does not declare, counted before the fix was
built); placeholder `DATA_STRUCTURE` 35 -> 34; no other row moved. Step counters against the
pre-fix runs: step 1 `props` 5,855 -> 5,856 (the one added rename), step 3 `+1,678/-1,678`, step
33 `-310,782 -> -324,094` (+13,312 = 13,311 fields + the area). Every `INCLUDES` edge in the
project (78,533) now points at a real area. Through the API,
`GET /korin/modules/AF000/data-structures` and `.../DISKRECH/data-structures` (the `CSTENTW`
module) both list `KORIN.COMMON`, `fieldCount 616`, `sourceFile KORIN/COMMON_DA.java`.
*Not fixed here, same blindness:* `NatJavDdmLibrary` keys `areaFields` by simple class name too
(`putIfAbsent` keeps whichever `COMMON_DA.java` the walk met first). Harmless in this corpus only
because the two `COMMON` areas have identical field names (587 shared, 0 exclusive); a
non-identical homonym pair would get one file's Natural field names for both. `usingPackage`
is the key that would fix it.
- [ ] **185. No uniqueness constraint on the `AstNode` merge key** (found 2026-09-10)
`MERGE (node:AstNode {type, name, sourceFile, project})` runs with a range index and no constraint
(`SHOW CONSTRAINTS`: only `Project.name`). Without a constraint `MERGE` is not atomic across
concurrent transactions, and placeholders (`sourceFile = ""`, shared by every referencing file)
are the keys most likely to be merged from two batches at once. Not the cause of 183 — that was
deterministic — and no duplicate has been observed for any other type/project (0 across `ac`,
`k3`, and `korin`'s MODULE/VARIABLE/CONSTANT/DB_TABLE). A four-property constraint over 2.9 M
nodes has a write cost on every node merge; measure it against the persist phase before adding.
- [x] **186. `data-structures` reports `area: UNKNOWN` for every resolved NatJav data area** (found 2026-09-10, implemented 2026-09-10)
Found by the API check that closed item 184: `GET /korin/modules/AF000/data-structures` answers
`KORIN.COMMON, fieldCount 616, sourceFile KORIN/COMMON_DA.java, area: UNKNOWN`. Measured: **all
78,533** NatJav `INCLUDES` edges point at `.java` areas, so every resolved `USING` row in every
NatJav module says `UNKNOWN`. That collides with the documented meaning — the API guide defines
`area: UNKNOWN` together with `sourceFile: null, fieldCount: 0` as "not resolved / file not
ingested". An agent reading the doc draws the wrong conclusion from a fully resolved row.
**The query's own premise is out of date.** `MODULE_DATA_STRUCTURES` derives `area` from the file
extension (`.pda/.lda/.gda`) and its javadoc says why: "the precise PARAMETER/LOCAL/GLOBAL USING
scope is not stored on the edge, so area is the available proxy". Since item B (NatJav `@Using`)
the scope **is** on the edge — on 100 % of the 78,533 (LOCAL 62,763, PARAMETER 14,072, GLOBAL
1,698). The 196 Natural `INCLUDES` edges carry none, so the extension stays as the fallback.
**Why the edge, not the node, is the only correct source:** 3,112 of 5,818 NatJav areas are
included under two different scopes by different modules — `YPKUNWE` *is* a PDA for one module and
an LDA for another. That is the very reason `SCOPE` lives on the edge (item B). A per-node `area`
would be wrong for 3,112 areas; per (module, area) it is single-valued — measured: 78,533 pairs,
each with exactly one edge and one scope — and `data-structures` is a per-module endpoint.
**Fix:** carry `r.scope` through the aggregation and map `PARAMETER -> PDA`, `LOCAL -> LDA`,
`GLOBAL -> GDA`, else the extension rule as today. One query, its javadoc, one paragraph in the API
guide (a response field changes value for NatJav), verification through the API on a GLOBAL
(`AF000`/`DISKRECH` -> `GDA`) and a PARAMETER include. No parser change, no refresh — the data is
already in the graph.
**Implemented.** `scope` travels through the aggregation inside the per-definition map, so the
item-102 grouping by `(name, sourceFile)` is untouched. **VALIDATE caught one thing the proposal
had wrong:** the 34 permanently unresolved areas carry a scope on their edge too, and the naive
CASE would have reported them as `LDA` with `sourceFile: null, fieldCount: 0` — destroying the
documented "UNKNOWN = not resolved" signal from the other side. The scope is therefore applied only
where `sourceFile IS NOT NULL`. Query javadoc corrected (it claimed the scope is not on the edge —
true before NatJav), API guide updated in the `data-structures` row and the NatJav
"Fields and data areas" section. CLI unchanged by construction (`ModuleDataStructuresCommand`
prints the response as is).
**Verified 2026-09-10 on the deployed build, through the API:** `AF000 -> KORIN.COMMON: GDA, 616`;
`DISKRECH -> KORIN.COMMON: GDA, 616` (the `CSTENTW` module — the case a directory heuristic would
have bound wrong); `YSBIDATE -> YPBIDATE: PDA, 10`; `YTKUNWE -> YPKUNWE: LDA, 4`. `AF000`'s rows
carry only `INLINE`/`GDA`/`LDA` — no `UNKNOWN` on a resolved row. Fallback path: `AnalysisResourceIT`
85/85 green including `MSGAREA -> PDA` (Natural, extension rule). The `UNKNOWN` branch can no longer
be shown on korin through the API — since item 184 no unresolved area has an includer left, the 34
survivors hang only on 380 unresolvable fields — so it is covered by the IT and the query text.
*Gap noted while verifying, closed by item 187:* `ac-code-server` had **no NatJav integration test
at all** — no `*IT` ingested a NatJav fixture through the API. Everything NatJav is covered by parser
unit tests and the live corpus. The Natural `MSGAREA -> PDA` assertion in `AnalysisResourceIT`
covers this item's fallback path only.
*Also seen, recorded so it is not chased twice:* `AnalysisResourceIT` failed **1 of 3** runs at
class level — `Tests run: 1, Failures: 1` after 21.6 s, no report file written — while a
`./manage-ac.sh deploy` docker build was running on the same host. The two runs on an idle machine
(with and without `-q -DskipUTs=true`) passed 85/85 in ~60 s each. Not this item, not the flags: a
boot-time flake under load (Quarkus + Testcontainers Neo4j starting next to a build). Cause not
diagnosed.
- [x] **187. A NatJav integration test** (2026-09-10)
`ac-code-server` had 77 `*IT` classes and not one that ingests NatJav: a whole language verified only
by parser unit tests and by the live reference corpus — every fix of 2026-09-09/10 included.
`NatJavIngestIT` runs a six-file project through creation, deep refresh and the API. The root
reproduces the corpus's shape where it matters: libraries are directories, `COMMON_DA.java` exists
in `KORIN` and `ABFRAGE`, and `CSTENTW/DISKTEST` includes the KORIN one. Fixtures come from the
parser module (`V_MIT04`, `YPUMPRUE_DA`, `COMMON_DA`) plus two ten-line programs and the ABFRAGE
copy with its `package` line changed — which is what makes the item-184 join on `package`, not on
directory, load-bearing.
One assertion per fix it protects: Natural-name identity and `moduleKind` (A/B); homonym by
qualifier from a third library (184); homonym by the file's own package for an unqualified
`@Using` — deliberately on the homonym, because with a single candidate the class name alone would
pass (184); `area` from the edge scope on resolved rows and `UNKNOWN` on unresolved ones, including
a PARAMETER include without a file (186); CALLNAT to an unresolved callee (B); and a second deep
refresh that must leave `data-structures` byte-identical and the identifier search at exactly one
hit per area (183).
**Green 2026-09-10: 6/6 in 41.9 s** (one Quarkus boot, two deep refreshes of the six-file root). The
first run failed one assertion for a reason worth keeping: there is **no bare `GET /modules/{name}`
route** — only sub-resources (`/context`, `/functions`, `/data-structures`, …). The identity check
therefore goes through `GET /modules?sourceFile=…` (name, `moduleKind`) and then addresses the module
by that name, the way every sub-resource call does. Five of six assertions — both item-184 forms,
186, CALLNAT, and the 183 identity check — passed at the first attempt on a fresh, empty graph,
which is the first time today's Cypher changes ran anywhere but on the accumulated `korin` store.
*Deliberately not in it:* a faithful reproduction of the 183 leak — a renamed area that survives
because a child field is known to the DDM library but absent from the real tree. That condition
arises only through the library's homonym-blind `putIfAbsent`, and which `COMMON_DA.java` wins is
walk-order dependent. An order-dependent test is worse than none; the identity check above tests
the property the leak violated.
- [ ] **188. The Neo4j service has no restart policy; the two services that depend on it do** (found 2026-09-24)
Found on resuming after 13 days: `ac-code-server` and `ac-ui` were "Up 5 minutes", the readiness
check was DOWN with `UnknownHostException: neo4j`, and `agenticcode-neo4j` had exited with code
255 at 14:11:03Z — `oom=false`, `restarts=0`, and not one log line since its start on 2026-09-09.
Exit 255 with no log is the daemon or host going away, not the database failing. The compose file
gives `restart: unless-stopped` to `ac-code-server` and `ac-ui` only; `neo4j` has none, so after a
Docker or host restart the stack comes back without its database and every API call fails until
someone notices. The fix is one line on the `neo4j` service, plus the question whether the server
should keep retrying the connection rather than caching the failed DNS lookup — the
`UnknownHostException` suggests it resolved once and did not try again. Config change; the human
runs the stack (CLAUDE.md), so it is filed rather than applied.
- [ ] **182. A deep refresh outlives its own HTTP request** (found 2026-09-09)
Quarkus's default `quarkus.http.idle-timeout` is 30 minutes. The first `korin` deep refresh after
item 181 took 30:56 (a one-off backlog run — see 181; steady state is 712-755 s), and the
connection was dropped mid-run: the client got an empty `200` after exactly 1,800,102 ms with a
Netty stack trace in the log, while **the refresh kept running server-side** and finished
normally. Today's refreshes stay under the limit; the failure mode is intact for any project or
backlog that crosses it. So the result is correct and the caller cannot see it — it has to be read
back from `GET /api/projects`. Worse, a caller that retries on the empty response would start a
second refresh over a running one.
Never surfaced before because no refresh had crossed 30 minutes. Two candidate fixes: raise the
idle timeout, or — better, and what the CLI would want anyway — make refresh asynchronous, return a
job id immediately and expose progress. The step-level progress is already logged, so there is
something real to report.
- [ ] **180. The `DOCUMENTS` edge carries two strings and nothing else — 20.2 % of all edges**
(found 2026-09-07 while measuring item 179)