gitnexus 1.4.8 → 1.4.10

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.
Files changed (211) hide show
  1. package/README.md +7 -0
  2. package/dist/cli/index-repo.d.ts +15 -0
  3. package/dist/cli/index-repo.js +115 -0
  4. package/dist/cli/index.js +11 -2
  5. package/dist/cli/setup.js +12 -9
  6. package/dist/cli/wiki.d.ts +4 -0
  7. package/dist/cli/wiki.js +174 -53
  8. package/dist/config/supported-languages.d.ts +7 -5
  9. package/dist/config/supported-languages.js +6 -4
  10. package/dist/core/graph/graph.js +9 -1
  11. package/dist/core/graph/types.d.ts +10 -2
  12. package/dist/core/ingestion/call-processor.d.ts +18 -1
  13. package/dist/core/ingestion/call-processor.js +297 -38
  14. package/dist/core/ingestion/call-routing.d.ts +3 -18
  15. package/dist/core/ingestion/call-routing.js +0 -19
  16. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  17. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  18. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  19. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  20. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  21. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  22. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  23. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  24. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  25. package/dist/core/ingestion/cobol-processor.js +1186 -0
  26. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +18 -4
  28. package/dist/core/ingestion/export-detection.d.ts +47 -8
  29. package/dist/core/ingestion/export-detection.js +29 -50
  30. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  31. package/dist/core/ingestion/field-extractor.js +25 -0
  32. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  33. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  34. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  35. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  36. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  37. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  38. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  39. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  40. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  41. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  42. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  43. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  44. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  45. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  46. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  47. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  48. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  49. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  50. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  51. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  52. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  53. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  54. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  56. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  57. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  58. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  59. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  60. package/dist/core/ingestion/field-types.d.ts +59 -0
  61. package/dist/core/ingestion/field-types.js +2 -0
  62. package/dist/core/ingestion/framework-detection.d.ts +87 -0
  63. package/dist/core/ingestion/framework-detection.js +65 -2
  64. package/dist/core/ingestion/heritage-processor.js +15 -17
  65. package/dist/core/ingestion/import-processor.d.ts +9 -10
  66. package/dist/core/ingestion/import-processor.js +59 -14
  67. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  68. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  69. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  70. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  71. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  72. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  73. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +9 -1
  74. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.js +56 -0
  75. package/dist/core/ingestion/{resolvers → import-resolvers}/php.d.ts +6 -10
  76. package/dist/core/ingestion/{resolvers → import-resolvers}/php.js +7 -2
  77. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  78. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  81. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  82. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  85. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  86. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  87. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  88. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +0 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +0 -9
  91. package/dist/core/ingestion/language-config.d.ts +4 -1
  92. package/dist/core/ingestion/language-provider.d.ts +121 -0
  93. package/dist/core/ingestion/language-provider.js +24 -0
  94. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  95. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  96. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  97. package/dist/core/ingestion/languages/cobol.js +26 -0
  98. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  99. package/dist/core/ingestion/languages/csharp.js +49 -0
  100. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  101. package/dist/core/ingestion/languages/dart.js +58 -0
  102. package/dist/core/ingestion/languages/go.d.ts +11 -0
  103. package/dist/core/ingestion/languages/go.js +28 -0
  104. package/dist/core/ingestion/languages/index.d.ts +38 -0
  105. package/dist/core/ingestion/languages/index.js +63 -0
  106. package/dist/core/ingestion/languages/java.d.ts +9 -0
  107. package/dist/core/ingestion/languages/java.js +29 -0
  108. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  109. package/dist/core/ingestion/languages/kotlin.js +53 -0
  110. package/dist/core/ingestion/languages/php.d.ts +8 -0
  111. package/dist/core/ingestion/languages/php.js +145 -0
  112. package/dist/core/ingestion/languages/python.d.ts +12 -0
  113. package/dist/core/ingestion/languages/python.js +39 -0
  114. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  115. package/dist/core/ingestion/languages/ruby.js +44 -0
  116. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  117. package/dist/core/ingestion/languages/rust.js +44 -0
  118. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  119. package/dist/core/ingestion/languages/swift.js +133 -0
  120. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  121. package/dist/core/ingestion/languages/typescript.js +60 -0
  122. package/dist/core/ingestion/mro-processor.js +14 -15
  123. package/dist/core/ingestion/{named-binding-extraction.d.ts → named-binding-processor.d.ts} +0 -9
  124. package/dist/core/ingestion/named-binding-processor.js +42 -0
  125. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  126. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  127. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  128. package/dist/core/ingestion/named-bindings/java.js +29 -0
  129. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  130. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  131. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  132. package/dist/core/ingestion/named-bindings/php.js +61 -0
  133. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  134. package/dist/core/ingestion/named-bindings/python.js +49 -0
  135. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  136. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  137. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  138. package/dist/core/ingestion/named-bindings/types.js +6 -0
  139. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  140. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  141. package/dist/core/ingestion/parsing-processor.d.ts +5 -1
  142. package/dist/core/ingestion/parsing-processor.js +115 -16
  143. package/dist/core/ingestion/pipeline.js +925 -424
  144. package/dist/core/ingestion/resolution-context.js +1 -1
  145. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  146. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  147. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  148. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  149. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  150. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  151. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  152. package/dist/core/ingestion/route-extractors/php.js +21 -0
  153. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  154. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  155. package/dist/core/ingestion/tree-sitter-queries.d.ts +8 -7
  156. package/dist/core/ingestion/tree-sitter-queries.js +231 -9
  157. package/dist/core/ingestion/type-env.d.ts +14 -17
  158. package/dist/core/ingestion/type-env.js +66 -14
  159. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +1 -1
  160. package/dist/core/ingestion/type-extractors/csharp.js +1 -1
  161. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  162. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  163. package/dist/core/ingestion/type-extractors/jvm.js +1 -1
  164. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -13
  165. package/dist/core/ingestion/type-extractors/shared.js +9 -102
  166. package/dist/core/ingestion/type-extractors/swift.js +334 -4
  167. package/dist/core/ingestion/type-extractors/types.d.ts +3 -1
  168. package/dist/core/ingestion/{ast-helpers.d.ts → utils/ast-helpers.d.ts} +16 -13
  169. package/dist/core/ingestion/{ast-helpers.js → utils/ast-helpers.js} +111 -32
  170. package/dist/core/ingestion/{call-analysis.js → utils/call-analysis.js} +37 -0
  171. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  172. package/dist/core/ingestion/utils/event-loop.js +5 -0
  173. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  174. package/dist/core/ingestion/utils/language-detection.js +70 -0
  175. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  176. package/dist/core/ingestion/utils/verbose.js +7 -0
  177. package/dist/core/ingestion/workers/parse-worker.d.ts +43 -2
  178. package/dist/core/ingestion/workers/parse-worker.js +361 -150
  179. package/dist/core/lbug/csv-generator.js +34 -1
  180. package/dist/core/lbug/lbug-adapter.js +6 -0
  181. package/dist/core/lbug/schema.d.ts +5 -3
  182. package/dist/core/lbug/schema.js +39 -2
  183. package/dist/core/tree-sitter/parser-loader.js +7 -1
  184. package/dist/core/wiki/cursor-client.d.ts +31 -0
  185. package/dist/core/wiki/cursor-client.js +127 -0
  186. package/dist/core/wiki/generator.d.ts +28 -9
  187. package/dist/core/wiki/generator.js +115 -18
  188. package/dist/core/wiki/graph-queries.d.ts +4 -0
  189. package/dist/core/wiki/graph-queries.js +7 -1
  190. package/dist/core/wiki/llm-client.d.ts +2 -0
  191. package/dist/core/wiki/llm-client.js +8 -4
  192. package/dist/core/wiki/prompts.d.ts +3 -3
  193. package/dist/core/wiki/prompts.js +6 -0
  194. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  195. package/dist/mcp/core/lbug-adapter.js +11 -1
  196. package/dist/mcp/local/local-backend.d.ts +16 -5
  197. package/dist/mcp/local/local-backend.js +711 -74
  198. package/dist/mcp/tools.js +71 -2
  199. package/dist/storage/repo-manager.d.ts +3 -0
  200. package/package.json +14 -14
  201. package/dist/core/ingestion/import-resolution.d.ts +0 -101
  202. package/dist/core/ingestion/import-resolution.js +0 -251
  203. package/dist/core/ingestion/named-binding-extraction.js +0 -373
  204. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  205. package/dist/core/ingestion/resolvers/index.js +0 -13
  206. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  207. package/dist/core/ingestion/type-extractors/index.js +0 -31
  208. package/dist/core/ingestion/utils.d.ts +0 -20
  209. package/dist/core/ingestion/utils.js +0 -242
  210. package/scripts/patch-tree-sitter-swift.cjs +0 -74
  211. /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
+ }