@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.
- package/LICENSE +21 -21
- package/README.md +186 -155
- package/codemap/adapters/crg.mjs +120 -120
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -658
- package/codemap/browser-stub.mjs +29 -29
- package/codemap/build.mjs +609 -609
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +480 -480
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +52 -52
- package/codemap/find.mjs +63 -63
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +172 -172
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +310 -310
- package/codemap/render-all.mjs +64 -64
- package/codemap/serve.mjs +578 -578
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +148 -148
- package/dist/chart.d.ts +2 -0
- package/dist/chart.js +15 -15
- package/dist/diagnostics.d.ts +9 -0
- package/dist/diagnostics.js +73 -0
- package/dist/geml.d.ts +3 -5
- package/dist/geml.js +261 -115
- package/dist/history.d.ts +10 -0
- package/dist/history.js +25 -24
- package/dist/inline.js +7 -7
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +543 -0
- package/dist/render-html.js +35 -35
- package/dist/render.js +136 -136
- package/dist/table.d.ts +2 -0
- package/dist/table.js +11 -11
- package/package.json +63 -62
package/codemap/entries.mjs
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
// geml-code-graph app-entry detection — WHERE does this repo start running?
|
|
2
|
-
//
|
|
3
|
-
// Emits entry HINTS ({ file, via, name? }) from three signal tiers, each
|
|
4
|
-
// carrying an honest `via` label (the codemap never claims an entry without
|
|
5
|
-
// saying what convention identified it):
|
|
6
|
-
// L2 manifest/layout Cargo [[bin]] & src/main.rs & src/bin/*, package.json
|
|
7
|
-
// bin, wrangler.toml main, Nuxt app.vue, Next root
|
|
8
|
-
// page, SvelteKit root route, Django manage.py,
|
|
9
|
-
// python __main__.py
|
|
10
|
-
// L3 source markers workers-rs #[event(...)], createApp().mount() /
|
|
11
|
-
// createRoot() / svelte mount (SPA bootstraps),
|
|
12
|
-
// .listen() (node servers), export default { fetch }
|
|
13
|
-
// (JS workers), Flask()/FastAPI() apps,
|
|
14
|
-
// @SpringBootApplication
|
|
15
|
-
// (L1 — a function literally named `main` — is already flagged by the scip
|
|
16
|
-
// and joern adapters at extraction time; hints here ADD to it.)
|
|
17
|
-
//
|
|
18
|
-
// Pure by design: given precomputed { files, manifests, pkgs } lists it walks
|
|
19
|
-
// nothing; `readText`/`readJson` are injectable, and every source peek is
|
|
20
|
-
// bounded to a handful of conventional entry files per project — never a
|
|
21
|
-
// repo-wide grep. A hint is only emitted for files the build actually indexes
|
|
22
|
-
// (present in `files`), so a pkg-bin pointing at dist/ never leaks in.
|
|
23
|
-
import { readFileSync } from "node:fs";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
|
|
26
|
-
const dirOf = (p) => (p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "");
|
|
27
|
-
|
|
28
|
-
export function detectEntries(root, { files = [], manifests = [], pkgs = [], readText, readJson } = {}) {
|
|
29
|
-
readText ??= (p) => readFileSync(p, "utf8");
|
|
30
|
-
readJson ??= (p) => JSON.parse(readText(p));
|
|
31
|
-
const fileSet = new Set(files);
|
|
32
|
-
const hints = [];
|
|
33
|
-
const seen = new Set();
|
|
34
|
-
const add = (file, via, name) => {
|
|
35
|
-
if (!file || !fileSet.has(file)) return;
|
|
36
|
-
const k = `${file}${via}${name ?? ""}`;
|
|
37
|
-
if (seen.has(k)) return;
|
|
38
|
-
seen.add(k);
|
|
39
|
-
hints.push(name ? { file, via, name } : { file, via });
|
|
40
|
-
};
|
|
41
|
-
const tryText = (rel) => {
|
|
42
|
-
try { return readText(join(root, ...rel.split("/"))); } catch { return null; }
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// ---- Rust: cargo bin targets + workers-rs event handlers ----
|
|
46
|
-
for (const m of manifests.filter((x) => x.endsWith("Cargo.toml"))) {
|
|
47
|
-
const dir = dirOf(m);
|
|
48
|
-
const at = (rel) => (dir ? `${dir}/${rel}` : rel);
|
|
49
|
-
add(at("src/main.rs"), "cargo-bin", "main");
|
|
50
|
-
for (const f of files) if (f.startsWith(at("src/bin/")) && f.endsWith(".rs")) add(f, "cargo-bin", "main");
|
|
51
|
-
const toml = tryText(m);
|
|
52
|
-
if (toml) {
|
|
53
|
-
for (const b of toml.matchAll(/^\[\[bin\]\][^[]*/gm)) {
|
|
54
|
-
const p = /path\s*=\s*"([^"]+)"/.exec(b[0]);
|
|
55
|
-
if (p) add(at(p[1].replace(/\\/g, "/")), "cargo-bin", "main");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
for (const rel of ["src/main.rs", "src/lib.rs"]) {
|
|
59
|
-
const t = fileSet.has(at(rel)) ? tryText(at(rel)) : null;
|
|
60
|
-
if (!t) continue;
|
|
61
|
-
for (const ev of t.matchAll(/#\[event\((\w+)[^)]*\)\]\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
62
|
-
add(at(rel), `worker-${ev[1]}`, ev[2]);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---- Node/TS/frontends: one look per package ----
|
|
68
|
-
for (const p of pkgs) {
|
|
69
|
-
const dir = dirOf(p);
|
|
70
|
-
const at = (rel) => (dir ? `${dir}/${rel}` : rel);
|
|
71
|
-
let pkg = {};
|
|
72
|
-
try { pkg = readJson(join(root, ...p.split("/"))) ?? {}; } catch { /* unreadable manifest */ }
|
|
73
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
74
|
-
const norm = (v) => (typeof v === "string" ? v.replace(/^\.\//, "").replace(/\\/g, "/") : null);
|
|
75
|
-
const bins = typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin ?? {});
|
|
76
|
-
for (const b of bins) { const f = norm(b); if (f) add(at(f), "pkg-bin"); }
|
|
77
|
-
const wrangler = tryText(at("wrangler.toml"));
|
|
78
|
-
if (wrangler) {
|
|
79
|
-
const mm = /^\s*main\s*=\s*"([^"]+)"/m.exec(wrangler);
|
|
80
|
-
if (mm) add(at(norm(mm[1])), "worker-fetch");
|
|
81
|
-
}
|
|
82
|
-
// Nuxt: the app shell is the entry; individual pages are routes, not
|
|
83
|
-
// program starts — deliberately NOT flooded into app-entries.
|
|
84
|
-
if (deps.nuxt || fileSet.has(at("nuxt.config.ts")) || fileSet.has(at("nuxt.config.js"))) {
|
|
85
|
-
if (fileSet.has(at("app.vue"))) add(at("app.vue"), "nuxt-app");
|
|
86
|
-
else add(at("pages/index.vue"), "nuxt-page");
|
|
87
|
-
}
|
|
88
|
-
if (deps.next) {
|
|
89
|
-
for (const rel of ["app/page.tsx", "app/page.jsx", "src/app/page.tsx", "pages/index.tsx", "pages/index.jsx", "src/pages/index.tsx"]) {
|
|
90
|
-
if (fileSet.has(at(rel))) { add(at(rel), "next-page"); break; }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (deps["@sveltejs/kit"]) add(at("src/routes/+page.svelte"), "kit-route");
|
|
94
|
-
// SPA bootstrap / server start markers — conventional entry files only.
|
|
95
|
-
for (const rel of ["src/main.ts", "src/main.tsx", "src/main.js", "src/main.jsx",
|
|
96
|
-
"src/index.ts", "src/index.tsx", "src/index.js", "index.ts", "index.js",
|
|
97
|
-
"src/server.ts", "src/server.js", "server.js", "src/app.ts", "app.js"]) {
|
|
98
|
-
const f = at(rel);
|
|
99
|
-
if (!fileSet.has(f)) continue;
|
|
100
|
-
const t = tryText(f);
|
|
101
|
-
if (!t) continue;
|
|
102
|
-
if (/createApp\s*\(/.test(t) && /\.mount\s*\(/.test(t)) add(f, "vue-mount");
|
|
103
|
-
else if (/createRoot\s*\(|ReactDOM\.render\s*\(/.test(t)) add(f, "react-mount");
|
|
104
|
-
else if (deps.svelte && /\bnew\s+\w+\s*\(\s*\{[^}]*target|\bmount\s*\(/.test(t)) add(f, "svelte-mount");
|
|
105
|
-
if (/\.listen\s*\(/.test(t)) add(f, "server-listen");
|
|
106
|
-
if (/export\s+default\s*\{[^}]*\bfetch\b/s.test(t)) add(f, "worker-fetch");
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ---- Python ----
|
|
111
|
-
for (const f of files) {
|
|
112
|
-
if (/(^|\/)manage\.py$/.test(f)) add(f, "django-manage");
|
|
113
|
-
else if (/(^|\/)__main__\.py$/.test(f)) add(f, "py-main");
|
|
114
|
-
else if (/(^|\/)(app|main|wsgi|asgi)\.py$/.test(f)) {
|
|
115
|
-
const t = tryText(f);
|
|
116
|
-
if (t && /\bFlask\s*\(|\bFastAPI\s*\(/.test(t)) add(f, "wsgi-app");
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ---- Java: Spring Boot (convention-named files only, never a repo grep) ----
|
|
121
|
-
for (const f of files) {
|
|
122
|
-
if (/Application\.java$/.test(f)) {
|
|
123
|
-
const t = tryText(f);
|
|
124
|
-
if (t && /@SpringBootApplication/.test(t)) add(f, "spring-boot", "main");
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return hints;
|
|
129
|
-
}
|
|
1
|
+
// geml-code-graph app-entry detection — WHERE does this repo start running?
|
|
2
|
+
//
|
|
3
|
+
// Emits entry HINTS ({ file, via, name? }) from three signal tiers, each
|
|
4
|
+
// carrying an honest `via` label (the codemap never claims an entry without
|
|
5
|
+
// saying what convention identified it):
|
|
6
|
+
// L2 manifest/layout Cargo [[bin]] & src/main.rs & src/bin/*, package.json
|
|
7
|
+
// bin, wrangler.toml main, Nuxt app.vue, Next root
|
|
8
|
+
// page, SvelteKit root route, Django manage.py,
|
|
9
|
+
// python __main__.py
|
|
10
|
+
// L3 source markers workers-rs #[event(...)], createApp().mount() /
|
|
11
|
+
// createRoot() / svelte mount (SPA bootstraps),
|
|
12
|
+
// .listen() (node servers), export default { fetch }
|
|
13
|
+
// (JS workers), Flask()/FastAPI() apps,
|
|
14
|
+
// @SpringBootApplication
|
|
15
|
+
// (L1 — a function literally named `main` — is already flagged by the scip
|
|
16
|
+
// and joern adapters at extraction time; hints here ADD to it.)
|
|
17
|
+
//
|
|
18
|
+
// Pure by design: given precomputed { files, manifests, pkgs } lists it walks
|
|
19
|
+
// nothing; `readText`/`readJson` are injectable, and every source peek is
|
|
20
|
+
// bounded to a handful of conventional entry files per project — never a
|
|
21
|
+
// repo-wide grep. A hint is only emitted for files the build actually indexes
|
|
22
|
+
// (present in `files`), so a pkg-bin pointing at dist/ never leaks in.
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
const dirOf = (p) => (p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "");
|
|
27
|
+
|
|
28
|
+
export function detectEntries(root, { files = [], manifests = [], pkgs = [], readText, readJson } = {}) {
|
|
29
|
+
readText ??= (p) => readFileSync(p, "utf8");
|
|
30
|
+
readJson ??= (p) => JSON.parse(readText(p));
|
|
31
|
+
const fileSet = new Set(files);
|
|
32
|
+
const hints = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
const add = (file, via, name) => {
|
|
35
|
+
if (!file || !fileSet.has(file)) return;
|
|
36
|
+
const k = `${file}${via}${name ?? ""}`;
|
|
37
|
+
if (seen.has(k)) return;
|
|
38
|
+
seen.add(k);
|
|
39
|
+
hints.push(name ? { file, via, name } : { file, via });
|
|
40
|
+
};
|
|
41
|
+
const tryText = (rel) => {
|
|
42
|
+
try { return readText(join(root, ...rel.split("/"))); } catch { return null; }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ---- Rust: cargo bin targets + workers-rs event handlers ----
|
|
46
|
+
for (const m of manifests.filter((x) => x.endsWith("Cargo.toml"))) {
|
|
47
|
+
const dir = dirOf(m);
|
|
48
|
+
const at = (rel) => (dir ? `${dir}/${rel}` : rel);
|
|
49
|
+
add(at("src/main.rs"), "cargo-bin", "main");
|
|
50
|
+
for (const f of files) if (f.startsWith(at("src/bin/")) && f.endsWith(".rs")) add(f, "cargo-bin", "main");
|
|
51
|
+
const toml = tryText(m);
|
|
52
|
+
if (toml) {
|
|
53
|
+
for (const b of toml.matchAll(/^\[\[bin\]\][^[]*/gm)) {
|
|
54
|
+
const p = /path\s*=\s*"([^"]+)"/.exec(b[0]);
|
|
55
|
+
if (p) add(at(p[1].replace(/\\/g, "/")), "cargo-bin", "main");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const rel of ["src/main.rs", "src/lib.rs"]) {
|
|
59
|
+
const t = fileSet.has(at(rel)) ? tryText(at(rel)) : null;
|
|
60
|
+
if (!t) continue;
|
|
61
|
+
for (const ev of t.matchAll(/#\[event\((\w+)[^)]*\)\]\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
62
|
+
add(at(rel), `worker-${ev[1]}`, ev[2]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- Node/TS/frontends: one look per package ----
|
|
68
|
+
for (const p of pkgs) {
|
|
69
|
+
const dir = dirOf(p);
|
|
70
|
+
const at = (rel) => (dir ? `${dir}/${rel}` : rel);
|
|
71
|
+
let pkg = {};
|
|
72
|
+
try { pkg = readJson(join(root, ...p.split("/"))) ?? {}; } catch { /* unreadable manifest */ }
|
|
73
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
74
|
+
const norm = (v) => (typeof v === "string" ? v.replace(/^\.\//, "").replace(/\\/g, "/") : null);
|
|
75
|
+
const bins = typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin ?? {});
|
|
76
|
+
for (const b of bins) { const f = norm(b); if (f) add(at(f), "pkg-bin"); }
|
|
77
|
+
const wrangler = tryText(at("wrangler.toml"));
|
|
78
|
+
if (wrangler) {
|
|
79
|
+
const mm = /^\s*main\s*=\s*"([^"]+)"/m.exec(wrangler);
|
|
80
|
+
if (mm) add(at(norm(mm[1])), "worker-fetch");
|
|
81
|
+
}
|
|
82
|
+
// Nuxt: the app shell is the entry; individual pages are routes, not
|
|
83
|
+
// program starts — deliberately NOT flooded into app-entries.
|
|
84
|
+
if (deps.nuxt || fileSet.has(at("nuxt.config.ts")) || fileSet.has(at("nuxt.config.js"))) {
|
|
85
|
+
if (fileSet.has(at("app.vue"))) add(at("app.vue"), "nuxt-app");
|
|
86
|
+
else add(at("pages/index.vue"), "nuxt-page");
|
|
87
|
+
}
|
|
88
|
+
if (deps.next) {
|
|
89
|
+
for (const rel of ["app/page.tsx", "app/page.jsx", "src/app/page.tsx", "pages/index.tsx", "pages/index.jsx", "src/pages/index.tsx"]) {
|
|
90
|
+
if (fileSet.has(at(rel))) { add(at(rel), "next-page"); break; }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (deps["@sveltejs/kit"]) add(at("src/routes/+page.svelte"), "kit-route");
|
|
94
|
+
// SPA bootstrap / server start markers — conventional entry files only.
|
|
95
|
+
for (const rel of ["src/main.ts", "src/main.tsx", "src/main.js", "src/main.jsx",
|
|
96
|
+
"src/index.ts", "src/index.tsx", "src/index.js", "index.ts", "index.js",
|
|
97
|
+
"src/server.ts", "src/server.js", "server.js", "src/app.ts", "app.js"]) {
|
|
98
|
+
const f = at(rel);
|
|
99
|
+
if (!fileSet.has(f)) continue;
|
|
100
|
+
const t = tryText(f);
|
|
101
|
+
if (!t) continue;
|
|
102
|
+
if (/createApp\s*\(/.test(t) && /\.mount\s*\(/.test(t)) add(f, "vue-mount");
|
|
103
|
+
else if (/createRoot\s*\(|ReactDOM\.render\s*\(/.test(t)) add(f, "react-mount");
|
|
104
|
+
else if (deps.svelte && /\bnew\s+\w+\s*\(\s*\{[^}]*target|\bmount\s*\(/.test(t)) add(f, "svelte-mount");
|
|
105
|
+
if (/\.listen\s*\(/.test(t)) add(f, "server-listen");
|
|
106
|
+
if (/export\s+default\s*\{[^}]*\bfetch\b/s.test(t)) add(f, "worker-fetch");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- Python ----
|
|
111
|
+
for (const f of files) {
|
|
112
|
+
if (/(^|\/)manage\.py$/.test(f)) add(f, "django-manage");
|
|
113
|
+
else if (/(^|\/)__main__\.py$/.test(f)) add(f, "py-main");
|
|
114
|
+
else if (/(^|\/)(app|main|wsgi|asgi)\.py$/.test(f)) {
|
|
115
|
+
const t = tryText(f);
|
|
116
|
+
if (t && /\bFlask\s*\(|\bFastAPI\s*\(/.test(t)) add(f, "wsgi-app");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- Java: Spring Boot (convention-named files only, never a repo grep) ----
|
|
121
|
+
for (const f of files) {
|
|
122
|
+
if (/Application\.java$/.test(f)) {
|
|
123
|
+
const t = tryText(f);
|
|
124
|
+
if (t && /@SpringBootApplication/.test(t)) add(f, "spring-boot", "main");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return hints;
|
|
129
|
+
}
|
package/codemap/exclude.mjs
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
// Source exclusion for the codemap build.
|
|
2
|
-
//
|
|
3
|
-
// Two mechanisms, both matching on a symbol's repo-relative POSIX file path:
|
|
4
|
-
// 1. .gitignore — the default. Whatever git ignores (vendored copies, build
|
|
5
|
-
// output, dependency dumps) never enters the graph. Uses `git check-ignore`
|
|
6
|
-
// so the semantics are exactly git's, including un-committed .gitignore
|
|
7
|
-
// edits (check-ignore reads the working tree).
|
|
8
|
-
// 2. --exclude <glob> — explicit, repeatable, for paths git still tracks that
|
|
9
|
-
// you nonetheless don't want in the graph.
|
|
10
|
-
// Neither touches the raw indexer output; excluded symbols are dropped before
|
|
11
|
-
// emit, and the edge tables (which key on surviving anchors) follow.
|
|
12
|
-
|
|
13
|
-
import { execFileSync as _execFileSync } from "node:child_process";
|
|
14
|
-
|
|
15
|
-
// Minimal gitignore-flavoured glob: `**` spans path separators, `*` stays
|
|
16
|
-
// within a segment, everything else is literal. Anchored to the whole path.
|
|
17
|
-
export function globToRegExp(glob) {
|
|
18
|
-
let re = "";
|
|
19
|
-
for (let i = 0; i < glob.length; i++) {
|
|
20
|
-
const c = glob[i];
|
|
21
|
-
if (c === "*") {
|
|
22
|
-
if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++; }
|
|
23
|
-
else re += "[^/]*";
|
|
24
|
-
} else if ("\\^$+?.()|{}[]".includes(c)) {
|
|
25
|
-
re += "\\" + c;
|
|
26
|
-
} else {
|
|
27
|
-
re += c;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return new RegExp("^" + re + "$");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Ask git which of `files` it ignores. Returns a Set of the ignored paths.
|
|
34
|
-
// check-ignore exits 1 when nothing matches and 128 when git is unavailable /
|
|
35
|
-
// the dir is not a repo — both mean "ignore nothing", not a build failure.
|
|
36
|
-
export function gitIgnored(root, files, exec = _execFileSync) {
|
|
37
|
-
if (!files.length) return new Set();
|
|
38
|
-
try {
|
|
39
|
-
const out = exec("git", ["-C", root, "check-ignore", "--stdin"], { input: files.join("\n"), encoding: "utf8" });
|
|
40
|
-
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
41
|
-
} catch (e) {
|
|
42
|
-
const out = e && e.stdout ? String(e.stdout) : "";
|
|
43
|
-
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Build a predicate (file) => shouldExclude.
|
|
48
|
-
export function makeExcluder({ root, globs = [], gitignore = true, files = [], exec } = {}) {
|
|
49
|
-
const res = globs.map(globToRegExp);
|
|
50
|
-
const ignored = gitignore ? gitIgnored(root, files, exec) : new Set();
|
|
51
|
-
return (file) => ignored.has(file) || res.some((r) => r.test(file));
|
|
52
|
-
}
|
|
1
|
+
// Source exclusion for the codemap build.
|
|
2
|
+
//
|
|
3
|
+
// Two mechanisms, both matching on a symbol's repo-relative POSIX file path:
|
|
4
|
+
// 1. .gitignore — the default. Whatever git ignores (vendored copies, build
|
|
5
|
+
// output, dependency dumps) never enters the graph. Uses `git check-ignore`
|
|
6
|
+
// so the semantics are exactly git's, including un-committed .gitignore
|
|
7
|
+
// edits (check-ignore reads the working tree).
|
|
8
|
+
// 2. --exclude <glob> — explicit, repeatable, for paths git still tracks that
|
|
9
|
+
// you nonetheless don't want in the graph.
|
|
10
|
+
// Neither touches the raw indexer output; excluded symbols are dropped before
|
|
11
|
+
// emit, and the edge tables (which key on surviving anchors) follow.
|
|
12
|
+
|
|
13
|
+
import { execFileSync as _execFileSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
// Minimal gitignore-flavoured glob: `**` spans path separators, `*` stays
|
|
16
|
+
// within a segment, everything else is literal. Anchored to the whole path.
|
|
17
|
+
export function globToRegExp(glob) {
|
|
18
|
+
let re = "";
|
|
19
|
+
for (let i = 0; i < glob.length; i++) {
|
|
20
|
+
const c = glob[i];
|
|
21
|
+
if (c === "*") {
|
|
22
|
+
if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++; }
|
|
23
|
+
else re += "[^/]*";
|
|
24
|
+
} else if ("\\^$+?.()|{}[]".includes(c)) {
|
|
25
|
+
re += "\\" + c;
|
|
26
|
+
} else {
|
|
27
|
+
re += c;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return new RegExp("^" + re + "$");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ask git which of `files` it ignores. Returns a Set of the ignored paths.
|
|
34
|
+
// check-ignore exits 1 when nothing matches and 128 when git is unavailable /
|
|
35
|
+
// the dir is not a repo — both mean "ignore nothing", not a build failure.
|
|
36
|
+
export function gitIgnored(root, files, exec = _execFileSync) {
|
|
37
|
+
if (!files.length) return new Set();
|
|
38
|
+
try {
|
|
39
|
+
const out = exec("git", ["-C", root, "check-ignore", "--stdin"], { input: files.join("\n"), encoding: "utf8" });
|
|
40
|
+
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
const out = e && e.stdout ? String(e.stdout) : "";
|
|
43
|
+
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build a predicate (file) => shouldExclude.
|
|
48
|
+
export function makeExcluder({ root, globs = [], gitignore = true, files = [], exec } = {}) {
|
|
49
|
+
const res = globs.map(globToRegExp);
|
|
50
|
+
const ignored = gitignore ? gitIgnored(root, files, exec) : new Set();
|
|
51
|
+
return (file) => ignored.has(file) || res.some((r) => r.test(file));
|
|
52
|
+
}
|
package/codemap/find.mjs
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// geml codemap find <name> [codemap-dir]
|
|
3
|
-
//
|
|
4
|
-
// Locate a function/class by (substring, case-insensitive) name in a built
|
|
5
|
-
// codemap. Prints each candidate as <name> \t <doc>#<id> \t <src> — the
|
|
6
|
-
// document + block id to open, and the true source location. NO browser: pure
|
|
7
|
-
// stdout, so it pipes/greps. `dir` defaults to ./.geml-code-graph.
|
|
8
|
-
//
|
|
9
|
-
// Same index the MCP `resolve_name` tool and the viewer search box use
|
|
10
|
-
// (_index/name-lookup.json); a name with several rows is real ambiguity
|
|
11
|
-
// (overloads / same short name across classes) — every candidate is printed.
|
|
12
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
// `find x | head` closes stdout after a few lines — that is normal pipe
|
|
16
|
-
// usage, not an error (POSIX would kill us silently with SIGPIPE; Windows
|
|
17
|
-
// node surfaces it as an EPIPE error event): exit quietly instead of
|
|
18
|
-
// crashing with an unhandled-error stack trace.
|
|
19
|
-
process.stdout.on("error", (e) => { if (e.code === "EPIPE") process.exit(0); throw e; });
|
|
20
|
-
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
23
|
-
console.error("usage: geml codemap find <name> [codemap-dir] # locate a symbol by substring name (dir defaults to ./.geml-code-graph)");
|
|
24
|
-
process.exit(args.length ? 0 : 2);
|
|
25
|
-
}
|
|
26
|
-
const query = args[0];
|
|
27
|
-
const dir = args[1] || ".geml-code-graph";
|
|
28
|
-
const lookupPath = join(dir, "_index", "name-lookup.json");
|
|
29
|
-
if (!existsSync(lookupPath)) {
|
|
30
|
-
console.error(`no name-lookup at ${lookupPath} — build the codemap first (geml codemap build)`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
const lookup = JSON.parse(readFileSync(lookupPath, "utf8"));
|
|
34
|
-
const q = query.toLowerCase();
|
|
35
|
-
const names = Object.keys(lookup).filter((n) => n.toLowerCase().includes(q)).sort();
|
|
36
|
-
if (!names.length) { console.error(`no symbol matching "${query}"`); process.exit(1); }
|
|
37
|
-
|
|
38
|
-
// src= lives on the block header line in the doc; read each doc once, index by id.
|
|
39
|
-
const docCache = new Map(); // doc -> Map(id -> src)
|
|
40
|
-
const srcOf = (doc, id) => {
|
|
41
|
-
if (!docCache.has(doc)) {
|
|
42
|
-
const map = new Map();
|
|
43
|
-
try {
|
|
44
|
-
const text = readFileSync(join(dir, doc), "utf8");
|
|
45
|
-
// src= may be quoted or a bare token (path#Lx-y, no spaces).
|
|
46
|
-
const re = /\{#([A-Za-z0-9._-]+)\b[^}]*?\bsrc=(?:"([^"]+)"|([^\s}]+))/g;
|
|
47
|
-
let m;
|
|
48
|
-
while ((m = re.exec(text))) map.set(m[1], m[2] || m[3]);
|
|
49
|
-
} catch { /* doc unreadable — skip src */ }
|
|
50
|
-
docCache.set(doc, map);
|
|
51
|
-
}
|
|
52
|
-
return docCache.get(doc).get(id) || "";
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
let n = 0;
|
|
56
|
-
for (const name of names) {
|
|
57
|
-
for (const c of lookup[name]) {
|
|
58
|
-
const src = srcOf(c.doc, c.id);
|
|
59
|
-
process.stdout.write(`${name}\t${c.doc}#${c.id}${src ? `\t${src}` : ""}\n`);
|
|
60
|
-
n++;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
console.error(`\n${n} match(es) for "${query}" across ${names.length} name(s).`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// geml codemap find <name> [codemap-dir]
|
|
3
|
+
//
|
|
4
|
+
// Locate a function/class by (substring, case-insensitive) name in a built
|
|
5
|
+
// codemap. Prints each candidate as <name> \t <doc>#<id> \t <src> — the
|
|
6
|
+
// document + block id to open, and the true source location. NO browser: pure
|
|
7
|
+
// stdout, so it pipes/greps. `dir` defaults to ./.geml-code-graph.
|
|
8
|
+
//
|
|
9
|
+
// Same index the MCP `resolve_name` tool and the viewer search box use
|
|
10
|
+
// (_index/name-lookup.json); a name with several rows is real ambiguity
|
|
11
|
+
// (overloads / same short name across classes) — every candidate is printed.
|
|
12
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// `find x | head` closes stdout after a few lines — that is normal pipe
|
|
16
|
+
// usage, not an error (POSIX would kill us silently with SIGPIPE; Windows
|
|
17
|
+
// node surfaces it as an EPIPE error event): exit quietly instead of
|
|
18
|
+
// crashing with an unhandled-error stack trace.
|
|
19
|
+
process.stdout.on("error", (e) => { if (e.code === "EPIPE") process.exit(0); throw e; });
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
23
|
+
console.error("usage: geml codemap find <name> [codemap-dir] # locate a symbol by substring name (dir defaults to ./.geml-code-graph)");
|
|
24
|
+
process.exit(args.length ? 0 : 2);
|
|
25
|
+
}
|
|
26
|
+
const query = args[0];
|
|
27
|
+
const dir = args[1] || ".geml-code-graph";
|
|
28
|
+
const lookupPath = join(dir, "_index", "name-lookup.json");
|
|
29
|
+
if (!existsSync(lookupPath)) {
|
|
30
|
+
console.error(`no name-lookup at ${lookupPath} — build the codemap first (geml codemap build)`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const lookup = JSON.parse(readFileSync(lookupPath, "utf8"));
|
|
34
|
+
const q = query.toLowerCase();
|
|
35
|
+
const names = Object.keys(lookup).filter((n) => n.toLowerCase().includes(q)).sort();
|
|
36
|
+
if (!names.length) { console.error(`no symbol matching "${query}"`); process.exit(1); }
|
|
37
|
+
|
|
38
|
+
// src= lives on the block header line in the doc; read each doc once, index by id.
|
|
39
|
+
const docCache = new Map(); // doc -> Map(id -> src)
|
|
40
|
+
const srcOf = (doc, id) => {
|
|
41
|
+
if (!docCache.has(doc)) {
|
|
42
|
+
const map = new Map();
|
|
43
|
+
try {
|
|
44
|
+
const text = readFileSync(join(dir, doc), "utf8");
|
|
45
|
+
// src= may be quoted or a bare token (path#Lx-y, no spaces).
|
|
46
|
+
const re = /\{#([A-Za-z0-9._-]+)\b[^}]*?\bsrc=(?:"([^"]+)"|([^\s}]+))/g;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = re.exec(text))) map.set(m[1], m[2] || m[3]);
|
|
49
|
+
} catch { /* doc unreadable — skip src */ }
|
|
50
|
+
docCache.set(doc, map);
|
|
51
|
+
}
|
|
52
|
+
return docCache.get(doc).get(id) || "";
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let n = 0;
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
for (const c of lookup[name]) {
|
|
58
|
+
const src = srcOf(c.doc, c.id);
|
|
59
|
+
process.stdout.write(`${name}\t${c.doc}#${c.id}${src ? `\t${src}` : ""}\n`);
|
|
60
|
+
n++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.error(`\n${n} match(es) for "${query}" across ${names.length} name(s).`);
|