@geml/geml
Advanced tools
@@ -516,2 +516,8 @@ #!/usr/bin/env node | ||
| ); | ||
| // A build that deletes files says which ones. Silence here is how the orphans | ||
| // accumulated in the first place — name them, so a rename that drops a whole | ||
| // naming scheme is visible in the log rather than three renamings later. | ||
| if (stats.pruned?.length) { | ||
| console.error(` pruned ${stats.pruned.length} document(s) no longer produced: ${stats.pruned.join(", ")}`); | ||
| } | ||
@@ -518,0 +524,0 @@ // --history: snapshot every changed document into its .gemlhistory sidecar — |
+31
-1
@@ -16,3 +16,3 @@ // geml-code-graph emit — exchange-format symbols/edges → the codemap document | ||
| import { createHash } from "node:crypto"; | ||
| import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; | ||
| import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync } from "node:fs"; | ||
| import { dirname, join, posix } from "node:path"; | ||
@@ -465,2 +465,31 @@ import { buildNormalizer } from "./normalize.mjs"; | ||
| // ---- prune documents this build no longer produces ---- | ||
| // A container that stops yielding symbols simply gets no document; without | ||
| // this, the one written by an earlier build stays behind describing code that | ||
| // is gone. Those orphans are not inert: `geml check` reads their `src=` line | ||
| // ranges and fails the build-time reference check, which is how nine of them | ||
| // — across two renamings of the naming scheme — went unnoticed until one | ||
| // orphan's source file happened to SHRINK past its recorded line numbers. | ||
| // | ||
| // `allDocs` is the authoritative set: every .geml this run emitted, written | ||
| // or byte-identical. Anything else at the top level of outDir is an orphan. | ||
| // Two guards keep this from eating a file it does not own: only the top level | ||
| // is scanned (never _index/, _build/, or any subtree), and a candidate must | ||
| // carry the generated-document marker `resolution-default` in its head — a | ||
| // hand-placed .geml parked in the directory is left alone. | ||
| const pruned = []; | ||
| const keep = new Set(allDocs); | ||
| let present = []; | ||
| try { present = readdirSync(outDir); } catch { present = []; } | ||
| for (const f of present) { | ||
| if (!f.endsWith(".geml") || keep.has(f)) continue; | ||
| const p = join(outDir, f); | ||
| try { | ||
| if (!statSync(p).isFile()) continue; | ||
| if (!/^===\s*meta\b[\s\S]*?\bresolution-default\s*=/.test(readFileSync(p, "utf8").slice(0, 2000))) continue; | ||
| unlinkSync(p); | ||
| pruned.push(f); | ||
| } catch { /* unreadable or already gone — not this build's problem */ } | ||
| } | ||
| return { | ||
@@ -470,2 +499,3 @@ ...stats, | ||
| writtenDocs, | ||
| pruned, | ||
| containers: containers.size, | ||
@@ -472,0 +502,0 @@ symbols: symbols.length, |
@@ -11,3 +11,3 @@ #!/usr/bin/env node | ||
| // stale, use `geml codemap serve` instead. | ||
| import { readdirSync, readFileSync, writeFileSync, realpathSync } from "node:fs"; | ||
| import { readdirSync, readFileSync, writeFileSync, realpathSync, unlinkSync } from "node:fs"; | ||
| import { join, basename, sep, resolve as resolvePath } from "node:path"; | ||
@@ -77,3 +77,16 @@ import { parse, renderHtml } from "../dist/geml.js"; | ||
| } | ||
| // Each tool prunes what it owns: build removes the documents it no longer | ||
| // produces, and this removes the pages whose document is gone. An orphan page | ||
| // is worse than a stale one — it is unreachable from index.html yet still | ||
| // served, so a copied folder ships a page describing deleted code with no way | ||
| // to notice. Only a `<base>.html` whose `<base>.geml` is absent qualifies, so | ||
| // nothing that has a document behind it is ever touched. | ||
| const prunedPages = []; | ||
| for (const f of files) { | ||
| if (!f.endsWith(".html")) continue; | ||
| if (files.includes(f.replace(/\.html$/, ".geml"))) continue; | ||
| try { unlinkSync(join(dir, f)); prunedPages.push(f); } catch { /* already gone */ } | ||
| } | ||
| console.error(`rendered ${n} page(s) -> ${dir}${failed.length ? `; FAILED: ${failed.join(", ")}` : ""}`); | ||
| if (prunedPages.length) console.error(` pruned ${prunedPages.length} orphan page(s): ${prunedPages.join(", ")}`); | ||
| process.exit(failed.length ? 1 : 0); |
+1
-1
| { | ||
| "name": "@geml/geml", | ||
| "version": "1.7.2", | ||
| "version": "1.7.3", | ||
| "mcpName": "io.github.geml-spec/geml", | ||
@@ -5,0 +5,0 @@ "publishConfig": { |
+24
-3
@@ -27,3 +27,4 @@ <p align="center"> | ||
| read or patch one section without re-emitting the whole file (on this repo's | ||
| own spec, ~**66× less context** than shipping the whole document). | ||
| own spec, ~**120× less context** than shipping the whole document — the block | ||
| is ~590 chars whatever the document grows to). | ||
| - **Verifiable** — references are checked at build time (a dangling `#id` is an | ||
@@ -105,2 +106,3 @@ error, not a silent dead link), and the parser emits a document-model JSON | ||
| geml set doc.geml '<selector>' [--head|--intro|--body] [--in F[#src]] # replace ONE block's content | ||
| geml replace doc.geml OLD NEW [--within '<selector>'] # EXPERIMENTAL: literal swap, checked and reported | ||
| geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment | ||
@@ -112,3 +114,4 @@ geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks | ||
| geml history <save|get|restore|verify> doc.geml [...] # .gemlhistory version sidecar (get = list revisions, or print one) | ||
| geml codemap <build|verify|render|serve|refresh|find|mcp> # your codebase's call graph as GEML docs | ||
| geml codemap <build|verify|render|serve|refresh|find> # your codebase's call graph as GEML docs | ||
| geml mcp --root <dir> [--graph <dir>] # serve documents (+ the code graph) over MCP | ||
| geml --help | --version # --version --json prints {"parser","spec"} | ||
@@ -144,2 +147,11 @@ ``` | ||
| `replace` is the cheap path when the exact old text is already known and nothing | ||
| needs reading — a version string in six places, a term renamed. It is the one | ||
| operation where GEML can beat `sed` outright rather than imitate it: the same | ||
| two short strings, but the result is re-parsed before it lands, the blocks it | ||
| touched are named back to you, and it is in `.gemlhistory` to revert. It swaps a | ||
| LITERAL, never a pattern, and refuses a swap that would rename an id — that is | ||
| `geml rename`, which fixes the references too. **It is EXPERIMENTAL and may be | ||
| withdrawn**; build nothing on it that cannot change. | ||
| A write is refused when it would break the document, never merely because it | ||
@@ -241,5 +253,9 @@ removes something. A replacement that drops blocks is carried out and the | ||
| ```sh | ||
| /mcp add npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs | ||
| claude mcp add geml -- npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs | ||
| ``` | ||
| With a code graph under `--root` (`geml codemap build`), the same server also | ||
| serves four read-only `geml_codemap_*` tools. Every tool and option: | ||
| [`docs/mcp-guide.md`](https://github.com/geml-spec/geml/blob/main/docs/mcp-guide.md). | ||
| ## Library | ||
@@ -267,4 +283,9 @@ | ||
| What changed between releases: | ||
| [`CHANGELOG.md`](https://github.com/geml-spec/geml/blob/main/CHANGELOG.md). | ||
| The parser and the specification version independently — `geml --version --json` | ||
| prints both. | ||
| ## License | ||
| MIT. |
+10
-0
@@ -64,2 +64,3 @@ --- | ||
| geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc) | ||
| geml replace file.geml OLD NEW # EXPERIMENTAL literal swap; --within '#id' to narrow | ||
| geml history save file.geml -m "…" # snapshot to .gemlhistory after each meaningful edit | ||
@@ -82,2 +83,11 @@ geml revert file.geml '#id' # roll ONE block back (--rev -2 | changed | <rev-id>) | ||
| When the exact old text is already known and nothing needs reading — a version | ||
| string in six places, a renamed term — `geml replace` is the cheap path, and the | ||
| one to prefer over dropping to `sed`: same two short strings, but the result is | ||
| re-parsed before it lands, the blocks it touched are named back to you, and it | ||
| is in `.gemlhistory` to revert. It swaps a LITERAL, never a pattern, and refuses | ||
| a swap that would rename an id (use `geml rename`, which fixes the references | ||
| too). **It is EXPERIMENTAL and may be withdrawn** — reach for it, but do not | ||
| build anything on it that cannot change. | ||
| A write is refused when it would break the document, never merely because it | ||
@@ -84,0 +94,0 @@ removes something: a replacement that drops blocks is carried out and NAMED on |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
953964
1.53%17411
1.18%286
7.92%53
1.92%