@cstart/coldstart 2.2.17 → 2.3.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 (72) hide show
  1. package/README.md +19 -0
  2. package/dist/cli.d.ts +9 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +44 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/graph/dump.d.ts +63 -0
  7. package/dist/graph/dump.d.ts.map +1 -0
  8. package/dist/graph/dump.js +183 -0
  9. package/dist/graph/dump.js.map +1 -0
  10. package/dist/graph/view-template.d.ts +10 -0
  11. package/dist/graph/view-template.d.ts.map +1 -0
  12. package/dist/graph/view-template.js +10 -0
  13. package/dist/graph/view-template.js.map +1 -0
  14. package/dist/graph/view.d.ts +20 -0
  15. package/dist/graph/view.d.ts.map +1 -0
  16. package/dist/graph/view.js +99 -0
  17. package/dist/graph/view.js.map +1 -0
  18. package/dist/index-manager.d.ts +16 -0
  19. package/dist/index-manager.d.ts.map +1 -1
  20. package/dist/index-manager.js +50 -0
  21. package/dist/index-manager.js.map +1 -1
  22. package/dist/index.js +8 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/indexer/cochange.d.ts +65 -0
  25. package/dist/indexer/cochange.d.ts.map +1 -0
  26. package/dist/indexer/cochange.js +237 -0
  27. package/dist/indexer/cochange.js.map +1 -0
  28. package/dist/indexer/git.d.ts.map +1 -1
  29. package/dist/indexer/git.js +5 -4
  30. package/dist/indexer/git.js.map +1 -1
  31. package/dist/init.js +1 -1
  32. package/dist/init.js.map +1 -1
  33. package/dist/kb/commit.js +1 -1
  34. package/dist/kb/commit.js.map +1 -1
  35. package/dist/kb/git.d.ts.map +1 -1
  36. package/dist/kb/git.js +1 -0
  37. package/dist/kb/git.js.map +1 -1
  38. package/dist/kb/view-template.d.ts +1 -1
  39. package/dist/kb/view-template.d.ts.map +1 -1
  40. package/dist/kb/view-template.js +1 -1
  41. package/dist/kb/view-template.js.map +1 -1
  42. package/dist/kb/view.d.ts +1 -0
  43. package/dist/kb/view.d.ts.map +1 -1
  44. package/dist/kb/view.js +52 -2
  45. package/dist/kb/view.js.map +1 -1
  46. package/dist/keeper.js +1 -1
  47. package/dist/keeper.js.map +1 -1
  48. package/dist/server/find.d.ts.map +1 -1
  49. package/dist/server/find.js +2 -1
  50. package/dist/server/find.js.map +1 -1
  51. package/dist/server/mcp.d.ts.map +1 -1
  52. package/dist/server/mcp.js +10 -4
  53. package/dist/server/mcp.js.map +1 -1
  54. package/dist/server/searcher.d.ts.map +1 -1
  55. package/dist/server/searcher.js +1 -0
  56. package/dist/server/searcher.js.map +1 -1
  57. package/dist/server/tools.d.ts +6 -1
  58. package/dist/server/tools.d.ts.map +1 -1
  59. package/dist/server/tools.js +63 -3
  60. package/dist/server/tools.js.map +1 -1
  61. package/dist/status.d.ts.map +1 -1
  62. package/dist/status.js +8 -0
  63. package/dist/status.js.map +1 -1
  64. package/hooks/codex-kb-recall.mjs +2 -0
  65. package/hooks/codex-nudge-handler.mjs +3 -3
  66. package/hooks/cursor-kb-recall.mjs +2 -0
  67. package/hooks/elicit-core.mjs +3 -3
  68. package/hooks/kb-recall.mjs +1 -0
  69. package/hooks/nudge-handler.mjs +3 -3
  70. package/package.json +1 -1
  71. package/templates/coldstart.cli.md +2 -0
  72. package/templates/coldstart.mcp.md +2 -0
@@ -5,5 +5,5 @@
5
5
  * `__DATA_JSON__` (the baked note data). Shipped as a string because tsc does
6
6
  * not copy non-.ts assets into dist.
7
7
  */
8
- export declare const VIEW_TEMPLATE = "<title>__TITLE__</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol\u2026\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Wikilink graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// \u2014 fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n const edgeCount = notes.reduce((s,n)=>s+n.outLinks.filter(l=>byId.has(l)).length,0);\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) \u2014 you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' \u00B7 '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors \u2014 verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">\u2014 queries that should surface this note</span><span class=\"chv\">\u203A</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id){linked.add(n.id);linked.add(l);edges.push([n.id,l]);}\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b])=>[idx.get(a),idx.get(b)]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n $('#graphHint').textContent=`${N} linked notes \u00B7 ${edges.length} links. Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} \u00B7 ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
8
+ export declare const VIEW_TEMPLATE = "<title>__TITLE__</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol\u2026\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Note graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// \u2014 fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n // Both link kinds, deduped the same way buildGraph does, so the header\n // stat and the graph hint can never disagree.\n const edgeKeys = new Set();\n for(const n of notes) for(const l of [...n.outLinks,...(n.anchorLinks||[])])\n if(byId.has(l)&&l!==n.id) edgeKeys.add(n.id<l?n.id+'|'+l:l+'|'+n.id);\n const edgeCount = edgeKeys.size;\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) \u2014 you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' \u00B7 '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors \u2014 verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">\u2014 queries that should surface this note</span><span class=\"chv\">\u203A</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n const seen=new Set();\n const addEdge=(a,b,kind)=>{const key=(a<b?a+'|'+b:b+'|'+a);if(seen.has(key))return;seen.add(key);\n linked.add(a);linked.add(b);edges.push([a,b,kind]);};\n // kind 0 = a hand-typed [[wikilink]]; kind 1 = the two notes anchor the\n // same file. Wikilinks are added first so an explicit link is never\n // downgraded to the derived kind by the dedup above.\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id) addEdge(n.id,l,0);\n for(const n of notes) for(const l of (n.anchorLinks||[])) if(byId.has(l)&&l!==n.id) addEdge(n.id,l,1);\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b,k])=>[idx.get(a),idx.get(b),k]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n const wl=edges.filter(e=>e[2]===0).length;\n $('#graphHint').textContent=`${N} linked notes \u00B7 ${edges.length} links `+\n `(${wl} written as [[links]], ${edges.length-wl} dashed: the notes share a file). Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b,k] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.setLineDash(k?[4,4]:[]);\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n ctx.setLineDash([]);\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} \u00B7 ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
9
9
  //# sourceMappingURL=view-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"view-template.d.ts","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,i7sCAAo4sC,CAAC"}
1
+ {"version":3,"file":"view-template.d.ts","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,01uCAAy1uC,CAAC"}
@@ -5,5 +5,5 @@
5
5
  * `__DATA_JSON__` (the baked note data). Shipped as a string because tsc does
6
6
  * not copy non-.ts assets into dist.
7
7
  */
8
- export const VIEW_TEMPLATE = "<title>__TITLE__</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol…\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Wikilink graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// — fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n const edgeCount = notes.reduce((s,n)=>s+n.outLinks.filter(l=>byId.has(l)).length,0);\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) — you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' · '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors — verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">— queries that should surface this note</span><span class=\"chv\">›</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id){linked.add(n.id);linked.add(l);edges.push([n.id,l]);}\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b])=>[idx.get(a),idx.get(b)]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n $('#graphHint').textContent=`${N} linked notes · ${edges.length} links. Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} · ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
8
+ export const VIEW_TEMPLATE = "<title>__TITLE__</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol\u2026\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Note graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// \u2014 fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n // Both link kinds, deduped the same way buildGraph does, so the header\n // stat and the graph hint can never disagree.\n const edgeKeys = new Set();\n for(const n of notes) for(const l of [...n.outLinks,...(n.anchorLinks||[])])\n if(byId.has(l)&&l!==n.id) edgeKeys.add(n.id<l?n.id+'|'+l:l+'|'+n.id);\n const edgeCount = edgeKeys.size;\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) \u2014 you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' \u00b7 '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors \u2014 verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">\u2014 queries that should surface this note</span><span class=\"chv\">\u203a</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n const seen=new Set();\n const addEdge=(a,b,kind)=>{const key=(a<b?a+'|'+b:b+'|'+a);if(seen.has(key))return;seen.add(key);\n linked.add(a);linked.add(b);edges.push([a,b,kind]);};\n // kind 0 = a hand-typed [[wikilink]]; kind 1 = the two notes anchor the\n // same file. Wikilinks are added first so an explicit link is never\n // downgraded to the derived kind by the dedup above.\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id) addEdge(n.id,l,0);\n for(const n of notes) for(const l of (n.anchorLinks||[])) if(byId.has(l)&&l!==n.id) addEdge(n.id,l,1);\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b,k])=>[idx.get(a),idx.get(b),k]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n const wl=edges.filter(e=>e[2]===0).length;\n $('#graphHint').textContent=`${N} linked notes \u00b7 ${edges.length} links `+\n `(${wl} written as [[links]], ${edges.length-wl} dashed: the notes share a file). Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b,k] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.setLineDash(k?[4,4]:[]);\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n ctx.setLineDash([]);\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} \u00b7 ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
9
9
  //# sourceMappingURL=view-template.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"view-template.js","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,i4sCAAi4sC,CAAC"}
1
+ {"version":3,"file":"view-template.js","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAE,u1uCAAu1uC,CAAC"}
package/dist/kb/view.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare function buildViewData(root: string, generated: string): {
28
28
  outLinks: string[];
29
29
  body: string;
30
30
  backlinks: string[];
31
+ anchorLinks: string[];
31
32
  }[];
32
33
  };
33
34
  /** Bake data into the template. `</` is neutralized so note text can't break the
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AA4CjE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;mBA3BvC,UAAU;;;;;;mBAsBX,MAAM,EAAE;;EA2B5B;AAOD;;;oDAGoD;AACpD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAStE;AAyBD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;0EAQ0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAYhF"}
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAqFjE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;mBApEvC,UAAU;;;;;;mBAsBX,MAAM,EAAE;qBAGN,MAAM,EAAE;;EAkE9B;AAOD;;;oDAGoD;AACpD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAStE;AA+BD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;0EAQ0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAYhF"}
package/dist/kb/view.js CHANGED
@@ -52,8 +52,51 @@ function toClientNote(root, note) {
52
52
  outLinks,
53
53
  body,
54
54
  backlinks: [],
55
+ // Notes that talk about the same file. Filled in by buildViewData once
56
+ // every note is known — see linkByAnchor.
57
+ anchorLinks: [],
55
58
  };
56
59
  }
60
+ /**
61
+ * Link notes that anchor the same file.
62
+ *
63
+ * The graph used to draw ONLY hand-typed `[[wikilinks]]`, and agents write
64
+ * almost none: on this repo's own notebook that was 7 links across 101 notes,
65
+ * so the graph showed nine nodes and the rest of the notebook was invisible.
66
+ * Sharing an anchored file is a real relation and it is already recorded, so
67
+ * derive it instead of asking anyone to hand-type it.
68
+ *
69
+ * Capped per note, strongest first (most shared files), because one hub file
70
+ * with twenty notes on it is a clique of 190 edges that tells you nothing.
71
+ */
72
+ const ANCHOR_LINK_CAP = 8;
73
+ function linkByAnchor(notes) {
74
+ const byPath = new Map();
75
+ for (const n of notes) {
76
+ for (const p of new Set(n.anchors.map((a) => a.path))) {
77
+ const list = byPath.get(p);
78
+ if (list)
79
+ list.push(n.id);
80
+ else
81
+ byPath.set(p, [n.id]);
82
+ }
83
+ }
84
+ const byId = new Map(notes.map((n) => [n.id, n]));
85
+ for (const n of notes) {
86
+ const shared = new Map();
87
+ for (const p of new Set(n.anchors.map((a) => a.path))) {
88
+ for (const other of byPath.get(p) ?? []) {
89
+ if (other !== n.id)
90
+ shared.set(other, (shared.get(other) ?? 0) + 1);
91
+ }
92
+ }
93
+ n.anchorLinks = [...shared]
94
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
95
+ .slice(0, ANCHOR_LINK_CAP)
96
+ .map(([id]) => id)
97
+ .filter((id) => byId.has(id));
98
+ }
99
+ }
57
100
  /** Build the `{ summary, notes }` payload the template bakes in. */
58
101
  export function buildViewData(root, generated) {
59
102
  const { notes: folded } = loadAll(root);
@@ -68,6 +111,7 @@ export function buildViewData(root, generated) {
68
111
  t.backlinks.push(n.id);
69
112
  }
70
113
  }
114
+ linkByAnchor(notes);
71
115
  notes.sort((a, b) => (b.updated || '').localeCompare(a.updated || ''));
72
116
  const byType = {};
73
117
  const byFreshness = {};
@@ -97,11 +141,17 @@ export function renderViewHtml(template, data) {
97
141
  }
98
142
  /** Best-effort open in the OS default browser. Never throws. */
99
143
  function openInBrowser(file) {
144
+ // Windows goes through rundll32 rather than `cmd /c start`: cmd re-parses its
145
+ // own command line, so a path containing `&`, `^` or `|` would be interpreted
146
+ // instead of opened. The path here is attacker-influencable in the ordinary
147
+ // sense (it derives from the repo location and from `--out`), and rundll32
148
+ // takes it as a single literal argument with no shell in between.
100
149
  const cmd = process.platform === 'darwin' ? { bin: 'open', args: [file] }
101
- : process.platform === 'win32' ? { bin: 'cmd', args: ['/c', 'start', '', file] }
150
+ : process.platform === 'win32'
151
+ ? { bin: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', file] }
102
152
  : { bin: 'xdg-open', args: [file] };
103
153
  try {
104
- const child = execFile(cmd.bin, cmd.args, () => { });
154
+ const child = execFile(cmd.bin, cmd.args, { windowsHide: true }, () => { });
105
155
  child.unref();
106
156
  }
107
157
  catch { /* ignore — the path is printed regardless */ }
@@ -1 +1 @@
1
- {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAI9C,MAAM,IAAI,GAA+B,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAE7F,kFAAkF;AAClF,SAAS,YAAY,CAAC,IAAgB;IACpC,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACvD,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,sCAAsC;IAChF,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC;AAED,mFAAmF;AACnF,SAAS,YAAY,CAAC,IAAY,EAAE,IAAgB;IAClD,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAmB;KAC7B,CAAC,CAAC,CAAC;IACJ,IAAI,IAAI,GAAe,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YAAE,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC;IAExE,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAExF,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,OAAO,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;QAC3E,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;QAC3B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;QACtB,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,IAAI;QACf,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc;QAC3D,QAAQ;QACR,IAAI;QACJ,SAAS,EAAE,EAAc;KAC1B,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,SAAiB;IAC3D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC;SACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACtB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAE,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;IAEvE,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,WAAW,GAA2B,EAAE,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3C,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC;AAC3G,CAAC;AAED,mFAAmF;AACnF,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC9E,CAAC;AAED;;;oDAGoD;AACpD,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,IAAa;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAI,IAAgD,EAAE,OAAO,EAAE,IAAI,CAAC;IAC9E,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE;QACnD,CAAC,CAAC,wBAAwB,UAAU,CAAC,IAAI,CAAC,EAAE;QAC5C,CAAC,CAAC,oBAAoB,CAAC;IACzB,OAAO,QAAQ;SACZ,OAAO,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC;SACjC,OAAO,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED,gEAAgE;AAChE,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,GAAG,GACP,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE;QAC7D,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE;YAChF,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACpD,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC,CAAC,6CAA6C,CAAC,CAAC;AAC3D,CAAC;AAED,wEAAwE;AACxE,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,YAAY,CAAC,EAAE,CAAC;YAC5D,aAAa,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC;AAC7B,CAAC;AAOD;;;;;;;;0EAQ0E;AAC1E,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAiB;IACtE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IACtD,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC,CAAC,yCAAyC,CAAC,CAAC;IACrD,IAAI,CAAC,SAAS;QAAE,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7C,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;QAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"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"}
package/dist/keeper.js CHANGED
@@ -34,7 +34,7 @@ export async function ensureKeeper(finalRoot) {
34
34
  return; // another reader is already spawning it
35
35
  try {
36
36
  const entry = process.argv[1];
37
- const child = spawn(process.execPath, [entry, '--root', finalRoot, '--daemon', '--quiet'], { detached: true, stdio: 'ignore' });
37
+ const child = spawn(process.execPath, [entry, '--root', finalRoot, '--daemon', '--quiet'], { detached: true, stdio: 'ignore', windowsHide: true });
38
38
  child.unref();
39
39
  }
40
40
  finally {