@geml/geml 1.0.0 → 1.3.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 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,72 +1,109 @@
1
- # @geml/geml
2
-
3
- Reference parser, validator, renderer, and CLI for **GEML** — the General
4
- Expressive Markup Language: a plain-text document format that stays legible to
5
- people and reliable for machines. Every kind of structured content — code,
6
- tables, diagrams, math, callouts, metadata — is carried on **one** primitive,
7
- the typed block:
8
-
9
- ```
10
- === code {#hello lang=python}
11
- print("hi")
12
- ===
13
- ```
14
-
15
- References are checked at build time (a dangling `#id` is an error, not a silent
16
- dead link), and the parser emits a document-model JSON with `diagnostics`, so
17
- agents and CI get a structured pass/fail signal.
18
-
19
- ## Install
20
-
21
- ```sh
22
- npm install -g @geml/geml # global CLI — installs the `geml` command
23
- # or, per project:
24
- npm install @geml/geml # library + local bin
25
- ```
26
-
27
- Requires Node 18.
28
-
29
- ## CLI
30
-
31
- Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
32
- `1` document/operation error · `2` usage error.
33
-
34
- ```sh
35
- geml check file.geml # validate only: diagnostics + exit code
36
- geml check --json file.geml # machine-readable: diagnostics array (or {"error":…} on IO failure)
37
- geml file.geml # full document-model JSON
38
- geml render file.geml -o out.html # one self-contained, interactive HTML file
39
- geml export file.geml -o out.md # project to GitHub-Flavored Markdown (lossy; notes on stderr)
40
- geml convert in.md -o out.geml # Markdown -> GEML
41
- geml fmt file.geml # canonical re-format (idempotent)
42
- geml history <commit|verify|show|restore> file.geml [...] # .gemlhistory sidecar
43
- geml --help | --version # --version --json prints {"parser","spec"}
44
- ```
45
-
46
- The agent loop: write `.geml` → `geml check` → fix on non-zero → done.
47
-
48
- ## Library
49
-
50
- ```js
51
- import { parse, serialize, renderHtml, gemlToMd, mdToGeml } from "@geml/geml";
52
-
53
- const doc = parse(src); // { kind:"document", children, ids, diagnostics }
54
- const ok = !doc.diagnostics.some(d => d.severity === "error");
55
- const html = renderHtml(doc); // one self-contained HTML string
56
- const md = gemlToMd(doc).md; // GitHub-Flavored Markdown (lossy)
57
- const geml = mdToGeml(markdown).geml; // the inverse
58
- const canonical = serialize(doc); // GEML text; parse(serialize(parse(x))) is stable
59
- ```
60
-
61
- `parse(src, { resolveDoc })` enables cross-document reference checking pass a
62
- function that returns another file's source by path (or `null`).
63
-
64
- ## Documentation
65
-
66
- Full normative spec, history-sidecar spec, and format comparison live in the
67
- [repository](https://github.com/xiongjy2104/geml-spec). The spec is itself
68
- written in GEML (`GEML-spec.geml`) and parsed clean on every test run.
69
-
70
- ## License
71
-
72
- MIT.
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
+ Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
50
+ `1` document/operation error · `2` usage error.
51
+
52
+ ```sh
53
+ geml get file.geml '#id' # print ONE block by id a heading id yields its whole section
54
+ geml set file.geml '#id' --from new.geml # replace just that block; re-parsed, refused if it breaks the doc
55
+ geml check file.geml # validate only: diagnostics + exit code
56
+ geml check --json file.geml # machine-readable: diagnostics array (or {"error":…} on IO failure)
57
+ geml file.geml # full document-model JSON
58
+ geml history <commit|verify|show|restore|log> file.geml [...] # .gemlhistory version sidecar
59
+ geml revert file.geml '#id' [--to -1] # roll ONE block back to an earlier revision (-N | latest | id)
60
+ geml render file.geml -o out.html # one self-contained, interactive HTML file
61
+ geml export file.geml -o out.md # project to GitHub-Flavored Markdown (lossy; notes on stderr)
62
+ geml convert in.md -o out.geml # Markdown -> GEML
63
+ geml fmt file.geml # canonical re-format (idempotent)
64
+ geml codemap <build|verify|render|serve|refresh|find|mcp> # your codebase's call graph as GEML docs
65
+ geml --help | --version # --version --json prints {"parser","spec"}
66
+ ```
67
+
68
+ The agent loop: `geml get` a block edit it `geml set` (guarded splice) →
69
+ `geml check` → `geml history commit` — small, precise, verifiable edits.
70
+
71
+ A **heading's** `#id` addresses its whole **section** — the heading line through
72
+ the line before the next heading of the same-or-higher level — so the prose
73
+ under a heading is block-editable with no extra syntax.
74
+ Spans overlap: blocks nested in the section keep their own ids, and a `set` on
75
+ the section that drops one of them is refused by the guard. `get --json` on a
76
+ heading covers the same content as the raw span: a section envelope
77
+ `{kind:"section", id, level, blocks:[heading, …its section's blocks]}` (a
78
+ block/footnote id still prints its single model node). `--head` narrows
79
+ `get`/`set`/`revert` to ANY id's head line — a heading's line, or a typed
80
+ block's opening fence line, so an agent renames a heading or edits a block's
81
+ attributes (caption, compute, …) without touching the body. Convention: keep
82
+ the document title in `=== meta` (`title = "…"`), not an H1 — a lone top-level
83
+ `#` section is the whole document, the telltale that it is really a title.
84
+
85
+ ## Library
86
+
87
+ ```js
88
+ import { parse, serialize, renderHtml, gemlToMd, mdToGeml } from "@geml/geml";
89
+
90
+ const doc = parse(src); // { kind:"document", children, ids, diagnostics }
91
+ const ok = !doc.diagnostics.some(d => d.severity === "error");
92
+ const html = renderHtml(doc); // one self-contained HTML string
93
+ const md = gemlToMd(doc).md; // GitHub-Flavored Markdown (lossy)
94
+ const geml = mdToGeml(markdown).geml; // the inverse
95
+ const canonical = serialize(doc); // GEML text; parse(serialize(parse(x))) is stable
96
+ ```
97
+
98
+ `parse(src, { resolveDoc })` enables cross-document reference checking — pass a
99
+ function that returns another file's source by path (or `null`).
100
+
101
+ ## Documentation
102
+
103
+ Full normative spec, history-sidecar spec, and format comparison live in the
104
+ [repository](https://github.com/geml-spec/geml). The spec is itself
105
+ written in GEML (`GEML-spec.geml`) and parsed clean on every test run.
106
+
107
+ ## License
108
+
109
+ MIT.
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }