@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/dist/geml.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// `meta` data block, ATX headings, lists and paragraphs, the attribute object
|
|
6
6
|
// with §4 value typing, and a document-model JSON serialization.
|
|
7
7
|
//
|
|
8
|
-
// M2: inline parsing of
|
|
9
|
-
// media embeds, links, auto-references, footnotes) and build-time
|
|
10
|
-
// validation (§8 — unique ids, resolvable internal/cross-document
|
|
8
|
+
// M2: inline parsing of unfenced blocks (§5 — emphasis/strong/strike, code,
|
|
9
|
+
// math, media embeds, links, auto-references, footnotes) and build-time
|
|
10
|
+
// reference validation (§8 — unique ids, resolvable internal/cross-document
|
|
11
|
+
// references).
|
|
11
12
|
import { readFileSync, writeFileSync, realpathSync, statSync, existsSync } from "node:fs";
|
|
12
13
|
import { basename, dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -15,6 +16,7 @@ import { spawnSync } from "node:child_process";
|
|
|
15
16
|
import { commit, restore, verify, listRevisions, resolveContent, firstChangedContent } from "./history.js";
|
|
16
17
|
import { renderHtml } from "./render-html.js";
|
|
17
18
|
import { normalizeBlockId } from "./block-edit.js";
|
|
19
|
+
import { normalizeSource } from "./diagnostics.js";
|
|
18
20
|
import { coerce, parseAttrs } from "./attrs.js";
|
|
19
21
|
import { META_REF_SRC, parseInline } from "./inline.js";
|
|
20
22
|
import { parseTable } from "./table.js";
|
|
@@ -34,6 +36,9 @@ export { gemlToMd } from "./to-md.js";
|
|
|
34
36
|
function reLit(s) {
|
|
35
37
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
38
|
}
|
|
39
|
+
// Re-exported from ./diagnostics.js so that `Diagnostic` stays importable from
|
|
40
|
+
// the package root. The catalogue of codes lives there (spec Appendix A).
|
|
41
|
+
export { SEVERITY } from "./diagnostics.js";
|
|
37
42
|
// Type registry: which body mode each typed block uses. Unknown types are a
|
|
38
43
|
// warning and fall back to `raw` (forward compatibility, §3/§8).
|
|
39
44
|
const REGISTRY = {
|
|
@@ -127,7 +132,7 @@ function interpolate(text, line, ctx) {
|
|
|
127
132
|
if (ctx.meta.has(key))
|
|
128
133
|
out += ctx.meta.get(key);
|
|
129
134
|
else {
|
|
130
|
-
ctx.diags.push({ severity: "error", message: `unknown metadata reference \`{{${key}}}\``, line });
|
|
135
|
+
ctx.diags.push({ severity: "error", code: "unknown-metadata-reference", message: `unknown metadata reference \`{{${key}}}\``, line });
|
|
131
136
|
out += m[0];
|
|
132
137
|
}
|
|
133
138
|
i = META_REF.lastIndex;
|
|
@@ -142,7 +147,7 @@ function interpolate(text, line, ctx) {
|
|
|
142
147
|
// Register a block id, flagging duplicates as errors (§4: ids unique per doc).
|
|
143
148
|
function registerId(ctx, id, line) {
|
|
144
149
|
if (ctx.ids.has(id)) {
|
|
145
|
-
ctx.diags.push({ severity: "error", message: `duplicate id \`#${id}\` (first defined at line ${ctx.ids.get(id)})`, line });
|
|
150
|
+
ctx.diags.push({ severity: "error", code: "duplicate-id", message: `duplicate id \`#${id}\` (first defined at line ${ctx.ids.get(id)})`, line });
|
|
146
151
|
}
|
|
147
152
|
else {
|
|
148
153
|
ctx.ids.set(id, line);
|
|
@@ -212,7 +217,7 @@ function parseList(lines, i, base, ctx) {
|
|
|
212
217
|
// rather than building a model that overflows the renderer (DoS). One
|
|
213
218
|
// diagnostic per over-deep list; content is preserved, just flattened.
|
|
214
219
|
if (!tooDeep) {
|
|
215
|
-
ctx.diags.push({ severity: "error", message: `list nesting too deep (max ${MAX_NESTING})`, line: base + i + 1 });
|
|
220
|
+
ctx.diags.push({ severity: "error", code: "list-nesting-too-deep", message: `list nesting too deep (max ${MAX_NESTING})`, line: base + i + 1 });
|
|
216
221
|
tooDeep = true;
|
|
217
222
|
}
|
|
218
223
|
cur = top.list;
|
|
@@ -298,11 +303,11 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
298
303
|
}
|
|
299
304
|
if (!closed) {
|
|
300
305
|
const how = attrs.id !== undefined ? `${"=".repeat(openLen)} or \`=== #${attrs.id}\`` : "=".repeat(openLen);
|
|
301
|
-
diags.push({ severity: "error", message: `unterminated \`${type}\` block (no matching ${how})`, line: openLineNo });
|
|
306
|
+
diags.push({ severity: "error", code: "unterminated-block", message: `unterminated \`${type}\` block (no matching ${how})`, line: openLineNo });
|
|
302
307
|
}
|
|
303
308
|
let mode = REGISTRY[type];
|
|
304
309
|
if (mode === undefined) {
|
|
305
|
-
diags.push({ severity: "warning", message: `unknown block type \`${type}\`; body kept as raw`, line: openLineNo });
|
|
310
|
+
diags.push({ severity: "warning", code: "unknown-block-type", message: `unknown block type \`${type}\`; body kept as raw`, line: openLineNo });
|
|
306
311
|
mode = "raw";
|
|
307
312
|
}
|
|
308
313
|
const block = {
|
|
@@ -326,7 +331,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
326
331
|
// Refuse to recurse past the cap: emit a diagnostic and keep the body
|
|
327
332
|
// as raw so the parser returns cleanly instead of overflowing the
|
|
328
333
|
// call stack on a pathologically nested document (DoS).
|
|
329
|
-
diags.push({ severity: "error", message: `block nesting too deep (max ${MAX_NESTING}); body kept as raw`, line: openLineNo });
|
|
334
|
+
diags.push({ severity: "error", code: "block-nesting-too-deep", message: `block nesting too deep (max ${MAX_NESTING}); body kept as raw`, line: openLineNo });
|
|
330
335
|
block.raw = body;
|
|
331
336
|
}
|
|
332
337
|
else {
|
|
@@ -356,7 +361,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
356
361
|
// §7: native chart — resolved in a second pass (data=#id may be
|
|
357
362
|
// defined later in the document).
|
|
358
363
|
if (body.length > 0 && body.some((l) => l.trim() !== "")) {
|
|
359
|
-
diags.push({ severity: "warning", message: "geml-chart body is ignored; the chart spec lives in attributes", line: openLineNo });
|
|
364
|
+
diags.push({ severity: "warning", code: "ignored-diagram-body", message: "geml-chart body is ignored; the chart spec lives in attributes", line: openLineNo });
|
|
360
365
|
}
|
|
361
366
|
(ctx.charts ??= []).push({ block, line: openLineNo });
|
|
362
367
|
}
|
|
@@ -366,18 +371,18 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
366
371
|
// ("view config travels with the data"). Body is empty.
|
|
367
372
|
const src = attrs.attrs["src"];
|
|
368
373
|
if (typeof src !== "string" || src === "") {
|
|
369
|
-
diags.push({ severity: "warning", message: "geml-code-graph: missing `src=` (nothing to render)", line: openLineNo });
|
|
374
|
+
diags.push({ severity: "warning", code: "code-graph-missing-src", message: "geml-code-graph: missing `src=` (nothing to render)", line: openLineNo });
|
|
370
375
|
}
|
|
371
376
|
else if (ctx.resolveDoc && ctx.resolveDoc(src) === null) {
|
|
372
|
-
diags.push({ severity: "warning", message: `geml-code-graph: cannot resolve document \`${src}\``, line: openLineNo });
|
|
377
|
+
diags.push({ severity: "warning", code: "code-graph-unresolvable-document", message: `geml-code-graph: cannot resolve document \`${src}\``, line: openLineNo });
|
|
373
378
|
}
|
|
374
379
|
if (body.length > 0 && body.some((l) => l.trim() !== "")) {
|
|
375
|
-
diags.push({ severity: "warning", message: "geml-code-graph body is ignored; the embed is configured by `src=` alone", line: openLineNo });
|
|
380
|
+
diags.push({ severity: "warning", code: "ignored-diagram-body", message: "geml-code-graph body is ignored; the embed is configured by `src=` alone", line: openLineNo });
|
|
376
381
|
}
|
|
377
382
|
}
|
|
378
383
|
else if (typeof fmt === "string" && !DIAGRAM_RENDERERS.has(fmt)) {
|
|
379
384
|
// §7: warn on a diagram format with no registered renderer.
|
|
380
|
-
diags.push({ severity: "warning", message: `no registered renderer for diagram format \`${fmt}\`; body kept raw`, line: openLineNo });
|
|
385
|
+
diags.push({ severity: "warning", code: "unknown-diagram-format", message: `no registered renderer for diagram format \`${fmt}\`; body kept raw`, line: openLineNo });
|
|
381
386
|
}
|
|
382
387
|
}
|
|
383
388
|
}
|
|
@@ -445,7 +450,7 @@ function parseData(lines) {
|
|
|
445
450
|
// resolving `other.geml#id` references.
|
|
446
451
|
function gatherIds(source) {
|
|
447
452
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map() };
|
|
448
|
-
scanBlocks(source
|
|
453
|
+
scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
449
454
|
return new Set(ctx.ids.keys());
|
|
450
455
|
}
|
|
451
456
|
// Pre-scan for `=== meta` blocks (at any fence depth) and merge their
|
|
@@ -477,14 +482,14 @@ function validateRefs(ctx, opts) {
|
|
|
477
482
|
if (!ref.doc)
|
|
478
483
|
continue;
|
|
479
484
|
if (!opts.resolveDoc) {
|
|
480
|
-
ctx.diags.push({ severity: "warning", message: `cross-document reference \`${ref.doc}${ref.anchor ? "#" + ref.anchor : ""}\` not checked (no document resolver)`, line: ref.line });
|
|
485
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `cross-document reference \`${ref.doc}${ref.anchor ? "#" + ref.anchor : ""}\` not checked (no document resolver)`, line: ref.line });
|
|
481
486
|
continue;
|
|
482
487
|
}
|
|
483
488
|
let ids = docIds.get(ref.doc);
|
|
484
489
|
if (ids === undefined) {
|
|
485
490
|
const src = opts.resolveDoc(ref.doc);
|
|
486
491
|
if (src === null) {
|
|
487
|
-
ctx.diags.push({ severity: "error", message: `cannot resolve document \`${ref.doc}\``, line: ref.line });
|
|
492
|
+
ctx.diags.push({ severity: "error", code: "unresolvable-document", message: `cannot resolve document \`${ref.doc}\``, line: ref.line });
|
|
488
493
|
docIds.set(ref.doc, new Set());
|
|
489
494
|
continue;
|
|
490
495
|
}
|
|
@@ -492,14 +497,16 @@ function validateRefs(ctx, opts) {
|
|
|
492
497
|
docIds.set(ref.doc, ids);
|
|
493
498
|
}
|
|
494
499
|
if (ref.anchor !== undefined && !ids.has(ref.anchor)) {
|
|
495
|
-
ctx.diags.push({ severity: "error", message: `unresolved reference \`${ref.doc}#${ref.anchor}\``, line: ref.line });
|
|
500
|
+
ctx.diags.push({ severity: "error", code: "unresolved-cross-document-reference", message: `unresolved reference \`${ref.doc}#${ref.anchor}\``, line: ref.line });
|
|
496
501
|
}
|
|
497
502
|
continue;
|
|
498
503
|
}
|
|
499
504
|
// internal, autoref, footnote — anchor must be a known id in this document.
|
|
500
505
|
if (ref.anchor !== undefined && !ctx.ids.has(ref.anchor)) {
|
|
501
|
-
const
|
|
502
|
-
|
|
506
|
+
const footnote = ref.kind === "footnote";
|
|
507
|
+
const what = footnote ? `footnote \`[^${ref.anchor}]\`` : `reference \`#${ref.anchor}\``;
|
|
508
|
+
const code = footnote ? "unresolved-footnote" : "unresolved-reference";
|
|
509
|
+
ctx.diags.push({ severity: "error", code, message: `unresolved ${what}`, line: ref.line });
|
|
503
510
|
}
|
|
504
511
|
}
|
|
505
512
|
}
|
|
@@ -510,13 +517,15 @@ function resolveCharts(ctx) {
|
|
|
510
517
|
const ref = typeof block.attrs["data"] === "string" ? block.attrs["data"] : "";
|
|
511
518
|
const id = ref.replace(/^#/, "");
|
|
512
519
|
if (id === "") {
|
|
513
|
-
ctx.diags.push({ severity: "error", message: "geml-chart: missing `data=#id`", line });
|
|
520
|
+
ctx.diags.push({ severity: "error", code: "chart-missing-data", message: "geml-chart: missing `data=#id`", line });
|
|
514
521
|
continue;
|
|
515
522
|
}
|
|
516
523
|
const table = ctx.tables?.get(id);
|
|
517
524
|
if (!table) {
|
|
518
|
-
const
|
|
519
|
-
|
|
525
|
+
const known = ctx.ids.has(id);
|
|
526
|
+
const what = known ? `data target \`#${id}\` is not a table` : `unresolved reference \`#${id}\``;
|
|
527
|
+
const code = known ? "chart-data-not-a-table" : "unresolved-reference";
|
|
528
|
+
ctx.diags.push({ severity: "error", code, message: `geml-chart: ${what}`, line });
|
|
520
529
|
continue;
|
|
521
530
|
}
|
|
522
531
|
if (table.src !== undefined) {
|
|
@@ -533,7 +542,7 @@ function resolveCharts(ctx) {
|
|
|
533
542
|
}
|
|
534
543
|
}
|
|
535
544
|
export function parse(source, opts = {}) {
|
|
536
|
-
const lines = source
|
|
545
|
+
const lines = normalizeSource(source).split("\n");
|
|
537
546
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines), resolveDoc: opts.resolveDoc };
|
|
538
547
|
const children = scanBlocks(lines, 0, ctx);
|
|
539
548
|
resolveCharts(ctx);
|
|
@@ -644,7 +653,7 @@ function collectSpans(lines, base, out, ctx, depth = 0) {
|
|
|
644
653
|
// with the physical lines produced by splitLines(source).
|
|
645
654
|
export function blockSpans(source) {
|
|
646
655
|
const out = new Map();
|
|
647
|
-
const lines = source
|
|
656
|
+
const lines = normalizeSource(source).split("\n");
|
|
648
657
|
// Inert context: heading auto-ids slug the interpolated text (parser parity);
|
|
649
658
|
// its diagnostics are discarded — the span scan never reports.
|
|
650
659
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines) };
|
|
@@ -728,48 +737,50 @@ function parseStamp(s) {
|
|
|
728
737
|
return new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +se));
|
|
729
738
|
}
|
|
730
739
|
const VERSION = "1.0"; // GEML spec version this CLI targets
|
|
731
|
-
const PARSER_VERSION = "1.4.
|
|
732
|
-
const USAGE = `geml — GEML reference CLI
|
|
733
|
-
|
|
734
|
-
Usage:
|
|
735
|
-
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [-o out] transform a document (default: --to json)
|
|
736
|
-
<
|
|
737
|
-
--to md -> Markdown (lossy)
|
|
738
|
-
--to html -> self-contained HTML
|
|
739
|
-
--to geml -> canonical re-format
|
|
740
|
-
--to json -> document-model JSON (default)
|
|
741
|
-
|
|
742
|
-
geml notes.md
|
|
743
|
-
|
|
744
|
-
geml
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
--json =
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
geml
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
geml
|
|
763
|
-
geml
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
740
|
+
const PARSER_VERSION = "1.4.3"; // reference implementation; keep in sync with package.json
|
|
741
|
+
const USAGE = `geml — GEML reference CLI
|
|
742
|
+
|
|
743
|
+
Usage:
|
|
744
|
+
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [-o out] transform a document (default: --to json)
|
|
745
|
+
--to <output>: json | html | md | geml
|
|
746
|
+
--to md -> Markdown (lossy)
|
|
747
|
+
--to html -> self-contained HTML
|
|
748
|
+
--to geml -> canonical re-format
|
|
749
|
+
--to json -> document-model JSON (default)
|
|
750
|
+
--from <input>: geml | md | json (overrides extension; html is output-only)
|
|
751
|
+
geml notes.md -> GEML (md inferred from extension)
|
|
752
|
+
geml model.json --to geml -> GEML (round-trips a prior --to json)
|
|
753
|
+
geml - --from md read Markdown on stdin
|
|
754
|
+
geml get <file.geml|-> [#id] [--json] [--head] with #id: print that block
|
|
755
|
+
(a heading id = its whole section; --head = head line;
|
|
756
|
+
--json = model node). Without #id: list all addressable
|
|
757
|
+
ids (--json = array).
|
|
758
|
+
geml set <file.geml|-> #id [--head|--body] [--in f[#src]|-] [-o f] replace ONE block by id
|
|
759
|
+
(--in F takes F's block #id, F#src takes #src, else stdin raw;
|
|
760
|
+
default = whole block · --head = head line · --body = body)
|
|
761
|
+
geml add <file.geml|-> (--append | --before #id | --after #id) [--in f[#src]|-] [-o f] insert a fragment
|
|
762
|
+
(1+ blocks and/or prose; content keeps its own ids, a clash is refused)
|
|
763
|
+
geml delete <file.geml|-> #id [#id2 …] [-o f] remove one or more blocks
|
|
764
|
+
(a missing id is skipped; a dangling reference is a warning, not a refusal)
|
|
765
|
+
geml rename <file.geml|-> #old #new [-o f] rename an id and every reference to it (id-boundary safe)
|
|
766
|
+
geml revert <file.geml> #id [--rev <sel>] [--head] undo one block to a past revision (splice / resurrect / remove)
|
|
767
|
+
(sel: 0 | -N | id-prefix | changed; default -1)
|
|
768
|
+
geml check <file.geml|-> [--root d] [--json] validate only: diagnostics + exit code
|
|
769
|
+
(--root widens cross-doc refs to dir d, e.g. the repo root)
|
|
770
|
+
geml history <commit|verify|show|restore|log> <file.geml> [...] .gemlhistory version sidecar
|
|
771
|
+
geml codemap <build|verify|render|serve|refresh|find|mcp> [...] code-graph toolkit (alias: codegraph)
|
|
772
|
+
geml mcp --workspace <dir> [--no-history] serve document CRUD over MCP (stdio)
|
|
773
|
+
(9 tools: list/read/check/history + write/add/delete/rename/revert;
|
|
774
|
+
every write is validated before it reaches disk)
|
|
775
|
+
geml --help | --version [--json]
|
|
776
|
+
|
|
777
|
+
Use '-' as the file to read from stdin.
|
|
778
|
+
Mutations (set/add/delete/rename) write the whole updated document in place for a
|
|
779
|
+
file, or to stdout for '-' input; -o redirects it (-o - = stdout).
|
|
780
|
+
Exit codes:
|
|
781
|
+
0 ok
|
|
782
|
+
1 document/operation error
|
|
783
|
+
2 command usage error.
|
|
773
784
|
`;
|
|
774
785
|
// One-line usage for each subcommand — the single source for both the error
|
|
775
786
|
// shown on misuse and the `<cmd> --help` text.
|
|
@@ -780,17 +791,32 @@ const SUBHELP = {
|
|
|
780
791
|
delete: "usage: geml delete <file.geml|-> #id [#id2 …] [-o out.geml] (remove one or more blocks; a missing id is skipped with a note, not an error; a reference left dangling is a warning, not a refusal — delete never fails on a live reference)",
|
|
781
792
|
rename: "usage: geml rename <file.geml|-> #old #new [-o out.geml] (rewrite an id's declaration AND every reference — [[#id]], [text](#id), chart data=#id, footnote [^id] — id-boundary safe, skipping raw block bodies; #new must be free; refused if it breaks the doc)",
|
|
782
793
|
check: "usage: geml check <file.geml|-> [--root <dir>] [--json] (--root: resolve cross-doc refs within <dir> instead of the file's own directory)",
|
|
783
|
-
revert: "usage: geml revert <file.geml> #id [--rev <sel>] [--
|
|
794
|
+
revert: "usage: geml revert <file.geml> #id [--rev <sel>] [--append|--before #x|--after #x] [--head] [--dry-run] [-o out] (reconcile #id to a revision: splice / resurrect / remove; sel: 0 | -N | id-prefix | changed; default -1)",
|
|
784
795
|
history: "usage: geml history <commit|verify|show|restore|log> <file.geml> [...]",
|
|
785
|
-
codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
|
|
786
|
-
geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
|
|
787
|
-
geml codemap verify [dir] geml check + profile reference checks
|
|
788
|
-
geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
|
|
789
|
-
geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
|
|
790
|
-
geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
|
|
791
|
-
geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
|
|
792
|
-
geml codemap mcp stdio MCP server (GEML_GRAPH_DIR or graph_dir arg)
|
|
796
|
+
codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
|
|
797
|
+
geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
|
|
798
|
+
geml codemap verify [dir] geml check + profile reference checks
|
|
799
|
+
geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
|
|
800
|
+
geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
|
|
801
|
+
geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
|
|
802
|
+
geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
|
|
803
|
+
geml codemap mcp stdio MCP server (GEML_GRAPH_DIR or graph_dir arg)
|
|
793
804
|
(<dir> for verify/render/serve/refresh/find defaults to ./.geml-code-graph; codegraph and code-graph are accepted as aliases of codemap)`,
|
|
805
|
+
mcp: `usage: geml mcp --workspace <dir> [--no-history]
|
|
806
|
+
|
|
807
|
+
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
|
|
808
|
+
Nine tools: geml_list_ids · geml_read_block · geml_check · geml_history_log
|
|
809
|
+
geml_write_block · geml_add_block · geml_delete_block
|
|
810
|
+
geml_rename_id · geml_revert_block
|
|
811
|
+
|
|
812
|
+
--workspace <dir> REQUIRED. Root holding the .geml documents. Every path a
|
|
813
|
+
client names is confined here; a client cannot widen it.
|
|
814
|
+
--no-history Skip the .gemlhistory commit taken before each write
|
|
815
|
+
(default: commit, so geml_revert_block always has a
|
|
816
|
+
revision to undo to).
|
|
817
|
+
|
|
818
|
+
Register with a client:
|
|
819
|
+
claude mcp add geml-docs -- geml mcp --workspace /abs/path/to/docs`,
|
|
794
820
|
};
|
|
795
821
|
// Set from argv at dispatch time; when true, errors are emitted as a JSON
|
|
796
822
|
// envelope so an agent that standardizes on --json never has to parse text.
|
|
@@ -805,6 +831,19 @@ function fail(msg, code = 2) {
|
|
|
805
831
|
console.error(`error: ${msg}`);
|
|
806
832
|
process.exit(code);
|
|
807
833
|
}
|
|
834
|
+
// Refuse a mutation whose RESULT would be broken (the pre-write check every
|
|
835
|
+
// mutation runs). Prose mode is the long-standing wording: the first error,
|
|
836
|
+
// phrased by the call site. `--json` additionally carries the FULL diagnostic
|
|
837
|
+
// list with the stable codes of spec Appendix A, so a programmatic caller —
|
|
838
|
+
// `geml mcp` above all — reports what actually broke instead of re-parsing
|
|
839
|
+
// English out of stderr.
|
|
840
|
+
function refuseBroken(prose, errs) {
|
|
841
|
+
if (jsonMode) {
|
|
842
|
+
console.error(JSON.stringify({ error: prose, code: 1, diagnostics: errs }));
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
fail(prose, 1);
|
|
846
|
+
}
|
|
808
847
|
// Read a file, or stdin when the path is "-". On failure emit a clean error.
|
|
809
848
|
function readInput(file) {
|
|
810
849
|
try {
|
|
@@ -968,10 +1007,10 @@ function runHistory(args) {
|
|
|
968
1007
|
console.log(`restored ${file} to ${rev}`);
|
|
969
1008
|
}
|
|
970
1009
|
else if (sub === "log") {
|
|
971
|
-
// Newest-first, with the `--
|
|
972
|
-
// (`
|
|
1010
|
+
// Newest-first, with the `--rev` selector for each row in the first column
|
|
1011
|
+
// (`0` for the tip, then `-1`, `-2`, …) so the output is copy-paste.
|
|
973
1012
|
for (const r of listRevisions(historyPath)) {
|
|
974
|
-
const sel = r.current ? "
|
|
1013
|
+
const sel = r.current ? "0" : `-${r.offset}`;
|
|
975
1014
|
console.log(`${sel.padEnd(7)} ${r.id} ${r.author ?? "-"} ${r.summary ?? ""}`.replace(/\s+$/, ""));
|
|
976
1015
|
}
|
|
977
1016
|
}
|
|
@@ -994,21 +1033,24 @@ function runTransform(argv) {
|
|
|
994
1033
|
// silent fall-through to the default — flag() would return undefined and we
|
|
995
1034
|
// must not quietly ignore it.
|
|
996
1035
|
if (argv.includes("--from") && fromRaw === undefined)
|
|
997
|
-
fail("--from needs a format (geml | md)", 2);
|
|
1036
|
+
fail("--from needs a format (geml | md | json)", 2);
|
|
998
1037
|
if (argv.includes("--to") && toRaw === undefined)
|
|
999
1038
|
fail("--to needs a format (json | html | md | geml)", 2);
|
|
1000
1039
|
// Input format: an explicit --from wins (for any input, file or stdin), else
|
|
1001
1040
|
// the file extension, else GEML (covers .geml, unknown extensions, and stdin).
|
|
1002
1041
|
let inFmt;
|
|
1003
1042
|
if (fromRaw !== undefined) {
|
|
1004
|
-
if (fromRaw !== "geml" && fromRaw !== "md") {
|
|
1005
|
-
fail(`--from: unknown input format '${fromRaw}' (want geml | md)`, 2);
|
|
1043
|
+
if (fromRaw !== "geml" && fromRaw !== "md" && fromRaw !== "json") {
|
|
1044
|
+
fail(`--from: unknown input format '${fromRaw}' (want geml | md | json)`, 2);
|
|
1006
1045
|
}
|
|
1007
1046
|
inFmt = fromRaw;
|
|
1008
1047
|
}
|
|
1009
1048
|
else if (/\.(md|markdown)$/i.test(file)) {
|
|
1010
1049
|
inFmt = "md";
|
|
1011
1050
|
}
|
|
1051
|
+
else if (/\.json$/i.test(file)) {
|
|
1052
|
+
inFmt = "json";
|
|
1053
|
+
}
|
|
1012
1054
|
else {
|
|
1013
1055
|
inFmt = "geml";
|
|
1014
1056
|
}
|
|
@@ -1021,7 +1063,7 @@ function runTransform(argv) {
|
|
|
1021
1063
|
outFmt = toRaw;
|
|
1022
1064
|
}
|
|
1023
1065
|
else {
|
|
1024
|
-
outFmt = inFmt === "
|
|
1066
|
+
outFmt = inFmt === "geml" ? "json" : "geml"; // geml->json; md/json->geml
|
|
1025
1067
|
}
|
|
1026
1068
|
const src = readInput(file);
|
|
1027
1069
|
// md -> geml is a direct projection, not a parse/serialize round-trip: emit
|
|
@@ -1037,7 +1079,10 @@ function runTransform(argv) {
|
|
|
1037
1079
|
// project it to the target.
|
|
1038
1080
|
let notes = [];
|
|
1039
1081
|
let doc;
|
|
1040
|
-
if (inFmt === "
|
|
1082
|
+
if (inFmt === "json") {
|
|
1083
|
+
doc = loadModelJson(src, file); // the inverse of `--to json`
|
|
1084
|
+
}
|
|
1085
|
+
else if (inFmt === "md") {
|
|
1041
1086
|
const conv = mdToGeml(src);
|
|
1042
1087
|
notes = conv.notes;
|
|
1043
1088
|
doc = parse(conv.geml, { resolveDoc: resolverFor(file) });
|
|
@@ -1076,6 +1121,28 @@ function runTransform(argv) {
|
|
|
1076
1121
|
if (doc.diagnostics.some((d) => d.severity === "error"))
|
|
1077
1122
|
process.exit(1);
|
|
1078
1123
|
}
|
|
1124
|
+
// Load a document-model JSON (the exact output of `--to json`) back into a
|
|
1125
|
+
// Document, so `--from json --to geml` is the inverse of a prior `--to json`.
|
|
1126
|
+
// The model is trusted as-is — no re-parse — so a clean round-trip is byte-stable
|
|
1127
|
+
// with `--to geml`. Anything that is not a document model is refused, and any
|
|
1128
|
+
// carried diagnostics are preserved (so a broken doc's JSON stays flagged).
|
|
1129
|
+
function loadModelJson(src, file) {
|
|
1130
|
+
let obj;
|
|
1131
|
+
try {
|
|
1132
|
+
obj = JSON.parse(src);
|
|
1133
|
+
}
|
|
1134
|
+
catch (e) {
|
|
1135
|
+
fail(`--from json: ${file === "-" ? "stdin" : file} is not valid JSON (${e.message})`, 1);
|
|
1136
|
+
}
|
|
1137
|
+
const d = obj;
|
|
1138
|
+
if (!d || typeof d !== "object" || d.kind !== "document" || !Array.isArray(d.children)) {
|
|
1139
|
+
fail(`--from json: not a GEML document-model JSON (expected {"kind":"document","children":[…]})`, 1);
|
|
1140
|
+
}
|
|
1141
|
+
const doc = d;
|
|
1142
|
+
if (!Array.isArray(doc.diagnostics))
|
|
1143
|
+
doc.diagnostics = [];
|
|
1144
|
+
return doc;
|
|
1145
|
+
}
|
|
1079
1146
|
// Write to `-o out` (with a `wrote` note on stderr) or to stdout.
|
|
1080
1147
|
function writeOut(text, out) {
|
|
1081
1148
|
if (out) {
|
|
@@ -1412,7 +1479,7 @@ function insertFragment(source, lines, at, fragment, file) {
|
|
|
1412
1479
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1413
1480
|
if (errs.length) {
|
|
1414
1481
|
const first = errs[0];
|
|
1415
|
-
|
|
1482
|
+
refuseBroken(`adding the content would break the document: ${first.message} (line ${first.line}); not written`, errs);
|
|
1416
1483
|
}
|
|
1417
1484
|
const now = new Set(reparsed.ids);
|
|
1418
1485
|
const dropped = beforeIds.find((x) => !now.has(x));
|
|
@@ -1489,7 +1556,7 @@ function runRename(args) {
|
|
|
1489
1556
|
const hp = historyPathFor(file);
|
|
1490
1557
|
if (existsSync(hp)) {
|
|
1491
1558
|
try {
|
|
1492
|
-
if (blockSpans(resolveContent(hp, "
|
|
1559
|
+
if (blockSpans(resolveContent(hp, "0").text).has(oldId)) {
|
|
1493
1560
|
console.error(`warning: #${oldId} has history; revert across this rename is not tracked — see docs`);
|
|
1494
1561
|
}
|
|
1495
1562
|
}
|
|
@@ -1501,7 +1568,7 @@ function runRename(args) {
|
|
|
1501
1568
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1502
1569
|
if (errs.length) {
|
|
1503
1570
|
const e = errs[0];
|
|
1504
|
-
|
|
1571
|
+
refuseBroken(`rename would break the document: ${e.message} (line ${e.line}); not written`, errs);
|
|
1505
1572
|
}
|
|
1506
1573
|
if (!reparsed.ids.includes(newId))
|
|
1507
1574
|
fail(`rename did not produce #${newId}; not written`, 1);
|
|
@@ -1663,7 +1730,7 @@ function spliceBlock(source, id, replacement, file, headOnly = false, guardCount
|
|
|
1663
1730
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1664
1731
|
if (errs.length) {
|
|
1665
1732
|
const first = errs[0];
|
|
1666
|
-
|
|
1733
|
+
refuseBroken(`replacement would break the document: ${first.message} (line ${first.line}); not written`, errs);
|
|
1667
1734
|
}
|
|
1668
1735
|
const now = new Set(reparsed.ids);
|
|
1669
1736
|
if (!now.has(id))
|
|
@@ -1685,19 +1752,26 @@ function spliceBlock(source, id, replacement, file, headOnly = false, guardCount
|
|
|
1685
1752
|
}
|
|
1686
1753
|
return updated;
|
|
1687
1754
|
}
|
|
1688
|
-
// `geml revert <file.geml> #id [--rev <sel>] [--
|
|
1755
|
+
// `geml revert <file.geml> #id [--rev <sel>] [--dry-run] [-o out] [--history PATH]`
|
|
1689
1756
|
// Restore ONE block to a past revision's version — a targeted, guarded splice
|
|
1690
|
-
// that leaves the rest of the document untouched. <sel> (default `-1`):
|
|
1691
|
-
// revisions back
|
|
1692
|
-
// skips revisions
|
|
1693
|
-
// *distinct* version. `--dry-run` prints what would be spliced in,
|
|
1694
|
-
// nothing. Writes in place by default (revert is a mutation); `-o` redirects.
|
|
1757
|
+
// that leaves the rest of the document untouched. <sel> (default `-1`): `0` (the
|
|
1758
|
+
// tip), `-N` (N revisions back), an id prefix/suffix, or `changed` — a content
|
|
1759
|
+
// selector that skips revisions which never touched the block, landing on its
|
|
1760
|
+
// previous *distinct* version. `--dry-run` prints what would be spliced in,
|
|
1761
|
+
// writing nothing. Writes in place by default (revert is a mutation); `-o` redirects.
|
|
1695
1762
|
function runRevert(args) {
|
|
1696
|
-
const changed = args.includes("--changed");
|
|
1697
1763
|
const dryRun = args.includes("--dry-run");
|
|
1698
1764
|
const headOnly = args.includes("--head");
|
|
1699
1765
|
const out = flag(args, "-o") ?? flag(args, "--out");
|
|
1700
1766
|
const to = flag(args, "--rev") ?? "-1";
|
|
1767
|
+
// `--rev changed` is a CONTENT selector, not a position: skip commits that
|
|
1768
|
+
// never touched this block, landing on its previous *distinct* version. It is
|
|
1769
|
+
// just a `--rev` value, so it cannot conflict with a positional `-N`.
|
|
1770
|
+
const changed = to === "changed";
|
|
1771
|
+
// The former standalone `--changed` flag is now this value; refuse the old
|
|
1772
|
+
// spelling loudly rather than silently ignoring it (and reverting to -1).
|
|
1773
|
+
if (args.includes("--changed"))
|
|
1774
|
+
fail("--changed is now `--rev changed`", 2);
|
|
1701
1775
|
const before = flag(args, "--before");
|
|
1702
1776
|
const after = flag(args, "--after");
|
|
1703
1777
|
const append = args.includes("--append");
|
|
@@ -1753,12 +1827,19 @@ function runRevert(args) {
|
|
|
1753
1827
|
};
|
|
1754
1828
|
// Reconcile #id between now and revision R across the four presence cells.
|
|
1755
1829
|
if (curBlock === undefined && oldBlock === undefined) {
|
|
1756
|
-
fail(`\`${id}\` exists in neither the document nor ${target.id} (try --changed)`, 1);
|
|
1830
|
+
fail(`\`${id}\` exists in neither the document nor ${target.id} (try --rev changed)`, 1);
|
|
1757
1831
|
}
|
|
1758
1832
|
// both present -> SPLICE (undo set)
|
|
1759
1833
|
if (curBlock !== undefined && oldBlock !== undefined) {
|
|
1760
1834
|
if (oldBlock === curBlock) {
|
|
1761
|
-
console.error(`#${id} is unchanged at ${target.id}; nothing to revert${changed ? "" : " (try --rev -2, or --changed)"}`);
|
|
1835
|
+
console.error(`#${id} is unchanged at ${target.id}; nothing to revert${changed ? "" : " (try --rev -2, or --rev changed)"}`);
|
|
1836
|
+
// A no-op still has to PRODUCE the document when an output destination was
|
|
1837
|
+
// asked for: `-o` means "write the result somewhere", and the result of a
|
|
1838
|
+
// no-op revert is the unchanged document. Returning silently here left
|
|
1839
|
+
// `-o -` consumers with exit 0 and empty stdout, which reads as "success,
|
|
1840
|
+
// and the document is now empty".
|
|
1841
|
+
if (out !== undefined)
|
|
1842
|
+
emit(source, `#${id} unchanged`);
|
|
1762
1843
|
return;
|
|
1763
1844
|
}
|
|
1764
1845
|
if (dryRun) {
|
|
@@ -1824,7 +1905,7 @@ function runRevert(args) {
|
|
|
1824
1905
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1825
1906
|
if (errs.length) {
|
|
1826
1907
|
const first = errs[0];
|
|
1827
|
-
|
|
1908
|
+
refuseBroken(`removing #${id} would break the document: ${first.message} (line ${first.line}); not written`, errs);
|
|
1828
1909
|
}
|
|
1829
1910
|
const now = new Set(reparsed.ids);
|
|
1830
1911
|
const dropped = beforeIds.find((x) => x !== id && !now.has(x));
|
|
@@ -1893,6 +1974,15 @@ function runCodemap(args) {
|
|
|
1893
1974
|
const r = spawnSync(process.execPath, [mod, ...args.slice(1)], { stdio: "inherit" });
|
|
1894
1975
|
process.exit(r.status ?? 1);
|
|
1895
1976
|
}
|
|
1977
|
+
// geml mcp: the document-CRUD MCP server. Runs as a child's MAIN module for the
|
|
1978
|
+
// same reason `codemap mcp` does — it owns stdin/stdout for the whole session
|
|
1979
|
+
// (the stdio transport), and dispatching by spawn keeps this module free of a
|
|
1980
|
+
// runtime import cycle (mcp.js imports the parser from here).
|
|
1981
|
+
function runMcp(args) {
|
|
1982
|
+
const mod = join(dirname(fileURLToPath(import.meta.url)), "mcp.js");
|
|
1983
|
+
const r = spawnSync(process.execPath, [mod, ...args], { stdio: "inherit" });
|
|
1984
|
+
process.exit(r.status ?? 1);
|
|
1985
|
+
}
|
|
1896
1986
|
// npm's unix bin shim is a symlink named plain `geml`, so detect "run as a
|
|
1897
1987
|
// CLI" by resolving argv[1] to its real path, not by its spelling.
|
|
1898
1988
|
const entry = (() => {
|
|
@@ -1962,6 +2052,9 @@ if (entry && (entry === fileURLToPath(import.meta.url) || entry.endsWith("geml.t
|
|
|
1962
2052
|
else if (cmd === "codemap") {
|
|
1963
2053
|
runCodemap(argv.slice(1));
|
|
1964
2054
|
}
|
|
2055
|
+
else if (cmd === "mcp") {
|
|
2056
|
+
runMcp(argv.slice(1));
|
|
2057
|
+
}
|
|
1965
2058
|
else if (cmd !== "-" && !/[.\/\\]/.test(cmd)) {
|
|
1966
2059
|
// A bare word that is neither a known command nor a path is almost always
|
|
1967
2060
|
// a mistyped command — say so, don't try to read it as a file. (The
|
package/dist/history.d.ts
CHANGED
|
@@ -70,6 +70,16 @@ export declare function listRevisions(historyPath: string): RevisionInfo[];
|
|
|
70
70
|
/** Resolve a revision selector to its id + reconstructed full text. Selectors:
|
|
71
71
|
* `-N` (N revisions back from current; `-0` is the tip), `latest`/`current`, or
|
|
72
72
|
* an unambiguous id prefix/suffix (the same forms `restore` accepts). */
|
|
73
|
+
/** Resolve a revision selector to its id, for EVERY command that takes one.
|
|
74
|
+
*
|
|
75
|
+
* There is exactly one selector grammar — `0` (the tip), `-N` (N revisions
|
|
76
|
+
* back), or an unambiguous revision id (prefix, suffix, or exact) — and it is
|
|
77
|
+
* the grammar `history log` prints in its first column, so its output is
|
|
78
|
+
* copy-pasteable into `revert --rev`, `history show`, and `history restore`
|
|
79
|
+
* alike. Keeping this in one function is what makes that true: it used to be
|
|
80
|
+
* written twice, and the copy in `restore` never grew the `0`/`-N` arm, so the
|
|
81
|
+
* selectors `history log` advertised were rejected by `history show`. */
|
|
82
|
+
export declare function resolveRevision(h: History, selector: string): string;
|
|
73
83
|
export declare function resolveContent(historyPath: string, selector: string): {
|
|
74
84
|
id: string;
|
|
75
85
|
text: string;
|