gitnexus 1.6.12-rc.25 → 1.6.12-rc.26

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.
@@ -6,8 +6,8 @@
6
6
  * does its own extraction, and writes directly to the graph.
7
7
  *
8
8
  * Pipeline:
9
- * 1. Separate programs from copybooks
10
- * 2. Build copybook map (name -> content)
9
+ * 1. Separate programs from copybooks and JCL
10
+ * 2. Index file content by path for COPY expansion
11
11
  * 3. For each program: expand COPY statements, then run regex extraction
12
12
  * 4. Map CobolRegexResults to graph nodes and relationships
13
13
  * 5. Optionally process JCL files for job-step cross-references
@@ -6,8 +6,8 @@
6
6
  * does its own extraction, and writes directly to the graph.
7
7
  *
8
8
  * Pipeline:
9
- * 1. Separate programs from copybooks
10
- * 2. Build copybook map (name -> content)
9
+ * 1. Separate programs from copybooks and JCL
10
+ * 2. Index file content by path for COPY expansion
11
11
  * 3. For each program: expand COPY statements, then run regex extraction
12
12
  * 4. Map CobolRegexResults to graph nodes and relationships
13
13
  * 5. Optionally process JCL files for job-step cross-references
@@ -19,6 +19,7 @@ import { SupportedLanguages } from '../../_shared/index.js';
19
19
  import { preprocessCobolSource, extractCobolSymbolsWithRegex, } from './cobol/cobol-preprocessor.js';
20
20
  import { expandCopies } from './cobol/cobol-copy-expander.js';
21
21
  import { processJclFiles } from './cobol/jcl-processor.js';
22
+ import { resolveCobolCopyTarget } from './languages/cobol/copy-target.js';
22
23
  import { logger } from '../logger.js';
23
24
  // ---------------------------------------------------------------------------
24
25
  // File detection
@@ -74,36 +75,22 @@ export const processCobol = (graph, files, allPathSet) => {
74
75
  };
75
76
  // ── 1. Separate programs, copybooks, and JCL ───────────────────────
76
77
  const programs = [];
77
- const copybooks = [];
78
78
  const jclFiles = [];
79
79
  for (const file of files) {
80
80
  const ext = path.extname(file.path).toLowerCase();
81
81
  if (JCL_EXTENSIONS.has(ext)) {
82
82
  jclFiles.push(file);
83
83
  }
84
- else if (isCopybook(file.path)) {
85
- copybooks.push(file);
86
- }
87
- else if (COBOL_EXTENSIONS.has(ext)) {
84
+ else if (COBOL_EXTENSIONS.has(ext) && !isCopybook(file.path)) {
88
85
  programs.push(file);
89
86
  }
90
87
  }
91
- // ── 2. Build copybook map (uppercase name -> content) ──────────────
92
- const copybookMap = new Map();
93
- for (const cb of copybooks) {
94
- const name = path.basename(cb.path, path.extname(cb.path)).toUpperCase();
95
- copybookMap.set(name, { content: cb.content, path: cb.path });
96
- }
97
- // Build reverse lookup: path -> content for O(1) readCopy
88
+ // Path → content for every COBOL/JCL file we ingested. Resolution picks
89
+ // the path (#2967 / census lockstep); this map only supplies the body.
98
90
  const copybookByPath = new Map();
99
- for (const [, entry] of copybookMap) {
100
- copybookByPath.set(entry.path, entry.content);
91
+ for (const f of files) {
92
+ copybookByPath.set(f.path, f.content);
101
93
  }
102
- // Resolve and read callbacks for expandCopies
103
- const resolveCopy = (name) => {
104
- const entry = copybookMap.get(name.toUpperCase());
105
- return entry ? entry.path : null;
106
- };
107
94
  // Memoize preprocessed copybook content for the duration of this
108
95
  // processCobol call. A single copybook is COPYed by many programs (and at
109
96
  // many COPY sites within a program); without this cache
@@ -142,6 +129,9 @@ export const processCobol = (graph, files, allPathSet) => {
142
129
  continue;
143
130
  // Preprocess: clean patch markers
144
131
  const cleaned = preprocessCobolSource(file.content);
132
+ // Per-program so COPY EXTERNAL in src/PROG.cbl cannot land on
133
+ // vendor/EXTERNAL.cpy when a copybooks/ dir is present (#2967).
134
+ const resolveCopy = (name) => resolveCobolCopyTarget(name, file.path, allPathSet);
145
135
  // Expand COPY statements
146
136
  const { expandedContent, copyResolutions } = expandCopies(cleaned, file.path, resolveCopy, readCopy);
147
137
  // Extract symbols from expanded source
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolve a COBOL `COPY` member name against the workspace file set.
3
+ *
4
+ * `fromFile` is the importing program (census) or the program currently
5
+ * being expanded (processor). It is used only to include the importer's
6
+ * directory in the preferred class when a well-known copybook dir exists.
7
+ */
8
+ export declare function resolveCobolCopyTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>): string | null;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shared COBOL `COPY` target resolution (#2967 / #2908).
3
+ *
4
+ * Both the census (`cobolScopeResolver.resolveImportTarget`) and the live
5
+ * regex processor (`resolveCopy` → `cobol-copy` IMPORTS) must answer the
6
+ * same path for the same `(name, fromFile, allFilePaths)`. Greening only
7
+ * the census would leave analyze fabricating an IMPORTS edge onto a vendor
8
+ * decoy the compiler would never have searched.
9
+ *
10
+ * Well-known copybook directory segments (`copybooks`, `COPYBOOKS`, `cpy`,
11
+ * `copy`) plus the importer's own directory are the preferred class. When
12
+ * the file set contains at least one of those well-known segments, a `COPY`
13
+ * name resolves only inside that class (first in Set-iteration order).
14
+ * `vendor/EXTERNAL.cpy` therefore misses while `copybooks/CUSTREC.cpy` hits.
15
+ *
16
+ * When the file set has no such directory, fail-open to today's two-tier
17
+ * first-wins basename index — shops whose copybooks *are* the tree have
18
+ * nothing to prefer. The importer directory is *not* enough on its own to
19
+ * leave fail-open (`{vendor/EXTERNAL.cpy, src/PROG.cbl}` must still answer
20
+ * `vendor/EXTERNAL.cpy`).
21
+ *
22
+ * The two-tier index is still memoized on the `allFilePaths` Set identity
23
+ * (#2908). Preferred-class filtering happens at pick time from the stored
24
+ * per-name arrays, so a pass still scans the set once.
25
+ */
26
+ import path from 'node:path';
27
+ import { perFileSet } from '../../import-resolvers/per-file-set.js';
28
+ const COPYBOOK_EXTENSIONS = new Set(['.cpy', '.copybook']);
29
+ const COBOL_SOURCE_EXTENSIONS = new Set(['.cbl', '.cob', '.cobol']);
30
+ /** Path-component names that mark a conventional copybook directory. */
31
+ const PREFERRED_DIR_NAMES = new Set(['copybooks', 'COPYBOOKS', 'cpy', 'copy']);
32
+ function pathHasPreferredDir(fp) {
33
+ const parts = fp.split('/');
34
+ for (let i = 0; i < parts.length - 1; i++) {
35
+ if (PREFERRED_DIR_NAMES.has(parts[i]))
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ function pushUnique(map, key, fp) {
41
+ const list = map.get(key);
42
+ if (list === undefined) {
43
+ map.set(key, [fp]);
44
+ return;
45
+ }
46
+ list.push(fp);
47
+ }
48
+ const getCobolCopyIndex = perFileSet((allFilePaths) => {
49
+ const copybooks = new Map();
50
+ const sources = new Map();
51
+ let hasPreferredDir = false;
52
+ for (const fp of allFilePaths) {
53
+ const extRaw = path.extname(fp);
54
+ const extLower = extRaw.toLowerCase();
55
+ const tier = COPYBOOK_EXTENSIONS.has(extLower)
56
+ ? copybooks
57
+ : COBOL_SOURCE_EXTENSIONS.has(extLower)
58
+ ? sources
59
+ : undefined;
60
+ if (tier === undefined)
61
+ continue;
62
+ if (!hasPreferredDir && pathHasPreferredDir(fp))
63
+ hasPreferredDir = true;
64
+ const basename = path.basename(fp, extRaw).toUpperCase();
65
+ pushUnique(tier, basename, fp);
66
+ }
67
+ return { copybooks, sources, hasPreferredDir };
68
+ });
69
+ function pickCopyPath(paths, fromFile, hasPreferredDir) {
70
+ if (paths === undefined || paths.length === 0)
71
+ return null;
72
+ if (!hasPreferredDir)
73
+ return paths[0] ?? null;
74
+ const fromDir = path.dirname(fromFile);
75
+ for (const fp of paths) {
76
+ if (pathHasPreferredDir(fp) || path.dirname(fp) === fromDir)
77
+ return fp;
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * Resolve a COBOL `COPY` member name against the workspace file set.
83
+ *
84
+ * `fromFile` is the importing program (census) or the program currently
85
+ * being expanded (processor). It is used only to include the importer's
86
+ * directory in the preferred class when a well-known copybook dir exists.
87
+ */
88
+ export function resolveCobolCopyTarget(targetRaw, fromFile, allFilePaths) {
89
+ const upper = targetRaw.toUpperCase();
90
+ const index = getCobolCopyIndex(allFilePaths);
91
+ return (pickCopyPath(index.copybooks.get(upper), fromFile, index.hasPreferredDir) ??
92
+ pickCopyPath(index.sources.get(upper), fromFile, index.hasPreferredDir) ??
93
+ null);
94
+ }
@@ -8,46 +8,18 @@
8
8
  *
9
9
  * Reference: `languages/python/scope-resolver.ts`.
10
10
  */
11
- import path from 'node:path';
12
11
  import { SupportedLanguages } from '../../../../_shared/index.js';
13
- import { perFileSet } from '../../import-resolvers/per-file-set.js';
14
12
  import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
15
13
  import { cobolProvider } from '../cobol.js';
16
- // Copybook file extensions for COPY name resolution
17
- const COPYBOOK_EXTENSIONS = new Set(['.cpy', '.copybook']);
18
- // COBOL source files, searched only after every copybook has missed.
19
- const COBOL_SOURCE_EXTENSIONS = new Set(['.cbl', '.cob', '.cobol']);
20
- const getCobolCopyIndex = perFileSet((allFilePaths) => {
21
- const copybooks = new Map();
22
- const sources = new Map();
23
- // One pass builds both tiers: the two scans walked the same files and
24
- // classified each by the same extension test.
25
- for (const fp of allFilePaths) {
26
- const ext = path.extname(fp).toLowerCase();
27
- const tier = COPYBOOK_EXTENSIONS.has(ext)
28
- ? copybooks
29
- : COBOL_SOURCE_EXTENSIONS.has(ext)
30
- ? sources
31
- : undefined;
32
- if (tier === undefined)
33
- continue;
34
- const basename = path.basename(fp, ext).toUpperCase();
35
- // First in Set-iteration order wins, as the scans' first-match `return` did.
36
- if (!tier.has(basename))
37
- tier.set(basename, fp);
38
- }
39
- return { copybooks, sources };
40
- });
14
+ import { resolveCobolCopyTarget } from './copy-target.js';
41
15
  const cobolScopeResolver = {
42
16
  language: SupportedLanguages.Cobol,
43
17
  languageProvider: cobolProvider,
44
18
  importEdgeReason: 'cobol-scope: copy',
45
19
  // ── Resolve COPY bookname to file path ─────────────────────────────
46
- resolveImportTarget: (targetRaw, _fromFile, allFilePaths) => {
47
- const upper = targetRaw.toUpperCase();
48
- const index = getCobolCopyIndex(allFilePaths);
49
- // Copybooks first, then COBOL sources — the tier order IS the tie-break.
50
- return index.copybooks.get(upper) ?? index.sources.get(upper) ?? null;
20
+ // Shared with the regex processor's resolveCopy (#2967 lockstep).
21
+ resolveImportTarget: (targetRaw, fromFile, allFilePaths) => {
22
+ return resolveCobolCopyTarget(targetRaw, fromFile, allFilePaths);
51
23
  },
52
24
  // COBOL has no binding-merge rules beyond the default (local-first-then-imports).
53
25
  mergeBindings: (existing) => [...existing],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.12-rc.25",
3
+ "version": "1.6.12-rc.26",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",