@geml/geml 1.0.0 → 1.1.1
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 +79 -72
- package/codemap/adapters/crg.mjs +109 -0
- package/codemap/adapters/joern.mjs +131 -0
- package/codemap/adapters/scip.mjs +229 -0
- package/codemap/browser-stub.mjs +24 -0
- package/codemap/build.mjs +354 -0
- package/codemap/detect.mjs +185 -0
- package/codemap/emit.mjs +361 -0
- package/codemap/exclude.mjs +52 -0
- package/codemap/joern-export.sc +83 -0
- package/codemap/mcp-server.mjs +143 -0
- package/codemap/normalize.mjs +0 -0
- package/codemap/refresh.mjs +160 -0
- package/codemap/render-all.mjs +64 -0
- package/codemap/serve.mjs +378 -0
- package/codemap/verify.mjs +126 -0
- package/dist/geml.d.ts +5 -0
- package/dist/geml.js +390 -14
- package/dist/history.d.ts +30 -0
- package/dist/history.js +153 -13
- package/dist/render.d.ts +59 -0
- package/dist/render.js +1685 -11
- package/dist/to-md.js +1 -3
- package/package.json +9 -5
package/dist/history.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface Revision {
|
|
|
15
15
|
author?: string;
|
|
16
16
|
summary?: string;
|
|
17
17
|
hash: string;
|
|
18
|
+
newline?: string;
|
|
18
19
|
ops: Op[];
|
|
19
20
|
}
|
|
20
21
|
interface History {
|
|
@@ -52,4 +53,33 @@ export interface RestoreOpts {
|
|
|
52
53
|
force?: boolean;
|
|
53
54
|
}
|
|
54
55
|
export declare function restore(o: RestoreOpts): string;
|
|
56
|
+
export interface RevisionInfo {
|
|
57
|
+
id: string;
|
|
58
|
+
parent?: string;
|
|
59
|
+
author?: string;
|
|
60
|
+
summary?: string;
|
|
61
|
+
hash: string;
|
|
62
|
+
offset: number;
|
|
63
|
+
current: boolean;
|
|
64
|
+
}
|
|
65
|
+
/** Is the working file byte-identical to the sidecar's tip revision? False
|
|
66
|
+
* means uncommitted drift (e.g. an earlier commit attempt was refused). */
|
|
67
|
+
export declare function isCurrent(historyPath: string, gemlPath: string): boolean;
|
|
68
|
+
/** Revisions newest-first, each tagged with the `-N` offset that selects it. */
|
|
69
|
+
export declare function listRevisions(historyPath: string): RevisionInfo[];
|
|
70
|
+
/** Resolve a revision selector to its id + reconstructed full text. Selectors:
|
|
71
|
+
* `-N` (N revisions back from current; `-0` is the tip), `latest`/`current`, or
|
|
72
|
+
* an unambiguous id prefix/suffix (the same forms `restore` accepts). */
|
|
73
|
+
export declare function resolveContent(historyPath: string, selector: string): {
|
|
74
|
+
id: string;
|
|
75
|
+
text: string;
|
|
76
|
+
};
|
|
77
|
+
/** Walk the chain newest→oldest; return the first revision whose block (as
|
|
78
|
+
* extracted by `pick`) differs from `currentBlock` — i.e. the block's previous
|
|
79
|
+
* *distinct* version, skipping revisions that never touched it. Used by
|
|
80
|
+
* `revert --changed`. `undefined` if no earlier revision changed the block. */
|
|
81
|
+
export declare function firstChangedContent(historyPath: string, currentBlock: string, pick: (fullText: string) => string | undefined): {
|
|
82
|
+
id: string;
|
|
83
|
+
text: string;
|
|
84
|
+
} | undefined;
|
|
55
85
|
export {};
|
package/dist/history.js
CHANGED
|
@@ -80,9 +80,12 @@ function fenceFor(contentLf) {
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
81
|
// Document units & reverse-patch engine (gap-aware)
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
|
-
// Unit-key = `#id` (explicit), or `@<8hex content hash>` (derived) with `~n`
|
|
84
|
-
// disambiguating equal
|
|
85
|
-
|
|
83
|
+
// Unit-key = `#id` (explicit), or `@<8hex content hash>` (derived), with `~n`
|
|
84
|
+
// disambiguating equal keys by document-order occurrence (§4). `~n` on an #id
|
|
85
|
+
// key only arises for OUT-OF-SPEC documents that repeat an id — without it the
|
|
86
|
+
// key is ambiguous, reverse-patch ops hit the wrong occurrence, and commit()'s
|
|
87
|
+
// round-trip gate (correctly) aborts. Well-formed documents never emit it.
|
|
88
|
+
const KEY = String.raw `(#[A-Za-z][A-Za-z0-9_-]*(?:~\d+)?|@[0-9a-f]+(?:~\d+)?)`;
|
|
86
89
|
function sha8(s) {
|
|
87
90
|
return createHash("sha256").update(Buffer.from(s, "utf8")).digest("hex").slice(0, 8);
|
|
88
91
|
}
|
|
@@ -127,9 +130,11 @@ function tile(lines) {
|
|
|
127
130
|
function keyedUnits(lines) {
|
|
128
131
|
const counts = new Map();
|
|
129
132
|
return tile(lines).map((u) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
// Both key kinds get the ~n occurrence suffix: content keys collide by
|
|
134
|
+
// nature (equal blank runs, repeated paragraphs); #id keys only collide in
|
|
135
|
+
// out-of-spec documents that repeat an id — but those exist in the wild,
|
|
136
|
+
// and an ambiguous key sends reverse-patch ops to the wrong occurrence.
|
|
137
|
+
const base = u.id ? `#${u.id}` : `@${sha8(lines.slice(u.start, u.bodyEnd).join("\n"))}`;
|
|
133
138
|
const n = counts.get(base) ?? 0;
|
|
134
139
|
counts.set(base, n + 1);
|
|
135
140
|
return { u, key: n === 0 ? base : `${base}~${n}` };
|
|
@@ -221,14 +226,59 @@ function applyReverse(textLf, ops, blobs) {
|
|
|
221
226
|
}
|
|
222
227
|
return lines.join("\n");
|
|
223
228
|
}
|
|
224
|
-
/** LCS alignment of unit-key sequences; aMatch[i] = matched index in b, or -1.
|
|
229
|
+
/** LCS alignment of unit-key sequences; aMatch[i] = matched index in b, or -1.
|
|
230
|
+
*
|
|
231
|
+
* Fast path: content-keyed units (`@hash~n`) are unique by construction, and in
|
|
232
|
+
* a well-formed document `#id` keys are unique too — and the LCS of two
|
|
233
|
+
* all-unique sequences is exactly the longest increasing subsequence of a's
|
|
234
|
+
* keys mapped to b's positions: O(n log n) instead of the O(n·m) DP table.
|
|
235
|
+
* That difference is GEP-0002's measured bottleneck (seconds at 10⁴ units,
|
|
236
|
+
* minutes at 10⁵ — code-graph documents live there).
|
|
237
|
+
*
|
|
238
|
+
* A document with DUPLICATE `#id`s (a GEML error, but history never parses) can
|
|
239
|
+
* break uniqueness, so the DP remains as the fallback for that case. Either
|
|
240
|
+
* path yields *a* maximal alignment; commit()'s byte-exact round-trip gate
|
|
241
|
+
* rejects any diff that fails to reproduce the parent, whichever path ran. */
|
|
225
242
|
function lcsMatch(a, b) {
|
|
226
243
|
const n = a.length, m = b.length;
|
|
244
|
+
const aMatch = new Array(n).fill(-1);
|
|
245
|
+
if (new Set(a).size === n && new Set(b).size === m) {
|
|
246
|
+
const posInB = new Map();
|
|
247
|
+
for (let j = 0; j < m; j++)
|
|
248
|
+
posInB.set(b[j], j);
|
|
249
|
+
const ai = [], bj = []; // a-index / b-position of common keys, in a-order
|
|
250
|
+
for (let i = 0; i < n; i++) {
|
|
251
|
+
const j = posInB.get(a[i]);
|
|
252
|
+
if (j !== undefined) {
|
|
253
|
+
ai.push(i);
|
|
254
|
+
bj.push(j);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Patience LIS over bj (strictly increasing) with predecessor links.
|
|
258
|
+
const tails = []; // index into bj of the smallest tail per LIS length
|
|
259
|
+
const prev = new Array(bj.length).fill(-1);
|
|
260
|
+
for (let x = 0; x < bj.length; x++) {
|
|
261
|
+
let lo = 0, hi = tails.length;
|
|
262
|
+
while (lo < hi) {
|
|
263
|
+
const mid = (lo + hi) >> 1;
|
|
264
|
+
if (bj[tails[mid]] < bj[x])
|
|
265
|
+
lo = mid + 1;
|
|
266
|
+
else
|
|
267
|
+
hi = mid;
|
|
268
|
+
}
|
|
269
|
+
prev[x] = lo > 0 ? tails[lo - 1] : -1;
|
|
270
|
+
tails[lo] = x;
|
|
271
|
+
}
|
|
272
|
+
for (let cur = tails.length ? tails[tails.length - 1] : -1; cur >= 0; cur = prev[cur]) {
|
|
273
|
+
aMatch[ai[cur]] = bj[cur];
|
|
274
|
+
}
|
|
275
|
+
return aMatch;
|
|
276
|
+
}
|
|
277
|
+
// Fallback (duplicate keys): classic DP.
|
|
227
278
|
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
228
279
|
for (let i = n - 1; i >= 0; i--)
|
|
229
280
|
for (let j = m - 1; j >= 0; j--)
|
|
230
281
|
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
231
|
-
const aMatch = new Array(n).fill(-1);
|
|
232
282
|
let i = 0, j = 0;
|
|
233
283
|
while (i < n && j < m) {
|
|
234
284
|
if (a[i] === b[j]) {
|
|
@@ -289,6 +339,22 @@ function opLine(op) {
|
|
|
289
339
|
return `insert <- blob:${op.blob} ${anchorStr(op.anchor)}`;
|
|
290
340
|
return `move ${op.key} ${anchorStr(op.anchor)}`;
|
|
291
341
|
}
|
|
342
|
+
// §8 hashes are over "the exact UTF-8 bytes of that version" — which includes
|
|
343
|
+
// its newline style. The sidecar's file-level nl is a SINGLE value that later
|
|
344
|
+
// commits overwrite, so a document whose line endings flipped (LF↔CRLF is
|
|
345
|
+
// routine on Windows) would leave older revisions' recorded hashes
|
|
346
|
+
// unreproducible. Each revision therefore records its own `newline`; for
|
|
347
|
+
// legacy revisions that never recorded one, accept either byte encoding of
|
|
348
|
+
// the reconstructed text — both still pin the CONTENT exactly.
|
|
349
|
+
const nlNamed = (nl) => (nl === "\r\n" ? "crlf" : "lf");
|
|
350
|
+
const nlOf = (name) => name === "crlf" ? "\r\n" : name === "lf" ? "\n" : undefined;
|
|
351
|
+
function hashMatchesRecorded(lf, r, fileNl) {
|
|
352
|
+
const own = nlOf(r.newline);
|
|
353
|
+
if (own)
|
|
354
|
+
return fullHash(lf, own) === r.hash;
|
|
355
|
+
return fullHash(lf, fileNl) === r.hash
|
|
356
|
+
|| fullHash(lf, fileNl === "\r\n" ? "\n" : "\r\n") === r.hash;
|
|
357
|
+
}
|
|
292
358
|
function parseHistory(path) {
|
|
293
359
|
const { lf, nl } = loadBytes(path);
|
|
294
360
|
const lines = lf.split("\n");
|
|
@@ -315,6 +381,7 @@ function parseHistory(path) {
|
|
|
315
381
|
author: attr(b.attrLine, "author"),
|
|
316
382
|
summary: attr(b.attrLine, "summary"),
|
|
317
383
|
hash: attr(b.attrLine, "hash") ?? "",
|
|
384
|
+
newline: attr(b.attrLine, "newline"),
|
|
318
385
|
ops: parseOps(body),
|
|
319
386
|
});
|
|
320
387
|
}
|
|
@@ -386,6 +453,7 @@ function renderHistory(h, baseName) {
|
|
|
386
453
|
r.author ? `author="${r.author}"` : "",
|
|
387
454
|
r.summary ? `summary="${r.summary}"` : "",
|
|
388
455
|
`hash="${r.hash}"`,
|
|
456
|
+
r.newline ? `newline="${r.newline}"` : "",
|
|
389
457
|
].filter(Boolean).join(" ");
|
|
390
458
|
parts.push(`=== revision {${at}}\n${r.ops.map(opLine).join("\n")}${r.ops.length ? "\n" : ""}===\n`);
|
|
391
459
|
// blobs referenced by this revision
|
|
@@ -417,9 +485,23 @@ export function commit(o) {
|
|
|
417
485
|
if (bytesOf(back, nl).compare(bytesOf(prevContent, nl)) !== 0) {
|
|
418
486
|
throw new Error("history: reverse patch does NOT round-trip to the previous revision; aborting commit");
|
|
419
487
|
}
|
|
488
|
+
// Blob ids are minted per-diff (b1, b2, …). Renumber this commit's blobs to
|
|
489
|
+
// start past the highest id already stored, so a later commit never reuses
|
|
490
|
+
// an earlier revision's blob id — an overwrite in the shared store silently
|
|
491
|
+
// corrupts reconstruction of older revisions, whose `replace … <- blob:bN`
|
|
492
|
+
// would then resolve to the wrong (newer) content.
|
|
493
|
+
let maxBlob = 0;
|
|
494
|
+
for (const k of h.blobs.keys()) {
|
|
495
|
+
const mm = /^b(\d+)$/.exec(k);
|
|
496
|
+
if (mm)
|
|
497
|
+
maxBlob = Math.max(maxBlob, Number(mm[1]));
|
|
498
|
+
}
|
|
499
|
+
const remap = new Map(patch.blobs.map((b, i) => [b.id, `b${maxBlob + i + 1}`]));
|
|
500
|
+
patch.ops = patch.ops.map((op) => (op.blob ? { ...op, blob: remap.get(op.blob) } : op));
|
|
501
|
+
patch.blobs = patch.blobs.map((b) => ({ id: remap.get(b.id), payload: b.payload }));
|
|
420
502
|
for (const b of patch.blobs)
|
|
421
503
|
h.blobs.set(b.id, b.payload);
|
|
422
|
-
h.revisions.set(id, { id, parent: prevId, author: o.author, summary: o.summary, hash, ops: patch.ops });
|
|
504
|
+
h.revisions.set(id, { id, parent: prevId, author: o.author, summary: o.summary, hash, newline: nlNamed(nl), ops: patch.ops });
|
|
423
505
|
h.keyframes.delete(prevId); // demote previous tip mirror (keyframes at intervals only)
|
|
424
506
|
h.keyframes.set(id, working);
|
|
425
507
|
h.current = id;
|
|
@@ -428,7 +510,7 @@ export function commit(o) {
|
|
|
428
510
|
h = {
|
|
429
511
|
nl, current: id,
|
|
430
512
|
keyframes: new Map([[id, working]]),
|
|
431
|
-
revisions: new Map([[id, { id, author: o.author, summary: o.summary, hash, ops: [] }]]),
|
|
513
|
+
revisions: new Map([[id, { id, author: o.author, summary: o.summary, hash, newline: nlNamed(nl), ops: [] }]]),
|
|
432
514
|
blobs: new Map(),
|
|
433
515
|
};
|
|
434
516
|
}
|
|
@@ -450,9 +532,9 @@ export function verify(historyPath, gemlPath) {
|
|
|
450
532
|
for (const r of chain) {
|
|
451
533
|
try {
|
|
452
534
|
const content = reconstruct(h, r.id);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
535
|
+
if (!hashMatchesRecorded(content, r, h.nl)) {
|
|
536
|
+
errors.push(`revision ${r.id}: reconstructed hash ${fullHash(content, nlOf(r.newline) ?? h.nl)} != recorded ${r.hash}`);
|
|
537
|
+
}
|
|
456
538
|
checked++;
|
|
457
539
|
}
|
|
458
540
|
catch (e) {
|
|
@@ -505,3 +587,61 @@ export function restore(o) {
|
|
|
505
587
|
}
|
|
506
588
|
return content;
|
|
507
589
|
}
|
|
590
|
+
/** Is the working file byte-identical to the sidecar's tip revision? False
|
|
591
|
+
* means uncommitted drift (e.g. an earlier commit attempt was refused). */
|
|
592
|
+
export function isCurrent(historyPath, gemlPath) {
|
|
593
|
+
const h = parseHistory(historyPath);
|
|
594
|
+
const tip = h.revisions.get(h.current);
|
|
595
|
+
if (!tip)
|
|
596
|
+
return false;
|
|
597
|
+
const { lf, nl } = loadBytes(gemlPath);
|
|
598
|
+
return fullHash(lf, nl) === tip.hash;
|
|
599
|
+
}
|
|
600
|
+
/** Revisions newest-first, each tagged with the `-N` offset that selects it. */
|
|
601
|
+
export function listRevisions(historyPath) {
|
|
602
|
+
const h = parseHistory(historyPath);
|
|
603
|
+
return chainFrom(h).map((r, i) => ({
|
|
604
|
+
id: r.id, parent: r.parent, author: r.author, summary: r.summary, hash: r.hash,
|
|
605
|
+
offset: i, current: i === 0,
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
/** Resolve a revision selector to its id + reconstructed full text. Selectors:
|
|
609
|
+
* `-N` (N revisions back from current; `-0` is the tip), `latest`/`current`, or
|
|
610
|
+
* an unambiguous id prefix/suffix (the same forms `restore` accepts). */
|
|
611
|
+
export function resolveContent(historyPath, selector) {
|
|
612
|
+
const h = parseHistory(historyPath);
|
|
613
|
+
const chain = chainFrom(h); // chain[0] = current tip
|
|
614
|
+
let id;
|
|
615
|
+
const off = /^-(\d+)$/.exec(selector);
|
|
616
|
+
if (off) {
|
|
617
|
+
const n = Number(off[1]);
|
|
618
|
+
if (n >= chain.length)
|
|
619
|
+
throw new Error(`history: offset -${n} is out of range (only ${chain.length} revision(s))`);
|
|
620
|
+
id = chain[n].id;
|
|
621
|
+
}
|
|
622
|
+
else if (selector === "latest" || selector === "current") {
|
|
623
|
+
id = chain[0].id;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
const ids = [...h.revisions.keys()];
|
|
627
|
+
const matches = ids.filter((x) => x === selector || x.startsWith(selector) || x.endsWith(selector));
|
|
628
|
+
if (matches.length !== 1)
|
|
629
|
+
throw new Error(`history: revision selector "${selector}" matched ${matches.length} revisions`);
|
|
630
|
+
id = matches[0];
|
|
631
|
+
}
|
|
632
|
+
return { id, text: reconstruct(h, id) };
|
|
633
|
+
}
|
|
634
|
+
/** Walk the chain newest→oldest; return the first revision whose block (as
|
|
635
|
+
* extracted by `pick`) differs from `currentBlock` — i.e. the block's previous
|
|
636
|
+
* *distinct* version, skipping revisions that never touched it. Used by
|
|
637
|
+
* `revert --changed`. `undefined` if no earlier revision changed the block. */
|
|
638
|
+
export function firstChangedContent(historyPath, currentBlock, pick) {
|
|
639
|
+
const h = parseHistory(historyPath);
|
|
640
|
+
for (const r of chainFrom(h)) {
|
|
641
|
+
const text = reconstruct(h, r.id);
|
|
642
|
+
const b = pick(text);
|
|
643
|
+
if (b !== undefined && b !== currentBlock)
|
|
644
|
+
return { id: r.id, text };
|
|
645
|
+
}
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
package/dist/render.d.ts
CHANGED
|
@@ -2,5 +2,64 @@ import { type Document } from "./geml.js";
|
|
|
2
2
|
export interface RenderOptions {
|
|
3
3
|
title?: string;
|
|
4
4
|
source?: string;
|
|
5
|
+
loadDoc?: (relPath: string) => string | null;
|
|
6
|
+
parseDoc?: (source: string) => Document;
|
|
7
|
+
tableRows?: number;
|
|
8
|
+
liveGraph?: string;
|
|
9
|
+
graphSidecar?: string;
|
|
5
10
|
}
|
|
11
|
+
interface CGNode {
|
|
12
|
+
n: string;
|
|
13
|
+
doc?: string;
|
|
14
|
+
src?: string;
|
|
15
|
+
leaf?: boolean | number;
|
|
16
|
+
test?: boolean;
|
|
17
|
+
acc?: boolean;
|
|
18
|
+
more?: boolean;
|
|
19
|
+
grp?: string[];
|
|
20
|
+
ext?: number;
|
|
21
|
+
}
|
|
22
|
+
interface CGData {
|
|
23
|
+
start: string;
|
|
24
|
+
depth: number;
|
|
25
|
+
roots: string[];
|
|
26
|
+
nodes: Record<string, CGNode>;
|
|
27
|
+
edges: [string, string, string, string][];
|
|
28
|
+
mode?: "modules";
|
|
29
|
+
module?: string;
|
|
30
|
+
dir?: "up";
|
|
31
|
+
focus?: string;
|
|
32
|
+
partial?: number;
|
|
33
|
+
mods?: {
|
|
34
|
+
p: string;
|
|
35
|
+
doc: string;
|
|
36
|
+
m?: number;
|
|
37
|
+
}[];
|
|
38
|
+
medges?: [string, string, number][];
|
|
39
|
+
entryDocs?: string[];
|
|
40
|
+
gpath?: string[];
|
|
41
|
+
}
|
|
42
|
+
declare function buildCodeGraph(startRel: string, opts: RenderOptions, view?: {
|
|
43
|
+
dir?: "up" | "down";
|
|
44
|
+
node?: string;
|
|
45
|
+
}): {
|
|
46
|
+
data?: CGData;
|
|
47
|
+
error?: string;
|
|
48
|
+
truncated?: boolean;
|
|
49
|
+
};
|
|
50
|
+
export declare function codeGraphRuntime(root: {
|
|
51
|
+
querySelectorAll(sel: string): ArrayLike<Element>;
|
|
52
|
+
}): void;
|
|
53
|
+
export declare function codeGraphWaves(fetchDoc: (rel: string) => Promise<string | null>, parseFn: (s: string) => Document): {
|
|
54
|
+
build: (src: string, view?: {
|
|
55
|
+
dir?: "up" | "down";
|
|
56
|
+
node?: string;
|
|
57
|
+
}) => Promise<{
|
|
58
|
+
data?: CGData;
|
|
59
|
+
error?: string;
|
|
60
|
+
truncated?: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
seed: (name: string, text: string | null) => void;
|
|
63
|
+
};
|
|
64
|
+
export { buildCodeGraph };
|
|
6
65
|
export declare function renderHtml(doc: Document, opts?: RenderOptions): string;
|