@cstart/coldstart 2.2.9 → 2.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/init.d.ts +33 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +75 -48
- package/dist/init.js.map +1 -1
- package/dist/kb/cli.d.ts.map +1 -1
- package/dist/kb/cli.js +53 -11
- package/dist/kb/cli.js.map +1 -1
- package/dist/kb/fold.d.ts +25 -1
- package/dist/kb/fold.d.ts.map +1 -1
- package/dist/kb/fold.js +39 -0
- package/dist/kb/fold.js.map +1 -1
- package/dist/kb/repair.d.ts +36 -0
- package/dist/kb/repair.d.ts.map +1 -0
- package/dist/kb/repair.js +116 -0
- package/dist/kb/repair.js.map +1 -0
- package/dist/kb/search.d.ts +3 -2
- package/dist/kb/search.d.ts.map +1 -1
- package/dist/kb/search.js +97 -7
- package/dist/kb/search.js.map +1 -1
- package/dist/kb/session-worklist.d.ts +10 -0
- package/dist/kb/session-worklist.d.ts.map +1 -0
- package/dist/kb/session-worklist.js +92 -0
- package/dist/kb/session-worklist.js.map +1 -0
- package/dist/kb/view-template.d.ts +4 -3
- package/dist/kb/view-template.d.ts.map +1 -1
- package/dist/kb/view-template.js +4 -3
- package/dist/kb/view-template.js.map +1 -1
- package/dist/kb/view.d.ts +4 -1
- package/dist/kb/view.d.ts.map +1 -1
- package/dist/kb/view.js +15 -2
- package/dist/kb/view.js.map +1 -1
- package/dist/kb/write-guide.d.ts +43 -1
- package/dist/kb/write-guide.d.ts.map +1 -1
- package/dist/kb/write-guide.js +69 -25
- package/dist/kb/write-guide.js.map +1 -1
- package/dist/kb/write.d.ts.map +1 -1
- package/dist/kb/write.js +10 -1
- package/dist/kb/write.js.map +1 -1
- package/dist/server/mcp.d.ts +8 -1
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +39 -5
- package/dist/server/mcp.js.map +1 -1
- package/dist/unwire.d.ts.map +1 -1
- package/dist/unwire.js +5 -2
- package/dist/unwire.js.map +1 -1
- package/hooks/capture-payload.mjs +73 -13
- package/hooks/note-shape.d.mts +61 -0
- package/hooks/note-shape.mjs +287 -0
- package/package.json +1 -1
package/dist/kb/view-template.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Embedded single-file notebook viewer (client). Generated from the
|
|
3
|
-
* design source; do not hand-edit.
|
|
4
|
-
*
|
|
3
|
+
* design source; do not hand-edit. Two placeholders are substituted by
|
|
4
|
+
* `renderViewHtml` in `kb view`: `__TITLE__` (the tab title, repo-scoped) and
|
|
5
|
+
* `__DATA_JSON__` (the baked note data). Shipped as a string because tsc does
|
|
5
6
|
* not copy non-.ts assets into dist.
|
|
6
7
|
*/
|
|
7
|
-
export const VIEW_TEMPLATE = "<title>Coldstart Notebook — arches</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 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=>({'&':'&','<':'<','>':'>'}[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 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=>({'&':'&','<':'<','>':'>'}[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
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
|
|
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"}
|
package/dist/kb/view.d.ts
CHANGED
|
@@ -30,7 +30,10 @@ export declare function buildViewData(root: string, generated: string): {
|
|
|
30
30
|
backlinks: string[];
|
|
31
31
|
}[];
|
|
32
32
|
};
|
|
33
|
-
/** Bake data into the template. `</` is neutralized so note text can't break the
|
|
33
|
+
/** Bake data into the template. `</` is neutralized so note text can't break the
|
|
34
|
+
* script tag; the tab title is scoped to the repo already carried in the data
|
|
35
|
+
* (`summary.repo`, = basename(root)) so every notebook's view is named for its
|
|
36
|
+
* own repo rather than a hardcoded placeholder. */
|
|
34
37
|
export declare function renderViewHtml(template: string, data: unknown): string;
|
|
35
38
|
export interface ViewOptions {
|
|
36
39
|
open?: boolean;
|
package/dist/kb/view.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/kb/view.js
CHANGED
|
@@ -77,10 +77,23 @@ export function buildViewData(root, generated) {
|
|
|
77
77
|
}
|
|
78
78
|
return { summary: { total: notes.length, byType, byFreshness, repo: basename(root), generated }, notes };
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
80
|
+
/** Escape the three chars that matter inside an HTML text node (here, <title>). */
|
|
81
|
+
function escapeHtml(s) {
|
|
82
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
83
|
+
}
|
|
84
|
+
/** Bake data into the template. `</` is neutralized so note text can't break the
|
|
85
|
+
* script tag; the tab title is scoped to the repo already carried in the data
|
|
86
|
+
* (`summary.repo`, = basename(root)) so every notebook's view is named for its
|
|
87
|
+
* own repo rather than a hardcoded placeholder. */
|
|
81
88
|
export function renderViewHtml(template, data) {
|
|
82
89
|
const json = JSON.stringify(data).replace(/<\//g, '<\\/');
|
|
83
|
-
|
|
90
|
+
const repo = data?.summary?.repo;
|
|
91
|
+
const title = typeof repo === 'string' && repo.trim()
|
|
92
|
+
? `Coldstart Notebook — ${escapeHtml(repo)}`
|
|
93
|
+
: 'Coldstart Notebook';
|
|
94
|
+
return template
|
|
95
|
+
.replace('__TITLE__', () => title)
|
|
96
|
+
.replace('__DATA_JSON__', () => json);
|
|
84
97
|
}
|
|
85
98
|
/** Best-effort open in the OS default browser. Never throws. */
|
|
86
99
|
function openInBrowser(file) {
|
package/dist/kb/view.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"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"}
|
package/dist/kb/write-guide.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WriteSpec } from './write.js';
|
|
2
|
-
export declare const WRITE_GUIDE_SHAPES
|
|
2
|
+
export declare const WRITE_GUIDE_SHAPES: string;
|
|
3
3
|
export declare function writeGuideCli(): string;
|
|
4
4
|
export declare function writeGuideMcp(): string;
|
|
5
5
|
/** Flow-evidence check: how many of the spec's step files did THIS session
|
|
@@ -9,6 +9,48 @@ export declare function flowEvidenceCount(spec: WriteSpec, session: string): {
|
|
|
9
9
|
read: number;
|
|
10
10
|
steps: number;
|
|
11
11
|
} | null;
|
|
12
|
+
/** The WARN text (never a rejection) when a flow ends up with no `steps` at all.
|
|
13
|
+
*
|
|
14
|
+
* A stepless flow folds to a note that is a title and a paragraph — no chain to
|
|
15
|
+
* follow into the code — and still reports `put → <id>` like any success. The
|
|
16
|
+
* shape is easy to reach by reusing the file-note spec from memory (title +
|
|
17
|
+
* summary), which is why it needs to be loud rather than silent.
|
|
18
|
+
*
|
|
19
|
+
* `after` is the note as it stands AFTER the write, and it is what makes this
|
|
20
|
+
* accurate rather than merely suspicious. The spec alone cannot tell the two
|
|
21
|
+
* cases apart: an UPDATE that touches only aliases legitimately omits `steps`
|
|
22
|
+
* and the fold keeps the ones already there — judging by the spec fired at
|
|
23
|
+
* notes that were perfectly fine. Pass the folded result and the claim is
|
|
24
|
+
* checked instead of guessed.
|
|
25
|
+
*
|
|
26
|
+
* Separate from flowEvidenceWarning below: this one needs no session, because
|
|
27
|
+
* a missing chain is a defect in the note itself, not in its evidence. */
|
|
28
|
+
export declare function flowStepsWarning(spec: WriteSpec, after?: {
|
|
29
|
+
steps?: unknown[];
|
|
30
|
+
} | null): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Fields a note needs to be FINDABLE, checked after the write rather than before it.
|
|
33
|
+
*
|
|
34
|
+
* The rules are not written here — they are the same NOTE_CHECKS entries that
|
|
35
|
+
* `kb repair` runs over the notebook, so the thing an agent is told at write
|
|
36
|
+
* time and the thing repair later flags it for cannot disagree.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately not a rejection. The capture prompt tells the agent to chain its
|
|
39
|
+
* writes with `&&`, so a non-zero exit silences every note after the offending
|
|
40
|
+
* one — a strict validator would destroy more knowledge than it saves, and the
|
|
41
|
+
* note is already correct, just under-indexed. The write lands, and the agent is
|
|
42
|
+
* handed the exact one-line re-put that completes it.
|
|
43
|
+
*
|
|
44
|
+
* `after` is the note AS FOLDED, and passing it is what makes this correct on an
|
|
45
|
+
* UPDATE. `op: 'put'` is not a document overwrite — it appends one record and
|
|
46
|
+
* the note is the fold of the whole log, where aliases and anchors UNION. So a
|
|
47
|
+
* spec that touches only `verified` legitimately omits `aliases`, and judging
|
|
48
|
+
* the record alone reported every such note as missing fields it already had.
|
|
49
|
+
* A field is missing only when the agent did not send it AND the folded note
|
|
50
|
+
* does not have it. Omit `after` (no folded note to hand) and this falls back to
|
|
51
|
+
* judging the spec, which is exact for a create — there, the two are the same.
|
|
52
|
+
*/
|
|
53
|
+
export declare function missingFieldsWarning(spec: WriteSpec, id: string, after?: unknown): string | null;
|
|
12
54
|
/** The WARN text (never a rejection) when a flow's steps lack read evidence.
|
|
13
55
|
* null when the spec is not a flow, no session is known, or evidence is fine. */
|
|
14
56
|
export declare function flowEvidenceWarning(spec: WriteSpec, session: string | undefined): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-guide.d.ts","sourceRoot":"","sources":["../../src/kb/write-guide.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"write-guide.d.ts","sourceRoot":"","sources":["../../src/kb/write-guide.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C,eAAO,MAAM,kBAAkB,QAaE,CAAC;AAElC,wBAAgB,aAAa,IAAI,MAAM,CAStC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAOD;;wEAEwE;AACxE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsB1G;AAED;;;;;;;;;;;;;;;2EAe2E;AAC3E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CASrG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAC3C,MAAM,GAAG,IAAI,CAef;AAED;kFACkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAS/F"}
|
package/dist/kb/write-guide.js
CHANGED
|
@@ -7,37 +7,20 @@
|
|
|
7
7
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
// and
|
|
10
|
+
import { shapesBlock, requiredFieldsLine, checksForSpec, stepsMissingAfterWrite, } from '../../hooks/note-shape.mjs';
|
|
11
|
+
// The shapes are NOT written here. They come from hooks/note-shape.mjs, which
|
|
12
|
+
// the capture prompt and the MCP tool description render from too — three
|
|
13
|
+
// hand-kept copies is exactly how "aliases" fell out of the flow shape and how
|
|
14
|
+
// the MCP description went years without ever mentioning it.
|
|
13
15
|
export const WRITE_GUIDE_SHAPES = `kb write — spec shapes (JSON, one note per spec; include only fields you have):
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
{"type":"file-single","path":"src/x.py",
|
|
17
|
-
"summary":"its one purpose + how (1-3 sentences)",
|
|
18
|
-
"aliases":["symptom or search words"]}
|
|
19
|
-
|
|
20
|
-
file (hub — ONLY for grab-bag files with NO single purpose: models.py, utils,
|
|
21
|
-
helpers. One facet PER SYMBOL you worked with. Touching many symbols
|
|
22
|
-
does not make a file a hub; a single-purpose file stays file-single):
|
|
23
|
-
{"type":"file-hub","path":"src/y.py","aliases":["search words"],
|
|
24
|
-
"facets":[{"symbol":"ClassOrFn","detail":"the non-obvious thing about THIS symbol",
|
|
25
|
-
"flows":["<flow id or the flow's exact title>"]}]}
|
|
26
|
-
|
|
27
|
-
flow (product-level mechanism — see the capture checklist's gate):
|
|
28
|
-
{"type":"flow","title":"how X happens","aliases":["other words for X"],
|
|
29
|
-
"summary":"first sentence = the product-level fact the file notes miss",
|
|
30
|
-
"steps":[{"path":"src/a.py","symbols":["entry"],"role":"receives the request"}],
|
|
31
|
-
"invariants":["what must hold"],"verified":["src/a.py"]}
|
|
32
|
-
|
|
33
|
-
lesson (confirmed ABSENCE only — one file/symbol facts are facets, not lessons):
|
|
34
|
-
{"type":"lesson","kind":"absence","title":"no retry logic in this repo",
|
|
35
|
-
"body":"what you looked for + that it is not there",
|
|
36
|
-
"scope":{"terms":["search","terms"]}}
|
|
17
|
+
${shapesBlock()}
|
|
37
18
|
|
|
38
19
|
update an existing note: same spec + its "id" (fields merge; yours win).
|
|
39
20
|
retract a wrong claim: {"op":"retract","id":"<id>","reason":"..."}
|
|
40
21
|
|
|
22
|
+
${requiredFieldsLine()}
|
|
23
|
+
|
|
41
24
|
Rules the writer enforces (fix any WARNING it prints, in this session):
|
|
42
25
|
- paths are join keys: repo-relative, exactly as in the repo
|
|
43
26
|
- "verified" re-stamps freshness: list ONLY files you opened this session
|
|
@@ -97,6 +80,67 @@ export function flowEvidenceCount(spec, session) {
|
|
|
97
80
|
return null;
|
|
98
81
|
}
|
|
99
82
|
}
|
|
83
|
+
/** The WARN text (never a rejection) when a flow ends up with no `steps` at all.
|
|
84
|
+
*
|
|
85
|
+
* A stepless flow folds to a note that is a title and a paragraph — no chain to
|
|
86
|
+
* follow into the code — and still reports `put → <id>` like any success. The
|
|
87
|
+
* shape is easy to reach by reusing the file-note spec from memory (title +
|
|
88
|
+
* summary), which is why it needs to be loud rather than silent.
|
|
89
|
+
*
|
|
90
|
+
* `after` is the note as it stands AFTER the write, and it is what makes this
|
|
91
|
+
* accurate rather than merely suspicious. The spec alone cannot tell the two
|
|
92
|
+
* cases apart: an UPDATE that touches only aliases legitimately omits `steps`
|
|
93
|
+
* and the fold keeps the ones already there — judging by the spec fired at
|
|
94
|
+
* notes that were perfectly fine. Pass the folded result and the claim is
|
|
95
|
+
* checked instead of guessed.
|
|
96
|
+
*
|
|
97
|
+
* Separate from flowEvidenceWarning below: this one needs no session, because
|
|
98
|
+
* a missing chain is a defect in the note itself, not in its evidence. */
|
|
99
|
+
export function flowStepsWarning(spec, after) {
|
|
100
|
+
if (!stepsMissingAfterWrite(spec, after))
|
|
101
|
+
return null;
|
|
102
|
+
return ('flow has no "steps": there is no chain to follow, so the note is a title and a paragraph. ' +
|
|
103
|
+
'Re-put it with steps [{path, symbols, role}], and list in "verified" the step files you ' +
|
|
104
|
+
'actually opened — "verified" is what FILES the note at those paths (anchors come from it, ' +
|
|
105
|
+
'NOT from the steps), and unfiled paths are invisible to `kb lookup`. Run `kb write` with ' +
|
|
106
|
+
'no spec to print the full flow shape.');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Fields a note needs to be FINDABLE, checked after the write rather than before it.
|
|
110
|
+
*
|
|
111
|
+
* The rules are not written here — they are the same NOTE_CHECKS entries that
|
|
112
|
+
* `kb repair` runs over the notebook, so the thing an agent is told at write
|
|
113
|
+
* time and the thing repair later flags it for cannot disagree.
|
|
114
|
+
*
|
|
115
|
+
* Deliberately not a rejection. The capture prompt tells the agent to chain its
|
|
116
|
+
* writes with `&&`, so a non-zero exit silences every note after the offending
|
|
117
|
+
* one — a strict validator would destroy more knowledge than it saves, and the
|
|
118
|
+
* note is already correct, just under-indexed. The write lands, and the agent is
|
|
119
|
+
* handed the exact one-line re-put that completes it.
|
|
120
|
+
*
|
|
121
|
+
* `after` is the note AS FOLDED, and passing it is what makes this correct on an
|
|
122
|
+
* UPDATE. `op: 'put'` is not a document overwrite — it appends one record and
|
|
123
|
+
* the note is the fold of the whole log, where aliases and anchors UNION. So a
|
|
124
|
+
* spec that touches only `verified` legitimately omits `aliases`, and judging
|
|
125
|
+
* the record alone reported every such note as missing fields it already had.
|
|
126
|
+
* A field is missing only when the agent did not send it AND the folded note
|
|
127
|
+
* does not have it. Omit `after` (no folded note to hand) and this falls back to
|
|
128
|
+
* judging the spec, which is exact for a create — there, the two are the same.
|
|
129
|
+
*/
|
|
130
|
+
export function missingFieldsWarning(spec, id, after) {
|
|
131
|
+
const s = spec;
|
|
132
|
+
if (!s || s.op === 'retract')
|
|
133
|
+
return null;
|
|
134
|
+
const type = s.type ?? '';
|
|
135
|
+
const gaps = checksForSpec(type).filter((c) => c.missingInSpec(spec) && (!after || c.missingInNote(after)));
|
|
136
|
+
if (!gaps.length)
|
|
137
|
+
return null;
|
|
138
|
+
return (`note ${id} is written but under-indexed — missing:\n` +
|
|
139
|
+
gaps.map((c) => ` - ${c.why}`).join('\n') +
|
|
140
|
+
`\n Complete it now with one more write (fields merge, nothing is lost):\n` +
|
|
141
|
+
` {"id":"${id}","type":"${type}"${s.path ? `,"path":"${s.path}"` : ''},` +
|
|
142
|
+
`${gaps.map((c) => c.fix({ path: s.path })).join(',')}}`);
|
|
143
|
+
}
|
|
100
144
|
/** The WARN text (never a rejection) when a flow's steps lack read evidence.
|
|
101
145
|
* null when the spec is not a flow, no session is known, or evidence is fine. */
|
|
102
146
|
export function flowEvidenceWarning(spec, session) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-guide.js","sourceRoot":"","sources":["../../src/kb/write-guide.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"write-guide.js","sourceRoot":"","sources":["../../src/kb/write-guide.ts"],"names":[],"mappings":"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"}
|
package/dist/kb/write.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/kb/write.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAS,aAAa,EAAc,UAAU,EAAkB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAUzG;;;+CAG+C;AAC/C,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAC;IAC7C,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,WAAW,CAAC;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC/C,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAClF;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,cAAc,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;kEAE8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/kb/write.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAS,aAAa,EAAc,UAAU,EAAkB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAUzG;;;+CAG+C;AAC/C,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAC;IAC7C,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,WAAW,CAAC;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC/C,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAClF;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,cAAc,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;kEAE8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA+O1G"}
|
package/dist/kb/write.js
CHANGED
|
@@ -239,7 +239,16 @@ export async function kbWrite(root, spec, opts = {}) {
|
|
|
239
239
|
...(facets?.map((f) => f.symbol) ?? []),
|
|
240
240
|
])];
|
|
241
241
|
record.anchors = [{ path, ...(symbols.length ? { symbols } : {}) }];
|
|
242
|
-
|
|
242
|
+
// The file IS the anchor, and a write carrying KNOWLEDGE about it (summary,
|
|
243
|
+
// facets, body) could only come from having read it — that counts verified.
|
|
244
|
+
// A metadata-only touch does NOT. `verified` makes raw-log.ts stamp the
|
|
245
|
+
// file's live hash into the anchor, which resets freshness; auto-stamping on
|
|
246
|
+
// every put meant an alias or title fix silently re-certified the note
|
|
247
|
+
// against bytes nobody looked at. Caught 2026-07-30 by a 34-note title
|
|
248
|
+
// backfill that marked every one of them freshly verified. An explicit
|
|
249
|
+
// `verified` in the spec still wins — that is the agent's own claim.
|
|
250
|
+
const carriesKnowledge = !!(spec.summary?.trim() || facets?.length || spec.body?.trim());
|
|
251
|
+
record.verified = [...new Set([...(carriesKnowledge ? [path] : []), ...(spec.verified ?? [])])];
|
|
243
252
|
if (character)
|
|
244
253
|
record.character = character; // sugar form ("file-hub") lands here
|
|
245
254
|
}
|