@geml/geml 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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);