@geml/geml 1.4.2 → 1.4.3
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 +2 -5
- package/dist/geml.js +191 -98
- 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 +528 -0
- package/dist/render.js +136 -136
- package/dist/table.d.ts +2 -0
- package/dist/table.js +11 -11
- package/package.json +62 -62
package/codemap/verify.mjs
CHANGED
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
-
//
|
|
4
|
-
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
-
// native references).
|
|
6
|
-
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
-
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
-
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
-
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
-
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
-
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
-
//
|
|
13
|
-
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
-
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
-
import { posix } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { spawnSync } from "node:child_process";
|
|
19
|
-
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
const flagI = args.indexOf("--geml");
|
|
22
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
-
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
-
process.exit(2);
|
|
25
|
-
}
|
|
26
|
-
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
-
const rootDir = resolve(dir);
|
|
28
|
-
|
|
29
|
-
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
-
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
-
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
-
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
-
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
-
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
-
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
-
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
-
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
-
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
-
// CRT rules for embedded " / trailing \.
|
|
40
|
-
const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
|
|
41
|
-
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
42
|
-
const runCheck = (file) => {
|
|
43
|
-
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
44
|
-
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, "check", file], { encoding: "utf8" });
|
|
45
|
-
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
46
|
-
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
47
|
-
// string (never an args array — that is the unescaped, injection-prone path).
|
|
48
|
-
if (process.platform === "win32") {
|
|
49
|
-
return spawnSync([q(cli), ...["check", file].map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
50
|
-
}
|
|
51
|
-
return spawnSync(cli, ["check", file], { encoding: "utf8" });
|
|
52
|
-
};
|
|
53
|
-
if (!existsSync(localParser)) {
|
|
54
|
-
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
58
|
-
|
|
59
|
-
const files = [];
|
|
60
|
-
const walk = (d) => {
|
|
61
|
-
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
62
|
-
const p = join(d, e.name);
|
|
63
|
-
if (e.isDirectory()) walk(p);
|
|
64
|
-
else if (e.name.endsWith(".geml")) files.push(p);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
walk(rootDir);
|
|
68
|
-
files.sort();
|
|
69
|
-
|
|
70
|
-
// ---- pass 1: geml check ----
|
|
71
|
-
let failed = 0;
|
|
72
|
-
for (const f of files) {
|
|
73
|
-
const r = runCheck(f);
|
|
74
|
-
if (r.status !== 0) {
|
|
75
|
-
failed++;
|
|
76
|
-
console.error(`FAIL ${f}`);
|
|
77
|
-
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---- pass 2: codemap profile references ----
|
|
82
|
-
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
83
|
-
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
84
|
-
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
85
|
-
// function — tolerated, nothing to resolve).
|
|
86
|
-
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
87
|
-
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
88
|
-
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
89
|
-
const collectIds = (blocks, ids) => {
|
|
90
|
-
for (const b of blocks) {
|
|
91
|
-
if (b.id) ids.add(b.id);
|
|
92
|
-
if (b.children) collectIds(b.children, ids);
|
|
93
|
-
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
for (const f of files) {
|
|
97
|
-
const doc = parse(readFileSync(f, "utf8"));
|
|
98
|
-
const ids = new Set();
|
|
99
|
-
collectIds(doc.children, ids);
|
|
100
|
-
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let refErrors = 0;
|
|
104
|
-
const err = (doc, where, msg) => {
|
|
105
|
-
refErrors++;
|
|
106
|
-
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
107
|
-
};
|
|
108
|
-
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
109
|
-
ref = String(ref).trim();
|
|
110
|
-
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
111
|
-
const h = ref.indexOf("#");
|
|
112
|
-
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
113
|
-
let targetDoc = fromDoc;
|
|
114
|
-
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
115
|
-
const id = ref.slice(h + 1);
|
|
116
|
-
const target = docs.get(targetDoc);
|
|
117
|
-
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
118
|
-
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
119
|
-
const bare = id.split(".")[0];
|
|
120
|
-
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
for (const [docPath, { blocks }] of docs) {
|
|
124
|
-
for (const b of blocks) {
|
|
125
|
-
if (b.kind !== "block") continue;
|
|
126
|
-
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
127
|
-
const lenient = LINK_TABLES.has(b.id);
|
|
128
|
-
const fromCol = b.table.columns.indexOf("from");
|
|
129
|
-
const toCol = b.table.columns.indexOf("to");
|
|
130
|
-
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
131
|
-
b.table.rows.forEach((row, i) => {
|
|
132
|
-
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
133
|
-
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
if (b.type === "meta" && b.data?.entry) {
|
|
137
|
-
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
138
|
-
checkRef(docPath, "meta entry", ref);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
console.error(
|
|
145
|
-
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
146
|
-
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
147
|
-
);
|
|
148
|
-
process.exit(failed || refErrors ? 1 : 0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
+
//
|
|
4
|
+
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
+
// native references).
|
|
6
|
+
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
+
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
+
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
+
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
+
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
+
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
+
//
|
|
13
|
+
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
+
import { posix } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const flagI = args.indexOf("--geml");
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
+
const rootDir = resolve(dir);
|
|
28
|
+
|
|
29
|
+
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
+
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
+
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
+
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
+
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
+
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
+
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
+
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
+
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
+
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
+
// CRT rules for embedded " / trailing \.
|
|
40
|
+
const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
|
|
41
|
+
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
42
|
+
const runCheck = (file) => {
|
|
43
|
+
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
44
|
+
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, "check", file], { encoding: "utf8" });
|
|
45
|
+
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
46
|
+
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
47
|
+
// string (never an args array — that is the unescaped, injection-prone path).
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
return spawnSync([q(cli), ...["check", file].map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
50
|
+
}
|
|
51
|
+
return spawnSync(cli, ["check", file], { encoding: "utf8" });
|
|
52
|
+
};
|
|
53
|
+
if (!existsSync(localParser)) {
|
|
54
|
+
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
58
|
+
|
|
59
|
+
const files = [];
|
|
60
|
+
const walk = (d) => {
|
|
61
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
62
|
+
const p = join(d, e.name);
|
|
63
|
+
if (e.isDirectory()) walk(p);
|
|
64
|
+
else if (e.name.endsWith(".geml")) files.push(p);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(rootDir);
|
|
68
|
+
files.sort();
|
|
69
|
+
|
|
70
|
+
// ---- pass 1: geml check ----
|
|
71
|
+
let failed = 0;
|
|
72
|
+
for (const f of files) {
|
|
73
|
+
const r = runCheck(f);
|
|
74
|
+
if (r.status !== 0) {
|
|
75
|
+
failed++;
|
|
76
|
+
console.error(`FAIL ${f}`);
|
|
77
|
+
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- pass 2: codemap profile references ----
|
|
82
|
+
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
83
|
+
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
84
|
+
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
85
|
+
// function — tolerated, nothing to resolve).
|
|
86
|
+
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
87
|
+
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
88
|
+
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
89
|
+
const collectIds = (blocks, ids) => {
|
|
90
|
+
for (const b of blocks) {
|
|
91
|
+
if (b.id) ids.add(b.id);
|
|
92
|
+
if (b.children) collectIds(b.children, ids);
|
|
93
|
+
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
for (const f of files) {
|
|
97
|
+
const doc = parse(readFileSync(f, "utf8"));
|
|
98
|
+
const ids = new Set();
|
|
99
|
+
collectIds(doc.children, ids);
|
|
100
|
+
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let refErrors = 0;
|
|
104
|
+
const err = (doc, where, msg) => {
|
|
105
|
+
refErrors++;
|
|
106
|
+
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
107
|
+
};
|
|
108
|
+
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
109
|
+
ref = String(ref).trim();
|
|
110
|
+
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
111
|
+
const h = ref.indexOf("#");
|
|
112
|
+
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
113
|
+
let targetDoc = fromDoc;
|
|
114
|
+
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
115
|
+
const id = ref.slice(h + 1);
|
|
116
|
+
const target = docs.get(targetDoc);
|
|
117
|
+
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
118
|
+
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
119
|
+
const bare = id.split(".")[0];
|
|
120
|
+
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const [docPath, { blocks }] of docs) {
|
|
124
|
+
for (const b of blocks) {
|
|
125
|
+
if (b.kind !== "block") continue;
|
|
126
|
+
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
127
|
+
const lenient = LINK_TABLES.has(b.id);
|
|
128
|
+
const fromCol = b.table.columns.indexOf("from");
|
|
129
|
+
const toCol = b.table.columns.indexOf("to");
|
|
130
|
+
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
131
|
+
b.table.rows.forEach((row, i) => {
|
|
132
|
+
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
133
|
+
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (b.type === "meta" && b.data?.entry) {
|
|
137
|
+
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
138
|
+
checkRef(docPath, "meta entry", ref);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.error(
|
|
145
|
+
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
146
|
+
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
147
|
+
);
|
|
148
|
+
process.exit(failed || refErrors ? 1 : 0);
|
package/dist/chart.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DiagnosticCode } from "./diagnostics.js";
|
|
1
2
|
import { type Value } from "./attrs.js";
|
|
2
3
|
import { type TableModel } from "./table.js";
|
|
3
4
|
export type ChartType = "bar" | "line" | "area" | "pie" | "scatter";
|
|
@@ -19,6 +20,7 @@ export interface ChartModel {
|
|
|
19
20
|
}
|
|
20
21
|
export interface ChartDiag {
|
|
21
22
|
severity: "error" | "warning";
|
|
23
|
+
code: DiagnosticCode;
|
|
22
24
|
message: string;
|
|
23
25
|
}
|
|
24
26
|
export interface ChartResult {
|
package/dist/chart.js
CHANGED
|
@@ -20,16 +20,16 @@ function str(v) {
|
|
|
20
20
|
}
|
|
21
21
|
export function buildChart(attrs, table) {
|
|
22
22
|
const diagnostics = [];
|
|
23
|
-
const err = (m) => diagnostics.push({ severity: "error", message: m });
|
|
24
|
-
const warn = (m) => diagnostics.push({ severity: "warning", message: m });
|
|
23
|
+
const err = (code, m) => diagnostics.push({ severity: "error", code, message: m });
|
|
24
|
+
const warn = (code, m) => diagnostics.push({ severity: "warning", code, message: m });
|
|
25
25
|
const fail = () => ({ model: null, diagnostics });
|
|
26
26
|
const typeRaw = str(attrs["type"]);
|
|
27
27
|
if (!typeRaw) {
|
|
28
|
-
err("chart: missing `type`");
|
|
28
|
+
err("chart-missing-type", "chart: missing `type`");
|
|
29
29
|
return fail();
|
|
30
30
|
}
|
|
31
31
|
if (!TYPES.has(typeRaw)) {
|
|
32
|
-
err(`chart: unknown type \`${typeRaw}\` (supported: bar, line, area, pie, scatter; use format=vega-lite for others)`);
|
|
32
|
+
err("chart-unknown-type", `chart: unknown type \`${typeRaw}\` (supported: bar, line, area, pie, scatter; use format=vega-lite for others)`);
|
|
33
33
|
return fail();
|
|
34
34
|
}
|
|
35
35
|
const type = typeRaw;
|
|
@@ -37,29 +37,29 @@ export function buildChart(attrs, table) {
|
|
|
37
37
|
// name is also wrong.
|
|
38
38
|
const rowsAttr = (str(attrs["rows"]) ?? "data");
|
|
39
39
|
if (!["data", "all", "summary"].includes(rowsAttr)) {
|
|
40
|
-
err(`chart: unknown rows scope \`${rowsAttr}\` (data|all|summary)`);
|
|
40
|
+
err("chart-unknown-rows-scope", `chart: unknown rows scope \`${rowsAttr}\` (data|all|summary)`);
|
|
41
41
|
return fail();
|
|
42
42
|
}
|
|
43
43
|
const x = str(attrs["x"]);
|
|
44
44
|
const yRaw = str(attrs["y"]);
|
|
45
45
|
if (!x)
|
|
46
|
-
err("chart: missing required channel `x`");
|
|
46
|
+
err("chart-missing-channel", "chart: missing required channel `x`");
|
|
47
47
|
if (!yRaw)
|
|
48
|
-
err("chart: missing required channel `y`");
|
|
48
|
+
err("chart-missing-channel", "chart: missing required channel `y`");
|
|
49
49
|
if (!x || !yRaw)
|
|
50
50
|
return fail();
|
|
51
51
|
let y = yRaw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
52
52
|
if (y.length === 0) {
|
|
53
|
-
err("chart: `y` lists no columns");
|
|
53
|
+
err("chart-empty-channel", "chart: `y` lists no columns");
|
|
54
54
|
return fail();
|
|
55
55
|
}
|
|
56
56
|
// Wrong-channel warnings (channel present but unused by this type).
|
|
57
57
|
if (attrs["size"] !== undefined && !USES[type].has("size"))
|
|
58
|
-
warn(`chart: \`size\` is ignored for type \`${type}\``);
|
|
58
|
+
warn("chart-unused-channel", `chart: \`size\` is ignored for type \`${type}\``);
|
|
59
59
|
if (attrs["series"] !== undefined && !USES[type].has("series"))
|
|
60
|
-
warn(`chart: \`series\` is ignored for type \`${type}\``);
|
|
60
|
+
warn("chart-unused-channel", `chart: \`series\` is ignored for type \`${type}\``);
|
|
61
61
|
if (type === "pie" && y.length > 1) {
|
|
62
|
-
warn("chart: pie uses a single `y`; extra columns ignored");
|
|
62
|
+
warn("chart-unused-channel", "chart: pie uses a single `y`; extra columns ignored");
|
|
63
63
|
y = [y[0]];
|
|
64
64
|
}
|
|
65
65
|
// Optional channels, only when used by this type.
|
|
@@ -69,7 +69,7 @@ export function buildChart(attrs, table) {
|
|
|
69
69
|
const idx = (name) => table.columns.indexOf(name);
|
|
70
70
|
for (const name of [x, ...y, ...(series ? [series] : []), ...(size ? [size] : [])]) {
|
|
71
71
|
if (idx(name) < 0)
|
|
72
|
-
err(`chart: column \`${name}\` not found in table`);
|
|
72
|
+
err("chart-unknown-column", `chart: column \`${name}\` not found in table`);
|
|
73
73
|
}
|
|
74
74
|
if (diagnostics.some((d) => d.severity === "error"))
|
|
75
75
|
return fail();
|
|
@@ -77,14 +77,14 @@ export function buildChart(attrs, table) {
|
|
|
77
77
|
let picked;
|
|
78
78
|
if (rowsAttr === "summary") {
|
|
79
79
|
if (!table.summary) {
|
|
80
|
-
err("chart: rows=summary but the table has no summary row");
|
|
80
|
+
err("chart-missing-summary-row", "chart: rows=summary but the table has no summary row");
|
|
81
81
|
return fail();
|
|
82
82
|
}
|
|
83
83
|
picked = [table.summary];
|
|
84
84
|
}
|
|
85
85
|
else if (rowsAttr === "all") {
|
|
86
86
|
if (!table.summary)
|
|
87
|
-
warn("chart: rows=all but the table has no summary row; using data rows");
|
|
87
|
+
warn("chart-summary-row-unavailable", "chart: rows=all but the table has no summary row; using data rows");
|
|
88
88
|
picked = table.summary ? [...table.rows, table.summary] : table.rows;
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
@@ -105,7 +105,7 @@ export function buildChart(attrs, table) {
|
|
|
105
105
|
for (const row of picked) {
|
|
106
106
|
const cells = numIs.map((i) => row[i]);
|
|
107
107
|
if (cells.some((cell) => (cell?.text ?? "") !== "" && typeof cell?.value !== "number")) {
|
|
108
|
-
err("chart: non-numeric value in a y column");
|
|
108
|
+
err("chart-non-numeric-value", "chart: non-numeric value in a y column");
|
|
109
109
|
return fail();
|
|
110
110
|
}
|
|
111
111
|
if (cells.some((cell) => (cell?.text ?? "") === ""))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "bad-span" | "span-outside-table" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value";
|
|
2
|
+
export interface Diagnostic {
|
|
3
|
+
severity: "error" | "warning";
|
|
4
|
+
code: DiagnosticCode;
|
|
5
|
+
message: string;
|
|
6
|
+
line: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const SEVERITY: Record<DiagnosticCode, "error" | "warning">;
|
|
9
|
+
export declare function normalizeSource(source: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// GEML reference parser — the diagnostic catalogue (spec Appendix A).
|
|
2
|
+
//
|
|
3
|
+
// Every diagnostic a conforming parser emits carries a STABLE `code` in
|
|
4
|
+
// addition to its human-readable `message`. The message is prose: it may be
|
|
5
|
+
// reworded, translated, or given more context between releases. The code is
|
|
6
|
+
// the contract — it is what a conformance test, an editor integration, or a CI
|
|
7
|
+
// gate matches on, and it is what the specification's Appendix A enumerates.
|
|
8
|
+
//
|
|
9
|
+
// `DiagnosticCode` below is the single source of truth: it is a closed union,
|
|
10
|
+
// so a misspelled or unregistered code is a compile error, and the spec's
|
|
11
|
+
// catalogue can be checked against this list mechanically.
|
|
12
|
+
// The severity each code is emitted with. The specification fixes severity per
|
|
13
|
+
// code (Appendix A), so this table is normative, not advisory: a second
|
|
14
|
+
// implementation reporting `unknown-block-type` as an error does not conform.
|
|
15
|
+
export const SEVERITY = {
|
|
16
|
+
"unterminated-block": "error",
|
|
17
|
+
"unknown-block-type": "warning",
|
|
18
|
+
"block-nesting-too-deep": "error",
|
|
19
|
+
"list-nesting-too-deep": "error",
|
|
20
|
+
"inline-nesting-too-deep": "error",
|
|
21
|
+
"duplicate-id": "error",
|
|
22
|
+
"unresolved-reference": "error",
|
|
23
|
+
"unresolved-footnote": "error",
|
|
24
|
+
"unresolved-cross-document-reference": "error",
|
|
25
|
+
"unresolvable-document": "error",
|
|
26
|
+
"unchecked-cross-document-reference": "warning",
|
|
27
|
+
"unknown-metadata-reference": "error",
|
|
28
|
+
"table-src-and-body": "error",
|
|
29
|
+
"unknown-table-format": "warning",
|
|
30
|
+
"bad-compute-formula": "error",
|
|
31
|
+
"unlexable-compute-formula": "error",
|
|
32
|
+
"compute-error": "error",
|
|
33
|
+
"bad-summary-entry": "error",
|
|
34
|
+
"summary-unknown-column": "error",
|
|
35
|
+
"unlexable-summary-expression": "error",
|
|
36
|
+
"summary-error": "error",
|
|
37
|
+
"bad-span": "error",
|
|
38
|
+
"span-outside-table": "warning",
|
|
39
|
+
"unknown-diagram-format": "warning",
|
|
40
|
+
"ignored-diagram-body": "warning",
|
|
41
|
+
"code-graph-missing-src": "warning",
|
|
42
|
+
"code-graph-unresolvable-document": "warning",
|
|
43
|
+
"chart-missing-data": "error",
|
|
44
|
+
"chart-data-not-a-table": "error",
|
|
45
|
+
"chart-missing-type": "error",
|
|
46
|
+
"chart-unknown-type": "error",
|
|
47
|
+
"chart-unknown-rows-scope": "error",
|
|
48
|
+
"chart-missing-channel": "error",
|
|
49
|
+
"chart-empty-channel": "error",
|
|
50
|
+
"chart-unknown-column": "error",
|
|
51
|
+
"chart-unused-channel": "warning",
|
|
52
|
+
"chart-missing-summary-row": "error",
|
|
53
|
+
"chart-summary-row-unavailable": "warning",
|
|
54
|
+
"chart-non-numeric-value": "error",
|
|
55
|
+
};
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Source normalization (spec §0)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// A conforming parser normalizes its input before scanning:
|
|
60
|
+
//
|
|
61
|
+
// 1. a single leading BOM (U+FEFF) is removed;
|
|
62
|
+
// 2. every line ending (CRLF, or a lone CR) becomes LF;
|
|
63
|
+
// 3. U+0000 becomes U+FFFD.
|
|
64
|
+
//
|
|
65
|
+
// All three preserve the LINE COUNT, which is what lets `blockSpans` index the
|
|
66
|
+
// original bytes by line: normalization only ever rewrites bytes *within* a
|
|
67
|
+
// line, never splits or joins one. (1) only touches the first line's leading
|
|
68
|
+
// bytes; (3) is a same-line substitution; (2) is per-line trailing bytes, and
|
|
69
|
+
// splitting on the normalized LF yields exactly the lines the original had.
|
|
70
|
+
export function normalizeSource(source) {
|
|
71
|
+
const noBom = source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
|
|
72
|
+
return noBom.replace(/\r\n?/g, "\n").replace(/\0/g, "�");
|
|
73
|
+
}
|
package/dist/geml.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type Diagnostic } from "./diagnostics.js";
|
|
2
3
|
import { type Value } from "./attrs.js";
|
|
3
4
|
import { type Inline } from "./inline.js";
|
|
4
5
|
import { type TableModel } from "./table.js";
|
|
@@ -54,11 +55,7 @@ export type Block = {
|
|
|
54
55
|
chart?: ChartModel;
|
|
55
56
|
hidden?: boolean;
|
|
56
57
|
};
|
|
57
|
-
export
|
|
58
|
-
severity: "error" | "warning";
|
|
59
|
-
message: string;
|
|
60
|
-
line: number;
|
|
61
|
-
}
|
|
58
|
+
export { type Diagnostic, type DiagnosticCode, SEVERITY } from "./diagnostics.js";
|
|
62
59
|
export interface Document {
|
|
63
60
|
kind: "document";
|
|
64
61
|
children: Block[];
|