@geml/geml 1.4.2 → 1.4.4

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,110 +1,110 @@
1
- // foldings.geml — the visible, human-owned config for codemap ceremony folding.
2
- // GEML (dogfooded; the viewer renders it): a meta header, three bullet-list
3
- // sections (fold-prefixes / source-roots / test-roots) and an options section.
4
- // Read via the bundled parser; group-id shared-prefix stripping stays
5
- // algorithmic (only its on/off toggle lives here).
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { parse } from "../dist/geml.js";
9
- import { deriveFoldLayers, DEFAULT_SOURCE_ROOTS, DEFAULT_TEST_ROOTS, LANG_FOLD_PREFIXES } from "./normalize.mjs";
10
-
11
- const SECTIONS = { "fold-prefixes": "foldPrefixes", "source-roots": "sourceRoots", "test-roots": "testRoots", "module-roots": "moduleRoots" };
12
- // Plain text of a bullet-list item. GEML list items carry `.text`; fall back to
13
- // joining inline `.value`s if a build ever changes that shape.
14
- const itemText = (it) => (typeof it.text === "string" ? it.text : (it.inlines ?? []).map((n) => n.value ?? "").join("")).trim();
15
-
16
- export function parseFoldings(text) {
17
- const doc = parse(text);
18
- // A malformed hand-edit must fail LOUDLY, not silently drop every rule: the
19
- // caller (loadOrSeedFoldings) catches this and falls back to the defaults.
20
- // An intentionally empty file carries NO error diagnostics and yields empty
21
- // sections — the "fold nothing" off-switch, a deliberately different outcome.
22
- const errs = doc.diagnostics.filter((d) => d.severity === "error");
23
- if (errs.length) throw new Error(`invalid GEML in foldings config: ${errs[0].message} (line ${errs[0].line})`);
24
- const cfg = { foldPrefixes: [], sourceRoots: [], testRoots: [], moduleRoots: [], stripSharedPrefix: true };
25
- let section = null;
26
- for (const b of doc.children) {
27
- if (b.kind === "heading") { section = b.text.trim().toLowerCase(); continue; }
28
- if (b.kind !== "list") continue;
29
- const items = b.items.map(itemText).filter(Boolean);
30
- if (Object.hasOwn(SECTIONS, section)) cfg[SECTIONS[section]] = items;
31
- else if (section === "options") {
32
- for (const it of items) {
33
- const m = it.match(/^strip-shared-prefix\s*:\s*(on|off|true|false)$/i);
34
- if (m) cfg.stripSharedPrefix = /^(on|true)$/i.test(m[1]);
35
- }
36
- }
37
- }
38
- return cfg;
39
- }
40
-
41
- export function serializeFoldings(cfg) {
42
- const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
43
- return [
44
- "=== meta",
45
- 'title = "codemap foldings"',
46
- "===",
47
- "",
48
- "Ceremony folded out of module display names. Seeded on first build; edit",
49
- "freely — build never rewrites this. The shared package prefix (group-ids",
50
- "like com/acme/app) is stripped automatically and needs no entry here; set",
51
- "strip-shared-prefix to off under Options to disable it.",
52
- "",
53
- "Add a directory under module-roots to force it to display as its own",
54
- "module — for a submodule a build tool declares centrally but whose folder",
55
- "carries no manifest, or any layout the detector cannot see.",
56
- "",
57
- "## fold-prefixes",
58
- "",
59
- list(cfg.foldPrefixes),
60
- "",
61
- "## source-roots",
62
- "",
63
- list(cfg.sourceRoots),
64
- "",
65
- "## test-roots",
66
- "",
67
- list(cfg.testRoots),
68
- "",
69
- "## module-roots",
70
- "",
71
- list(cfg.moduleRoots ?? []),
72
- "",
73
- "## options",
74
- "",
75
- `- strip-shared-prefix: ${cfg.stripSharedPrefix ? "on" : "off"}`,
76
- "",
77
- ].join("\n");
78
- }
79
-
80
- // Seeded config for a first build: structural fold-layers (above-root
81
- // ceremony dirs, derived from the discovered module roots) unioned with
82
- // known per-language ceremony (Cargo's `crates/`). Source/test roots always
83
- // start from the same global defaults — the human edits foldings.geml from
84
- // there; the build never rewrites it once it exists.
85
- export function defaultFoldings({ moduleRoots, languages }) {
86
- const langPrefixes = (languages ?? []).flatMap((l) => LANG_FOLD_PREFIXES[l] ?? []);
87
- const foldPrefixes = [...new Set([...deriveFoldLayers(moduleRoots ?? []), ...langPrefixes])].sort();
88
- return { foldPrefixes, sourceRoots: [...DEFAULT_SOURCE_ROOTS], testRoots: [...DEFAULT_TEST_ROOTS], moduleRoots: [], stripSharedPrefix: true };
89
- }
90
-
91
- // Read <outDir>/_index/foldings.geml, or seed it on first build. Write-once:
92
- // an existing file is the human's — we read it and never rewrite it (the
93
- // refresh.json contract). A missing or unreadable file never crashes the
94
- // build: seed/fall back to defaults and say so. Returns { config, seeded }.
95
- export function loadOrSeedFoldings({ outDir, moduleRoots, languages }) {
96
- const path = join(outDir, "_index", "foldings.geml");
97
- if (existsSync(path)) {
98
- try { return { config: parseFoldings(readFileSync(path, "utf8")), seeded: false }; }
99
- catch (e) {
100
- console.error(`warning: ignoring ${path} (${e.message}) — using default foldings; fix the file to re-enable your edits.`);
101
- return { config: defaultFoldings({ moduleRoots, languages }), seeded: false };
102
- }
103
- }
104
- const config = defaultFoldings({ moduleRoots, languages });
105
- try {
106
- mkdirSync(join(outDir, "_index"), { recursive: true });
107
- writeFileSync(path, serializeFoldings(config));
108
- } catch (e) { console.error(`warning: could not seed ${path} (${e.message}).`); }
109
- return { config, seeded: true };
110
- }
1
+ // foldings.geml — the visible, human-owned config for codemap ceremony folding.
2
+ // GEML (dogfooded; the viewer renders it): a meta header, three bullet-list
3
+ // sections (fold-prefixes / source-roots / test-roots) and an options section.
4
+ // Read via the bundled parser; group-id shared-prefix stripping stays
5
+ // algorithmic (only its on/off toggle lives here).
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { parse } from "../dist/geml.js";
9
+ import { deriveFoldLayers, DEFAULT_SOURCE_ROOTS, DEFAULT_TEST_ROOTS, LANG_FOLD_PREFIXES } from "./normalize.mjs";
10
+
11
+ const SECTIONS = { "fold-prefixes": "foldPrefixes", "source-roots": "sourceRoots", "test-roots": "testRoots", "module-roots": "moduleRoots" };
12
+ // Plain text of a bullet-list item. GEML list items carry `.text`; fall back to
13
+ // joining inline `.value`s if a build ever changes that shape.
14
+ const itemText = (it) => (typeof it.text === "string" ? it.text : (it.inlines ?? []).map((n) => n.value ?? "").join("")).trim();
15
+
16
+ export function parseFoldings(text) {
17
+ const doc = parse(text);
18
+ // A malformed hand-edit must fail LOUDLY, not silently drop every rule: the
19
+ // caller (loadOrSeedFoldings) catches this and falls back to the defaults.
20
+ // An intentionally empty file carries NO error diagnostics and yields empty
21
+ // sections — the "fold nothing" off-switch, a deliberately different outcome.
22
+ const errs = doc.diagnostics.filter((d) => d.severity === "error");
23
+ if (errs.length) throw new Error(`invalid GEML in foldings config: ${errs[0].message} (line ${errs[0].line})`);
24
+ const cfg = { foldPrefixes: [], sourceRoots: [], testRoots: [], moduleRoots: [], stripSharedPrefix: true };
25
+ let section = null;
26
+ for (const b of doc.children) {
27
+ if (b.kind === "heading") { section = b.text.trim().toLowerCase(); continue; }
28
+ if (b.kind !== "list") continue;
29
+ const items = b.items.map(itemText).filter(Boolean);
30
+ if (Object.hasOwn(SECTIONS, section)) cfg[SECTIONS[section]] = items;
31
+ else if (section === "options") {
32
+ for (const it of items) {
33
+ const m = it.match(/^strip-shared-prefix\s*:\s*(on|off|true|false)$/i);
34
+ if (m) cfg.stripSharedPrefix = /^(on|true)$/i.test(m[1]);
35
+ }
36
+ }
37
+ }
38
+ return cfg;
39
+ }
40
+
41
+ export function serializeFoldings(cfg) {
42
+ const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
43
+ return [
44
+ "=== meta",
45
+ 'title = "codemap foldings"',
46
+ "===",
47
+ "",
48
+ "Ceremony folded out of module display names. Seeded on first build; edit",
49
+ "freely — build never rewrites this. The shared package prefix (group-ids",
50
+ "like com/acme/app) is stripped automatically and needs no entry here; set",
51
+ "strip-shared-prefix to off under Options to disable it.",
52
+ "",
53
+ "Add a directory under module-roots to force it to display as its own",
54
+ "module — for a submodule a build tool declares centrally but whose folder",
55
+ "carries no manifest, or any layout the detector cannot see.",
56
+ "",
57
+ "## fold-prefixes",
58
+ "",
59
+ list(cfg.foldPrefixes),
60
+ "",
61
+ "## source-roots",
62
+ "",
63
+ list(cfg.sourceRoots),
64
+ "",
65
+ "## test-roots",
66
+ "",
67
+ list(cfg.testRoots),
68
+ "",
69
+ "## module-roots",
70
+ "",
71
+ list(cfg.moduleRoots ?? []),
72
+ "",
73
+ "## options",
74
+ "",
75
+ `- strip-shared-prefix: ${cfg.stripSharedPrefix ? "on" : "off"}`,
76
+ "",
77
+ ].join("\n");
78
+ }
79
+
80
+ // Seeded config for a first build: structural fold-layers (above-root
81
+ // ceremony dirs, derived from the discovered module roots) unioned with
82
+ // known per-language ceremony (Cargo's `crates/`). Source/test roots always
83
+ // start from the same global defaults — the human edits foldings.geml from
84
+ // there; the build never rewrites it once it exists.
85
+ export function defaultFoldings({ moduleRoots, languages }) {
86
+ const langPrefixes = (languages ?? []).flatMap((l) => LANG_FOLD_PREFIXES[l] ?? []);
87
+ const foldPrefixes = [...new Set([...deriveFoldLayers(moduleRoots ?? []), ...langPrefixes])].sort();
88
+ return { foldPrefixes, sourceRoots: [...DEFAULT_SOURCE_ROOTS], testRoots: [...DEFAULT_TEST_ROOTS], moduleRoots: [], stripSharedPrefix: true };
89
+ }
90
+
91
+ // Read <outDir>/_index/foldings.geml, or seed it on first build. Write-once:
92
+ // an existing file is the human's — we read it and never rewrite it (the
93
+ // refresh.json contract). A missing or unreadable file never crashes the
94
+ // build: seed/fall back to defaults and say so. Returns { config, seeded }.
95
+ export function loadOrSeedFoldings({ outDir, moduleRoots, languages }) {
96
+ const path = join(outDir, "_index", "foldings.geml");
97
+ if (existsSync(path)) {
98
+ try { return { config: parseFoldings(readFileSync(path, "utf8")), seeded: false }; }
99
+ catch (e) {
100
+ console.error(`warning: ignoring ${path} (${e.message}) — using default foldings; fix the file to re-enable your edits.`);
101
+ return { config: defaultFoldings({ moduleRoots, languages }), seeded: false };
102
+ }
103
+ }
104
+ const config = defaultFoldings({ moduleRoots, languages });
105
+ try {
106
+ mkdirSync(join(outDir, "_index"), { recursive: true });
107
+ writeFileSync(path, serializeFoldings(config));
108
+ } catch (e) { console.error(`warning: could not seed ${path} (${e.message}).`); }
109
+ return { config, seeded: true };
110
+ }
@@ -1,83 +1,83 @@
1
- // geml-code-graph Joern export (P1, DESIGN §3.4).
2
- // Runs INSIDE joern; emits raw method/call records as JSONL for adapters/joern.mjs.
3
- //
4
- // Parameters come from ENVIRONMENT VARIABLES, not --param: on Windows the
5
- // joern.bat -> repl-bridge.bat hop re-tokenizes %* and cmd.exe treats `=` as a
6
- // delimiter, so `--param k=v` never survives intact. Env vars pass through
7
- // every layer on every OS:
8
- //
9
- // GEML_SRC=/abs/path/to/src GEML_OUT=/abs/path/to/build/raw \
10
- // joern --script geml-parser/codemap/joern-export.sc
11
- //
12
- // Output:
13
- // <GEML_OUT>/methods.jsonl one record per internal method
14
- // <GEML_OUT>/calls.jsonl one record per call site, callees resolved by Joern
15
- //
16
- // Identity: methods are keyed by fullName|signature|filename — the adapter
17
- // mints anchors and stable ids from these; this script stays dumb on purpose.
18
- import java.io.{File, PrintWriter}
19
-
20
- @main def exec(): Unit = {
21
- val codeDir = sys.env.getOrElse("GEML_SRC", { System.err.println("GEML_SRC not set"); sys.exit(2) })
22
- val outDir = sys.env.getOrElse("GEML_OUT", { System.err.println("GEML_OUT not set"); sys.exit(2) })
23
- // GEML_LANG (optional): force a frontend in mixed-language repos, where
24
- // auto-detection may pick the majority language instead of the intended one.
25
- // Values are Joern's --language names: JAVASRC, NEWC, PYTHONSRC, JSSRC, …
26
- sys.env.get("GEML_LANG") match {
27
- case Some(lang) => importCode(inputPath = codeDir, projectName = "geml-code-graph", language = lang)
28
- case None => importCode(inputPath = codeDir, projectName = "geml-code-graph")
29
- }
30
-
31
- def esc(s: String): String =
32
- s.replace("\\", "\\\\").replace("\"", "\\\"")
33
- .replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
34
- def jstr(s: String): String = "\"" + esc(s) + "\""
35
-
36
- new File(outDir).mkdirs()
37
-
38
- val skipName = (n: String) => n == "<global>" || n.startsWith("<operator>") || n.startsWith("<clinit>")
39
-
40
- // ---- methods ----
41
- val mOut = new PrintWriter(new File(outDir, "methods.jsonl"), "UTF-8")
42
- cpg.method.filter(m => !m.isExternal && !skipName(m.name)).foreach { m =>
43
- mOut.println(
44
- s"""{"name":${jstr(m.name)},"fullName":${jstr(m.fullName)},"signature":${jstr(m.signature)},""" +
45
- s""""file":${jstr(m.filename)},"lineStart":${m.lineNumber.map(_.toString).getOrElse("null")},""" +
46
- s""""lineEnd":${m.lineNumberEnd.map(_.toString).getOrElse("null")}}"""
47
- )
48
- }
49
- mOut.close()
50
-
51
- // ---- calls ----
52
- // For each call site inside an internal method: Joern-resolved callees.
53
- // Several internal callees = dispatch candidates (the adapter keeps them ALL,
54
- // per the "never force a single candidate" red line). No internal callee =
55
- // unresolved from the graph's point of view (external / pointer call).
56
- // Operator calls are noise (arithmetic, casts, field access) EXCEPT
57
- // <operator>.pointerCall — a function-pointer invocation is a real dispatch
58
- // site the graph cannot resolve statically, so it must surface as an
59
- // unresolved call (blind spots are shown, not hidden). Its readable label is
60
- // the source expression itself.
61
- val cOut = new PrintWriter(new File(outDir, "calls.jsonl"), "UTF-8")
62
- cpg.call.filterNot(c => c.name.startsWith("<operator>") && c.name != "<operator>.pointerCall").foreach { c =>
63
- val caller = c.method
64
- if (!caller.isExternal && !skipName(caller.name)) {
65
- val callees = c.callee.l
66
- val internal = callees.filter(m => !m.isExternal && !skipName(m.name))
67
- val tos = internal.map(m =>
68
- s"""{"fullName":${jstr(m.fullName)},"signature":${jstr(m.signature)},"file":${jstr(m.filename)}}"""
69
- ).mkString("[", ",", "]")
70
- val label =
71
- if (c.name == "<operator>.pointerCall") c.code.takeWhile(_ != '\n').take(48)
72
- else c.name
73
- cOut.println(
74
- s"""{"callerFullName":${jstr(caller.fullName)},"callerSignature":${jstr(caller.signature)},""" +
75
- s""""callerFile":${jstr(caller.filename)},"name":${jstr(label)},""" +
76
- s""""line":${c.lineNumber.map(_.toString).getOrElse("null")},"callees":$tos}"""
77
- )
78
- }
79
- }
80
- cOut.close()
81
-
82
- println(s"geml-code-graph joern-export: done -> $outDir")
83
- }
1
+ // geml-code-graph Joern export (P1, DESIGN §3.4).
2
+ // Runs INSIDE joern; emits raw method/call records as JSONL for adapters/joern.mjs.
3
+ //
4
+ // Parameters come from ENVIRONMENT VARIABLES, not --param: on Windows the
5
+ // joern.bat -> repl-bridge.bat hop re-tokenizes %* and cmd.exe treats `=` as a
6
+ // delimiter, so `--param k=v` never survives intact. Env vars pass through
7
+ // every layer on every OS:
8
+ //
9
+ // GEML_SRC=/abs/path/to/src GEML_OUT=/abs/path/to/build/raw \
10
+ // joern --script geml-parser/codemap/joern-export.sc
11
+ //
12
+ // Output:
13
+ // <GEML_OUT>/methods.jsonl one record per internal method
14
+ // <GEML_OUT>/calls.jsonl one record per call site, callees resolved by Joern
15
+ //
16
+ // Identity: methods are keyed by fullName|signature|filename — the adapter
17
+ // mints anchors and stable ids from these; this script stays dumb on purpose.
18
+ import java.io.{File, PrintWriter}
19
+
20
+ @main def exec(): Unit = {
21
+ val codeDir = sys.env.getOrElse("GEML_SRC", { System.err.println("GEML_SRC not set"); sys.exit(2) })
22
+ val outDir = sys.env.getOrElse("GEML_OUT", { System.err.println("GEML_OUT not set"); sys.exit(2) })
23
+ // GEML_LANG (optional): force a frontend in mixed-language repos, where
24
+ // auto-detection may pick the majority language instead of the intended one.
25
+ // Values are Joern's --language names: JAVASRC, NEWC, PYTHONSRC, JSSRC, …
26
+ sys.env.get("GEML_LANG") match {
27
+ case Some(lang) => importCode(inputPath = codeDir, projectName = "geml-code-graph", language = lang)
28
+ case None => importCode(inputPath = codeDir, projectName = "geml-code-graph")
29
+ }
30
+
31
+ def esc(s: String): String =
32
+ s.replace("\\", "\\\\").replace("\"", "\\\"")
33
+ .replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
34
+ def jstr(s: String): String = "\"" + esc(s) + "\""
35
+
36
+ new File(outDir).mkdirs()
37
+
38
+ val skipName = (n: String) => n == "<global>" || n.startsWith("<operator>") || n.startsWith("<clinit>")
39
+
40
+ // ---- methods ----
41
+ val mOut = new PrintWriter(new File(outDir, "methods.jsonl"), "UTF-8")
42
+ cpg.method.filter(m => !m.isExternal && !skipName(m.name)).foreach { m =>
43
+ mOut.println(
44
+ s"""{"name":${jstr(m.name)},"fullName":${jstr(m.fullName)},"signature":${jstr(m.signature)},""" +
45
+ s""""file":${jstr(m.filename)},"lineStart":${m.lineNumber.map(_.toString).getOrElse("null")},""" +
46
+ s""""lineEnd":${m.lineNumberEnd.map(_.toString).getOrElse("null")}}"""
47
+ )
48
+ }
49
+ mOut.close()
50
+
51
+ // ---- calls ----
52
+ // For each call site inside an internal method: Joern-resolved callees.
53
+ // Several internal callees = dispatch candidates (the adapter keeps them ALL,
54
+ // per the "never force a single candidate" red line). No internal callee =
55
+ // unresolved from the graph's point of view (external / pointer call).
56
+ // Operator calls are noise (arithmetic, casts, field access) EXCEPT
57
+ // <operator>.pointerCall — a function-pointer invocation is a real dispatch
58
+ // site the graph cannot resolve statically, so it must surface as an
59
+ // unresolved call (blind spots are shown, not hidden). Its readable label is
60
+ // the source expression itself.
61
+ val cOut = new PrintWriter(new File(outDir, "calls.jsonl"), "UTF-8")
62
+ cpg.call.filterNot(c => c.name.startsWith("<operator>") && c.name != "<operator>.pointerCall").foreach { c =>
63
+ val caller = c.method
64
+ if (!caller.isExternal && !skipName(caller.name)) {
65
+ val callees = c.callee.l
66
+ val internal = callees.filter(m => !m.isExternal && !skipName(m.name))
67
+ val tos = internal.map(m =>
68
+ s"""{"fullName":${jstr(m.fullName)},"signature":${jstr(m.signature)},"file":${jstr(m.filename)}}"""
69
+ ).mkString("[", ",", "]")
70
+ val label =
71
+ if (c.name == "<operator>.pointerCall") c.code.takeWhile(_ != '\n').take(48)
72
+ else c.name
73
+ cOut.println(
74
+ s"""{"callerFullName":${jstr(caller.fullName)},"callerSignature":${jstr(caller.signature)},""" +
75
+ s""""callerFile":${jstr(caller.filename)},"name":${jstr(label)},""" +
76
+ s""""line":${c.lineNumber.map(_.toString).getOrElse("null")},"callees":$tos}"""
77
+ )
78
+ }
79
+ }
80
+ cOut.close()
81
+
82
+ println(s"geml-code-graph joern-export: done -> $outDir")
83
+ }