@geml/geml 1.6.0 → 1.7.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.
package/dist/mcp.js CHANGED
@@ -284,16 +284,37 @@ export const TOOLS = [
284
284
  type: "string",
285
285
  description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, or a `@<hex>` content address for a block with no id — the forms `geml_list` prints",
286
286
  },
287
+ view: {
288
+ type: "boolean",
289
+ description: "Read THROUGH an `embed` block to the entity block it stands for, following a multi-layer chain to its end. An `embed` has no content of its own, so this is the only way to see what it points at; on any other block it changes nothing. Returns {from, content}: `from` names the document the content actually came from, and its references and relative paths resolve against THAT document, not this one.",
290
+ },
291
+ part: {
292
+ type: "string",
293
+ enum: ["whole", "head", "body"],
294
+ description: "How much of the block to return (default: whole). `body` is usually what you want together with `view`.",
295
+ },
287
296
  },
288
297
  required: ["file", "id"],
289
298
  },
290
299
  run: (args) => {
291
300
  const real = resolveInRoot(args.file);
292
301
  const sel = selectorArg(args.id);
293
- const run = runCli(["get", real, sel]);
302
+ // Same name, same enum, same validation as `geml_set` — one concept for a
303
+ // model to learn, and `body` is already taken there for the replacement text.
304
+ const part = args.part ?? "whole";
305
+ if (!["whole", "head", "body"].includes(part))
306
+ throw new Error(`part must be whole|head|body, got \`${part}\``);
307
+ const flag = part === "head" ? ["--head"] : part === "body" ? ["--body"] : [];
308
+ const run = runCli(["get", real, sel, ...flag, ...(args.view ? ["--view"] : [])]);
294
309
  if (!run.ok)
295
310
  throw new Error(run.stderr || `nothing matches ${sel}`);
296
- return run.stdout;
311
+ if (!args.view)
312
+ return run.stdout;
313
+ // There is no stderr across an MCP call, and provenance is mandatory: lift
314
+ // it out of the CLI's pinned `view: <sel> -> <doc>[#<id>]` line into a
315
+ // field of its own.
316
+ const m = /^view: .*? -> (.+)$/m.exec(run.stderr);
317
+ return { from: m ? m[1].trim() : null, content: run.stdout };
297
318
  },
298
319
  },
299
320
  {
@@ -1,3 +1,8 @@
1
1
  import { type Document } from "./geml.js";
2
2
  import { type RenderOptions } from "./render.js";
3
3
  export declare function renderHtml(doc: Document, opts?: RenderOptions): string;
4
+ export declare const pageAssets: {
5
+ readonly css: "\n:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }\n* { box-sizing: border-box; }\nbody { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,\"PingFang SC\",\"Microsoft Yahei\",sans-serif; }\nmain { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }\nh1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }\nh1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh3 { font-size:1.25em; } h4 { font-size:1em; }\np { margin:.7em 0; }\na { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }\ncode { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }\npre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }\npre code { background:none; padding:0; font-size:.85em; }\npre.output { background:#0d1117; color:#e6edf3; }\npre.output code { color:inherit; }\nul,ol { padding-left:1.6em; } li { margin:.2em 0; }\nul.task-list { list-style:none; padding-left:.2em; }\nli.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }\nli.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }\nli.task input[type=checkbox]:checked::after { content:\"✓\"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }\naside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }\naside.aside { border-left-color:#8b949e; background:#f6f8fa; }\naside.warning { border-left-color:#d97706; background:#fff8f0; }\naside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }\nfigure { margin:1.2em 0; }\nfigcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }\ntable.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }\ntable.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }\ntable.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }\ntable.geml-table thead th::after { content:\" \\2195\"; color:var(--muted); font-size:.8em; }\ntable.geml-table thead th.asc::after { content:\" \\2191\"; color:var(--accent); }\ntable.geml-table thead th.desc::after { content:\" \\2193\"; color:var(--accent); }\ntable.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }\ntable.geml-table td.computed { color:#0a7c52; }\ntable.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }\n.table-tools { margin-bottom:6px; } .table-filter { width:240px; max-width:100%; padding:5px 9px; border:1px solid var(--bd); border-radius:7px; font-size:.85em; }\n.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }\n.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }\n.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }\n.c-title { font-size:15px; font-weight:600; fill:var(--fg); }\n.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }\n.media { max-width:100%; border-radius:8px; }\n.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }\n.math-block { overflow-x:auto; padding:.4em 0; }\nsup.fn a { font-size:.75em; }\n.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }\n.geml-footer code { font-size:.95em; }\n.code-graph { margin:1.4em 0; }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }\n.cg-svg { display:block; }\n.cg-search-wrap { position:relative; display:inline-block; }\n.cg-search { font:12px/1.4 inherit; padding:2px 7px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); min-width:13ch; }\n.cg-search-menu { position:absolute; z-index:30; top:calc(100% + 2px); left:0; min-width:24ch; max-width:52ch; max-height:52vh; overflow:auto; background:var(--bg); border:1px solid var(--bd); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.18); }\n.cg-search-row { display:block; width:100%; text-align:left; padding:4px 9px 4px 18px; border:0; background:none; color:var(--fg); cursor:pointer; font:12px/1.4 inherit; }\n.cg-search-row:hover { background:var(--bd); }\n.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }\n.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }\n.cg-search-grp:first-of-type { border-top:0; }\n.cg-stage { display:flex; gap:10px; align-items:flex-start; }\n.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }\n.cg-src { flex:0 0 42%; max-width:46%; display:flex; flex-direction:column; border:1px solid var(--bd); border-radius:6px; overflow:hidden; background:var(--bg); }\n.cg-src-hd { display:flex; gap:8px; align-items:center; justify-content:space-between; padding:4px 8px; border-bottom:1px solid var(--bd); color:var(--muted); font:.76em ui-monospace,Consolas,monospace; word-break:break-all; }\n.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }\n.cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:72vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }\n.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }\n.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }\n.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }\n.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }\n.cg-crumb .cg-seg:hover { text-decoration:underline; }\n.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }\n.cg-flash { color:#b42318; }\n.cg-legend { display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; font-size:.75em; color:var(--muted); margin-top:6px; }\n.cg-upbtn { cursor:pointer; }\n.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }\n.cg-upbtn text { font-size:11px; fill:#57606a; }\n.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }\n.cg-upbtn:hover text { fill:var(--accent); }\n.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }\n.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }\n.cg-chip { display:inline-flex; align-items:center; gap:4px; }\n.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }\n.cg-note { font-size:.8em; color:#9a6700; }\n.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }\n.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }\n.cg-n { cursor:pointer; }\n.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }\n.cg-n.leaf { opacity:.45; }\n.cg-n.test rect { stroke-dasharray:3 2; }\n.cg-n.grp rect { stroke-width:1.8; }\n.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }\n.cg-e.cand { stroke-dasharray:2 3; }\n.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }\n.cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\n.cg-e.soft { opacity:.55; }\n.cg-svg.hl .cg-n { opacity:.22; }\n.cg-svg.hl .cg-e { opacity:.1; }\n.cg-svg.hl .cg-n.hl { opacity:1; }\n.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }\n";
6
+ readonly js: "\n(function () {\n function cmp(a, b) {\n var na = a.dataset.sort, nb = b.dataset.sort;\n if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);\n return (a.textContent || \"\").localeCompare(b.textContent || \"\");\n }\n document.querySelectorAll(\"table.geml-table\").forEach(function (table) {\n var tbody = table.tBodies[0];\n if (!tbody) return;\n // Sort on header click.\n var ths = table.tHead ? table.tHead.rows[0].cells : [];\n Array.prototype.forEach.call(ths, function (th, col) {\n th.addEventListener(\"click\", function () {\n var dir = th.classList.contains(\"asc\") ? \"desc\" : \"asc\";\n Array.prototype.forEach.call(ths, function (h) { h.classList.remove(\"asc\", \"desc\"); });\n th.classList.add(dir);\n var rows = Array.prototype.slice.call(tbody.rows);\n rows.sort(function (r1, r2) {\n var c = cmp(r1.cells[col], r2.cells[col]);\n return dir === \"asc\" ? c : -c;\n });\n rows.forEach(function (r) { tbody.appendChild(r); });\n });\n });\n // Filter rows.\n var fig = table.closest(\".table-figure\");\n var input = fig ? fig.querySelector(\".table-filter\") : null;\n if (input) input.addEventListener(\"input\", function () {\n var q = input.value.toLowerCase();\n Array.prototype.forEach.call(tbody.rows, function (r) {\n r.style.display = (r.textContent || \"\").toLowerCase().indexOf(q) >= 0 ? \"\" : \"none\";\n });\n });\n });\n})();\n";
7
+ readonly codeGraphJs: string;
8
+ };
@@ -33,7 +33,7 @@ function page(title, body, ctx, source) {
33
33
  const wantLive = ctx.usedCodeGraph && !!ctx.opts.liveGraph;
34
34
  const lg = wantLive ? escAttr(ctx.opts.liveGraph) : "";
35
35
  const importMap = wantLive
36
- ? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js"}}</script>\n`
36
+ ? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js","node:os":"${lg}_node-stub.js"}}</script>\n`
37
37
  : "";
38
38
  const liveJs = wantLive
39
39
  ? `<script type="module">
@@ -90,6 +90,15 @@ export function renderHtml(doc, opts = {}) {
90
90
  : `layered method flow — roots: in-degree-zero methods (no <code>entry</code> declared)`;
91
91
  body = ctx.codeGraphFigure(opts.source, "", `<figcaption>${cap}</figcaption>`) + "\n" + body;
92
92
  }
93
+ // Fragment mode: the body markup alone, for embedding into an existing
94
+ // layout. No shell, no CDN tags, no inline CSS/JS — see RenderOptions.
95
+ if (opts.fragment)
96
+ return body + "\n";
93
97
  const title = opts.title ?? ctx.docTitle() ?? "GEML document";
94
98
  return page(title, body, ctx, opts.source);
95
99
  }
100
+ // The page shell's static assets, for fragment consumers: `css` styles every
101
+ // geml-* class a fragment emits (include once per site); `js` is the tables'
102
+ // sort/filter enhancement (once per page); `codeGraphJs` matters only when a
103
+ // fragment carries a code-graph mount. The full-page output inlines all three.
104
+ export const pageAssets = { css: CSS, js: JS, codeGraphJs: CODE_GRAPH_JS };
package/dist/render.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface RenderOptions {
8
8
  tableRows?: number;
9
9
  liveGraph?: string;
10
10
  graphSidecar?: string;
11
+ fragment?: boolean;
11
12
  }
12
13
  export declare function esc(s: string): string;
13
14
  export declare function escAttr(s: string): string;
package/dist/render.js CHANGED
@@ -538,6 +538,42 @@ export class RenderCtx {
538
538
  const classes = classAttr(["text", ...b.classes]);
539
539
  return `<div class="${classes}"${idAttr}>\n${inner}\n</div>`;
540
540
  }
541
+ case "data": {
542
+ // GEP-0005: the page shows a PREVIEW under the same row discipline
543
+ // tables use (opts.tableRows, default 500) — the MODEL always keeps
544
+ // everything. Direction follows the format's reading order: jsonl is
545
+ // an append-log, so the newest (last) lines are the preview; json is
546
+ // one value and reads from the top.
547
+ const limit = this.opts.tableRows ?? 500;
548
+ const src = typeof b.attrs["src"] === "string" ? b.attrs["src"] : undefined;
549
+ const fmt = typeof b.attrs["format"] === "string" ? b.attrs["format"]
550
+ : src !== undefined && /\.jsonl$/i.test(src) ? "jsonl" : "json";
551
+ let lines = b.raw ?? [];
552
+ if (lines.every((l) => l.trim() === "")) {
553
+ if (b.value !== undefined) {
554
+ // src= content loaded at build time: preview the canonical form.
555
+ lines = fmt === "jsonl" && Array.isArray(b.value)
556
+ ? b.value.map((v) => JSON.stringify(v))
557
+ : JSON.stringify(b.value, null, 2).split("\n");
558
+ }
559
+ else if (src !== undefined) {
560
+ // §9.4: a render-time source (http, or no resolver at build).
561
+ const cap0 = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
562
+ return `<figure${idAttr}><p class="table-note">external data <code>${esc(src)}</code> — loaded at render time</p>${cap0}</figure>`;
563
+ }
564
+ }
565
+ const tail = fmt === "jsonl" && lines.length > limit;
566
+ const shown = tail ? lines.slice(-limit) : lines.slice(0, limit);
567
+ const omitted = lines.length - shown.length;
568
+ const note = omitted > 0
569
+ ? `<p class="table-note">${tail
570
+ ? `showing the last ${shown.length} of ${lines.length} lines — earlier lines are in the document source`
571
+ : `showing the first ${shown.length} of ${lines.length} lines — the complete data is in the document source`}</p>`
572
+ : "";
573
+ const cap = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
574
+ const pre = `<pre class="data-src" data-format="${escAttr(fmt)}">${esc(shown.join("\n"))}</pre>`;
575
+ return `<figure${idAttr}>${tail ? note + pre : pre + note}${cap}</figure>`;
576
+ }
541
577
  case "table":
542
578
  return b.table ? this.table(b.table, b.id, caption) : `<p class="render-error">table failed to parse</p>`;
543
579
  case "diagram":
package/dist/serialize.js CHANGED
@@ -174,6 +174,18 @@ function serTypedBlock(b) {
174
174
  else if (b.mode === "data") {
175
175
  body = Object.entries(b.data ?? {}).map(([k, v]) => `${k} = ${serDataValue(v)}`);
176
176
  }
177
+ else if (b.type === "data" && b.value !== undefined && b.attrs["src"] === undefined) {
178
+ // src= content lives in the FILE: a value loaded from it must never be
179
+ // inlined into the document by a re-format — the body stays as authored
180
+ // (normally empty), or the source of truth would silently fork.
181
+ // GEP-0005 canonical form — legal because this is DATA, not verbatim text
182
+ // (code stays byte-preserved): json pretty-prints at two spaces, jsonl is
183
+ // one compact value per line. Engine-less bodies (yaml/toml/unknown)
184
+ // never reach here (no `value`) and fall through byte-preserved.
185
+ body = String(b.attrs["format"] ?? "json") === "jsonl" && Array.isArray(b.value)
186
+ ? b.value.map((v) => JSON.stringify(v))
187
+ : JSON.stringify(b.value, null, 2).split("\n");
188
+ }
177
189
  else {
178
190
  body = b.raw ?? [];
179
191
  }
package/dist/table.js CHANGED
@@ -60,6 +60,27 @@ function parseDelimited(body, sep, header) {
60
60
  const width = rows.reduce((m, r) => Math.max(m, r.length), 0);
61
61
  return { columns: letters(width), align: [], header: false, cells: rows };
62
62
  }
63
+ // The character a data body splits on: the format's natural delimiter unless
64
+ // `delim=` names another single one — `;` for a European CSV, `|` for a
65
+ // pipe-delimited export (§6). Measured in code points, so an astral character
66
+ // counts as one. §4 attribute values carry no escape syntax, so a tab
67
+ // delimiter is spelled `format=tsv`, never `delim="\t"`; a value that is not
68
+ // exactly one character is an error and the natural delimiter is used, which
69
+ // keeps the rest of the table readable.
70
+ function resolveDelim(fmt, raw, diagnostics) {
71
+ const natural = fmt === "tsv" ? "\t" : ",";
72
+ if (raw === undefined)
73
+ return natural;
74
+ const d = String(raw);
75
+ if ([...d].length === 1)
76
+ return d;
77
+ diagnostics.push({
78
+ severity: "error",
79
+ code: "bad-table-delimiter",
80
+ message: `\`delim="${d}"\` must be exactly one character (for a tab use \`format=tsv\`); split on ${fmt === "tsv" ? "a tab" : "`,`"} instead`,
81
+ });
82
+ return natural;
83
+ }
63
84
  function letters(n) {
64
85
  const out = [];
65
86
  for (let i = 0; i < n; i++)
@@ -271,11 +292,16 @@ export function parseTable(body, attrs, line, sink) {
271
292
  if (fmt === "csv" || fmt === "tsv") {
272
293
  const headerAttr = attrs["header"];
273
294
  const header = headerAttr === undefined ? true : headerAttr === true || headerAttr === 1 || headerAttr === "1";
274
- raw = parseDelimited(body, fmt === "tsv" ? "\t" : ",", header);
295
+ raw = parseDelimited(body, resolveDelim(fmt, attrs["delim"], diagnostics), header);
275
296
  }
276
297
  else {
277
298
  if (fmt !== undefined)
278
299
  diagnostics.push({ severity: "warning", code: "unknown-table-format", message: `unknown table format \`${fmt}\`; parsed as visual grid` });
300
+ // `delim` refines the data form; it does not select it. Silently dropping it
301
+ // would leave a `=== table {delim=";"}` parsed as a one-column visual grid
302
+ // with nothing to say why.
303
+ if (attrs["delim"] !== undefined)
304
+ diagnostics.push({ severity: "warning", code: "ignored-table-delimiter", message: "`delim` applies to a data body (`format=csv`/`tsv`); this body was parsed as a visual grid, so it is ignored" });
279
305
  raw = parseVisual(body);
280
306
  }
281
307
  const columns = [...raw.columns];
package/dist/to-md.js CHANGED
@@ -144,6 +144,11 @@ function typedToMd(b, notes) {
144
144
  const raw = b.raw ?? [];
145
145
  if (b.type === "code")
146
146
  return fence(attr(b, "lang") ?? "", raw);
147
+ // GEP-0005: a data block projects as a fenced code block in its format —
148
+ // the nearest GFM shape (Markdown has no verified-data construct; that loss
149
+ // is the usual --to md lossiness, not a defect of the projection).
150
+ if (b.type === "data")
151
+ return fence(attr(b, "format") ?? "json", raw);
147
152
  if (b.type === "math")
148
153
  return ["$$", ...raw, "$$"].join("\n");
149
154
  if (b.type === "table" && b.table)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geml/geml",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "mcpName": "io.github.geml-spec/geml",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "codemap",
18
+ "skill",
18
19
  "README.md",
19
20
  "LICENSE"
20
21
  ],
package/skill/SKILL.md ADDED
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: geml
3
+ description: >-
4
+ Read, author, edit, or validate GEML — the General Expressive Markup Language
5
+ (.geml files) and its .gemlhistory versioning sidecar. Use whenever creating
6
+ or modifying a .geml/.gemlhistory file, converting Markdown to GEML, or when
7
+ the user mentions GEML, typed blocks, === fences, or geml-chart. Ensures the
8
+ output parses cleanly (zero error diagnostics) against the reference parser.
9
+ ---
10
+
11
+ # Writing and reading GEML correctly
12
+
13
+ GEML expresses **every** kind of structured content — code, tables, diagrams,
14
+ math, callouts, metadata — through **one** primitive: the **typed block**
15
+ (`=== <type> {#id .class key=val}` … `===`). Always finish by **validating**: a
16
+ GEML file is correct only when `geml check` reports **no error diagnostics**
17
+ (exit 0).
18
+
19
+ ## Golden rules (the things that are easy to get wrong)
20
+
21
+ 1. **Fences are runs of `=` (≥3).** A block closes at a `=` run of **exactly
22
+ the opening length**, or — when the block has an `#id` — at the labeled
23
+ fence `=== #id` (any `=` run ≥3 followed by the id; no length counting).
24
+ 2. **Nest with longer fences.** A body containing `===` lines needs a
25
+ **longer** outer fence: `====` wraps `===`. Careful: a same-length bare
26
+ `===` in the body closes the block even if you intend a labeled close —
27
+ the labeled close only spares you length-counting, it does NOT protect
28
+ same-length inner fences.
29
+ 3. **Headings are ATX `#` only** (`#`…`######`). No setext underlines, no
30
+ `---` breaks, no YAML frontmatter — metadata is a `=== meta` block, and the
31
+ document TITLE lives there (`title = "…"`), not in an H1. A heading may
32
+ carry a stable explicit id: `## Title {#sec}`.
33
+ 4. **Every `#id` is unique per document**, and **every reference must
34
+ resolve** — `[t](#id)`, `[[#id]]`, `[^id]`, `src=`, `data=`,
35
+ `other.geml#id`. An unresolved reference is a build **error**.
36
+ 5. **No raw HTML.** Notes → `=== note`, comments → `%%` lines, hidden content
37
+ → `{hidden}`, addressable prose → `=== text`, verified data → `=== data`
38
+ (json/jsonl; `code` shows text, `data` IS data).
39
+
40
+ ## Validate every time
41
+
42
+ ```sh
43
+ geml check file.geml # diagnostics + exit code only; exit 0 = correct
44
+ geml check --json file.geml # machine-readable diagnostics array
45
+ ```
46
+
47
+ If `geml` is not on PATH: `npm i -g @geml/geml` (package `@geml/geml`, command
48
+ `geml`), or run without installing via `npx -y @geml/geml check file.geml`.
49
+ Inside the geml-spec repo prefer the local build:
50
+ `node geml-parser/dist/geml.js <args>`. If no parser is reachable, follow the
51
+ golden rules and validate once it is.
52
+
53
+ ## Work blockwise (agent editing)
54
+
55
+ ```sh
56
+ geml get file.geml '#id' # read ONE block (a heading id = its whole section)
57
+ geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc)
58
+ geml history save file.geml -m "…" # snapshot to .gemlhistory after each meaningful edit
59
+ geml revert file.geml '#id' # roll ONE block back (--rev -2 | changed | <rev-id>)
60
+ ```
61
+
62
+ ## Full reference — pull ONE section, not the whole file
63
+
64
+ `references/authoring.geml` (under this skill's base directory) holds the
65
+ detailed reference. Fetch just the section you need:
66
+
67
+ ```sh
68
+ geml get <skill-base>/references/authoring.geml '#tables'
69
+ ```
70
+
71
+ | section | covers |
72
+ |---|---|
73
+ | `#typed-block` | block anatomy, attribute object, examples of every registered type |
74
+ | `#tables` | pipe/CSV bodies, `compute=`, `summary=`, printf display, `span=` merges |
75
+ | `#charts` | `geml-chart` diagrams bound to a table via `data=#id` |
76
+ | `#data` | the `data` block — value tree, `json`/`jsonl` formats, blind append, chart binding |
77
+ | `#inline` | inline markup, links/refs/footnotes, task lists, media embeds |
78
+ | `#hidden` | `%%` comments, `{hidden}`, `{{key}}` interpolation, `=== embed` |
79
+ | `#cli` | every CLI verb — get/set/add/delete/rename, `--to` conversion, check |
80
+ | `#editing` | the blockwise editing loop + `.gemlhistory` versioning |
81
+ | `#project-config` | carrying a project's Claude config docs in GEML, quietly |
82
+ | `#checklist` | full pre-flight authoring checklist |