@geml/geml 1.3.2 → 1.4.2

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.
@@ -1,131 +1,131 @@
1
- // geml-code-graph adapter: Joern CPG export → exchange format (DESIGN §3.2/3.4).
2
- //
3
- // Consumes the raw JSONL written by joern-export.sc (methods.jsonl +
4
- // calls.jsonl) — so the node side never touches the JVM. Everything here is
5
- // resolution:"cpg". Confidence mapping (§3.4):
6
- // exactly one internal callee → high
7
- // several internal callees (dispatch) → first as `to` + the rest as
8
- // `candidates`, medium
9
- // no internal callee (external/pointer) → to_text, low (unresolved)
10
- import { readFileSync } from "node:fs";
11
- import { join } from "node:path";
12
-
13
- const LANG_BY_EXT = {
14
- c: "c", h: "c", cc: "cpp", cpp: "cpp", cxx: "cpp", hpp: "cpp", hh: "cpp",
15
- java: "java", js: "javascript", mjs: "javascript", ts: "typescript",
16
- py: "python", kt: "kotlin", go: "go", rb: "ruby", swift: "swift",
17
- cs: "csharp", php: "php",
18
- };
19
- const langOf = (file) => LANG_BY_EXT[file.split(".").pop()?.toLowerCase()] ?? "unknown";
20
-
21
- const readJsonl = (p) => readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
22
-
23
- export function extract({ raw, root }) {
24
- const rootFs = root ? root.replace(/\\/g, "/").replace(/\/?$/, "/") : "";
25
- const rel = (p) => {
26
- p = String(p).replace(/\\/g, "/");
27
- return rootFs && p.startsWith(rootFs) ? p.slice(rootFs.length) : p.replace(/^\//, "");
28
- };
29
-
30
- const methods = readJsonl(join(raw, "methods.jsonl"));
31
- const calls = readJsonl(join(raw, "calls.jsonl"));
32
-
33
- // Identity: fullName|signature|file — the same tuple joern-export.sc keys on.
34
- const keyOf = (m) => `${m.fullName}|${m.signature}|${rel(m.file)}`;
35
-
36
- // Some frontends (javasrc2cpg: constructors, overload/bridge pairs) emit
37
- // several method records with an IDENTICAL fullName|signature|file — one
38
- // logical method must become one symbol, so dedupe by key first, keeping the
39
- // record with the widest line span (the real body over a synthetic stub).
40
- const byKey = new Map();
41
- for (const m of methods) {
42
- const k = keyOf(m);
43
- const prev = byKey.get(k);
44
- if (!prev || (m.lineEnd ?? 0) - (m.lineStart ?? 0) > (prev.lineEnd ?? 0) - (prev.lineStart ?? 0)) {
45
- byKey.set(k, m);
46
- }
47
- }
48
- const uniqueMethods = [...byKey.values()];
49
-
50
- // Anchors: "<lang>:<relfile>#<name>(<sig>)"; same-file same-name-and-sig
51
- // duplicates that remain distinct (different fullName) get ~2, ~3 by line order.
52
- const byAnchorBase = new Map();
53
- for (const m of uniqueMethods) {
54
- const base = `${langOf(m.file)}:${rel(m.file)}#${m.name}(${m.signature})`;
55
- if (!byAnchorBase.has(base)) byAnchorBase.set(base, []);
56
- byAnchorBase.get(base).push(m);
57
- }
58
- const anchorByKey = new Map();
59
- for (const [base, list] of byAnchorBase) {
60
- list.sort((a, b) => (a.lineStart ?? 0) - (b.lineStart ?? 0));
61
- list.forEach((m, i) => anchorByKey.set(keyOf(m), i === 0 ? base : `${base}~${i + 1}`));
62
- }
63
-
64
- // Class-qualified display names: in a class language a method's natural
65
- // short name is `Cls.method` — bare names ("invoke", "execute") repeat in
66
- // every other class and read as noise. Constructors are `Cls.new`, static
67
- // initialisers `Cls.static{}`. C (no classes: fullName has no dot) keeps
68
- // plain names. The anchor keeps the raw identity — presentation only.
69
- const displayName = (m) => {
70
- const head = String(m.fullName ?? "").split(":")[0]; // pkg.Outer$Inner.method
71
- const di = head.lastIndexOf(".");
72
- const owner = di > 0 ? head.slice(0, di) : "";
73
- const simple = (owner.split(".").pop() || "").split("$").pop() || "";
74
- if (m.name === "<init>") return simple ? `${simple}.new` : "new";
75
- if (m.name === "<clinit>") return simple ? `${simple}.static{}` : "static{}";
76
- return simple ? `${simple}.${m.name}` : m.name;
77
- };
78
-
79
- const symbols = [];
80
- const seenFiles = new Set();
81
- for (const m of uniqueMethods) {
82
- const file = rel(m.file);
83
- symbols.push({
84
- anchor: anchorByKey.get(keyOf(m)),
85
- lang: langOf(file),
86
- kind: "Function",
87
- name: displayName(m),
88
- file,
89
- line_start: m.lineStart ?? undefined,
90
- line_end: m.lineEnd ?? undefined,
91
- signature: m.signature || undefined,
92
- entry: m.name === "main" ? true : undefined,
93
- resolution: "cpg",
94
- });
95
- // Derive one File symbol per source file so emit gets stable heading ids.
96
- if (!seenFiles.has(file)) {
97
- seenFiles.add(file);
98
- symbols.push({
99
- anchor: `${langOf(file)}:${file}`,
100
- lang: langOf(file),
101
- kind: "File",
102
- name: file.split("/").pop(),
103
- file,
104
- resolution: "cpg",
105
- });
106
- }
107
- }
108
-
109
- const edges = [];
110
- for (const c of calls) {
111
- const from = anchorByKey.get(`${c.callerFullName}|${c.callerSignature}|${rel(c.callerFile)}`);
112
- if (!from) continue;
113
- const site = { file: rel(c.callerFile), line: c.line ?? 0 };
114
- const targets = (c.callees ?? [])
115
- .map((t) => anchorByKey.get(`${t.fullName}|${t.signature}|${rel(t.file)}`))
116
- .filter(Boolean);
117
- if (targets.length === 0) {
118
- edges.push({ kind: "calls", from, to_text: c.name, resolution: "cpg", confidence: "low", site });
119
- } else if (targets.length === 1) {
120
- edges.push({ kind: "calls", from, to: targets[0], resolution: "cpg", confidence: "high", site });
121
- } else {
122
- const [first, ...rest] = [...new Set(targets)].sort();
123
- edges.push({
124
- kind: "calls", from, to: first, resolution: "cpg", confidence: "medium",
125
- note: `dispatch, ${targets.length} candidates`, candidates: rest, site,
126
- });
127
- }
128
- }
129
-
130
- return { symbols, edges };
131
- }
1
+ // geml-code-graph adapter: Joern CPG export → exchange format (DESIGN §3.2/3.4).
2
+ //
3
+ // Consumes the raw JSONL written by joern-export.sc (methods.jsonl +
4
+ // calls.jsonl) — so the node side never touches the JVM. Everything here is
5
+ // resolution:"cpg". Confidence mapping (§3.4):
6
+ // exactly one internal callee → high
7
+ // several internal callees (dispatch) → first as `to` + the rest as
8
+ // `candidates`, medium
9
+ // no internal callee (external/pointer) → to_text, low (unresolved)
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ const LANG_BY_EXT = {
14
+ c: "c", h: "c", cc: "cpp", cpp: "cpp", cxx: "cpp", hpp: "cpp", hh: "cpp",
15
+ java: "java", js: "javascript", mjs: "javascript", ts: "typescript",
16
+ py: "python", kt: "kotlin", go: "go", rb: "ruby", swift: "swift",
17
+ cs: "csharp", php: "php",
18
+ };
19
+ const langOf = (file) => LANG_BY_EXT[file.split(".").pop()?.toLowerCase()] ?? "unknown";
20
+
21
+ const readJsonl = (p) => readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
22
+
23
+ export function extract({ raw, root }) {
24
+ const rootFs = root ? root.replace(/\\/g, "/").replace(/\/?$/, "/") : "";
25
+ const rel = (p) => {
26
+ p = String(p).replace(/\\/g, "/");
27
+ return rootFs && p.startsWith(rootFs) ? p.slice(rootFs.length) : p.replace(/^\//, "");
28
+ };
29
+
30
+ const methods = readJsonl(join(raw, "methods.jsonl"));
31
+ const calls = readJsonl(join(raw, "calls.jsonl"));
32
+
33
+ // Identity: fullName|signature|file — the same tuple joern-export.sc keys on.
34
+ const keyOf = (m) => `${m.fullName}|${m.signature}|${rel(m.file)}`;
35
+
36
+ // Some frontends (javasrc2cpg: constructors, overload/bridge pairs) emit
37
+ // several method records with an IDENTICAL fullName|signature|file — one
38
+ // logical method must become one symbol, so dedupe by key first, keeping the
39
+ // record with the widest line span (the real body over a synthetic stub).
40
+ const byKey = new Map();
41
+ for (const m of methods) {
42
+ const k = keyOf(m);
43
+ const prev = byKey.get(k);
44
+ if (!prev || (m.lineEnd ?? 0) - (m.lineStart ?? 0) > (prev.lineEnd ?? 0) - (prev.lineStart ?? 0)) {
45
+ byKey.set(k, m);
46
+ }
47
+ }
48
+ const uniqueMethods = [...byKey.values()];
49
+
50
+ // Anchors: "<lang>:<relfile>#<name>(<sig>)"; same-file same-name-and-sig
51
+ // duplicates that remain distinct (different fullName) get ~2, ~3 by line order.
52
+ const byAnchorBase = new Map();
53
+ for (const m of uniqueMethods) {
54
+ const base = `${langOf(m.file)}:${rel(m.file)}#${m.name}(${m.signature})`;
55
+ if (!byAnchorBase.has(base)) byAnchorBase.set(base, []);
56
+ byAnchorBase.get(base).push(m);
57
+ }
58
+ const anchorByKey = new Map();
59
+ for (const [base, list] of byAnchorBase) {
60
+ list.sort((a, b) => (a.lineStart ?? 0) - (b.lineStart ?? 0));
61
+ list.forEach((m, i) => anchorByKey.set(keyOf(m), i === 0 ? base : `${base}~${i + 1}`));
62
+ }
63
+
64
+ // Class-qualified display names: in a class language a method's natural
65
+ // short name is `Cls.method` — bare names ("invoke", "execute") repeat in
66
+ // every other class and read as noise. Constructors are `Cls.new`, static
67
+ // initialisers `Cls.static{}`. C (no classes: fullName has no dot) keeps
68
+ // plain names. The anchor keeps the raw identity — presentation only.
69
+ const displayName = (m) => {
70
+ const head = String(m.fullName ?? "").split(":")[0]; // pkg.Outer$Inner.method
71
+ const di = head.lastIndexOf(".");
72
+ const owner = di > 0 ? head.slice(0, di) : "";
73
+ const simple = (owner.split(".").pop() || "").split("$").pop() || "";
74
+ if (m.name === "<init>") return simple ? `${simple}.new` : "new";
75
+ if (m.name === "<clinit>") return simple ? `${simple}.static{}` : "static{}";
76
+ return simple ? `${simple}.${m.name}` : m.name;
77
+ };
78
+
79
+ const symbols = [];
80
+ const seenFiles = new Set();
81
+ for (const m of uniqueMethods) {
82
+ const file = rel(m.file);
83
+ symbols.push({
84
+ anchor: anchorByKey.get(keyOf(m)),
85
+ lang: langOf(file),
86
+ kind: "Function",
87
+ name: displayName(m),
88
+ file,
89
+ line_start: m.lineStart ?? undefined,
90
+ line_end: m.lineEnd ?? undefined,
91
+ signature: m.signature || undefined,
92
+ entry: m.name === "main" ? true : undefined,
93
+ resolution: "cpg",
94
+ });
95
+ // Derive one File symbol per source file so emit gets stable heading ids.
96
+ if (!seenFiles.has(file)) {
97
+ seenFiles.add(file);
98
+ symbols.push({
99
+ anchor: `${langOf(file)}:${file}`,
100
+ lang: langOf(file),
101
+ kind: "File",
102
+ name: file.split("/").pop(),
103
+ file,
104
+ resolution: "cpg",
105
+ });
106
+ }
107
+ }
108
+
109
+ const edges = [];
110
+ for (const c of calls) {
111
+ const from = anchorByKey.get(`${c.callerFullName}|${c.callerSignature}|${rel(c.callerFile)}`);
112
+ if (!from) continue;
113
+ const site = { file: rel(c.callerFile), line: c.line ?? 0 };
114
+ const targets = (c.callees ?? [])
115
+ .map((t) => anchorByKey.get(`${t.fullName}|${t.signature}|${rel(t.file)}`))
116
+ .filter(Boolean);
117
+ if (targets.length === 0) {
118
+ edges.push({ kind: "calls", from, to_text: c.name, resolution: "cpg", confidence: "low", site });
119
+ } else if (targets.length === 1) {
120
+ edges.push({ kind: "calls", from, to: targets[0], resolution: "cpg", confidence: "high", site });
121
+ } else {
122
+ const [first, ...rest] = [...new Set(targets)].sort();
123
+ edges.push({
124
+ kind: "calls", from, to: first, resolution: "cpg", confidence: "medium",
125
+ note: `dispatch, ${targets.length} candidates`, candidates: rest, site,
126
+ });
127
+ }
128
+ }
129
+
130
+ return { symbols, edges };
131
+ }