@geml/geml 1.7.1 → 1.7.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/README.md +44 -7
- package/codemap/build.mjs +6 -0
- package/codemap/emit.mjs +31 -1
- package/codemap/render-all.mjs +14 -1
- package/dist/block-edit.js +15 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2560 -0
- package/dist/from-md.js +1 -1
- package/dist/geml.d.ts +32 -0
- package/dist/geml.js +197 -2038
- package/dist/history.js +7 -3
- package/dist/mcp.js +46 -8
- package/dist/render-html.d.ts +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +180 -15
- package/dist/selector.d.ts +7 -0
- package/dist/selector.js +41 -1
- package/package.json +67 -67
- package/skill/SKILL.md +35 -0
- package/skill/references/authoring.geml +8 -1
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ print("hi")
|
|
|
24
24
|
- **Addressable** — every block can be named: an `#id`, or a content address for
|
|
25
25
|
the ones nobody named; `geml get` / `geml set '<selector>'`
|
|
26
26
|
read or patch one section without re-emitting the whole file (on this repo's
|
|
27
|
-
own spec, ~**
|
|
27
|
+
own spec, ~**120× less context** than shipping the whole document — the block
|
|
28
|
+
is ~590 chars whatever the document grows to).
|
|
28
29
|
- **Verifiable** — references are checked at build time (a dangling `#id` is an
|
|
29
30
|
error, not a silent dead link), and the parser emits a document-model JSON
|
|
30
31
|
with a `diagnostics` array, so agents and CI get a structured pass/fail signal.
|
|
@@ -96,15 +97,20 @@ Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
|
|
|
96
97
|
```sh
|
|
97
98
|
geml doc.geml # document-model JSON (default --to json)
|
|
98
99
|
geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML
|
|
100
|
+
geml list doc.geml # CALL FIRST: every block, its address, kind, line range
|
|
101
|
+
geml find "text" doc.geml|dir # search block CONTENT -> file<TAB>address; exit 1 = no hit
|
|
99
102
|
geml get doc.geml ['<selector>'] # list addressable blocks, or print what the selector matches
|
|
100
|
-
geml
|
|
103
|
+
geml get doc.geml '#sec' --intro # a section cuts three ways: --head | --intro | --body
|
|
104
|
+
geml set doc.geml '<selector>' [--head|--intro|--body] [--in F[#src]] # replace ONE block's content
|
|
105
|
+
geml replace doc.geml OLD NEW [--within '<selector>'] # EXPERIMENTAL: literal swap, checked and reported
|
|
101
106
|
geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment
|
|
102
107
|
geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks
|
|
103
108
|
geml rename doc.geml '#old' '#new' # rename an id + every reference to it
|
|
104
109
|
geml revert doc.geml '#id' [--rev -1] # undo a block: splice / resurrect / remove
|
|
105
110
|
geml check doc.geml [--root <dir>] # validate only: diagnostics + exit code (--json for the array)
|
|
106
111
|
geml history <save|get|restore|verify> doc.geml [...] # .gemlhistory version sidecar (get = list revisions, or print one)
|
|
107
|
-
geml codemap <build|verify|render|serve|refresh|find
|
|
112
|
+
geml codemap <build|verify|render|serve|refresh|find> # your codebase's call graph as GEML docs
|
|
113
|
+
geml mcp --root <dir> [--graph <dir>] # serve documents (+ the code graph) over MCP
|
|
108
114
|
geml --help | --version # --version --json prints {"parser","spec"}
|
|
109
115
|
```
|
|
110
116
|
|
|
@@ -126,9 +132,31 @@ The agent loop: `geml get` a block → `set`/`add`/`delete`/`rename` it →
|
|
|
126
132
|
|
|
127
133
|
`get` answers with N contents when N match (document order, count on stderr);
|
|
128
134
|
`set` writes ONE block, so a selector matching several is refused (exit 2) with
|
|
129
|
-
the unique address of each candidate.
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
the unique address of each candidate. A section cuts three ways: `--head` is the
|
|
136
|
+
heading line, `--intro` its opening region — everything under it up to its first
|
|
137
|
+
subheading — and `--body` everything under it, so `--body` always contains
|
|
138
|
+
`--intro`, and equals it when the section has no subheading. All three
|
|
139
|
+
round-trip — `geml get f X --body | geml set f X --body` leaves the file
|
|
140
|
+
byte-identical — and `--intro` is how a section's opening is edited without
|
|
141
|
+
pulling its subsections into context. A block has no intro; asking for one is a
|
|
142
|
+
usage error rather than a quiet fall back to the body.
|
|
143
|
+
|
|
144
|
+
`replace` is the cheap path when the exact old text is already known and nothing
|
|
145
|
+
needs reading — a version string in six places, a term renamed. It is the one
|
|
146
|
+
operation where GEML can beat `sed` outright rather than imitate it: the same
|
|
147
|
+
two short strings, but the result is re-parsed before it lands, the blocks it
|
|
148
|
+
touched are named back to you, and it is in `.gemlhistory` to revert. It swaps a
|
|
149
|
+
LITERAL, never a pattern, and refuses a swap that would rename an id — that is
|
|
150
|
+
`geml rename`, which fixes the references too. **It is EXPERIMENTAL and may be
|
|
151
|
+
withdrawn**; build nothing on it that cannot change.
|
|
152
|
+
|
|
153
|
+
A write is refused when it would break the document, never merely because it
|
|
154
|
+
removes something. A replacement that drops blocks is carried out and the
|
|
155
|
+
dropped blocks are named on stderr — unnamed ones counted, references left
|
|
156
|
+
dangling reported — with `geml revert` as the way back. That is the same stance
|
|
157
|
+
`delete` takes, so removing content has one rule rather than two, and no region
|
|
158
|
+
becomes uneditable because something inside it happens to carry an id. The
|
|
159
|
+
round trip above drops nothing: the blocks came back in the text you sent.
|
|
132
160
|
|
|
133
161
|
A `@<hex>` **content address** is the first 8 hex of the SHA-256 of the block's
|
|
134
162
|
own text (line endings normalized to LF, no trailing newline), with `~1`, `~2`…
|
|
@@ -218,9 +246,13 @@ Add to your `claude_desktop_config.json`:
|
|
|
218
246
|
### Claude Code / CLI Clients
|
|
219
247
|
Run the following command to add the server:
|
|
220
248
|
```sh
|
|
221
|
-
|
|
249
|
+
claude mcp add geml -- npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs
|
|
222
250
|
```
|
|
223
251
|
|
|
252
|
+
With a code graph under `--root` (`geml codemap build`), the same server also
|
|
253
|
+
serves four read-only `geml_codemap_*` tools. Every tool and option:
|
|
254
|
+
[`docs/mcp-guide.md`](https://github.com/geml-spec/geml/blob/main/docs/mcp-guide.md).
|
|
255
|
+
|
|
224
256
|
## Library
|
|
225
257
|
|
|
226
258
|
```js
|
|
@@ -243,6 +275,11 @@ Full normative spec, history-sidecar spec, and format comparison live in the
|
|
|
243
275
|
[repository](https://github.com/geml-spec/geml). The spec is itself
|
|
244
276
|
written in GEML (`GEML-spec.geml`) and parsed clean on every test run.
|
|
245
277
|
|
|
278
|
+
What changed between releases:
|
|
279
|
+
[`CHANGELOG.md`](https://github.com/geml-spec/geml/blob/main/CHANGELOG.md).
|
|
280
|
+
The parser and the specification version independently — `geml --version --json`
|
|
281
|
+
prints both.
|
|
282
|
+
|
|
246
283
|
## License
|
|
247
284
|
|
|
248
285
|
MIT.
|
package/codemap/build.mjs
CHANGED
|
@@ -513,6 +513,12 @@ console.error(
|
|
|
513
513
|
+ `${stats.containers} containers (${stats.written} of ${stats.docs} files written), `
|
|
514
514
|
+ `${(stats.bytes / 1048576).toFixed(2)} MB -> ${outDir}`,
|
|
515
515
|
);
|
|
516
|
+
// A build that deletes files says which ones. Silence here is how the orphans
|
|
517
|
+
// accumulated in the first place — name them, so a rename that drops a whole
|
|
518
|
+
// naming scheme is visible in the log rather than three renamings later.
|
|
519
|
+
if (stats.pruned?.length) {
|
|
520
|
+
console.error(` pruned ${stats.pruned.length} document(s) no longer produced: ${stats.pruned.join(", ")}`);
|
|
521
|
+
}
|
|
516
522
|
|
|
517
523
|
// --history: snapshot every changed document into its .gemlhistory sidecar —
|
|
518
524
|
// the graph's own architectural history (geml history get / revert per node).
|
package/codemap/emit.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Emission is deterministic: stable sort orders everywhere; a file is only
|
|
14
14
|
// written when its bytes changed (mtime = "what a change touched").
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
16
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
17
17
|
import { dirname, join, posix } from "node:path";
|
|
18
18
|
import { buildNormalizer } from "./normalize.mjs";
|
|
19
19
|
|
|
@@ -461,10 +461,40 @@ export function emit({ symbols, edges, outDir, buildDir, repoName, container = "
|
|
|
461
461
|
if (!existsSync(p) || readFileSync(p, "utf8") !== content) writeFileSync(p, content);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
// ---- prune documents this build no longer produces ----
|
|
465
|
+
// A container that stops yielding symbols simply gets no document; without
|
|
466
|
+
// this, the one written by an earlier build stays behind describing code that
|
|
467
|
+
// is gone. Those orphans are not inert: `geml check` reads their `src=` line
|
|
468
|
+
// ranges and fails the build-time reference check, which is how nine of them
|
|
469
|
+
// — across two renamings of the naming scheme — went unnoticed until one
|
|
470
|
+
// orphan's source file happened to SHRINK past its recorded line numbers.
|
|
471
|
+
//
|
|
472
|
+
// `allDocs` is the authoritative set: every .geml this run emitted, written
|
|
473
|
+
// or byte-identical. Anything else at the top level of outDir is an orphan.
|
|
474
|
+
// Two guards keep this from eating a file it does not own: only the top level
|
|
475
|
+
// is scanned (never _index/, _build/, or any subtree), and a candidate must
|
|
476
|
+
// carry the generated-document marker `resolution-default` in its head — a
|
|
477
|
+
// hand-placed .geml parked in the directory is left alone.
|
|
478
|
+
const pruned = [];
|
|
479
|
+
const keep = new Set(allDocs);
|
|
480
|
+
let present = [];
|
|
481
|
+
try { present = readdirSync(outDir); } catch { present = []; }
|
|
482
|
+
for (const f of present) {
|
|
483
|
+
if (!f.endsWith(".geml") || keep.has(f)) continue;
|
|
484
|
+
const p = join(outDir, f);
|
|
485
|
+
try {
|
|
486
|
+
if (!statSync(p).isFile()) continue;
|
|
487
|
+
if (!/^===\s*meta\b[\s\S]*?\bresolution-default\s*=/.test(readFileSync(p, "utf8").slice(0, 2000))) continue;
|
|
488
|
+
unlinkSync(p);
|
|
489
|
+
pruned.push(f);
|
|
490
|
+
} catch { /* unreadable or already gone — not this build's problem */ }
|
|
491
|
+
}
|
|
492
|
+
|
|
464
493
|
return {
|
|
465
494
|
...stats,
|
|
466
495
|
allDocs,
|
|
467
496
|
writtenDocs,
|
|
497
|
+
pruned,
|
|
468
498
|
containers: containers.size,
|
|
469
499
|
symbols: symbols.length,
|
|
470
500
|
methods: methods.length,
|
package/codemap/render-all.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// graph area (nested frame), so the whole map is browsable offline — this is
|
|
9
9
|
// the "copy the folder to someone" mode. For a live view that never goes
|
|
10
10
|
// stale, use `geml codemap serve` instead.
|
|
11
|
-
import { readdirSync, readFileSync, writeFileSync, realpathSync } from "node:fs";
|
|
11
|
+
import { readdirSync, readFileSync, writeFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
12
12
|
import { join, basename, sep, resolve as resolvePath } from "node:path";
|
|
13
13
|
import { parse, renderHtml } from "../dist/geml.js";
|
|
14
14
|
|
|
@@ -73,5 +73,18 @@ for (const f of files) {
|
|
|
73
73
|
console.error(`render: ${f}: ${e.message}`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
// Each tool prunes what it owns: build removes the documents it no longer
|
|
77
|
+
// produces, and this removes the pages whose document is gone. An orphan page
|
|
78
|
+
// is worse than a stale one — it is unreachable from index.html yet still
|
|
79
|
+
// served, so a copied folder ships a page describing deleted code with no way
|
|
80
|
+
// to notice. Only a `<base>.html` whose `<base>.geml` is absent qualifies, so
|
|
81
|
+
// nothing that has a document behind it is ever touched.
|
|
82
|
+
const prunedPages = [];
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
if (!f.endsWith(".html")) continue;
|
|
85
|
+
if (files.includes(f.replace(/\.html$/, ".geml"))) continue;
|
|
86
|
+
try { unlinkSync(join(dir, f)); prunedPages.push(f); } catch { /* already gone */ }
|
|
87
|
+
}
|
|
76
88
|
console.error(`rendered ${n} page(s) -> ${dir}${failed.length ? `; FAILED: ${failed.join(", ")}` : ""}`);
|
|
89
|
+
if (prunedPages.length) console.error(` pruned ${prunedPages.length} orphan page(s): ${prunedPages.join(", ")}`);
|
|
77
90
|
process.exit(failed.length ? 1 : 0);
|
package/dist/block-edit.js
CHANGED
|
@@ -25,6 +25,20 @@ function splitLines(source) {
|
|
|
25
25
|
function stripEnding(line) {
|
|
26
26
|
return line.replace(/(\r\n|\r|\n)$/, "");
|
|
27
27
|
}
|
|
28
|
+
// A local mirror of geml.ts's trimSpaceTabEnd — geml.ts already imports THIS
|
|
29
|
+
// module (normalizeBlockId), so importing back would close a cycle. Linear on
|
|
30
|
+
// purpose: `/[ \t]+$/` restarts the run at every index inside a run that never
|
|
31
|
+
// reaches the end of the line, which is quadratic in the line's length.
|
|
32
|
+
function trimSpaceTabEnd(s) {
|
|
33
|
+
let i = s.length;
|
|
34
|
+
while (i > 0) {
|
|
35
|
+
const c = s.charCodeAt(i - 1);
|
|
36
|
+
if (c !== 0x20 && c !== 0x09)
|
|
37
|
+
break;
|
|
38
|
+
i--;
|
|
39
|
+
}
|
|
40
|
+
return i === s.length ? s : s.slice(0, i);
|
|
41
|
+
}
|
|
28
42
|
// Rewrite the id inside a `{…}` attribute block to `#newId`, keeping the braces
|
|
29
43
|
// and every other class/attr byte. If no id is present, insert `#newId` as the
|
|
30
44
|
// first token. The id token sits at a token boundary (`{` or whitespace) and
|
|
@@ -98,7 +112,7 @@ export function normalizeBlockId(blockSrc, newId) {
|
|
|
98
112
|
const openLen = /^=+/.exec(f[1])[0].length;
|
|
99
113
|
for (let j = hi + 1; j < lines.length; j++) {
|
|
100
114
|
const ct = stripEnding(lines[j]);
|
|
101
|
-
const trimmed = ct
|
|
115
|
+
const trimmed = trimSpaceTabEnd(ct);
|
|
102
116
|
if (/^=+$/.test(trimmed) && trimmed.length === openLen)
|
|
103
117
|
break; // plain close: done
|
|
104
118
|
const cm = /^(={3,}[ \t]+#)([^\s}]+)([ \t]*)$/.exec(ct);
|
package/dist/cli.d.ts
ADDED