@geml/geml 1.5.1 → 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.
Files changed (45) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +248 -217
  3. package/codemap/adapters/crg.mjs +120 -120
  4. package/codemap/adapters/joern.mjs +131 -131
  5. package/codemap/adapters/scip.mjs +658 -658
  6. package/codemap/browser-stub.mjs +34 -29
  7. package/codemap/build.mjs +609 -609
  8. package/codemap/cross-stack.mjs +303 -303
  9. package/codemap/detect.mjs +399 -399
  10. package/codemap/emit.mjs +480 -480
  11. package/codemap/entries.mjs +129 -129
  12. package/codemap/exclude.mjs +52 -52
  13. package/codemap/find.mjs +49 -49
  14. package/codemap/foldings.mjs +110 -110
  15. package/codemap/joern-export.sc +83 -83
  16. package/codemap/mcp-server.mjs +431 -431
  17. package/codemap/normalize.mjs +275 -275
  18. package/codemap/recipe-trust.mjs +103 -103
  19. package/codemap/refresh.mjs +310 -310
  20. package/codemap/render-all.mjs +77 -77
  21. package/codemap/serve.mjs +585 -585
  22. package/codemap/sfc-virtualize.mjs +367 -367
  23. package/codemap/verify.mjs +155 -148
  24. package/dist/chart.d.ts +1 -0
  25. package/dist/chart.js +4 -1
  26. package/dist/diagnostics.d.ts +1 -1
  27. package/dist/diagnostics.js +16 -0
  28. package/dist/geml.d.ts +5 -1
  29. package/dist/geml.js +1277 -283
  30. package/dist/history.d.ts +11 -8
  31. package/dist/history.js +20 -15
  32. package/dist/mcp.d.ts +1 -1
  33. package/dist/mcp.js +108 -38
  34. package/dist/render-html.d.ts +5 -0
  35. package/dist/render-html.js +45 -36
  36. package/dist/render.d.ts +1 -0
  37. package/dist/render.js +172 -136
  38. package/dist/selector.d.ts +55 -0
  39. package/dist/selector.js +112 -0
  40. package/dist/serialize.js +12 -0
  41. package/dist/table.js +27 -1
  42. package/dist/to-md.js +5 -0
  43. package/package.json +67 -66
  44. package/skill/SKILL.md +82 -0
  45. package/skill/references/authoring.geml +333 -0
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":
@@ -1229,143 +1265,143 @@ function trunc(s, n) {
1229
1265
  // ---------------------------------------------------------------------------
1230
1266
  // Page shell, inline CSS, inline interactivity JS
1231
1267
  // ---------------------------------------------------------------------------
1232
- export const CSS = `
1233
- :root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
1234
- * { box-sizing: border-box; }
1235
- body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
1236
- main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
1237
- h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
1238
- h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1239
- h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1240
- h3 { font-size:1.25em; } h4 { font-size:1em; }
1241
- p { margin:.7em 0; }
1242
- a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
1243
- code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
1244
- pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
1245
- pre code { background:none; padding:0; font-size:.85em; }
1246
- pre.output { background:#0d1117; color:#e6edf3; }
1247
- pre.output code { color:inherit; }
1248
- ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
1249
- ul.task-list { list-style:none; padding-left:.2em; }
1250
- li.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; }
1251
- li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
1252
- li.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; }
1253
- aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
1254
- aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
1255
- aside.warning { border-left-color:#d97706; background:#fff8f0; }
1256
- aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
1257
- figure { margin:1.2em 0; }
1258
- figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
1259
- table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
1260
- table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
1261
- table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
1262
- table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
1263
- table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
1264
- table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
1265
- table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
1266
- table.geml-table td.computed { color:#0a7c52; }
1267
- table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
1268
- .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; }
1269
- .table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
1270
- .table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
1271
- .geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
1272
- .c-title { font-size:15px; font-weight:600; fill:var(--fg); }
1273
- .c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
1274
- .media { max-width:100%; border-radius:8px; }
1275
- .diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
1276
- .math-block { overflow-x:auto; padding:.4em 0; }
1277
- sup.fn a { font-size:.75em; }
1278
- .geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
1279
- .geml-footer code { font-size:.95em; }
1280
- .code-graph { margin:1.4em 0; }
1281
- .cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
1282
- .cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
1283
- .cg-svg { display:block; }
1284
- .cg-search-wrap { position:relative; display:inline-block; }
1285
- .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; }
1286
- .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); }
1287
- .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; }
1288
- .cg-search-row:hover { background:var(--bd); }
1289
- .cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
1290
- .cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
1291
- .cg-search-grp:first-of-type { border-top:0; }
1292
- .cg-stage { display:flex; gap:10px; align-items:flex-start; }
1293
- .cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
1294
- .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); }
1295
- .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; }
1296
- .cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
1297
- .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; }
1298
- .cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
1299
- .cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
1300
- .cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
1301
- .cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
1302
- .cg-crumb .cg-seg:hover { text-decoration:underline; }
1303
- .cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }
1304
- .cg-flash { color:#b42318; }
1305
- .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; }
1306
- .cg-upbtn { cursor:pointer; }
1307
- .cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
1308
- .cg-upbtn text { font-size:11px; fill:#57606a; }
1309
- .cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
1310
- .cg-upbtn:hover text { fill:var(--accent); }
1311
- .cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
1312
- .cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
1313
- .cg-chip { display:inline-flex; align-items:center; gap:4px; }
1314
- .cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
1315
- .cg-note { font-size:.8em; color:#9a6700; }
1316
- .cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
1317
- .cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
1318
- .cg-n { cursor:pointer; }
1319
- .cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
1320
- .cg-n.leaf { opacity:.45; }
1321
- .cg-n.test rect { stroke-dasharray:3 2; }
1322
- .cg-n.grp rect { stroke-width:1.8; }
1323
- .cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
1324
- .cg-e.cand { stroke-dasharray:2 3; }
1325
- .cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
1326
- .cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
1327
- .cg-e.soft { opacity:.55; }
1328
- .cg-svg.hl .cg-n { opacity:.22; }
1329
- .cg-svg.hl .cg-e { opacity:.1; }
1330
- .cg-svg.hl .cg-n.hl { opacity:1; }
1331
- .cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
1268
+ export const CSS = `
1269
+ :root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
1270
+ * { box-sizing: border-box; }
1271
+ body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
1272
+ main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
1273
+ h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
1274
+ h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1275
+ h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1276
+ h3 { font-size:1.25em; } h4 { font-size:1em; }
1277
+ p { margin:.7em 0; }
1278
+ a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
1279
+ code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
1280
+ pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
1281
+ pre code { background:none; padding:0; font-size:.85em; }
1282
+ pre.output { background:#0d1117; color:#e6edf3; }
1283
+ pre.output code { color:inherit; }
1284
+ ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
1285
+ ul.task-list { list-style:none; padding-left:.2em; }
1286
+ li.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; }
1287
+ li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
1288
+ li.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; }
1289
+ aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
1290
+ aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
1291
+ aside.warning { border-left-color:#d97706; background:#fff8f0; }
1292
+ aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
1293
+ figure { margin:1.2em 0; }
1294
+ figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
1295
+ table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
1296
+ table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
1297
+ table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
1298
+ table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
1299
+ table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
1300
+ table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
1301
+ table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
1302
+ table.geml-table td.computed { color:#0a7c52; }
1303
+ table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
1304
+ .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; }
1305
+ .table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
1306
+ .table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
1307
+ .geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
1308
+ .c-title { font-size:15px; font-weight:600; fill:var(--fg); }
1309
+ .c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
1310
+ .media { max-width:100%; border-radius:8px; }
1311
+ .diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
1312
+ .math-block { overflow-x:auto; padding:.4em 0; }
1313
+ sup.fn a { font-size:.75em; }
1314
+ .geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
1315
+ .geml-footer code { font-size:.95em; }
1316
+ .code-graph { margin:1.4em 0; }
1317
+ .cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
1318
+ .cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
1319
+ .cg-svg { display:block; }
1320
+ .cg-search-wrap { position:relative; display:inline-block; }
1321
+ .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; }
1322
+ .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); }
1323
+ .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; }
1324
+ .cg-search-row:hover { background:var(--bd); }
1325
+ .cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
1326
+ .cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
1327
+ .cg-search-grp:first-of-type { border-top:0; }
1328
+ .cg-stage { display:flex; gap:10px; align-items:flex-start; }
1329
+ .cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
1330
+ .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); }
1331
+ .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; }
1332
+ .cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
1333
+ .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; }
1334
+ .cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
1335
+ .cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
1336
+ .cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
1337
+ .cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
1338
+ .cg-crumb .cg-seg:hover { text-decoration:underline; }
1339
+ .cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }
1340
+ .cg-flash { color:#b42318; }
1341
+ .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; }
1342
+ .cg-upbtn { cursor:pointer; }
1343
+ .cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
1344
+ .cg-upbtn text { font-size:11px; fill:#57606a; }
1345
+ .cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
1346
+ .cg-upbtn:hover text { fill:var(--accent); }
1347
+ .cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
1348
+ .cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
1349
+ .cg-chip { display:inline-flex; align-items:center; gap:4px; }
1350
+ .cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
1351
+ .cg-note { font-size:.8em; color:#9a6700; }
1352
+ .cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
1353
+ .cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
1354
+ .cg-n { cursor:pointer; }
1355
+ .cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
1356
+ .cg-n.leaf { opacity:.45; }
1357
+ .cg-n.test rect { stroke-dasharray:3 2; }
1358
+ .cg-n.grp rect { stroke-width:1.8; }
1359
+ .cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
1360
+ .cg-e.cand { stroke-dasharray:2 3; }
1361
+ .cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
1362
+ .cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
1363
+ .cg-e.soft { opacity:.55; }
1364
+ .cg-svg.hl .cg-n { opacity:.22; }
1365
+ .cg-svg.hl .cg-e { opacity:.1; }
1366
+ .cg-svg.hl .cg-n.hl { opacity:1; }
1367
+ .cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
1332
1368
  `;
1333
- export const JS = `
1334
- (function () {
1335
- function cmp(a, b) {
1336
- var na = a.dataset.sort, nb = b.dataset.sort;
1337
- if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
1338
- return (a.textContent || "").localeCompare(b.textContent || "");
1339
- }
1340
- document.querySelectorAll("table.geml-table").forEach(function (table) {
1341
- var tbody = table.tBodies[0];
1342
- if (!tbody) return;
1343
- // Sort on header click.
1344
- var ths = table.tHead ? table.tHead.rows[0].cells : [];
1345
- Array.prototype.forEach.call(ths, function (th, col) {
1346
- th.addEventListener("click", function () {
1347
- var dir = th.classList.contains("asc") ? "desc" : "asc";
1348
- Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
1349
- th.classList.add(dir);
1350
- var rows = Array.prototype.slice.call(tbody.rows);
1351
- rows.sort(function (r1, r2) {
1352
- var c = cmp(r1.cells[col], r2.cells[col]);
1353
- return dir === "asc" ? c : -c;
1354
- });
1355
- rows.forEach(function (r) { tbody.appendChild(r); });
1356
- });
1357
- });
1358
- // Filter rows.
1359
- var fig = table.closest(".table-figure");
1360
- var input = fig ? fig.querySelector(".table-filter") : null;
1361
- if (input) input.addEventListener("input", function () {
1362
- var q = input.value.toLowerCase();
1363
- Array.prototype.forEach.call(tbody.rows, function (r) {
1364
- r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
1365
- });
1366
- });
1367
- });
1368
- })();
1369
+ export const JS = `
1370
+ (function () {
1371
+ function cmp(a, b) {
1372
+ var na = a.dataset.sort, nb = b.dataset.sort;
1373
+ if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
1374
+ return (a.textContent || "").localeCompare(b.textContent || "");
1375
+ }
1376
+ document.querySelectorAll("table.geml-table").forEach(function (table) {
1377
+ var tbody = table.tBodies[0];
1378
+ if (!tbody) return;
1379
+ // Sort on header click.
1380
+ var ths = table.tHead ? table.tHead.rows[0].cells : [];
1381
+ Array.prototype.forEach.call(ths, function (th, col) {
1382
+ th.addEventListener("click", function () {
1383
+ var dir = th.classList.contains("asc") ? "desc" : "asc";
1384
+ Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
1385
+ th.classList.add(dir);
1386
+ var rows = Array.prototype.slice.call(tbody.rows);
1387
+ rows.sort(function (r1, r2) {
1388
+ var c = cmp(r1.cells[col], r2.cells[col]);
1389
+ return dir === "asc" ? c : -c;
1390
+ });
1391
+ rows.forEach(function (r) { tbody.appendChild(r); });
1392
+ });
1393
+ });
1394
+ // Filter rows.
1395
+ var fig = table.closest(".table-figure");
1396
+ var input = fig ? fig.querySelector(".table-filter") : null;
1397
+ if (input) input.addEventListener("input", function () {
1398
+ var q = input.value.toLowerCase();
1399
+ Array.prototype.forEach.call(tbody.rows, function (r) {
1400
+ r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
1401
+ });
1402
+ });
1403
+ });
1404
+ })();
1369
1405
  `;
1370
1406
  // geml-code-graph runtime: layered layout AT DRAW TIME (GEP-0003 / v2-D8) so
1371
1407
  // clicking a node re-roots the view inside the embedded slice. Algorithm as
@@ -0,0 +1,55 @@
1
+ export interface Span {
2
+ start: number;
3
+ end: number;
4
+ }
5
+ export interface Unit {
6
+ span: Span;
7
+ kind: "block" | "heading" | "footnote";
8
+ type?: string;
9
+ id?: string;
10
+ level?: number;
11
+ text?: string;
12
+ }
13
+ export type Selector = {
14
+ form: "list";
15
+ } | {
16
+ form: "id";
17
+ raw: string;
18
+ } | {
19
+ form: "type";
20
+ type: string;
21
+ } | {
22
+ form: "content";
23
+ type?: string;
24
+ hex: string;
25
+ nth: number;
26
+ } | {
27
+ form: "attr";
28
+ type: string;
29
+ key: string;
30
+ };
31
+ export declare function sha8(text: string): string;
32
+ export declare function parseSelector(raw: string | undefined, attrsIdOf: (braces: string) => string | undefined): Selector;
33
+ export interface Addressed {
34
+ unit: Unit;
35
+ hex: string;
36
+ nth: number;
37
+ }
38
+ export declare function addressUnits(units: Unit[], textOf: (u: Unit) => string): Addressed[];
39
+ export declare function shortestAddress(a: Addressed, all: Addressed[]): string;
40
+ export type ContentHit = {
41
+ ok: true;
42
+ unit: Unit;
43
+ } | {
44
+ ok: false;
45
+ why: "no-match";
46
+ } | {
47
+ ok: false;
48
+ why: "wrong-type";
49
+ found: string;
50
+ };
51
+ export declare function matchContent(sel: Extract<Selector, {
52
+ form: "content";
53
+ }>, all: Addressed[]): ContentHit;
54
+ export declare function discoveryHint(where: string): string;
55
+ export declare function matchType(type: string, all: Addressed[]): Unit[];
@@ -0,0 +1,112 @@
1
+ // Block selectors — the one syntax `get`, `set` and `history get` all address
2
+ // blocks with (design: docs/design/specs/2026-08-04-geml-get-set-selector-design-change.md).
3
+ //
4
+ // §2's rule: a selector is a FILTER over blocks, `{…}` holds keys, and the same
5
+ // abbreviation rule applies twice — `#id` is `{#id}` short, `@<hex>` is
6
+ // `{@<hex>}` short. Both are keys; they differ only in selectivity.
7
+ //
8
+ // This module is PURE: it parses selector text and matches it against a unit
9
+ // index the caller supplies. It deliberately imports nothing from geml.ts —
10
+ // that module runs the CLI dispatch on import, so depending on it here would
11
+ // turn `import { parseSelector }` into "run the CLI". The scan that produces
12
+ // `Unit[]` therefore stays in geml.ts (one walk, several sinks) and the
13
+ // selector logic stays here, where it can be unit-tested on plain data.
14
+ import { createHash } from "node:crypto";
15
+ // The content address's hash. Same spelling as the `.gemlhistory` unit key
16
+ // (history.ts:112) because both answer the same question — how to address a
17
+ // unit that carries no id. The VALUES are deliberately not promised to match:
18
+ // history hashes a tile (trailing blank lines included), this hashes a block's
19
+ // span (§3.3). Do not port an address from one layer to the other.
20
+ export function sha8(text) {
21
+ return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex").slice(0, 8);
22
+ }
23
+ const FENCE_SEL = /^={3,}[ \t]*([A-Za-z][A-Za-z0-9_-]*)[ \t]*(@[0-9a-fA-F]{1,}(?:~\d+)?)?[ \t]*(\{.*\})?[ \t]*$/;
24
+ const BARE_AT = /^@([0-9a-fA-F]+)(?:~(\d+))?$/;
25
+ // Parse selector TEXT. Never touches a document: every form is decided by
26
+ // lexis alone, which is also what keeps the two selector namespaces on
27
+ // `history get <file> <rev> <selector>` from overlapping (history design §10.2).
28
+ // `attrsIdOf` lets the caller reuse its own `{…}` parser (parseAttrs) rather
29
+ // than this module growing a second one.
30
+ export function parseSelector(raw, attrsIdOf) {
31
+ if (raw === undefined || raw.trim() === "")
32
+ return { form: "list" };
33
+ const s = raw.trim();
34
+ const bare = BARE_AT.exec(s);
35
+ if (bare)
36
+ return { form: "content", hex: bare[1].toLowerCase(), nth: bare[2] ? Number(bare[2]) : 0 };
37
+ const fence = FENCE_SEL.exec(s);
38
+ if (fence) {
39
+ const type = fence[1];
40
+ const at = fence[2];
41
+ const braces = fence[3];
42
+ if (braces !== undefined) {
43
+ // `=== type {#id}` is the id key written out in full — redundant but
44
+ // legal (§2). Any OTHER key is the declared-not-implemented form.
45
+ const id = attrsIdOf(braces);
46
+ if (id !== undefined)
47
+ return { form: "id", raw: `#${id}` };
48
+ return { form: "attr", type, key: firstKey(braces) };
49
+ }
50
+ if (at !== undefined) {
51
+ const m = BARE_AT.exec(at);
52
+ return { form: "content", type, hex: m[1].toLowerCase(), nth: m[2] ? Number(m[2]) : 0 };
53
+ }
54
+ return { form: "type", type };
55
+ }
56
+ // Anything else is an id or a pasted heading line; the caller resolves it.
57
+ return { form: "id", raw: s };
58
+ }
59
+ // The first key inside `{…}`, for the §7 error message. Best-effort: it only
60
+ // has to name what the caller typed, and a class (`.warn`) is reported as
61
+ // written so the message does not claim a key that is not there.
62
+ function firstKey(braces) {
63
+ const inner = braces.replace(/^\{/, "").replace(/\}$/, "").trim();
64
+ const m = /^([.#]?[A-Za-z_][A-Za-z0-9_-]*)/.exec(inner);
65
+ return m ? m[1] : inner.split(/[\s=]/)[0] ?? "";
66
+ }
67
+ export function addressUnits(units, textOf) {
68
+ const seen = new Map();
69
+ return units.map((unit) => {
70
+ // LF-normalized so a CRLF checkout and an LF one address the same block.
71
+ const hex = sha8(textOf(unit).replace(/\r\n?/g, "\n"));
72
+ const nth = seen.get(hex) ?? 0;
73
+ seen.set(hex, nth + 1);
74
+ return { unit, hex, nth };
75
+ });
76
+ }
77
+ // §6.1 — the SHORTEST address that identifies this unit uniquely, which is what
78
+ // the listing prints. `#id` when it has one; else the bare type when the
79
+ // document holds exactly one block of it; else the content address. The three
80
+ // cases are one rule ("shortest unique"), not three rules.
81
+ export function shortestAddress(a, all) {
82
+ const u = a.unit;
83
+ if (u.id !== undefined)
84
+ return `#${u.id}`;
85
+ if (u.type === undefined)
86
+ return `@${a.hex}${a.nth ? `~${a.nth}` : ""}`;
87
+ const sameType = all.filter((x) => x.unit.type === u.type).length;
88
+ if (sameType === 1)
89
+ return `=== ${u.type}`;
90
+ return `=== ${u.type}@${a.hex}${a.nth ? `~${a.nth}` : ""}`;
91
+ }
92
+ export function matchContent(sel, all) {
93
+ const hit = all.find((a) => a.hex === sel.hex && a.nth === sel.nth);
94
+ if (!hit)
95
+ return { ok: false, why: "no-match" };
96
+ if (sel.type !== undefined && hit.unit.type !== sel.type) {
97
+ return { ok: false, why: "wrong-type", found: hit.unit.type ?? hit.unit.kind };
98
+ }
99
+ return { ok: true, unit: hit.unit };
100
+ }
101
+ // Where to send a caller whose selector found nothing: the listing IS the
102
+ // discovery command, and every address it prints pastes straight back (§6.2).
103
+ // One place, so `get`, `set` and `history get` all point at the same next step.
104
+ export function discoveryHint(where) {
105
+ return ` — run \`geml get ${where}\` to list every addressable block`;
106
+ }
107
+ // Match a type filter: every block of that type in document order. Blocks that
108
+ // carry an id are INCLUDED — the selector says nothing about ids, so filtering
109
+ // by whether one is present would be a rule nobody wrote down (§2).
110
+ export function matchType(type, all) {
111
+ return all.filter((a) => a.unit.type === type).map((a) => a.unit);
112
+ }
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)