gitnexus 1.6.12-rc.24 → 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.
- package/dist/core/ingestion/cobol-processor.d.ts +2 -2
- package/dist/core/ingestion/cobol-processor.js +11 -21
- package/dist/core/ingestion/community-processor.js +24 -5
- package/dist/core/ingestion/languages/cobol/copy-target.d.ts +8 -0
- package/dist/core/ingestion/languages/cobol/copy-target.js +94 -0
- package/dist/core/ingestion/languages/cobol/scope-resolver.js +4 -32
- package/dist/core/ingestion/process-processor.js +16 -5
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
//
|
|
92
|
-
|
|
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
|
|
100
|
-
copybookByPath.set(
|
|
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
|
|
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { dirname, resolve } from 'node:path';
|
|
17
17
|
import { Worker } from 'node:worker_threads';
|
|
18
|
+
import { isHeuristicEdgeReason } from '../graph/edge-reasons.js';
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = dirname(__filename);
|
|
20
21
|
// Navigate to package root (works from both src/ and dist/)
|
|
@@ -49,6 +50,16 @@ const DEFAULT_COMMUNITY_ENGINE = 'graphology';
|
|
|
49
50
|
const LEIDEN_TIMEOUT_MS = 60_000;
|
|
50
51
|
const ICEBUG_TIMEOUT_MS = 60_000;
|
|
51
52
|
const MIN_CONFIDENCE_LARGE = 0.5;
|
|
53
|
+
/**
|
|
54
|
+
* Whether a large graph's projection may include this edge.
|
|
55
|
+
*
|
|
56
|
+
* The confidence floor alone lets every global-name-fallback edge through — it
|
|
57
|
+
* is emitted at exactly `MIN_CONFIDENCE_LARGE`, so `confidence <` never
|
|
58
|
+
* excludes it. Clustering on unique-name guesses fuses unrelated areas into one
|
|
59
|
+
* community, which is precisely the noise the large-graph floor exists to
|
|
60
|
+
* remove, so the reason is checked too — see `graph/edge-reasons.ts`.
|
|
61
|
+
*/
|
|
62
|
+
const isLargeGraphEligible = (confidence, reason) => confidence >= MIN_CONFIDENCE_LARGE && !isHeuristicEdgeReason(reason);
|
|
52
63
|
export const resolveCommunityDetectionEngine = (raw = process.env[COMMUNITY_ENGINE_ENV]) => {
|
|
53
64
|
if (raw === undefined || raw.trim() === '')
|
|
54
65
|
return DEFAULT_COMMUNITY_ENGINE;
|
|
@@ -153,12 +164,18 @@ export const buildCommunityProjection = (knowledgeGraph) => {
|
|
|
153
164
|
const isLarge = symbolCount > 10_000;
|
|
154
165
|
const connectedNodes = new Set();
|
|
155
166
|
const nodeDegree = new Map();
|
|
156
|
-
// Field-wise scan (#2680): this walks every edge and reads only these
|
|
167
|
+
// Field-wise scan (#2680): this walks every edge and reads only these five,
|
|
157
168
|
// so taking objects would allocate one per edge for nothing.
|
|
158
|
-
knowledgeGraph.forEachRelationshipFields((sourceId, targetId, type, confidence) => {
|
|
169
|
+
knowledgeGraph.forEachRelationshipFields((sourceId, targetId, type, confidence, reason) => {
|
|
159
170
|
if (!isClusteringRelationship(type) || sourceId === targetId)
|
|
160
171
|
return;
|
|
161
|
-
|
|
172
|
+
// A name-guessed edge must not join two nodes into a community at ANY graph
|
|
173
|
+
// size — the rationale in `graph/edge-reasons.ts` is about what the edge
|
|
174
|
+
// claims, not about how many symbols surround it. Process tracing applies
|
|
175
|
+
// the same reason gate unconditionally (`process-processor.ts`).
|
|
176
|
+
if (isHeuristicEdgeReason(reason))
|
|
177
|
+
return;
|
|
178
|
+
if (isLarge && !isLargeGraphEligible(confidence, reason))
|
|
162
179
|
return;
|
|
163
180
|
connectedNodes.add(sourceId);
|
|
164
181
|
connectedNodes.add(targetId);
|
|
@@ -189,10 +206,12 @@ export const buildCommunityProjection = (knowledgeGraph) => {
|
|
|
189
206
|
}
|
|
190
207
|
const seenEdges = new Set();
|
|
191
208
|
const edges = [];
|
|
192
|
-
knowledgeGraph.forEachRelationshipFields((sourceId, targetId, type, confidence) => {
|
|
209
|
+
knowledgeGraph.forEachRelationshipFields((sourceId, targetId, type, confidence, reason) => {
|
|
193
210
|
if (!isClusteringRelationship(type) || sourceId === targetId)
|
|
194
211
|
return;
|
|
195
|
-
if (
|
|
212
|
+
if (isHeuristicEdgeReason(reason))
|
|
213
|
+
return;
|
|
214
|
+
if (isLarge && !isLargeGraphEligible(confidence, reason))
|
|
196
215
|
return;
|
|
197
216
|
const sourceIndex = nodeIndexById.get(sourceId);
|
|
198
217
|
const targetIndex = nodeIndexById.get(targetId);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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],
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Processes help agents understand how features work through the codebase.
|
|
11
11
|
*/
|
|
12
|
+
import { isHeuristicEdgeReason } from '../graph/edge-reasons.js';
|
|
12
13
|
import { calculateEntryPointScore, isTestFile } from './entry-point-scoring.js';
|
|
13
14
|
import { SupportedLanguages } from '../../_shared/index.js';
|
|
14
15
|
import { isDev } from './utils/env.js';
|
|
@@ -289,11 +290,21 @@ outwardActionSites = []) => {
|
|
|
289
290
|
* traces to jump across unrelated code areas.
|
|
290
291
|
*/
|
|
291
292
|
const MIN_TRACE_CONFIDENCE = 0.5;
|
|
293
|
+
/**
|
|
294
|
+
* True when an edge may seed or extend a traced flow.
|
|
295
|
+
*
|
|
296
|
+
* The confidence floor alone is not sufficient: the global-name fallback emits
|
|
297
|
+
* at exactly `MIN_TRACE_CONFIDENCE`, so a `<` comparison admits every one of
|
|
298
|
+
* its guesses. A flow assembled from name guesses reads as a real execution
|
|
299
|
+
* path through code that may never call each other, so the reason is checked
|
|
300
|
+
* too — see `graph/edge-reasons.ts`.
|
|
301
|
+
*/
|
|
302
|
+
const isTraceableCallsEdge = (type, confidence, reason) => type === 'CALLS' && confidence >= MIN_TRACE_CONFIDENCE && !isHeuristicEdgeReason(reason);
|
|
292
303
|
const buildCallsGraph = (graph) => {
|
|
293
304
|
const adj = new Map();
|
|
294
|
-
// Field-wise scan (#2680) — whole-graph walk,
|
|
295
|
-
graph.forEachRelationshipFields((sourceId, targetId, type, confidence) => {
|
|
296
|
-
if (type
|
|
305
|
+
// Field-wise scan (#2680) — whole-graph walk, five fields, no object needed.
|
|
306
|
+
graph.forEachRelationshipFields((sourceId, targetId, type, confidence, reason) => {
|
|
307
|
+
if (!isTraceableCallsEdge(type, confidence, reason))
|
|
297
308
|
return;
|
|
298
309
|
const existing = adj.get(sourceId);
|
|
299
310
|
if (existing === undefined)
|
|
@@ -305,8 +316,8 @@ const buildCallsGraph = (graph) => {
|
|
|
305
316
|
};
|
|
306
317
|
const buildReverseCallsGraph = (graph) => {
|
|
307
318
|
const adj = new Map();
|
|
308
|
-
graph.forEachRelationshipFields((sourceId, targetId, type, confidence) => {
|
|
309
|
-
if (type
|
|
319
|
+
graph.forEachRelationshipFields((sourceId, targetId, type, confidence, reason) => {
|
|
320
|
+
if (!isTraceableCallsEdge(type, confidence, reason))
|
|
310
321
|
return;
|
|
311
322
|
const existing = adj.get(targetId);
|
|
312
323
|
if (existing === undefined)
|
package/package.json
CHANGED