@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.
- package/LICENSE +21 -21
- package/README.md +155 -109
- 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 -579
- package/codemap/cross-stack.mjs +303 -0
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +480 -432
- 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 -272
- 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 -143
- package/dist/block-edit.d.ts +1 -0
- package/dist/block-edit.js +112 -0
- package/dist/geml.js +759 -161
- package/dist/render.d.ts +2 -2
- package/dist/render.js +261 -178
- package/package.json +1 -2
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 GEML contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GEML contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,109 +1,155 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-dark.svg">
|
|
4
|
-
<img src="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-light.svg" alt="GEML" width="300">
|
|
5
|
-
</picture>
|
|
6
|
-
</p>
|
|
7
|
-
|
|
8
|
-
# @geml/geml
|
|
9
|
-
|
|
10
|
-
The reference parser, validator, renderer, and CLI for **GEML** (General
|
|
11
|
-
Expressive Markup Language) — **one format, two readers.** People and AI agents
|
|
12
|
-
co-write the same document: plain text that stays legible for people, and
|
|
13
|
-
**addressable, verifiable, and versioned** for machines.
|
|
14
|
-
|
|
15
|
-
Every kind of structured content — code, tables, diagrams, math, callouts,
|
|
16
|
-
metadata — rides on **one** primitive, the typed block:
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
=== code {#hello lang=python}
|
|
20
|
-
print("hi")
|
|
21
|
-
===
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
- **Addressable** — every block has an `#id`; `geml get` / `geml set '#id'`
|
|
25
|
-
read or patch one section without re-emitting the whole file (on this repo's
|
|
26
|
-
own spec, ~**31× less context** than shipping the whole document).
|
|
27
|
-
- **Verifiable** — references are checked at build time (a dangling `#id` is an
|
|
28
|
-
error, not a silent dead link), and the parser emits a document-model JSON
|
|
29
|
-
with a `diagnostics` array, so agents and CI get a structured pass/fail signal.
|
|
30
|
-
- **Versioned** — `geml history` and `geml revert` snapshot and rewind
|
|
31
|
-
revisions over a plain-text `.gemlhistory` sidecar.
|
|
32
|
-
|
|
33
|
-
Try the format in the [playground](https://geml-spec.github.io/geml/playground/)
|
|
34
|
-
— no install. Full pitch, spec, and format comparison live in the
|
|
35
|
-
[repository](https://github.com/geml-spec/geml).
|
|
36
|
-
|
|
37
|
-
## Install
|
|
38
|
-
|
|
39
|
-
```sh
|
|
40
|
-
npm install -g @geml/geml # global CLI — installs the `geml` command
|
|
41
|
-
# or, per project:
|
|
42
|
-
npm install @geml/geml # library + local bin
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Requires Node ≥ 22.
|
|
46
|
-
|
|
47
|
-
## CLI
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
geml
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-dark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-light.svg" alt="GEML" width="300">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
# @geml/geml
|
|
9
|
+
|
|
10
|
+
The reference parser, validator, renderer, and CLI for **GEML** (General
|
|
11
|
+
Expressive Markup Language) — **one format, two readers.** People and AI agents
|
|
12
|
+
co-write the same document: plain text that stays legible for people, and
|
|
13
|
+
**addressable, verifiable, and versioned** for machines.
|
|
14
|
+
|
|
15
|
+
Every kind of structured content — code, tables, diagrams, math, callouts,
|
|
16
|
+
metadata — rides on **one** primitive, the typed block:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
=== code {#hello lang=python}
|
|
20
|
+
print("hi")
|
|
21
|
+
===
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Addressable** — every block has an `#id`; `geml get` / `geml set '#id'`
|
|
25
|
+
read or patch one section without re-emitting the whole file (on this repo's
|
|
26
|
+
own spec, ~**31× less context** than shipping the whole document).
|
|
27
|
+
- **Verifiable** — references are checked at build time (a dangling `#id` is an
|
|
28
|
+
error, not a silent dead link), and the parser emits a document-model JSON
|
|
29
|
+
with a `diagnostics` array, so agents and CI get a structured pass/fail signal.
|
|
30
|
+
- **Versioned** — `geml history` and `geml revert` snapshot and rewind
|
|
31
|
+
revisions over a plain-text `.gemlhistory` sidecar.
|
|
32
|
+
|
|
33
|
+
Try the format in the [playground](https://geml-spec.github.io/geml/playground/)
|
|
34
|
+
— no install. Full pitch, spec, and format comparison live in the
|
|
35
|
+
[repository](https://github.com/geml-spec/geml).
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install -g @geml/geml # global CLI — installs the `geml` command
|
|
41
|
+
# or, per project:
|
|
42
|
+
npm install @geml/geml # library + local bin
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Node ≥ 22.
|
|
46
|
+
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
The CLI is built around one question: can a single agent author and maintain an
|
|
50
|
+
entire `.geml` file from the command line — create, add, edit, delete, and copy
|
|
51
|
+
blocks in from other files? Three tests keep the command set honest:
|
|
52
|
+
|
|
53
|
+
- **Complete** — every step of a document's life has a verb, so an agent never
|
|
54
|
+
rewrites the whole file to change one block.
|
|
55
|
+
- **Ergonomic** — few flags, sensible defaults, and pipeline-friendly I/O, so
|
|
56
|
+
multi-step edits chain without ceremony.
|
|
57
|
+
- **Consistent** — behavior is uniform and predictable: name a target `#id` and
|
|
58
|
+
the content adopts it, every write is guarded, a file is edited in place while
|
|
59
|
+
`-` streams to stdout.
|
|
60
|
+
|
|
61
|
+
Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
|
|
62
|
+
`1` document/operation error · `2` usage error.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
geml doc.geml # document-model JSON (default --to json)
|
|
66
|
+
geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML
|
|
67
|
+
geml get doc.geml ['#id'] # list addressable ids, or print one block (heading id = its section)
|
|
68
|
+
geml set doc.geml '#id' [--head|--body] [--in F[#src]] # replace a block's content (id kept)
|
|
69
|
+
geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment
|
|
70
|
+
geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks
|
|
71
|
+
geml rename doc.geml '#old' '#new' # rename an id + every reference to it
|
|
72
|
+
geml revert doc.geml '#id' [--rev -1] # undo a block: splice / resurrect / remove
|
|
73
|
+
geml check doc.geml [--root <dir>] # validate only: diagnostics + exit code (--json for the array)
|
|
74
|
+
geml history <commit|verify|show|restore|log> doc.geml [...] # .gemlhistory version sidecar
|
|
75
|
+
geml codemap <build|verify|render|serve|refresh|find|mcp> # your codebase's call graph as GEML docs
|
|
76
|
+
geml --help | --version # --version --json prints {"parser","spec"}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The agent loop: `geml get` a block → `set`/`add`/`delete`/`rename` it →
|
|
80
|
+
`geml check` → `geml history commit` — small, precise, verifiable edits.
|
|
81
|
+
|
|
82
|
+
Conversion is one entry — `geml <file> [--to json|html|md|geml]`; the input
|
|
83
|
+
format is inferred (`--from` overrides > extension > GEML), the target is `--to`
|
|
84
|
+
(default: GEML → JSON, Markdown → GEML), and `-o` names the output path.
|
|
85
|
+
|
|
86
|
+
`set` and `add` take their content from `--in F` (F's block whose id equals the
|
|
87
|
+
target), `--in F#src` (F's block `#src`), or stdin (raw bytes). `set` **replaces
|
|
88
|
+
a whole block** and normalizes the content's id to the target — so you can fork
|
|
89
|
+
any block into this slot without hand-editing its id (`--head` swaps just the
|
|
90
|
+
head line, `--body` just the body). `add` **inserts a fragment** (one or more
|
|
91
|
+
blocks, or bare prose) at `--append` / `--before #id` / `--after #id`, keeping
|
|
92
|
+
the content's own ids (a collision is refused). `delete` removes one or more
|
|
93
|
+
ids; `rename` rewrites an id's declaration and every reference to it.
|
|
94
|
+
|
|
95
|
+
Mutations (`set`/`add`/`delete`/`rename`) write the **whole updated document**:
|
|
96
|
+
in place when the input is a file, or to **stdout** when the input is `-`; `-o`
|
|
97
|
+
redirects the write (`-o -` forces stdout), so edits pipe cleanly. Every write
|
|
98
|
+
is guarded — re-parsed and refused if it would break the document or drop an id
|
|
99
|
+
(a reference left dangling by `delete` is a warning, not a refusal; `geml check`
|
|
100
|
+
flags it later).
|
|
101
|
+
|
|
102
|
+
Undo is `revert`, which reconciles one block to a past revision (`--rev`, default
|
|
103
|
+
`-1`): it **splices** back changed content, **resurrects** a deleted block (placed
|
|
104
|
+
by its old neighbours, or `--append`/`--before`/`--after`), or **removes** a block
|
|
105
|
+
that did not exist then. So each forward edit has an inverse:
|
|
106
|
+
|
|
107
|
+
| forward edit | undo |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `set #id` | `revert #id` (splice) |
|
|
110
|
+
| `delete #id` | `revert #id` (resurrect) |
|
|
111
|
+
| `add #id` | `revert #id` (remove) — or `delete #id` |
|
|
112
|
+
| `rename #old #new` | `rename #new #old` (self-inverse) |
|
|
113
|
+
|
|
114
|
+
`revert` reads the `.gemlhistory` sidecar, so `set`/`delete`/`add` undo needs a
|
|
115
|
+
prior `geml history commit`; `rename` is its own inverse and needs no history.
|
|
116
|
+
|
|
117
|
+
A **heading's** `#id` addresses its whole **section** — the heading line through
|
|
118
|
+
the line before the next heading of the same-or-higher level — so the prose
|
|
119
|
+
under a heading is block-editable with no extra syntax.
|
|
120
|
+
Spans overlap: blocks nested in the section keep their own ids, and a `set` on
|
|
121
|
+
the section that drops one of them is refused by the guard. `get --json` on a
|
|
122
|
+
heading covers the same content as the raw span: a section envelope
|
|
123
|
+
`{kind:"section", id, level, blocks:[heading, …its section's blocks]}` (a
|
|
124
|
+
block/footnote id still prints its single model node). `--head` narrows
|
|
125
|
+
`get`/`set`/`revert` to ANY id's head line — a heading's line, or a typed
|
|
126
|
+
block's opening fence line, so an agent renames a heading or edits a block's
|
|
127
|
+
attributes (caption, compute, …) without touching the body. Convention: keep
|
|
128
|
+
the document title in `=== meta` (`title = "…"`), not an H1 — a lone top-level
|
|
129
|
+
`#` section is the whole document, the telltale that it is really a title.
|
|
130
|
+
|
|
131
|
+
## Library
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import { parse, serialize, renderHtml, gemlToMd, mdToGeml } from "@geml/geml";
|
|
135
|
+
|
|
136
|
+
const doc = parse(src); // { kind:"document", children, ids, diagnostics }
|
|
137
|
+
const ok = !doc.diagnostics.some(d => d.severity === "error");
|
|
138
|
+
const html = renderHtml(doc); // one self-contained HTML string
|
|
139
|
+
const md = gemlToMd(doc).md; // GitHub-Flavored Markdown (lossy)
|
|
140
|
+
const geml = mdToGeml(markdown).geml; // the inverse
|
|
141
|
+
const canonical = serialize(doc); // GEML text; parse(serialize(parse(x))) is stable
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`parse(src, { resolveDoc })` enables cross-document reference checking — pass a
|
|
145
|
+
function that returns another file's source by path (or `null`).
|
|
146
|
+
|
|
147
|
+
## Documentation
|
|
148
|
+
|
|
149
|
+
Full normative spec, history-sidecar spec, and format comparison live in the
|
|
150
|
+
[repository](https://github.com/geml-spec/geml). The spec is itself
|
|
151
|
+
written in GEML (`GEML-spec.geml`) and parsed clean on every test run.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT.
|
package/codemap/adapters/crg.mjs
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
// geml-code-graph adapter: code-review-graph SQLite (`graph.db`, tree-sitter based) →
|
|
2
|
-
// the exchange format of docs/DESIGN-geml-code-graph.md §3 (symbols + edges).
|
|
3
|
-
//
|
|
4
|
-
// Everything this adapter emits is syntax-level extraction, so per §3.3 every
|
|
5
|
-
// symbol and edge carries resolution:"heuristic"; resolved targets get
|
|
6
|
-
// confidence:"medium", unresolved ones become `to_text` rows (confidence:"low").
|
|
7
|
-
// CONTAINS is not an edge — containment is document structure (GEP-0002).
|
|
8
|
-
//
|
|
9
|
-
// Beyond the core schema, symbols may carry optional navigation flags the
|
|
10
|
-
// engine knows about: `entry` (a `main` function) and `flow_crit` (max
|
|
11
|
-
// criticality of execution flows entered at this symbol, when >= 0.6).
|
|
12
|
-
import { DatabaseSync } from "node:sqlite";
|
|
13
|
-
|
|
14
|
-
const EDGE_KIND = {
|
|
15
|
-
CALLS: "calls",
|
|
16
|
-
IMPORTS_FROM: "imports",
|
|
17
|
-
INHERITS: "inherits",
|
|
18
|
-
TESTED_BY: "tested-by",
|
|
19
|
-
REFERENCES: "references",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function extract({ db: dbPath, root }) {
|
|
23
|
-
const db = new DatabaseSync(dbPath);
|
|
24
|
-
try {
|
|
25
|
-
return extractFrom(db, root);
|
|
26
|
-
} finally {
|
|
27
|
-
// Always release the handle: an open DatabaseSync keeps graph.db locked for
|
|
28
|
-
// the process lifetime, so on Windows the caller cannot delete or replace
|
|
29
|
-
// it (EPERM). try/finally closes it even if extraction throws.
|
|
30
|
-
db.close();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function extractFrom(db, root) {
|
|
35
|
-
const rootFs = root.replace(/\\/g, "/").replace(/\/?$/, "/");
|
|
36
|
-
const rel = (p) => {
|
|
37
|
-
p = String(p).replace(/\\/g, "/");
|
|
38
|
-
return p.startsWith(rootFs) ? p.slice(rootFs.length) : p;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const rows = db.prepare(
|
|
42
|
-
"SELECT id, kind, name, qualified_name, file_path, line_start, line_end, language, is_test FROM nodes",
|
|
43
|
-
).all();
|
|
44
|
-
|
|
45
|
-
// anchor = "<lang>:<relfile>#<name>", File symbols just "<lang>:<relfile>".
|
|
46
|
-
// Same-file same-name collisions get ~2, ~3 … ordered by line_start so the
|
|
47
|
-
// numbering is stable across rebuilds (§4.2 / risk 3).
|
|
48
|
-
const byKey = new Map();
|
|
49
|
-
for (const r of rows) {
|
|
50
|
-
const key = `${r.language ?? "unknown"}:${rel(r.file_path)}#${r.name}`;
|
|
51
|
-
if (!byKey.has(key)) byKey.set(key, []);
|
|
52
|
-
byKey.get(key).push(r);
|
|
53
|
-
}
|
|
54
|
-
const anchorOf = new Map(); // node rowid -> anchor
|
|
55
|
-
for (const [key, list] of byKey) {
|
|
56
|
-
list.sort((a, b) => (a.line_start ?? 0) - (b.line_start ?? 0) || a.id - b.id);
|
|
57
|
-
list.forEach((r, i) => {
|
|
58
|
-
const base = r.kind === "File" ? key.slice(0, key.lastIndexOf("#")) : key;
|
|
59
|
-
anchorOf.set(r.id, i === 0 ? base : `${base}~${i + 1}`);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Navigation flags from engine-specific tables.
|
|
64
|
-
const mains = new Set(
|
|
65
|
-
db.prepare("SELECT id FROM nodes WHERE kind='Function' AND name='main'").all().map((r) => r.id),
|
|
66
|
-
);
|
|
67
|
-
const flowCrit = new Map();
|
|
68
|
-
try {
|
|
69
|
-
for (const r of db.prepare(
|
|
70
|
-
"SELECT entry_point_id id, max(criticality) c FROM flows GROUP BY 1 HAVING c >= 0.6",
|
|
71
|
-
).all()) flowCrit.set(r.id, r.c);
|
|
72
|
-
} catch { /* no flows table */ }
|
|
73
|
-
|
|
74
|
-
const symbols = rows.map((r) => {
|
|
75
|
-
const s = {
|
|
76
|
-
anchor: anchorOf.get(r.id),
|
|
77
|
-
lang: r.language ?? "unknown",
|
|
78
|
-
kind: r.kind,
|
|
79
|
-
name: r.name,
|
|
80
|
-
file: rel(r.file_path),
|
|
81
|
-
line_start: r.line_start ?? undefined,
|
|
82
|
-
line_end: r.line_end ?? undefined,
|
|
83
|
-
is_test: r.is_test ? true : undefined,
|
|
84
|
-
entry: mains.has(r.id) ? true : undefined,
|
|
85
|
-
flow_crit: flowCrit.get(r.id),
|
|
86
|
-
resolution: "heuristic",
|
|
87
|
-
};
|
|
88
|
-
return s;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const idByQual = new Map(rows.map((r) => [r.qualified_name, r.id]));
|
|
92
|
-
const edges = [];
|
|
93
|
-
for (const e of db.prepare(
|
|
94
|
-
"SELECT kind, source_qualified, target_qualified, file_path, line FROM edges",
|
|
95
|
-
).all()) {
|
|
96
|
-
const kind = EDGE_KIND[e.kind];
|
|
97
|
-
if (!kind) continue; // CONTAINS and anything unknown
|
|
98
|
-
const fromId = idByQual.get(e.source_qualified);
|
|
99
|
-
if (fromId === undefined) continue; // dangling source: nothing to attach to
|
|
100
|
-
const toId = idByQual.get(e.target_qualified);
|
|
101
|
-
const edge = {
|
|
102
|
-
kind,
|
|
103
|
-
from: anchorOf.get(fromId),
|
|
104
|
-
resolution: "heuristic",
|
|
105
|
-
site: { file: rel(e.file_path), line: e.line ?? 0 },
|
|
106
|
-
};
|
|
107
|
-
if (toId !== undefined) {
|
|
108
|
-
edge.to = anchorOf.get(toId);
|
|
109
|
-
edge.confidence = "medium";
|
|
110
|
-
} else {
|
|
111
|
-
// Keep only a readable short name for the unresolved target (§5.2's
|
|
112
|
-
// calls-unresolved line) — qualified names here are often paths.
|
|
113
|
-
edge.to_text = String(e.target_qualified).replace(/\\/g, "/").split("/").pop();
|
|
114
|
-
edge.confidence = "low";
|
|
115
|
-
}
|
|
116
|
-
edges.push(edge);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { symbols, edges };
|
|
120
|
-
}
|
|
1
|
+
// geml-code-graph adapter: code-review-graph SQLite (`graph.db`, tree-sitter based) →
|
|
2
|
+
// the exchange format of docs/DESIGN-geml-code-graph.md §3 (symbols + edges).
|
|
3
|
+
//
|
|
4
|
+
// Everything this adapter emits is syntax-level extraction, so per §3.3 every
|
|
5
|
+
// symbol and edge carries resolution:"heuristic"; resolved targets get
|
|
6
|
+
// confidence:"medium", unresolved ones become `to_text` rows (confidence:"low").
|
|
7
|
+
// CONTAINS is not an edge — containment is document structure (GEP-0002).
|
|
8
|
+
//
|
|
9
|
+
// Beyond the core schema, symbols may carry optional navigation flags the
|
|
10
|
+
// engine knows about: `entry` (a `main` function) and `flow_crit` (max
|
|
11
|
+
// criticality of execution flows entered at this symbol, when >= 0.6).
|
|
12
|
+
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
|
|
14
|
+
const EDGE_KIND = {
|
|
15
|
+
CALLS: "calls",
|
|
16
|
+
IMPORTS_FROM: "imports",
|
|
17
|
+
INHERITS: "inherits",
|
|
18
|
+
TESTED_BY: "tested-by",
|
|
19
|
+
REFERENCES: "references",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function extract({ db: dbPath, root }) {
|
|
23
|
+
const db = new DatabaseSync(dbPath);
|
|
24
|
+
try {
|
|
25
|
+
return extractFrom(db, root);
|
|
26
|
+
} finally {
|
|
27
|
+
// Always release the handle: an open DatabaseSync keeps graph.db locked for
|
|
28
|
+
// the process lifetime, so on Windows the caller cannot delete or replace
|
|
29
|
+
// it (EPERM). try/finally closes it even if extraction throws.
|
|
30
|
+
db.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractFrom(db, root) {
|
|
35
|
+
const rootFs = root.replace(/\\/g, "/").replace(/\/?$/, "/");
|
|
36
|
+
const rel = (p) => {
|
|
37
|
+
p = String(p).replace(/\\/g, "/");
|
|
38
|
+
return p.startsWith(rootFs) ? p.slice(rootFs.length) : p;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const rows = db.prepare(
|
|
42
|
+
"SELECT id, kind, name, qualified_name, file_path, line_start, line_end, language, is_test FROM nodes",
|
|
43
|
+
).all();
|
|
44
|
+
|
|
45
|
+
// anchor = "<lang>:<relfile>#<name>", File symbols just "<lang>:<relfile>".
|
|
46
|
+
// Same-file same-name collisions get ~2, ~3 … ordered by line_start so the
|
|
47
|
+
// numbering is stable across rebuilds (§4.2 / risk 3).
|
|
48
|
+
const byKey = new Map();
|
|
49
|
+
for (const r of rows) {
|
|
50
|
+
const key = `${r.language ?? "unknown"}:${rel(r.file_path)}#${r.name}`;
|
|
51
|
+
if (!byKey.has(key)) byKey.set(key, []);
|
|
52
|
+
byKey.get(key).push(r);
|
|
53
|
+
}
|
|
54
|
+
const anchorOf = new Map(); // node rowid -> anchor
|
|
55
|
+
for (const [key, list] of byKey) {
|
|
56
|
+
list.sort((a, b) => (a.line_start ?? 0) - (b.line_start ?? 0) || a.id - b.id);
|
|
57
|
+
list.forEach((r, i) => {
|
|
58
|
+
const base = r.kind === "File" ? key.slice(0, key.lastIndexOf("#")) : key;
|
|
59
|
+
anchorOf.set(r.id, i === 0 ? base : `${base}~${i + 1}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Navigation flags from engine-specific tables.
|
|
64
|
+
const mains = new Set(
|
|
65
|
+
db.prepare("SELECT id FROM nodes WHERE kind='Function' AND name='main'").all().map((r) => r.id),
|
|
66
|
+
);
|
|
67
|
+
const flowCrit = new Map();
|
|
68
|
+
try {
|
|
69
|
+
for (const r of db.prepare(
|
|
70
|
+
"SELECT entry_point_id id, max(criticality) c FROM flows GROUP BY 1 HAVING c >= 0.6",
|
|
71
|
+
).all()) flowCrit.set(r.id, r.c);
|
|
72
|
+
} catch { /* no flows table */ }
|
|
73
|
+
|
|
74
|
+
const symbols = rows.map((r) => {
|
|
75
|
+
const s = {
|
|
76
|
+
anchor: anchorOf.get(r.id),
|
|
77
|
+
lang: r.language ?? "unknown",
|
|
78
|
+
kind: r.kind,
|
|
79
|
+
name: r.name,
|
|
80
|
+
file: rel(r.file_path),
|
|
81
|
+
line_start: r.line_start ?? undefined,
|
|
82
|
+
line_end: r.line_end ?? undefined,
|
|
83
|
+
is_test: r.is_test ? true : undefined,
|
|
84
|
+
entry: mains.has(r.id) ? true : undefined,
|
|
85
|
+
flow_crit: flowCrit.get(r.id),
|
|
86
|
+
resolution: "heuristic",
|
|
87
|
+
};
|
|
88
|
+
return s;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const idByQual = new Map(rows.map((r) => [r.qualified_name, r.id]));
|
|
92
|
+
const edges = [];
|
|
93
|
+
for (const e of db.prepare(
|
|
94
|
+
"SELECT kind, source_qualified, target_qualified, file_path, line FROM edges",
|
|
95
|
+
).all()) {
|
|
96
|
+
const kind = EDGE_KIND[e.kind];
|
|
97
|
+
if (!kind) continue; // CONTAINS and anything unknown
|
|
98
|
+
const fromId = idByQual.get(e.source_qualified);
|
|
99
|
+
if (fromId === undefined) continue; // dangling source: nothing to attach to
|
|
100
|
+
const toId = idByQual.get(e.target_qualified);
|
|
101
|
+
const edge = {
|
|
102
|
+
kind,
|
|
103
|
+
from: anchorOf.get(fromId),
|
|
104
|
+
resolution: "heuristic",
|
|
105
|
+
site: { file: rel(e.file_path), line: e.line ?? 0 },
|
|
106
|
+
};
|
|
107
|
+
if (toId !== undefined) {
|
|
108
|
+
edge.to = anchorOf.get(toId);
|
|
109
|
+
edge.confidence = "medium";
|
|
110
|
+
} else {
|
|
111
|
+
// Keep only a readable short name for the unresolved target (§5.2's
|
|
112
|
+
// calls-unresolved line) — qualified names here are often paths.
|
|
113
|
+
edge.to_text = String(e.target_qualified).replace(/\\/g, "/").split("/").pop();
|
|
114
|
+
edge.confidence = "low";
|
|
115
|
+
}
|
|
116
|
+
edges.push(edge);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { symbols, edges };
|
|
120
|
+
}
|