gitnexus 1.4.8 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/cli/index-repo.d.ts +15 -0
- package/dist/cli/index-repo.js +115 -0
- package/dist/cli/index.js +11 -2
- package/dist/cli/setup.js +12 -9
- package/dist/cli/wiki.d.ts +4 -0
- package/dist/cli/wiki.js +174 -53
- package/dist/config/supported-languages.d.ts +7 -5
- package/dist/config/supported-languages.js +6 -4
- package/dist/core/graph/graph.js +9 -1
- package/dist/core/graph/types.d.ts +10 -2
- package/dist/core/ingestion/call-processor.d.ts +18 -1
- package/dist/core/ingestion/call-processor.js +297 -38
- package/dist/core/ingestion/call-routing.d.ts +3 -18
- package/dist/core/ingestion/call-routing.js +0 -19
- package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
- package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
- package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
- package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
- package/dist/core/ingestion/cobol-processor.d.ts +54 -0
- package/dist/core/ingestion/cobol-processor.js +1186 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
- package/dist/core/ingestion/entry-point-scoring.js +18 -4
- package/dist/core/ingestion/export-detection.d.ts +47 -8
- package/dist/core/ingestion/export-detection.js +29 -50
- package/dist/core/ingestion/field-extractor.d.ts +29 -0
- package/dist/core/ingestion/field-extractor.js +25 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
- package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
- package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
- package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
- package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
- package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
- package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
- package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
- package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
- package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
- package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
- package/dist/core/ingestion/field-extractors/generic.js +111 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
- package/dist/core/ingestion/field-extractors/typescript.js +291 -0
- package/dist/core/ingestion/field-types.d.ts +59 -0
- package/dist/core/ingestion/field-types.js +2 -0
- package/dist/core/ingestion/framework-detection.d.ts +87 -0
- package/dist/core/ingestion/framework-detection.js +65 -2
- package/dist/core/ingestion/heritage-processor.js +15 -17
- package/dist/core/ingestion/import-processor.d.ts +9 -10
- package/dist/core/ingestion/import-processor.js +59 -14
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
- package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/dart.js +44 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +9 -1
- package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.js +56 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/php.d.ts +6 -10
- package/dist/core/ingestion/{resolvers → import-resolvers}/php.js +7 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
- package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/swift.js +23 -0
- package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
- package/dist/core/ingestion/import-resolvers/types.js +6 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +0 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +0 -9
- package/dist/core/ingestion/language-config.d.ts +4 -1
- package/dist/core/ingestion/language-provider.d.ts +121 -0
- package/dist/core/ingestion/language-provider.js +24 -0
- package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
- package/dist/core/ingestion/languages/c-cpp.js +71 -0
- package/dist/core/ingestion/languages/cobol.d.ts +1 -0
- package/dist/core/ingestion/languages/cobol.js +26 -0
- package/dist/core/ingestion/languages/csharp.d.ts +8 -0
- package/dist/core/ingestion/languages/csharp.js +49 -0
- package/dist/core/ingestion/languages/dart.d.ts +12 -0
- package/dist/core/ingestion/languages/dart.js +58 -0
- package/dist/core/ingestion/languages/go.d.ts +11 -0
- package/dist/core/ingestion/languages/go.js +28 -0
- package/dist/core/ingestion/languages/index.d.ts +38 -0
- package/dist/core/ingestion/languages/index.js +63 -0
- package/dist/core/ingestion/languages/java.d.ts +9 -0
- package/dist/core/ingestion/languages/java.js +29 -0
- package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
- package/dist/core/ingestion/languages/kotlin.js +53 -0
- package/dist/core/ingestion/languages/php.d.ts +8 -0
- package/dist/core/ingestion/languages/php.js +145 -0
- package/dist/core/ingestion/languages/python.d.ts +12 -0
- package/dist/core/ingestion/languages/python.js +39 -0
- package/dist/core/ingestion/languages/ruby.d.ts +9 -0
- package/dist/core/ingestion/languages/ruby.js +44 -0
- package/dist/core/ingestion/languages/rust.d.ts +12 -0
- package/dist/core/ingestion/languages/rust.js +44 -0
- package/dist/core/ingestion/languages/swift.d.ts +12 -0
- package/dist/core/ingestion/languages/swift.js +133 -0
- package/dist/core/ingestion/languages/typescript.d.ts +10 -0
- package/dist/core/ingestion/languages/typescript.js +60 -0
- package/dist/core/ingestion/mro-processor.js +14 -15
- package/dist/core/ingestion/{named-binding-extraction.d.ts → named-binding-processor.d.ts} +0 -9
- package/dist/core/ingestion/named-binding-processor.js +42 -0
- package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/csharp.js +37 -0
- package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/java.js +29 -0
- package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
- package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/php.js +61 -0
- package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/python.js +49 -0
- package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/rust.js +64 -0
- package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
- package/dist/core/ingestion/named-bindings/types.js +6 -0
- package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/typescript.js +58 -0
- package/dist/core/ingestion/parsing-processor.d.ts +5 -1
- package/dist/core/ingestion/parsing-processor.js +115 -16
- package/dist/core/ingestion/pipeline.js +925 -424
- package/dist/core/ingestion/resolution-context.js +1 -1
- package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
- package/dist/core/ingestion/route-extractors/expo.js +36 -0
- package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
- package/dist/core/ingestion/route-extractors/middleware.js +143 -0
- package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
- package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
- package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
- package/dist/core/ingestion/route-extractors/php.js +21 -0
- package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
- package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +8 -7
- package/dist/core/ingestion/tree-sitter-queries.js +231 -9
- package/dist/core/ingestion/type-env.d.ts +14 -17
- package/dist/core/ingestion/type-env.js +66 -14
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/csharp.js +1 -1
- package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
- package/dist/core/ingestion/type-extractors/dart.js +371 -0
- package/dist/core/ingestion/type-extractors/jvm.js +1 -1
- package/dist/core/ingestion/type-extractors/shared.d.ts +1 -13
- package/dist/core/ingestion/type-extractors/shared.js +9 -102
- package/dist/core/ingestion/type-extractors/swift.js +334 -4
- package/dist/core/ingestion/type-extractors/types.d.ts +3 -1
- package/dist/core/ingestion/{ast-helpers.d.ts → utils/ast-helpers.d.ts} +16 -13
- package/dist/core/ingestion/{ast-helpers.js → utils/ast-helpers.js} +111 -32
- package/dist/core/ingestion/{call-analysis.js → utils/call-analysis.js} +37 -0
- package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
- package/dist/core/ingestion/utils/event-loop.js +5 -0
- package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
- package/dist/core/ingestion/utils/language-detection.js +70 -0
- package/dist/core/ingestion/utils/verbose.d.ts +1 -0
- package/dist/core/ingestion/utils/verbose.js +7 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +43 -2
- package/dist/core/ingestion/workers/parse-worker.js +361 -150
- package/dist/core/lbug/csv-generator.js +34 -1
- package/dist/core/lbug/lbug-adapter.js +6 -0
- package/dist/core/lbug/schema.d.ts +5 -3
- package/dist/core/lbug/schema.js +39 -2
- package/dist/core/tree-sitter/parser-loader.js +7 -1
- package/dist/core/wiki/cursor-client.d.ts +31 -0
- package/dist/core/wiki/cursor-client.js +127 -0
- package/dist/core/wiki/generator.d.ts +28 -9
- package/dist/core/wiki/generator.js +115 -18
- package/dist/core/wiki/graph-queries.d.ts +4 -0
- package/dist/core/wiki/graph-queries.js +7 -1
- package/dist/core/wiki/llm-client.d.ts +2 -0
- package/dist/core/wiki/llm-client.js +8 -4
- package/dist/core/wiki/prompts.d.ts +3 -3
- package/dist/core/wiki/prompts.js +6 -0
- package/dist/mcp/core/lbug-adapter.d.ts +5 -0
- package/dist/mcp/core/lbug-adapter.js +11 -1
- package/dist/mcp/local/local-backend.d.ts +16 -5
- package/dist/mcp/local/local-backend.js +711 -74
- package/dist/mcp/tools.js +71 -2
- package/dist/storage/repo-manager.d.ts +3 -0
- package/package.json +17 -16
- package/dist/core/ingestion/import-resolution.d.ts +0 -101
- package/dist/core/ingestion/import-resolution.js +0 -251
- package/dist/core/ingestion/named-binding-extraction.js +0 -373
- package/dist/core/ingestion/resolvers/index.d.ts +0 -18
- package/dist/core/ingestion/resolvers/index.js +0 -13
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
- package/dist/core/ingestion/type-extractors/index.js +0 -31
- package/dist/core/ingestion/utils.d.ts +0 -20
- package/dist/core/ingestion/utils.js +0 -242
- package/scripts/patch-tree-sitter-swift.cjs +0 -74
- /package/dist/core/ingestion/{call-analysis.d.ts → utils/call-analysis.d.ts} +0 -0
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COBOL Processor
|
|
3
|
+
*
|
|
4
|
+
* Standalone regex-based processor for COBOL and JCL files.
|
|
5
|
+
* Follows the markdown-processor.ts pattern: takes (graph, files, allPathSet),
|
|
6
|
+
* does its own extraction, and writes directly to the graph.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline:
|
|
9
|
+
* 1. Separate programs from copybooks
|
|
10
|
+
* 2. Build copybook map (name -> content)
|
|
11
|
+
* 3. For each program: expand COPY statements, then run regex extraction
|
|
12
|
+
* 4. Map CobolRegexResults to graph nodes and relationships
|
|
13
|
+
* 5. Optionally process JCL files for job-step cross-references
|
|
14
|
+
*/
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { generateId } from '../../lib/utils.js';
|
|
17
|
+
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
18
|
+
import { preprocessCobolSource, extractCobolSymbolsWithRegex, } from './cobol/cobol-preprocessor.js';
|
|
19
|
+
import { expandCopies } from './cobol/cobol-copy-expander.js';
|
|
20
|
+
import { processJclFiles } from './cobol/jcl-processor.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// File detection
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const COBOL_EXTENSIONS = new Set([
|
|
25
|
+
'.cob', '.cbl', '.cobol', '.cpy', '.copybook',
|
|
26
|
+
]);
|
|
27
|
+
const JCL_EXTENSIONS = new Set(['.jcl', '.job', '.proc']);
|
|
28
|
+
const COPYBOOK_EXTENSIONS = new Set(['.cpy', '.copybook']);
|
|
29
|
+
/** Returns true if the file is a COBOL or copybook file. */
|
|
30
|
+
export function isCobolFile(filePath) {
|
|
31
|
+
return COBOL_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
/** Returns true if the file is a JCL file. */
|
|
34
|
+
export function isJclFile(filePath) {
|
|
35
|
+
return JCL_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
/** Returns true if the file is a COBOL copybook. */
|
|
38
|
+
function isCopybook(filePath) {
|
|
39
|
+
return COPYBOOK_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Main processor
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Process COBOL and JCL files into the knowledge graph.
|
|
46
|
+
*
|
|
47
|
+
* @param graph - The in-memory knowledge graph
|
|
48
|
+
* @param files - Array of { path, content } for COBOL/JCL files
|
|
49
|
+
* @param allPathSet - Set of all file paths in the repository
|
|
50
|
+
* @returns Summary of what was extracted
|
|
51
|
+
*/
|
|
52
|
+
export const processCobol = (graph, files, allPathSet) => {
|
|
53
|
+
const result = {
|
|
54
|
+
programs: 0,
|
|
55
|
+
paragraphs: 0,
|
|
56
|
+
sections: 0,
|
|
57
|
+
dataItems: 0,
|
|
58
|
+
calls: 0,
|
|
59
|
+
copies: 0,
|
|
60
|
+
execSqlBlocks: 0,
|
|
61
|
+
execCicsBlocks: 0,
|
|
62
|
+
entryPoints: 0,
|
|
63
|
+
moves: 0,
|
|
64
|
+
fileDeclarations: 0,
|
|
65
|
+
jclJobs: 0,
|
|
66
|
+
jclSteps: 0,
|
|
67
|
+
sqlIncludes: 0,
|
|
68
|
+
execDliBlocks: 0,
|
|
69
|
+
declaratives: 0,
|
|
70
|
+
sets: 0,
|
|
71
|
+
inspects: 0,
|
|
72
|
+
initializes: 0,
|
|
73
|
+
};
|
|
74
|
+
// ── 1. Separate programs, copybooks, and JCL ───────────────────────
|
|
75
|
+
const programs = [];
|
|
76
|
+
const copybooks = [];
|
|
77
|
+
const jclFiles = [];
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const ext = path.extname(file.path).toLowerCase();
|
|
80
|
+
if (JCL_EXTENSIONS.has(ext)) {
|
|
81
|
+
jclFiles.push(file);
|
|
82
|
+
}
|
|
83
|
+
else if (isCopybook(file.path)) {
|
|
84
|
+
copybooks.push(file);
|
|
85
|
+
}
|
|
86
|
+
else if (COBOL_EXTENSIONS.has(ext)) {
|
|
87
|
+
programs.push(file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── 2. Build copybook map (uppercase name -> content) ──────────────
|
|
91
|
+
const copybookMap = new Map();
|
|
92
|
+
for (const cb of copybooks) {
|
|
93
|
+
const name = path.basename(cb.path, path.extname(cb.path)).toUpperCase();
|
|
94
|
+
copybookMap.set(name, { content: cb.content, path: cb.path });
|
|
95
|
+
}
|
|
96
|
+
// Build reverse lookup: path -> content for O(1) readCopy
|
|
97
|
+
const copybookByPath = new Map();
|
|
98
|
+
for (const [, entry] of copybookMap) {
|
|
99
|
+
copybookByPath.set(entry.path, entry.content);
|
|
100
|
+
}
|
|
101
|
+
// Resolve and read callbacks for expandCopies
|
|
102
|
+
const resolveCopy = (name) => {
|
|
103
|
+
const entry = copybookMap.get(name.toUpperCase());
|
|
104
|
+
return entry ? entry.path : null;
|
|
105
|
+
};
|
|
106
|
+
const readCopy = (copyPath) => {
|
|
107
|
+
const content = copybookByPath.get(copyPath);
|
|
108
|
+
return content ? preprocessCobolSource(content) : null;
|
|
109
|
+
};
|
|
110
|
+
// Track module names for cross-program CALL resolution
|
|
111
|
+
const moduleNodeIds = new Map(); // uppercase program name -> node id
|
|
112
|
+
// ── 3. Process each COBOL program ──────────────────────────────────
|
|
113
|
+
for (const file of programs) {
|
|
114
|
+
const fileNodeId = generateId('File', file.path);
|
|
115
|
+
// Skip if file node doesn't exist (structure-processor creates it)
|
|
116
|
+
if (!graph.getNode(fileNodeId))
|
|
117
|
+
continue;
|
|
118
|
+
// Preprocess: clean patch markers
|
|
119
|
+
const cleaned = preprocessCobolSource(file.content);
|
|
120
|
+
// Expand COPY statements
|
|
121
|
+
const { expandedContent, copyResolutions } = expandCopies(cleaned, file.path, resolveCopy, readCopy);
|
|
122
|
+
// Extract symbols from expanded source
|
|
123
|
+
const extracted = extractCobolSymbolsWithRegex(expandedContent, file.path);
|
|
124
|
+
// Map to graph
|
|
125
|
+
mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds);
|
|
126
|
+
// Accumulate stats
|
|
127
|
+
result.programs += extracted.programs.length || (extracted.programName ? 1 : 0);
|
|
128
|
+
result.paragraphs += extracted.paragraphs.length;
|
|
129
|
+
result.sections += extracted.sections.length;
|
|
130
|
+
result.dataItems += extracted.dataItems.length;
|
|
131
|
+
result.calls += extracted.calls.length;
|
|
132
|
+
result.copies += extracted.copies.length;
|
|
133
|
+
result.execSqlBlocks += extracted.execSqlBlocks.length;
|
|
134
|
+
result.sqlIncludes += extracted.execSqlBlocks.filter(s => s.includeMember).length;
|
|
135
|
+
result.execCicsBlocks += extracted.execCicsBlocks.length;
|
|
136
|
+
result.entryPoints += extracted.entryPoints.length;
|
|
137
|
+
result.moves += extracted.moves.length;
|
|
138
|
+
result.fileDeclarations += extracted.fileDeclarations.length;
|
|
139
|
+
result.execDliBlocks += extracted.execDliBlocks.length;
|
|
140
|
+
result.declaratives += extracted.declaratives.length;
|
|
141
|
+
result.sets += extracted.sets.length;
|
|
142
|
+
result.inspects += extracted.inspects.length;
|
|
143
|
+
result.initializes += extracted.initializes.length;
|
|
144
|
+
}
|
|
145
|
+
// ── 4. Second pass: resolve cross-program CALL targets ─────────────
|
|
146
|
+
// During mapToGraph, early programs create unresolved CALL edges
|
|
147
|
+
// (target = <unresolved>:PROGNAME) because later programs haven't
|
|
148
|
+
// been registered in moduleNodeIds yet. Now that ALL programs are
|
|
149
|
+
// processed, re-scan unresolved CALLS edges and patch them.
|
|
150
|
+
// This covers both `cobol-call-unresolved` and CICS LINK/XCTL edges
|
|
151
|
+
// whose targets contain `<unresolved>:`.
|
|
152
|
+
const unresolvedToRemove = [];
|
|
153
|
+
graph.forEachRelationship(rel => {
|
|
154
|
+
if (rel.type !== 'CALLS')
|
|
155
|
+
return;
|
|
156
|
+
const match = rel.targetId.match(/<unresolved>:(.+)/);
|
|
157
|
+
if (!match)
|
|
158
|
+
return;
|
|
159
|
+
const resolvedId = moduleNodeIds.get(match[1]);
|
|
160
|
+
if (!resolvedId)
|
|
161
|
+
return;
|
|
162
|
+
if (rel.reason?.startsWith('cobol-call-unresolved') || rel.reason === 'cobol-cancel-unresolved') {
|
|
163
|
+
// Replace unresolved CALL/CANCEL with resolved edge
|
|
164
|
+
const resolvedReason = rel.reason === 'cobol-cancel-unresolved' ? 'cobol-cancel' : 'cobol-call';
|
|
165
|
+
graph.addRelationship({
|
|
166
|
+
id: rel.id + ':resolved',
|
|
167
|
+
type: 'CALLS',
|
|
168
|
+
sourceId: rel.sourceId,
|
|
169
|
+
targetId: resolvedId,
|
|
170
|
+
confidence: rel.reason === 'cobol-cancel-unresolved' ? 0.9 : 0.95,
|
|
171
|
+
reason: resolvedReason,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if (rel.reason?.startsWith('cics-') && rel.reason.endsWith('-unresolved')) {
|
|
175
|
+
// Replace unresolved CICS LINK/XCTL/LOAD with resolved edge
|
|
176
|
+
graph.addRelationship({
|
|
177
|
+
id: rel.id + ':resolved',
|
|
178
|
+
type: 'CALLS',
|
|
179
|
+
sourceId: rel.sourceId,
|
|
180
|
+
targetId: resolvedId,
|
|
181
|
+
confidence: 0.95,
|
|
182
|
+
reason: rel.reason.replace('-unresolved', ''),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Mark original unresolved edge for removal after iteration
|
|
186
|
+
unresolvedToRemove.push(rel.id);
|
|
187
|
+
});
|
|
188
|
+
// Remove orphan unresolved edges (cannot delete during Map.forEach iteration)
|
|
189
|
+
for (const id of unresolvedToRemove) {
|
|
190
|
+
graph.removeRelationship(id);
|
|
191
|
+
}
|
|
192
|
+
// ── 5. Process JCL files ───────────────────────────────────────────
|
|
193
|
+
if (jclFiles.length > 0) {
|
|
194
|
+
const jclPaths = jclFiles.map(f => f.path);
|
|
195
|
+
const jclContents = new Map();
|
|
196
|
+
for (const f of jclFiles) {
|
|
197
|
+
jclContents.set(f.path, f.content);
|
|
198
|
+
}
|
|
199
|
+
const jclResult = processJclFiles(graph, jclPaths, jclContents);
|
|
200
|
+
result.jclJobs += jclResult.jobCount;
|
|
201
|
+
result.jclSteps += jclResult.stepCount;
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
};
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Graph mapping
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
/** Generate a deterministic Property node ID using composite key (section:level:name). */
|
|
209
|
+
function generatePropertyId(filePath, item) {
|
|
210
|
+
return generateId('Property', `${filePath}:${item.section}:${item.level}:${item.name}`);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Build a lookup Map from data item name (uppercase) to its Property node ID.
|
|
214
|
+
* First-wins semantics: if the same name appears in multiple sections,
|
|
215
|
+
* the first occurrence in extraction order is used for MOVE edge resolution.
|
|
216
|
+
*/
|
|
217
|
+
function buildDataItemMap(dataItems, filePath) {
|
|
218
|
+
const map = new Map();
|
|
219
|
+
for (const item of dataItems) {
|
|
220
|
+
if (item.name === 'FILLER')
|
|
221
|
+
continue;
|
|
222
|
+
const key = item.name.toUpperCase();
|
|
223
|
+
if (!map.has(key)) {
|
|
224
|
+
map.set(key, generatePropertyId(filePath, item));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return map;
|
|
228
|
+
}
|
|
229
|
+
function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
|
|
230
|
+
const { path: filePath, content } = file;
|
|
231
|
+
const lines = content.split(/\r?\n/);
|
|
232
|
+
const fileNodeId = generateId('File', filePath);
|
|
233
|
+
// ── PROGRAM-ID -> Module node ────────────────────────────────────
|
|
234
|
+
let moduleId;
|
|
235
|
+
if (extracted.programName) {
|
|
236
|
+
moduleId = generateId('Module', `${filePath}:${extracted.programName}`);
|
|
237
|
+
const metaDesc = [
|
|
238
|
+
extracted.programMetadata.author && `author:${extracted.programMetadata.author}`,
|
|
239
|
+
extracted.programMetadata.dateWritten && `date:${extracted.programMetadata.dateWritten}`,
|
|
240
|
+
extracted.programMetadata.dateCompiled && `compiled:${extracted.programMetadata.dateCompiled}`,
|
|
241
|
+
extracted.programMetadata.installation && `install:${extracted.programMetadata.installation}`,
|
|
242
|
+
].filter(Boolean).join(' ');
|
|
243
|
+
graph.addNode({
|
|
244
|
+
id: moduleId,
|
|
245
|
+
label: 'Module',
|
|
246
|
+
properties: {
|
|
247
|
+
name: extracted.programName,
|
|
248
|
+
filePath,
|
|
249
|
+
startLine: 1,
|
|
250
|
+
endLine: lines.length,
|
|
251
|
+
language: SupportedLanguages.Cobol,
|
|
252
|
+
isExported: true,
|
|
253
|
+
description: metaDesc || undefined,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
graph.addRelationship({
|
|
257
|
+
id: generateId('CONTAINS', `${fileNodeId}->${moduleId}`),
|
|
258
|
+
type: 'CONTAINS',
|
|
259
|
+
sourceId: fileNodeId,
|
|
260
|
+
targetId: moduleId,
|
|
261
|
+
confidence: 1.0,
|
|
262
|
+
reason: 'cobol-program-id',
|
|
263
|
+
});
|
|
264
|
+
moduleNodeIds.set(extracted.programName.toUpperCase(), moduleId);
|
|
265
|
+
}
|
|
266
|
+
// ── Nested programs -> additional Module nodes ───────────────────
|
|
267
|
+
// programs[] contains all PROGRAM-IDs with line ranges. The first entry
|
|
268
|
+
// is the primary (outer) program (already created above). Additional
|
|
269
|
+
// entries are nested programs that get their own Module nodes.
|
|
270
|
+
const programModuleIds = new Map();
|
|
271
|
+
if (moduleId) {
|
|
272
|
+
programModuleIds.set(extracted.programName.toUpperCase(), moduleId);
|
|
273
|
+
}
|
|
274
|
+
for (const prog of extracted.programs) {
|
|
275
|
+
if (prog.name.toUpperCase() === extracted.programName?.toUpperCase())
|
|
276
|
+
continue; // skip primary
|
|
277
|
+
const nestedModuleId = generateId('Module', `${filePath}:${prog.name}`);
|
|
278
|
+
graph.addNode({
|
|
279
|
+
id: nestedModuleId,
|
|
280
|
+
label: 'Module',
|
|
281
|
+
properties: {
|
|
282
|
+
name: prog.name,
|
|
283
|
+
filePath,
|
|
284
|
+
startLine: prog.startLine,
|
|
285
|
+
endLine: prog.endLine,
|
|
286
|
+
language: SupportedLanguages.Cobol,
|
|
287
|
+
isExported: true,
|
|
288
|
+
description: `nested-program${prog.isCommon ? ' common' : ''}`,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
// Find enclosing program by line-range containment
|
|
292
|
+
const enclosing = extracted.programs.find(p => p.startLine < prog.startLine && p.endLine > prog.endLine && p.nestingDepth < prog.nestingDepth);
|
|
293
|
+
const nestedParent = enclosing
|
|
294
|
+
? (programModuleIds.get(enclosing.name.toUpperCase()) ?? moduleId ?? fileNodeId)
|
|
295
|
+
: (moduleId ?? fileNodeId);
|
|
296
|
+
graph.addRelationship({
|
|
297
|
+
id: generateId('CONTAINS', `${nestedParent}->${nestedModuleId}`),
|
|
298
|
+
type: 'CONTAINS',
|
|
299
|
+
sourceId: nestedParent,
|
|
300
|
+
targetId: nestedModuleId,
|
|
301
|
+
confidence: 1.0,
|
|
302
|
+
reason: 'cobol-nested-program',
|
|
303
|
+
});
|
|
304
|
+
moduleNodeIds.set(prog.name.toUpperCase(), nestedModuleId);
|
|
305
|
+
programModuleIds.set(prog.name.toUpperCase(), nestedModuleId);
|
|
306
|
+
}
|
|
307
|
+
const parentId = moduleId ?? fileNodeId;
|
|
308
|
+
// ── SECTIONs -> Namespace nodes ──────────────────────────────────
|
|
309
|
+
const sectionNodeIds = new Map();
|
|
310
|
+
for (let i = 0; i < extracted.sections.length; i++) {
|
|
311
|
+
const sec = extracted.sections[i];
|
|
312
|
+
const nextLine = i + 1 < extracted.sections.length
|
|
313
|
+
? extracted.sections[i + 1].line - 1
|
|
314
|
+
: lines.length;
|
|
315
|
+
const owningPgm = findOwningProgramName(sec.line, extracted.programs);
|
|
316
|
+
const secId = generateId('Namespace', `${filePath}:${owningPgm ? owningPgm + ':' : ''}${sec.name}`);
|
|
317
|
+
graph.addNode({
|
|
318
|
+
id: secId,
|
|
319
|
+
label: 'Namespace',
|
|
320
|
+
properties: {
|
|
321
|
+
name: sec.name,
|
|
322
|
+
filePath,
|
|
323
|
+
startLine: sec.line,
|
|
324
|
+
endLine: nextLine,
|
|
325
|
+
language: SupportedLanguages.Cobol,
|
|
326
|
+
isExported: true,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
const secParent = programModuleIds.get(owningPgm ?? '') ?? parentId;
|
|
330
|
+
graph.addRelationship({
|
|
331
|
+
id: generateId('CONTAINS', `${secParent}->${secId}`),
|
|
332
|
+
type: 'CONTAINS',
|
|
333
|
+
sourceId: secParent,
|
|
334
|
+
targetId: secId,
|
|
335
|
+
confidence: 1.0,
|
|
336
|
+
reason: 'cobol-section',
|
|
337
|
+
});
|
|
338
|
+
sectionNodeIds.set(`${owningPgm ?? ''}:${sec.name.toUpperCase()}`, secId);
|
|
339
|
+
}
|
|
340
|
+
// ── PARAGRAPHs -> Function nodes ─────────────────────────────────
|
|
341
|
+
const paraNodeIds = new Map();
|
|
342
|
+
for (let i = 0; i < extracted.paragraphs.length; i++) {
|
|
343
|
+
const para = extracted.paragraphs[i];
|
|
344
|
+
const nextLine = i + 1 < extracted.paragraphs.length
|
|
345
|
+
? extracted.paragraphs[i + 1].line - 1
|
|
346
|
+
: lines.length;
|
|
347
|
+
const owningPgmPara = findOwningProgramName(para.line, extracted.programs);
|
|
348
|
+
const paraId = generateId('Function', `${filePath}:${owningPgmPara ? owningPgmPara + ':' : ''}${para.name}`);
|
|
349
|
+
graph.addNode({
|
|
350
|
+
id: paraId,
|
|
351
|
+
label: 'Function',
|
|
352
|
+
properties: {
|
|
353
|
+
name: para.name,
|
|
354
|
+
filePath,
|
|
355
|
+
startLine: para.line,
|
|
356
|
+
endLine: nextLine,
|
|
357
|
+
language: SupportedLanguages.Cobol,
|
|
358
|
+
isExported: true,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
// Parent: find the containing section, or fall back to module/file
|
|
362
|
+
const containerId = findContainingSection(para.line, extracted.sections, sectionNodeIds, extracted.programs)
|
|
363
|
+
?? (programModuleIds.get(owningPgmPara ?? '') ?? parentId);
|
|
364
|
+
graph.addRelationship({
|
|
365
|
+
id: generateId('CONTAINS', `${containerId}->${paraId}`),
|
|
366
|
+
type: 'CONTAINS',
|
|
367
|
+
sourceId: containerId,
|
|
368
|
+
targetId: paraId,
|
|
369
|
+
confidence: 1.0,
|
|
370
|
+
reason: 'cobol-paragraph',
|
|
371
|
+
});
|
|
372
|
+
paraNodeIds.set(`${owningPgmPara ?? ''}:${para.name.toUpperCase()}`, paraId);
|
|
373
|
+
}
|
|
374
|
+
// ── Data items -> Property nodes ─────────────────────────────────
|
|
375
|
+
for (const item of extracted.dataItems) {
|
|
376
|
+
if (item.name === 'FILLER')
|
|
377
|
+
continue; // Skip anonymous fillers
|
|
378
|
+
const propId = generatePropertyId(filePath, item);
|
|
379
|
+
const itemOwner = findOwningProgramName(item.line, extracted.programs);
|
|
380
|
+
const itemParent = programModuleIds.get(itemOwner ?? '') ?? parentId;
|
|
381
|
+
graph.addNode({
|
|
382
|
+
id: propId,
|
|
383
|
+
label: 'Property',
|
|
384
|
+
properties: {
|
|
385
|
+
name: item.name,
|
|
386
|
+
filePath,
|
|
387
|
+
startLine: item.line,
|
|
388
|
+
endLine: item.line,
|
|
389
|
+
language: SupportedLanguages.Cobol,
|
|
390
|
+
description: `level:${item.level} section:${item.section}${item.pic ? ` pic:${item.pic}` : ''}`,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
graph.addRelationship({
|
|
394
|
+
id: generateId('CONTAINS', `${itemParent}->${propId}`),
|
|
395
|
+
type: 'CONTAINS',
|
|
396
|
+
sourceId: itemParent,
|
|
397
|
+
targetId: propId,
|
|
398
|
+
confidence: 1.0,
|
|
399
|
+
reason: 'cobol-data-item',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
// ── Build data item Map early (needed by CALL USING, CICS INTO/FROM, MOVE, and USING) ──
|
|
403
|
+
const dataItemMap = buildDataItemMap(extracted.dataItems, filePath);
|
|
404
|
+
// ── OCCURS DEPENDING ON -> ACCESSES edges (variable-length table deps) ──
|
|
405
|
+
for (const item of extracted.dataItems) {
|
|
406
|
+
if (item.name === 'FILLER' || !item.dependingOn)
|
|
407
|
+
continue;
|
|
408
|
+
const propId = generatePropertyId(filePath, item);
|
|
409
|
+
const depFieldId = dataItemMap.get(item.dependingOn.toUpperCase());
|
|
410
|
+
if (depFieldId) {
|
|
411
|
+
graph.addRelationship({
|
|
412
|
+
id: generateId('ACCESSES', `${propId}->depends-on->${item.dependingOn}`),
|
|
413
|
+
type: 'ACCESSES',
|
|
414
|
+
sourceId: propId,
|
|
415
|
+
targetId: depFieldId,
|
|
416
|
+
confidence: 1.0,
|
|
417
|
+
reason: 'cobol-depends-on',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Helper: look up paragraph/section by name scoped to the owning program
|
|
422
|
+
const scopedParaLookup = (name, lineNum) => {
|
|
423
|
+
const pgm = findOwningProgramName(lineNum, extracted.programs);
|
|
424
|
+
return paraNodeIds.get(`${pgm ?? ''}:${name.toUpperCase()}`)
|
|
425
|
+
?? sectionNodeIds.get(`${pgm ?? ''}:${name.toUpperCase()}`);
|
|
426
|
+
};
|
|
427
|
+
const scopedCallerLookup = (name, lineNum) => {
|
|
428
|
+
if (!name)
|
|
429
|
+
return owningModuleId(lineNum);
|
|
430
|
+
const pgm = findOwningProgramName(lineNum, extracted.programs);
|
|
431
|
+
return paraNodeIds.get(`${pgm ?? ''}:${name.toUpperCase()}`)
|
|
432
|
+
?? (programModuleIds.get(pgm ?? '') ?? parentId);
|
|
433
|
+
};
|
|
434
|
+
/** Resolve the owning program's module ID for a given line (for nested program edge attribution). */
|
|
435
|
+
const owningModuleId = (lineNum) => {
|
|
436
|
+
const pgm = findOwningProgramName(lineNum, extracted.programs);
|
|
437
|
+
return programModuleIds.get(pgm ?? '') ?? parentId;
|
|
438
|
+
};
|
|
439
|
+
// ── PERFORM -> CALLS relationship (intra-file) ──────────────────
|
|
440
|
+
for (const perf of extracted.performs) {
|
|
441
|
+
const targetId = scopedParaLookup(perf.target, perf.line);
|
|
442
|
+
if (!targetId)
|
|
443
|
+
continue;
|
|
444
|
+
// Source: the paragraph containing the PERFORM, or the module
|
|
445
|
+
const sourceId = scopedCallerLookup(perf.caller, perf.line);
|
|
446
|
+
graph.addRelationship({
|
|
447
|
+
id: generateId('CALLS', `${sourceId}->perform->${targetId}:L${perf.line}`),
|
|
448
|
+
type: 'CALLS',
|
|
449
|
+
sourceId,
|
|
450
|
+
targetId,
|
|
451
|
+
confidence: 1.0,
|
|
452
|
+
reason: 'cobol-perform',
|
|
453
|
+
});
|
|
454
|
+
// PERFORM THRU -> expanded CALLS edge to thru target
|
|
455
|
+
if (perf.thruTarget) {
|
|
456
|
+
const thruTargetId = scopedParaLookup(perf.thruTarget, perf.line);
|
|
457
|
+
if (thruTargetId && thruTargetId !== targetId) {
|
|
458
|
+
graph.addRelationship({
|
|
459
|
+
id: generateId('CALLS', `${sourceId}->perform-thru->${thruTargetId}:L${perf.line}`),
|
|
460
|
+
type: 'CALLS',
|
|
461
|
+
sourceId,
|
|
462
|
+
targetId: thruTargetId,
|
|
463
|
+
confidence: 1.0,
|
|
464
|
+
reason: 'cobol-perform-thru',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// ── CALL -> CALLS relationship (cross-program) ──────────────────
|
|
470
|
+
for (const call of extracted.calls) {
|
|
471
|
+
if (!call.isQuoted) {
|
|
472
|
+
// Dynamic CALL via data item — not statically resolvable.
|
|
473
|
+
// Emit a CodeElement annotation for visibility in impact analysis.
|
|
474
|
+
graph.addNode({
|
|
475
|
+
id: generateId('CodeElement', `${filePath}:dynamic-call:${call.target}:L${call.line}`),
|
|
476
|
+
label: 'CodeElement',
|
|
477
|
+
properties: {
|
|
478
|
+
name: `CALL ${call.target}`,
|
|
479
|
+
filePath,
|
|
480
|
+
startLine: call.line,
|
|
481
|
+
endLine: call.line,
|
|
482
|
+
language: SupportedLanguages.Cobol,
|
|
483
|
+
description: 'dynamic-call (target is a data item, not resolvable statically)',
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const dynCallOwner = owningModuleId(call.line);
|
|
487
|
+
graph.addRelationship({
|
|
488
|
+
id: generateId('CONTAINS', `${dynCallOwner}->dynamic-call:${call.target}:L${call.line}`),
|
|
489
|
+
type: 'CONTAINS',
|
|
490
|
+
sourceId: dynCallOwner,
|
|
491
|
+
targetId: generateId('CodeElement', `${filePath}:dynamic-call:${call.target}:L${call.line}`),
|
|
492
|
+
confidence: 1.0,
|
|
493
|
+
reason: 'cobol-dynamic-call',
|
|
494
|
+
});
|
|
495
|
+
// CALL USING parameters for dynamic call too
|
|
496
|
+
if (call.parameters && call.parameters.length > 0) {
|
|
497
|
+
for (const param of call.parameters) {
|
|
498
|
+
const paramPropId = dataItemMap.get(param.toUpperCase());
|
|
499
|
+
if (paramPropId) {
|
|
500
|
+
graph.addRelationship({
|
|
501
|
+
id: generateId('ACCESSES', `${dynCallOwner}->call-using->${param}:L${call.line}`),
|
|
502
|
+
type: 'ACCESSES',
|
|
503
|
+
sourceId: dynCallOwner,
|
|
504
|
+
targetId: paramPropId,
|
|
505
|
+
confidence: 0.9,
|
|
506
|
+
reason: 'cobol-call-using',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// CALL RETURNING target for dynamic call too
|
|
512
|
+
if (call.returning) {
|
|
513
|
+
const retPropId = dataItemMap.get(call.returning.toUpperCase());
|
|
514
|
+
if (retPropId) {
|
|
515
|
+
graph.addRelationship({
|
|
516
|
+
id: generateId('ACCESSES', `${dynCallOwner}->call-returning->${call.returning}:L${call.line}`),
|
|
517
|
+
type: 'ACCESSES',
|
|
518
|
+
sourceId: dynCallOwner,
|
|
519
|
+
targetId: retPropId,
|
|
520
|
+
confidence: 0.9,
|
|
521
|
+
reason: 'cobol-call-returning',
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const targetModuleId = moduleNodeIds.get(call.target.toUpperCase());
|
|
528
|
+
// Create edge even if target not yet known — use a synthetic target id
|
|
529
|
+
const targetId = targetModuleId
|
|
530
|
+
?? generateId('Module', `<unresolved>:${call.target.toUpperCase()}`);
|
|
531
|
+
const callOwner = owningModuleId(call.line);
|
|
532
|
+
graph.addRelationship({
|
|
533
|
+
id: generateId('CALLS', `${callOwner}->call->${call.target}:L${call.line}`),
|
|
534
|
+
type: 'CALLS',
|
|
535
|
+
sourceId: callOwner,
|
|
536
|
+
targetId,
|
|
537
|
+
confidence: targetModuleId ? 0.95 : 0.5,
|
|
538
|
+
reason: targetModuleId ? 'cobol-call' : 'cobol-call-unresolved',
|
|
539
|
+
});
|
|
540
|
+
// CALL USING parameters -> ACCESSES edges (data flow across programs)
|
|
541
|
+
if (call.parameters && call.parameters.length > 0) {
|
|
542
|
+
for (const param of call.parameters) {
|
|
543
|
+
const paramPropId = dataItemMap.get(param.toUpperCase());
|
|
544
|
+
if (paramPropId) {
|
|
545
|
+
graph.addRelationship({
|
|
546
|
+
id: generateId('ACCESSES', `${callOwner}->call-using->${param}:L${call.line}`),
|
|
547
|
+
type: 'ACCESSES',
|
|
548
|
+
sourceId: callOwner,
|
|
549
|
+
targetId: paramPropId,
|
|
550
|
+
confidence: 0.9,
|
|
551
|
+
reason: 'cobol-call-using',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// CALL RETURNING target -> ACCESSES edge (return value data flow)
|
|
557
|
+
if (call.returning) {
|
|
558
|
+
const retPropId = dataItemMap.get(call.returning.toUpperCase());
|
|
559
|
+
if (retPropId) {
|
|
560
|
+
graph.addRelationship({
|
|
561
|
+
id: generateId('ACCESSES', `${callOwner}->call-returning->${call.returning}:L${call.line}`),
|
|
562
|
+
type: 'ACCESSES',
|
|
563
|
+
sourceId: callOwner,
|
|
564
|
+
targetId: retPropId,
|
|
565
|
+
confidence: 0.9,
|
|
566
|
+
reason: 'cobol-call-returning',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// ── COPY -> IMPORTS relationship ─────────────────────────────────
|
|
572
|
+
for (const res of copyResolutions) {
|
|
573
|
+
if (!res.resolvedPath)
|
|
574
|
+
continue;
|
|
575
|
+
const targetFileId = generateId('File', res.resolvedPath);
|
|
576
|
+
graph.addRelationship({
|
|
577
|
+
id: generateId('IMPORTS', `${fileNodeId}->${targetFileId}:${res.copyTarget}`),
|
|
578
|
+
type: 'IMPORTS',
|
|
579
|
+
sourceId: fileNodeId,
|
|
580
|
+
targetId: targetFileId,
|
|
581
|
+
confidence: 1.0,
|
|
582
|
+
reason: 'cobol-copy',
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
// ── EXEC SQL blocks -> CodeElement nodes + ACCESSES edges ──────
|
|
586
|
+
for (const sql of extracted.execSqlBlocks) {
|
|
587
|
+
const sqlId = generateId('CodeElement', `${filePath}:exec-sql:L${sql.line}`);
|
|
588
|
+
graph.addNode({
|
|
589
|
+
id: sqlId,
|
|
590
|
+
label: 'CodeElement',
|
|
591
|
+
properties: {
|
|
592
|
+
name: `EXEC SQL ${sql.operation}`,
|
|
593
|
+
filePath,
|
|
594
|
+
startLine: sql.line,
|
|
595
|
+
endLine: sql.line,
|
|
596
|
+
language: SupportedLanguages.Cobol,
|
|
597
|
+
description: `tables:[${sql.tables.join(',')}] cursors:[${sql.cursors.join(',')}]`,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
const sqlOwner = owningModuleId(sql.line);
|
|
601
|
+
graph.addRelationship({
|
|
602
|
+
id: generateId('CONTAINS', `${sqlOwner}->${sqlId}`),
|
|
603
|
+
type: 'CONTAINS',
|
|
604
|
+
sourceId: sqlOwner,
|
|
605
|
+
targetId: sqlId,
|
|
606
|
+
confidence: 1.0,
|
|
607
|
+
reason: 'cobol-exec-sql',
|
|
608
|
+
});
|
|
609
|
+
// ACCESSES edges to tables
|
|
610
|
+
for (const table of sql.tables) {
|
|
611
|
+
const tableId = generateId('Record', `<db>:${table}`);
|
|
612
|
+
graph.addRelationship({
|
|
613
|
+
id: generateId('ACCESSES', `${sqlId}->${tableId}:${sql.operation}`),
|
|
614
|
+
type: 'ACCESSES',
|
|
615
|
+
sourceId: sqlId,
|
|
616
|
+
targetId: tableId,
|
|
617
|
+
confidence: 0.9,
|
|
618
|
+
reason: `sql-${sql.operation.toLowerCase()}`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
// EXEC SQL INCLUDE -> IMPORTS edge
|
|
622
|
+
if (sql.includeMember) {
|
|
623
|
+
// Try to resolve as a copybook
|
|
624
|
+
const includeTarget = sql.includeMember.toUpperCase();
|
|
625
|
+
// We don't have copybookMap here, so emit directly as IMPORTS
|
|
626
|
+
// The edge uses reason 'sql-include' to distinguish from COPY
|
|
627
|
+
graph.addRelationship({
|
|
628
|
+
id: generateId('IMPORTS', `${fileNodeId}->sql-include->${includeTarget}:L${sql.line}`),
|
|
629
|
+
type: 'IMPORTS',
|
|
630
|
+
sourceId: fileNodeId,
|
|
631
|
+
targetId: generateId('File', `<unresolved>:${includeTarget}`),
|
|
632
|
+
confidence: 0.8,
|
|
633
|
+
reason: 'sql-include',
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// ── PROCEDURE DIVISION USING -> ACCESSES edges (parameter contract) ──
|
|
638
|
+
// Iterate per-program to handle nested programs with their own USING clauses
|
|
639
|
+
for (const prog of extracted.programs) {
|
|
640
|
+
const progModId = programModuleIds.get(prog.name.toUpperCase()) ?? moduleId;
|
|
641
|
+
if (progModId && prog.procedureUsing && prog.procedureUsing.length > 0) {
|
|
642
|
+
for (const param of prog.procedureUsing) {
|
|
643
|
+
const paramPropId = dataItemMap.get(param.toUpperCase());
|
|
644
|
+
if (paramPropId) {
|
|
645
|
+
graph.addRelationship({
|
|
646
|
+
id: generateId('ACCESSES', `${progModId}->using->${param}`),
|
|
647
|
+
type: 'ACCESSES',
|
|
648
|
+
sourceId: progModId,
|
|
649
|
+
targetId: paramPropId,
|
|
650
|
+
confidence: 1.0,
|
|
651
|
+
reason: 'cobol-procedure-using',
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// ── EXEC CICS blocks -> CodeElement nodes + CALLS edges ────────
|
|
658
|
+
for (const cics of extracted.execCicsBlocks) {
|
|
659
|
+
const cicsId = generateId('CodeElement', `${filePath}:exec-cics:L${cics.line}`);
|
|
660
|
+
graph.addNode({
|
|
661
|
+
id: cicsId,
|
|
662
|
+
label: 'CodeElement',
|
|
663
|
+
properties: {
|
|
664
|
+
name: `EXEC CICS ${cics.command}`,
|
|
665
|
+
filePath,
|
|
666
|
+
startLine: cics.line,
|
|
667
|
+
endLine: cics.line,
|
|
668
|
+
language: SupportedLanguages.Cobol,
|
|
669
|
+
description: [
|
|
670
|
+
cics.mapName && `map:${cics.mapName}`,
|
|
671
|
+
cics.programName && `program:${cics.programName}${cics.programIsLiteral === false ? ' (dynamic)' : ''}`,
|
|
672
|
+
cics.transId && `transid:${cics.transId}`,
|
|
673
|
+
cics.fileName && `file:${cics.fileName}`,
|
|
674
|
+
cics.queueName && `queue:${cics.queueName}`,
|
|
675
|
+
cics.labelName && `label:${cics.labelName}`,
|
|
676
|
+
].filter(Boolean).join(' ') || undefined,
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
const cicsOwner = owningModuleId(cics.line);
|
|
680
|
+
graph.addRelationship({
|
|
681
|
+
id: generateId('CONTAINS', `${cicsOwner}->${cicsId}`),
|
|
682
|
+
type: 'CONTAINS',
|
|
683
|
+
sourceId: cicsOwner,
|
|
684
|
+
targetId: cicsId,
|
|
685
|
+
confidence: 1.0,
|
|
686
|
+
reason: 'cobol-exec-cics',
|
|
687
|
+
});
|
|
688
|
+
// LINK/XCTL -> cross-program CALLS (handles both literal and variable PROGRAM)
|
|
689
|
+
if (cics.programName && ['LINK', 'XCTL', 'LOAD'].includes(cics.command)) {
|
|
690
|
+
if (cics.programIsLiteral === false) {
|
|
691
|
+
// Dynamic PROGRAM reference via variable — annotate, don't resolve
|
|
692
|
+
graph.addNode({
|
|
693
|
+
id: generateId('CodeElement', `${filePath}:cics-dynamic-pgm:${cics.programName}:L${cics.line}`),
|
|
694
|
+
label: 'CodeElement',
|
|
695
|
+
properties: {
|
|
696
|
+
name: `CICS ${cics.command} ${cics.programName}`,
|
|
697
|
+
filePath, startLine: cics.line, endLine: cics.line,
|
|
698
|
+
language: SupportedLanguages.Cobol,
|
|
699
|
+
description: `cics-dynamic-program (target is data item ${cics.programName})`,
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
graph.addRelationship({
|
|
703
|
+
id: generateId('CONTAINS', `${cicsOwner}->cics-dynamic-pgm:${cics.programName}:L${cics.line}`),
|
|
704
|
+
type: 'CONTAINS', sourceId: cicsOwner,
|
|
705
|
+
targetId: generateId('CodeElement', `${filePath}:cics-dynamic-pgm:${cics.programName}:L${cics.line}`),
|
|
706
|
+
confidence: 1.0, reason: 'cics-dynamic-program',
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const cicsTargetModuleId = moduleNodeIds.get(cics.programName.toUpperCase());
|
|
711
|
+
const targetId = cicsTargetModuleId
|
|
712
|
+
?? generateId('Module', `<unresolved>:${cics.programName.toUpperCase()}`);
|
|
713
|
+
const cicsReason = `cics-${cics.command.toLowerCase()}`;
|
|
714
|
+
graph.addRelationship({
|
|
715
|
+
id: generateId('CALLS', `${cicsOwner}->cics-${cics.command.toLowerCase()}->${cics.programName}:L${cics.line}`),
|
|
716
|
+
type: 'CALLS', sourceId: cicsOwner, targetId,
|
|
717
|
+
confidence: cicsTargetModuleId ? 0.95 : 0.5,
|
|
718
|
+
reason: cicsTargetModuleId ? cicsReason : `${cicsReason}-unresolved`,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// CICS FILE I/O -> ACCESSES edges (READ/WRITE/REWRITE/DELETE/STARTBR/ENDBR FILE)
|
|
723
|
+
if (cics.fileName) {
|
|
724
|
+
const fileRecordId = generateId('Record', `<cics-file>:${cics.fileName.toUpperCase()}`);
|
|
725
|
+
const ioCommand = cics.command.toUpperCase();
|
|
726
|
+
const isRead = ['READ', 'STARTBR', 'READNEXT', 'READPREV', 'READ NEXT', 'READ PREV', 'ENDBR'].includes(ioCommand);
|
|
727
|
+
const isWrite = ['WRITE', 'REWRITE', 'DELETE'].includes(ioCommand);
|
|
728
|
+
const reason = isRead ? 'cics-file-read' : isWrite ? 'cics-file-write' : 'cics-file-access';
|
|
729
|
+
graph.addRelationship({
|
|
730
|
+
id: generateId('ACCESSES', `${cicsId}->file->${cics.fileName}:L${cics.line}`),
|
|
731
|
+
type: 'ACCESSES', sourceId: cicsId, targetId: fileRecordId,
|
|
732
|
+
confidence: 0.9, reason,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
// CICS QUEUE -> ACCESSES edge with differentiated reason (WRITEQ/READQ/DELETEQ TS/TD)
|
|
736
|
+
if (cics.queueName) {
|
|
737
|
+
const queueId = generateId('Record', `<queue>:${cics.queueName}`);
|
|
738
|
+
const qCmd = cics.command.toUpperCase();
|
|
739
|
+
const qReason = qCmd.startsWith('READQ') ? 'cics-queue-read'
|
|
740
|
+
: qCmd.startsWith('WRITEQ') ? 'cics-queue-write'
|
|
741
|
+
: qCmd.startsWith('DELETEQ') ? 'cics-queue-delete'
|
|
742
|
+
: 'cics-queue';
|
|
743
|
+
graph.addRelationship({
|
|
744
|
+
id: generateId('ACCESSES', `${cicsId}->queue->${cics.queueName}:L${cics.line}`),
|
|
745
|
+
type: 'ACCESSES', sourceId: cicsId, targetId: queueId,
|
|
746
|
+
confidence: 0.85, reason: qReason,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
// CICS RETURN/START TRANSID -> CALLS edge (transaction flow)
|
|
750
|
+
if (cics.transId) {
|
|
751
|
+
const cmd = cics.command.toUpperCase();
|
|
752
|
+
if (cmd === 'RETURN' || cmd.startsWith('START')) {
|
|
753
|
+
const transNodeId = generateId('CodeElement', `<transid>:${cics.transId}`);
|
|
754
|
+
graph.addRelationship({
|
|
755
|
+
id: generateId('CALLS', `${cicsOwner}->${cmd === 'RETURN' ? 'return' : 'start'}-transid->${cics.transId}:L${cics.line}`),
|
|
756
|
+
type: 'CALLS', sourceId: cicsOwner, targetId: transNodeId,
|
|
757
|
+
confidence: 0.8,
|
|
758
|
+
reason: cmd === 'RETURN' ? 'cics-return-transid' : 'cics-start-transid',
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// CICS MAP -> ACCESSES edge (screen/mapset traceability)
|
|
763
|
+
if (cics.mapName) {
|
|
764
|
+
const mapId = generateId('Record', `<map>:${cics.mapName}`);
|
|
765
|
+
graph.addRelationship({
|
|
766
|
+
id: generateId('ACCESSES', `${cicsId}->map->${cics.mapName}:L${cics.line}`),
|
|
767
|
+
type: 'ACCESSES', sourceId: cicsId, targetId: mapId,
|
|
768
|
+
confidence: 0.85, reason: 'cics-map',
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
// CICS INTO(data-area) -> ACCESSES edge (data write target)
|
|
772
|
+
if (cics.intoField) {
|
|
773
|
+
const intoPropId = dataItemMap.get(cics.intoField.toUpperCase());
|
|
774
|
+
if (intoPropId) {
|
|
775
|
+
graph.addRelationship({
|
|
776
|
+
id: generateId('ACCESSES', `${cicsId}->into->${cics.intoField}:L${cics.line}`),
|
|
777
|
+
type: 'ACCESSES', sourceId: cicsId, targetId: intoPropId,
|
|
778
|
+
confidence: 0.9, reason: 'cics-receive-into',
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// CICS FROM(data-area) -> ACCESSES edge (data read source)
|
|
783
|
+
if (cics.fromField) {
|
|
784
|
+
const fromPropId = dataItemMap.get(cics.fromField.toUpperCase());
|
|
785
|
+
if (fromPropId) {
|
|
786
|
+
graph.addRelationship({
|
|
787
|
+
id: generateId('ACCESSES', `${cicsId}->from->${cics.fromField}:L${cics.line}`),
|
|
788
|
+
type: 'ACCESSES', sourceId: cicsId, targetId: fromPropId,
|
|
789
|
+
confidence: 0.9, reason: 'cics-send-from',
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// CICS HANDLE ABEND LABEL -> CALLS edge to error handler paragraph
|
|
794
|
+
if (cics.labelName) {
|
|
795
|
+
const labelTargetId = scopedParaLookup(cics.labelName, cics.line);
|
|
796
|
+
if (labelTargetId) {
|
|
797
|
+
graph.addRelationship({
|
|
798
|
+
id: generateId('CALLS', `${cicsOwner}->abend-label->${cics.labelName}:L${cics.line}`),
|
|
799
|
+
type: 'CALLS', sourceId: cicsOwner, targetId: labelTargetId,
|
|
800
|
+
confidence: 0.9, reason: 'cics-handle-abend',
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// ── ENTRY points -> Constructor nodes ──────────────────────────
|
|
806
|
+
for (const entry of extracted.entryPoints) {
|
|
807
|
+
const entryId = generateId('Constructor', `${filePath}:${entry.name}`);
|
|
808
|
+
graph.addNode({
|
|
809
|
+
id: entryId,
|
|
810
|
+
label: 'Constructor',
|
|
811
|
+
properties: {
|
|
812
|
+
name: entry.name,
|
|
813
|
+
filePath,
|
|
814
|
+
startLine: entry.line,
|
|
815
|
+
endLine: entry.line,
|
|
816
|
+
language: SupportedLanguages.Cobol,
|
|
817
|
+
isExported: true,
|
|
818
|
+
description: entry.parameters.length > 0 ? `using:${entry.parameters.join(',')}` : undefined,
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
const entryOwner = owningModuleId(entry.line);
|
|
822
|
+
graph.addRelationship({
|
|
823
|
+
id: generateId('CONTAINS', `${entryOwner}->${entryId}`),
|
|
824
|
+
type: 'CONTAINS',
|
|
825
|
+
sourceId: entryOwner,
|
|
826
|
+
targetId: entryId,
|
|
827
|
+
confidence: 1.0,
|
|
828
|
+
reason: 'cobol-entry-point',
|
|
829
|
+
});
|
|
830
|
+
// Register in moduleNodeIds for cross-program resolution
|
|
831
|
+
moduleNodeIds.set(entry.name.toUpperCase(), entryId);
|
|
832
|
+
}
|
|
833
|
+
// ── DECLARATIVES error handlers -> ACCESSES edges ──────────────────
|
|
834
|
+
for (const decl of extracted.declaratives) {
|
|
835
|
+
// Find the section's Namespace node
|
|
836
|
+
const pgm = findOwningProgramName(decl.line, extracted.programs);
|
|
837
|
+
const sectionId = sectionNodeIds.get(`${pgm ?? ''}:${decl.sectionName.toUpperCase()}`);
|
|
838
|
+
if (!sectionId)
|
|
839
|
+
continue;
|
|
840
|
+
// Create ACCESSES edge from handler section to file/mode
|
|
841
|
+
const targetId = generateId('Record', `${filePath}:${decl.target}`);
|
|
842
|
+
graph.addRelationship({
|
|
843
|
+
id: generateId('ACCESSES', `${sectionId}->error-handler->${decl.target}:L${decl.line}`),
|
|
844
|
+
type: 'ACCESSES',
|
|
845
|
+
sourceId: sectionId,
|
|
846
|
+
targetId,
|
|
847
|
+
confidence: 0.9,
|
|
848
|
+
reason: 'cobol-error-handler',
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
// ── SET statement -> ACCESSES edges ──────────────────
|
|
852
|
+
for (const set of extracted.sets) {
|
|
853
|
+
const callerId = scopedCallerLookup(set.caller, set.line);
|
|
854
|
+
const reason = set.form === 'to-true' ? 'cobol-set-condition' : 'cobol-set-index';
|
|
855
|
+
for (const target of set.targets) {
|
|
856
|
+
const targetPropId = dataItemMap.get(target.toUpperCase());
|
|
857
|
+
if (targetPropId) {
|
|
858
|
+
graph.addRelationship({
|
|
859
|
+
id: generateId('ACCESSES', `${callerId}->set->${target}:L${set.line}`),
|
|
860
|
+
type: 'ACCESSES',
|
|
861
|
+
sourceId: callerId,
|
|
862
|
+
targetId: targetPropId,
|
|
863
|
+
confidence: 0.9,
|
|
864
|
+
reason,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// If SET index has a value that is an identifier (not a number), add read edge
|
|
869
|
+
if (set.value && /^[A-Z][A-Z0-9-]+$/i.test(set.value)) {
|
|
870
|
+
const valuePropId = dataItemMap.get(set.value.toUpperCase());
|
|
871
|
+
if (valuePropId) {
|
|
872
|
+
graph.addRelationship({
|
|
873
|
+
id: generateId('ACCESSES', `${callerId}->set-read->${set.value}:L${set.line}`),
|
|
874
|
+
type: 'ACCESSES',
|
|
875
|
+
sourceId: callerId,
|
|
876
|
+
targetId: valuePropId,
|
|
877
|
+
confidence: 0.9,
|
|
878
|
+
reason: 'cobol-set-read',
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// ── INSPECT -> ACCESSES edges ──────────────────
|
|
884
|
+
for (const insp of extracted.inspects) {
|
|
885
|
+
const callerId = scopedCallerLookup(insp.caller, insp.line);
|
|
886
|
+
const inspFieldId = dataItemMap.get(insp.inspectedField.toUpperCase());
|
|
887
|
+
if (inspFieldId) {
|
|
888
|
+
// Read edge (always — INSPECT reads the field)
|
|
889
|
+
graph.addRelationship({
|
|
890
|
+
id: generateId('ACCESSES', `${callerId}->inspect-read->${insp.inspectedField}:L${insp.line}`),
|
|
891
|
+
type: 'ACCESSES',
|
|
892
|
+
sourceId: callerId,
|
|
893
|
+
targetId: inspFieldId,
|
|
894
|
+
confidence: 0.9,
|
|
895
|
+
reason: 'cobol-inspect-read',
|
|
896
|
+
});
|
|
897
|
+
// Write edge (if REPLACING or CONVERTING — modifies the field in-place)
|
|
898
|
+
if (insp.form !== 'tallying') {
|
|
899
|
+
graph.addRelationship({
|
|
900
|
+
id: generateId('ACCESSES', `${callerId}->inspect-write->${insp.inspectedField}:L${insp.line}`),
|
|
901
|
+
type: 'ACCESSES',
|
|
902
|
+
sourceId: callerId,
|
|
903
|
+
targetId: inspFieldId,
|
|
904
|
+
confidence: 0.9,
|
|
905
|
+
reason: 'cobol-inspect-write',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// Tally counter write edges
|
|
910
|
+
for (const counter of insp.counters) {
|
|
911
|
+
const counterPropId = dataItemMap.get(counter.toUpperCase());
|
|
912
|
+
if (counterPropId) {
|
|
913
|
+
graph.addRelationship({
|
|
914
|
+
id: generateId('ACCESSES', `${callerId}->inspect-tally->${counter}:L${insp.line}`),
|
|
915
|
+
type: 'ACCESSES',
|
|
916
|
+
sourceId: callerId,
|
|
917
|
+
targetId: counterPropId,
|
|
918
|
+
confidence: 0.9,
|
|
919
|
+
reason: 'cobol-inspect-tally',
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// ── INITIALIZE -> ACCESSES write edges ──────────────────
|
|
925
|
+
for (const init of extracted.initializes) {
|
|
926
|
+
const callerId = scopedCallerLookup(init.caller, init.line);
|
|
927
|
+
const targetPropId = dataItemMap.get(init.target.toUpperCase());
|
|
928
|
+
if (targetPropId) {
|
|
929
|
+
graph.addRelationship({
|
|
930
|
+
id: generateId('ACCESSES', `${callerId}->initialize->${init.target}:L${init.line}`),
|
|
931
|
+
type: 'ACCESSES',
|
|
932
|
+
sourceId: callerId,
|
|
933
|
+
targetId: targetPropId,
|
|
934
|
+
confidence: 0.9,
|
|
935
|
+
reason: 'cobol-initialize',
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// ── EXEC DLI (IMS/DB) -> CodeElement + ACCESSES edges ──────────────
|
|
940
|
+
for (const dli of extracted.execDliBlocks) {
|
|
941
|
+
const dliId = generateId('CodeElement', `${filePath}:exec-dli:L${dli.line}`);
|
|
942
|
+
const dliOwner = owningModuleId(dli.line);
|
|
943
|
+
graph.addNode({
|
|
944
|
+
id: dliId,
|
|
945
|
+
label: 'CodeElement',
|
|
946
|
+
properties: {
|
|
947
|
+
name: `EXEC DLI ${dli.verb}`,
|
|
948
|
+
filePath,
|
|
949
|
+
startLine: dli.line,
|
|
950
|
+
endLine: dli.line,
|
|
951
|
+
language: SupportedLanguages.Cobol,
|
|
952
|
+
description: [
|
|
953
|
+
dli.segmentName && `segment:${dli.segmentName}`,
|
|
954
|
+
dli.pcbNumber !== undefined && `pcb:${dli.pcbNumber}`,
|
|
955
|
+
dli.psbName && `psb:${dli.psbName}`,
|
|
956
|
+
].filter(Boolean).join(' ') || undefined,
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
graph.addRelationship({
|
|
960
|
+
id: generateId('CONTAINS', `${dliOwner}->${dliId}`),
|
|
961
|
+
type: 'CONTAINS',
|
|
962
|
+
sourceId: dliOwner,
|
|
963
|
+
targetId: dliId,
|
|
964
|
+
confidence: 1.0,
|
|
965
|
+
reason: 'cobol-exec-dli',
|
|
966
|
+
});
|
|
967
|
+
// ACCESSES edge to IMS segment (like SQL table)
|
|
968
|
+
if (dli.segmentName) {
|
|
969
|
+
const segId = generateId('Record', `<ims>:${dli.segmentName}`);
|
|
970
|
+
graph.addRelationship({
|
|
971
|
+
id: generateId('ACCESSES', `${dliId}->${dli.segmentName}:${dli.verb}`),
|
|
972
|
+
type: 'ACCESSES',
|
|
973
|
+
sourceId: dliId,
|
|
974
|
+
targetId: segId,
|
|
975
|
+
confidence: 0.9,
|
|
976
|
+
reason: `dli-${dli.verb.toLowerCase()}`,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
// ACCESSES to INTO/FROM data areas
|
|
980
|
+
if (dli.intoField) {
|
|
981
|
+
const intoPropId = dataItemMap.get(dli.intoField.toUpperCase());
|
|
982
|
+
if (intoPropId) {
|
|
983
|
+
graph.addRelationship({
|
|
984
|
+
id: generateId('ACCESSES', `${dliId}->into->${dli.intoField}:L${dli.line}`),
|
|
985
|
+
type: 'ACCESSES',
|
|
986
|
+
sourceId: dliId,
|
|
987
|
+
targetId: intoPropId,
|
|
988
|
+
confidence: 0.9,
|
|
989
|
+
reason: 'dli-into',
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (dli.fromField) {
|
|
994
|
+
const fromPropId = dataItemMap.get(dli.fromField.toUpperCase());
|
|
995
|
+
if (fromPropId) {
|
|
996
|
+
graph.addRelationship({
|
|
997
|
+
id: generateId('ACCESSES', `${dliId}->from->${dli.fromField}:L${dli.line}`),
|
|
998
|
+
type: 'ACCESSES',
|
|
999
|
+
sourceId: dliId,
|
|
1000
|
+
targetId: fromPropId,
|
|
1001
|
+
confidence: 0.9,
|
|
1002
|
+
reason: 'dli-from',
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// ── MOVE data flow -> ACCESSES edges (read/write) ──────────────
|
|
1008
|
+
for (const move of extracted.moves) {
|
|
1009
|
+
const fromPropId = dataItemMap.get(move.from.toUpperCase());
|
|
1010
|
+
const callerId = scopedCallerLookup(move.caller, move.line);
|
|
1011
|
+
// One read edge per MOVE (regardless of number of targets)
|
|
1012
|
+
if (fromPropId) {
|
|
1013
|
+
graph.addRelationship({
|
|
1014
|
+
id: generateId('ACCESSES', `${callerId}->read->${move.from}:L${move.line}`),
|
|
1015
|
+
type: 'ACCESSES',
|
|
1016
|
+
sourceId: callerId,
|
|
1017
|
+
targetId: fromPropId,
|
|
1018
|
+
confidence: 0.9,
|
|
1019
|
+
reason: move.corresponding ? 'cobol-move-corresponding-read' : 'cobol-move-read',
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
// One write edge per target
|
|
1023
|
+
for (const target of move.targets) {
|
|
1024
|
+
const toPropId = dataItemMap.get(target.toUpperCase());
|
|
1025
|
+
if (toPropId) {
|
|
1026
|
+
graph.addRelationship({
|
|
1027
|
+
id: generateId('ACCESSES', `${callerId}->write->${target}:L${move.line}`),
|
|
1028
|
+
type: 'ACCESSES',
|
|
1029
|
+
sourceId: callerId,
|
|
1030
|
+
targetId: toPropId,
|
|
1031
|
+
confidence: 0.9,
|
|
1032
|
+
reason: move.corresponding ? 'cobol-move-corresponding-write' : 'cobol-move-write',
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// ── File declarations -> Record nodes ──────────────────────────
|
|
1038
|
+
for (const fd of extracted.fileDeclarations) {
|
|
1039
|
+
const fdId = generateId('Record', `${filePath}:${fd.selectName}`);
|
|
1040
|
+
graph.addNode({
|
|
1041
|
+
id: fdId,
|
|
1042
|
+
label: 'Record',
|
|
1043
|
+
properties: {
|
|
1044
|
+
name: fd.selectName,
|
|
1045
|
+
filePath,
|
|
1046
|
+
startLine: fd.line,
|
|
1047
|
+
endLine: fd.line,
|
|
1048
|
+
language: SupportedLanguages.Cobol,
|
|
1049
|
+
description: `assign:${fd.assignTo}${fd.isOptional ? ' optional' : ''}${fd.organization ? ` org:${fd.organization}` : ''}${fd.access ? ` access:${fd.access}` : ''}`,
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
const fdOwner = owningModuleId(fd.line);
|
|
1053
|
+
graph.addRelationship({
|
|
1054
|
+
id: generateId('CONTAINS', `${fdOwner}->${fdId}`),
|
|
1055
|
+
type: 'CONTAINS',
|
|
1056
|
+
sourceId: fdOwner,
|
|
1057
|
+
targetId: fdId,
|
|
1058
|
+
confidence: 1.0,
|
|
1059
|
+
reason: 'cobol-file-declaration',
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
// ── GO TO -> CALLS edges ──────────────────────────────────────
|
|
1063
|
+
for (const gt of extracted.gotos) {
|
|
1064
|
+
const callerId = scopedCallerLookup(gt.caller, gt.line);
|
|
1065
|
+
const targetId = scopedParaLookup(gt.target, gt.line);
|
|
1066
|
+
if (targetId) {
|
|
1067
|
+
graph.addRelationship({
|
|
1068
|
+
id: generateId('CALLS', `${callerId}->goto->${gt.target}:L${gt.line}`),
|
|
1069
|
+
type: 'CALLS',
|
|
1070
|
+
sourceId: callerId,
|
|
1071
|
+
targetId,
|
|
1072
|
+
confidence: 1.0,
|
|
1073
|
+
reason: 'cobol-goto',
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// ── SORT/MERGE -> ACCESSES edges ──────────────────────────────
|
|
1078
|
+
for (const sort of extracted.sorts) {
|
|
1079
|
+
const sortFileId = generateId('Record', `${filePath}:${sort.sortFile}`);
|
|
1080
|
+
const sortOwner = owningModuleId(sort.line);
|
|
1081
|
+
for (const usingFile of sort.usingFiles) {
|
|
1082
|
+
const usingId = generateId('Record', `${filePath}:${usingFile}`);
|
|
1083
|
+
graph.addRelationship({
|
|
1084
|
+
id: generateId('ACCESSES', `${sortOwner}->sort-using->${usingFile}:L${sort.line}`),
|
|
1085
|
+
type: 'ACCESSES',
|
|
1086
|
+
sourceId: sortFileId,
|
|
1087
|
+
targetId: usingId,
|
|
1088
|
+
confidence: 0.85,
|
|
1089
|
+
reason: 'sort-using',
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
for (const givingFile of sort.givingFiles) {
|
|
1093
|
+
const givingId = generateId('Record', `${filePath}:${givingFile}`);
|
|
1094
|
+
graph.addRelationship({
|
|
1095
|
+
id: generateId('ACCESSES', `${sortOwner}->sort-giving->${givingFile}:L${sort.line}`),
|
|
1096
|
+
type: 'ACCESSES',
|
|
1097
|
+
sourceId: sortFileId,
|
|
1098
|
+
targetId: givingId,
|
|
1099
|
+
confidence: 0.85,
|
|
1100
|
+
reason: 'sort-giving',
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// ── SEARCH -> ACCESSES edges ──────────────────────────────────
|
|
1105
|
+
for (const search of extracted.searches) {
|
|
1106
|
+
const targetPropId = dataItemMap.get(search.target.toUpperCase());
|
|
1107
|
+
if (targetPropId) {
|
|
1108
|
+
const searchOwner = owningModuleId(search.line);
|
|
1109
|
+
graph.addRelationship({
|
|
1110
|
+
id: generateId('ACCESSES', `${searchOwner}->search->${search.target}:L${search.line}`),
|
|
1111
|
+
type: 'ACCESSES',
|
|
1112
|
+
sourceId: searchOwner,
|
|
1113
|
+
targetId: targetPropId,
|
|
1114
|
+
confidence: 0.9,
|
|
1115
|
+
reason: 'cobol-search',
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// ── CANCEL -> CALLS edges (with two-pass resolution like CALL) ──
|
|
1120
|
+
for (const cancel of extracted.cancels) {
|
|
1121
|
+
if (!cancel.isQuoted) {
|
|
1122
|
+
// Dynamic CANCEL via data item — annotate, don't resolve
|
|
1123
|
+
graph.addNode({
|
|
1124
|
+
id: generateId('CodeElement', `${filePath}:dynamic-cancel:${cancel.target}:L${cancel.line}`),
|
|
1125
|
+
label: 'CodeElement',
|
|
1126
|
+
properties: {
|
|
1127
|
+
name: `CANCEL ${cancel.target}`,
|
|
1128
|
+
filePath, startLine: cancel.line, endLine: cancel.line,
|
|
1129
|
+
language: SupportedLanguages.Cobol,
|
|
1130
|
+
description: 'dynamic-cancel (target is a data item, not resolvable statically)',
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
const cancelOwner = owningModuleId(cancel.line);
|
|
1134
|
+
graph.addRelationship({
|
|
1135
|
+
id: generateId('CONTAINS', `${cancelOwner}->dynamic-cancel:${cancel.target}:L${cancel.line}`),
|
|
1136
|
+
type: 'CONTAINS', sourceId: cancelOwner,
|
|
1137
|
+
targetId: generateId('CodeElement', `${filePath}:dynamic-cancel:${cancel.target}:L${cancel.line}`),
|
|
1138
|
+
confidence: 1.0, reason: 'cobol-dynamic-cancel',
|
|
1139
|
+
});
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const targetModuleId = moduleNodeIds.get(cancel.target.toUpperCase());
|
|
1143
|
+
const targetId = targetModuleId
|
|
1144
|
+
?? generateId('Module', `<unresolved>:${cancel.target.toUpperCase()}`);
|
|
1145
|
+
const cancelCallOwner = owningModuleId(cancel.line);
|
|
1146
|
+
graph.addRelationship({
|
|
1147
|
+
id: generateId('CALLS', `${cancelCallOwner}->cancel->${cancel.target}:L${cancel.line}`),
|
|
1148
|
+
type: 'CALLS',
|
|
1149
|
+
sourceId: cancelCallOwner,
|
|
1150
|
+
targetId,
|
|
1151
|
+
confidence: targetModuleId ? 0.9 : 0.5,
|
|
1152
|
+
reason: targetModuleId ? 'cobol-cancel' : 'cobol-cancel-unresolved',
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
// ---------------------------------------------------------------------------
|
|
1157
|
+
// Helpers
|
|
1158
|
+
// ---------------------------------------------------------------------------
|
|
1159
|
+
/** Find the enclosing program name for a given line number (innermost wins). */
|
|
1160
|
+
function findOwningProgramName(lineNum, programs) {
|
|
1161
|
+
let best;
|
|
1162
|
+
for (const p of programs) {
|
|
1163
|
+
if (p.startLine <= lineNum && p.endLine >= lineNum) {
|
|
1164
|
+
if (!best || p.nestingDepth > best.nestingDepth)
|
|
1165
|
+
best = p;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return best?.name;
|
|
1169
|
+
}
|
|
1170
|
+
/** Find the section that contains a given line number. */
|
|
1171
|
+
function findContainingSection(line, sections, sectionNodeIds, programs) {
|
|
1172
|
+
const pgm = findOwningProgramName(line, programs);
|
|
1173
|
+
// Sections are in order; find the last section whose start line <= the target line
|
|
1174
|
+
let best;
|
|
1175
|
+
for (const sec of sections) {
|
|
1176
|
+
if (sec.line <= line) {
|
|
1177
|
+
const resolved = sectionNodeIds.get(`${pgm ?? ''}:${sec.name.toUpperCase()}`);
|
|
1178
|
+
if (resolved)
|
|
1179
|
+
best = resolved; // only update if lookup succeeds
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return best;
|
|
1186
|
+
}
|