graph-engine-memory 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. graph_engine/__init__.py +1 -0
  2. graph_engine/__main__.py +1275 -0
  3. graph_engine/access.py +202 -0
  4. graph_engine/agent_memory.py +103 -0
  5. graph_engine/brain.py +731 -0
  6. graph_engine/brain_engine.py +559 -0
  7. graph_engine/communities.py +503 -0
  8. graph_engine/consolidation.py +191 -0
  9. graph_engine/context.py +203 -0
  10. graph_engine/decay.py +65 -0
  11. graph_engine/demo.py +165 -0
  12. graph_engine/dream.py +408 -0
  13. graph_engine/embedder.py +84 -0
  14. graph_engine/engine.py +58 -0
  15. graph_engine/entities.py +376 -0
  16. graph_engine/evals.py +1206 -0
  17. graph_engine/feedback.py +155 -0
  18. graph_engine/gaps.py +209 -0
  19. graph_engine/graph.py +133 -0
  20. graph_engine/hygiene.py +213 -0
  21. graph_engine/inference.py +251 -0
  22. graph_engine/intent.py +207 -0
  23. graph_engine/mcp/__init__.py +1 -0
  24. graph_engine/mcp/agent/instructions.md +34 -0
  25. graph_engine/mcp/format.py +66 -0
  26. graph_engine/mcp/server.py +407 -0
  27. graph_engine/merge.py +220 -0
  28. graph_engine/model.py +56 -0
  29. graph_engine/namespaces.py +74 -0
  30. graph_engine/recall.py +161 -0
  31. graph_engine/report.py +401 -0
  32. graph_engine/reranker.py +86 -0
  33. graph_engine/retrieval.py +296 -0
  34. graph_engine/review.py +95 -0
  35. graph_engine/runtime.py +83 -0
  36. graph_engine/server.py +239 -0
  37. graph_engine/similarity.py +42 -0
  38. graph_engine/store.py +63 -0
  39. graph_engine/suggester.py +63 -0
  40. graph_engine/temporal.py +216 -0
  41. graph_engine/tiers.py +135 -0
  42. graph_engine/web/app.js +578 -0
  43. graph_engine/web/index.html +169 -0
  44. graph_engine/web/report.html +39 -0
  45. graph_engine/web/report.js +50 -0
  46. graph_engine/web/review.html +104 -0
  47. graph_engine/web/review.js +211 -0
  48. graph_engine_memory-0.6.1.dist-info/METADATA +125 -0
  49. graph_engine_memory-0.6.1.dist-info/RECORD +53 -0
  50. graph_engine_memory-0.6.1.dist-info/WHEEL +5 -0
  51. graph_engine_memory-0.6.1.dist-info/entry_points.txt +3 -0
  52. graph_engine_memory-0.6.1.dist-info/licenses/LICENSE +21 -0
  53. graph_engine_memory-0.6.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,169 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="theme-color" content="#0d1117">
7
+ <meta name="description" content="Capture ideas, discover connections, and review suggestions with your graph.">
8
+ <title>GraphEngine ยท Connect your ideas</title>
9
+ <style>
10
+ :root { color-scheme:dark; --bg:#0d1117; --panel:#151c25; --raised:#1c2633; --border:#2c3847; --fg:#eaf0f7; --dim:#a0afc1; --blue:#83b9ff; --green:#6ed5a0; --red:#ff9393; --amber:#edc27d; font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
11
+ * { box-sizing:border-box; }
12
+ body { margin:0; background:var(--bg); color:var(--fg); height:100vh; height:100dvh; display:flex; flex-direction:column; overflow:hidden; }
13
+ button,input,textarea,select { font:inherit; }
14
+ button { cursor:pointer; color:var(--fg); background:var(--raised); border:1px solid var(--border); border-radius:8px; min-height:40px; padding:8px 13px; font-size:.875rem; }
15
+ button:hover { background:#27374b; border-color:#64758b; }
16
+ button:disabled { opacity:.5; cursor:wait; }
17
+ :focus-visible { outline:2px solid var(--blue); outline-offset:3px; }
18
+ input,textarea { min-width:0; background:var(--bg); color:var(--fg); border:1px solid var(--border); border-radius:8px; padding:11px 13px; }
19
+ input::placeholder,textarea::placeholder { color:var(--dim); }
20
+ select { color:var(--fg); background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:11px; max-width:160px; }
21
+ .source { display:flex; flex-direction:column; gap:8px; font-size:.875rem; }
22
+ .review-link { color:var(--blue); font-size:.875rem; text-underline-offset:4px; }
23
+ #graph3d { position:absolute; inset:0; }
24
+ #graph-mode[aria-pressed="true"] { color:var(--blue); border-color:var(--blue); }
25
+ .primary { background:#a9cdff; color:#102139; border-color:#a9cdff; font-weight:650; }
26
+ .primary:hover { background:#c5ddff; border-color:#c5ddff; }
27
+ .quiet { background:transparent; }
28
+ .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
29
+ [hidden] { display:none !important; }
30
+ header { display:flex; align-items:center; gap:24px; padding:18px 24px; border-bottom:1px solid var(--border); flex:none; }
31
+ .brand { display:flex; align-items:center; gap:10px; font-size:1.15rem; font-weight:650; letter-spacing:-.03em; }
32
+ .brand svg { color:var(--blue); }
33
+ #stats { color:var(--dim); font-size:.875rem; }
34
+ #connection { margin-left:auto; font-size:.8125rem; color:var(--dim); }
35
+ #connection[data-state="live"] { color:var(--green); }
36
+ #connection[data-state="offline"] { color:var(--amber); }
37
+ #bar { display:flex; align-items:end; gap:12px; padding:16px 24px; background:var(--panel); border-bottom:1px solid var(--border); }
38
+ .compose { flex:1; min-width:0; }
39
+ .compose label { display:block; font-size:.875rem; font-weight:550; margin-bottom:8px; }
40
+ #text { display:block; width:100%; resize:vertical; min-height:46px; max-height:160px; line-height:1.4; }
41
+ #bar button { min-height:46px; }
42
+ #mobile-tabs { display:none; }
43
+ #main { flex:1; display:flex; min-height:0; }
44
+ #graphwrap { flex:1; min-width:0; display:flex; flex-direction:column; position:relative; }
45
+ .graph-toolbar { display:flex; align-items:center; gap:14px; padding:16px 24px; flex:none; }
46
+ .graph-toolbar h1 { font-size:1rem; font-weight:600; margin:0; white-space:nowrap; }
47
+ .search { position:relative; margin-left:auto; width:min(300px,65%); }
48
+ #search { width:100%; font-size:.875rem; padding:10px 12px; }
49
+ #search-results { position:absolute; top:calc(100% + 8px); right:0; width:min(360px,80vw); max-height:320px; overflow:auto; z-index:3; margin:0; padding:6px; list-style:none; background:var(--panel); border:1px solid var(--border); border-radius:10px; box-shadow:0 12px 35px #0008; }
50
+ #search-results button { text-align:left; width:100%; background:transparent; border:0; line-height:1.5; overflow-wrap:anywhere; }
51
+ #search-results button:hover { background:var(--raised); }
52
+ #search-results .no-results { padding:12px; color:var(--dim); font-size:.875rem; }
53
+ #canvas { flex:1; min-height:0; position:relative; overflow:hidden; background-image:radial-gradient(#263241 1px,transparent 1px); background-size:24px 24px; }
54
+ #graph { display:block; width:100%; height:100%; touch-action:none; }
55
+ .node { cursor:pointer; outline:none; }
56
+ .node .halo { fill:transparent; stroke:transparent; stroke-width:2; }
57
+ .node .dot { fill:#adbbce; stroke:var(--bg); stroke-width:3; }
58
+ .node text { fill:#bbc8d9; font-family:inherit; font-size:14px; paint-order:stroke; stroke:var(--bg); stroke-width:5px; stroke-linejoin:round; pointer-events:none; }
59
+ .node.selected .halo,.node:focus-visible .halo,.node:hover .halo { fill:#83b9ff15; stroke:var(--blue); }
60
+ .node.selected .dot,.node:hover .dot { fill:var(--blue); }
61
+ .node.selected text,.node:hover text,.node:focus-visible text { fill:var(--fg); }
62
+ .node.dimmed { opacity:.18; }
63
+ .node:focus-visible,.node:hover { opacity:1; }
64
+ .graph-bottom { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; padding:12px 24px; border-top:1px solid var(--border); }
65
+ .legend { display:flex; flex-wrap:wrap; gap:8px 14px; color:var(--dim); font-size:.75rem; }
66
+ .legend span { display:flex; align-items:center; gap:6px; }
67
+ .line { display:inline-block; width:16px; border-top:2px solid currentColor; }
68
+ .line.similar { color:var(--green); } .line.contradiction { color:var(--red); } .line.extends { color:var(--blue); } .line.pending { border-top-style:dashed; }
69
+ .graph-controls { display:flex; gap:6px; }
70
+ .graph-controls button { min-width:40px; }
71
+ #graph-empty { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:28px; pointer-events:none; }
72
+ #graph-empty h2 { font-size:1.35rem; font-weight:550; margin:0 0 10px; }
73
+ #graph-empty p { max-width:360px; color:var(--dim); line-height:1.6; margin:0 0 20px; }
74
+ #graph-empty button { pointer-events:auto; }
75
+ #tooltip { position:absolute; max-width:min(340px,85%); padding:12px 14px; background:var(--raised); border:1px solid var(--border); border-radius:8px; font-size:.875rem; line-height:1.5; overflow-wrap:anywhere; pointer-events:none; z-index:2; box-shadow:0 8px 25px #0005; }
76
+ #inbox { width:clamp(340px,31vw,440px); flex:none; border-left:1px solid var(--border); background:#10161e; display:flex; flex-direction:column; min-height:0; }
77
+ .inbox-heading { padding:19px 20px 16px; border-bottom:1px solid var(--border); }
78
+ .inbox-heading h2 { font-size:1rem; font-weight:600; margin:0 0 6px; display:flex; align-items:center; gap:9px; }
79
+ #pending-count { font-size:.75rem; border-radius:5px; padding:2px 7px; color:var(--amber); background:#edc27d16; }
80
+ .inbox-heading p { font-size:.875rem; color:var(--dim); line-height:1.5; margin:0; }
81
+ #cards { padding:16px; overflow-y:auto; flex:1; }
82
+ .card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; margin-bottom:12px; }
83
+ .card.active { border-color:var(--blue); box-shadow:inset 3px 0 var(--blue); }
84
+ .card-heading { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:12px; }
85
+ .kind { display:inline-flex; align-items:center; gap:6px; font-size:.75rem; color:var(--dim); }
86
+ .kind[data-kind="similar"] { color:var(--green); } .kind[data-kind="contradicts"] { color:var(--red); } .kind[data-kind="extends"] { color:var(--blue); }
87
+ .select-pair { min-height:32px; font-size:.75rem; padding:4px 8px; }
88
+ .idea-preview { display:block; width:100%; text-align:left; background:var(--bg); border:1px solid transparent; padding:12px; font-size:1rem; line-height:1.5; overflow-wrap:anywhere; }
89
+ .idea-preview + .idea-preview { margin-top:8px; }
90
+ .idea-preview .excerpt { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; }
91
+ .idea-preview .read { display:block; font-size:.75rem; color:var(--dim); margin-top:5px; }
92
+ .actions { display:flex; gap:8px; margin-top:14px; }
93
+ .actions button { flex:1; }
94
+ .actions .ok { color:var(--green); background:#6ed5a013; border-color:#6ed5a050; font-weight:600; }
95
+ .actions .ok:hover { background:#6ed5a028; }
96
+ .empty-inbox { padding:26px 8px; }
97
+ .empty-inbox h3 { font-size:1rem; font-weight:550; margin:0 0 10px; }
98
+ .empty-inbox p { color:var(--dim); line-height:1.6; margin:0; }
99
+ #help { border-top:1px solid var(--border); padding:9px 24px; font-size:.75rem; color:var(--dim); display:flex; flex-wrap:wrap; gap:8px 18px; }
100
+ kbd { font:inherit; font-family:ui-monospace,monospace; color:var(--fg); }
101
+ dialog { width:min(620px,calc(100% - 32px)); max-height:85dvh; padding:24px; border:1px solid #52647c; border-radius:16px; background:var(--panel); color:var(--fg); box-shadow:0 24px 100px #0009; }
102
+ dialog::backdrop { background:#070c15bd; backdrop-filter:blur(4px); }
103
+ .detail-header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
104
+ .detail-header h2 { font-size:1.1rem; margin:0; }
105
+ #detail-meta { color:var(--dim); font-size:.8125rem; margin-top:12px; overflow-wrap:anywhere; }
106
+ #detail-text { font-size:1rem; white-space:pre-wrap; line-height:1.75; overflow-wrap:anywhere; margin:24px 0; }
107
+ #detail-relations { border-top:1px solid var(--border); padding-top:18px; }
108
+ #detail-relations h3 { font-size:.875rem; margin:0 0 12px; }
109
+ .relation { display:block; width:100%; text-align:left; margin-top:8px; line-height:1.5; overflow-wrap:anywhere; }
110
+ .relation span { display:block; color:var(--dim); font-size:.75rem; margin-bottom:3px; }
111
+ #notice { position:fixed; bottom:54px; left:50%; transform:translateX(-50%); z-index:5; max-width:calc(100% - 32px); width:max-content; padding:12px 18px; background:var(--raised); border:1px solid var(--border); border-radius:10px; box-shadow:0 12px 35px #0008; font-size:.875rem; }
112
+ #notice:empty { display:none; }
113
+ #notice.error { border-color:var(--red); }
114
+ #decision { display:flex; align-items:center; gap:12px; padding:10px 24px; background:#19283a; border-top:1px solid #3d5775; font-size:.875rem; }
115
+ #decision-message { flex:1; }
116
+ @media (max-width:1000px) { header { gap:16px; } .graph-toolbar { flex-wrap:wrap; padding:16px; } .search { width:100%; } .graph-bottom { padding:12px 16px; } #inbox { width:340px; } }
117
+ @media (max-width:720px) {
118
+ header { padding:14px 16px; gap:6px 16px; flex-wrap:wrap; } .brand { font-size:1.05rem; } #stats { order:3; width:100%; font-size:.8125rem; }
119
+ .source { flex-direction:row; align-items:center; width:100%; order:3; } .source select { flex:1; max-width:none; padding:6px; }
120
+ #connection { font-size:.75rem; } #bar { padding:12px 16px; gap:8px; flex-wrap:wrap; } #bar button { padding:10px; } .compose label { font-size:.8125rem; }
121
+ #mobile-tabs { display:flex; gap:4px; padding:8px 16px; border-bottom:1px solid var(--border); }
122
+ #mobile-tabs button { flex:1; background:transparent; border-color:transparent; }
123
+ #mobile-tabs button[aria-pressed="true"] { background:var(--raised); color:var(--blue); border-color:var(--border); }
124
+ #inbox { display:none; width:100%; border-left:0; }
125
+ body[data-view="inbox"] #inbox { display:flex; } body[data-view="inbox"] #graphwrap { display:none; }
126
+ .graph-toolbar { flex-wrap:nowrap; padding:12px 16px; } .search { width:60%; } .graph-toolbar h1 { font-size:.875rem; }
127
+ .graph-bottom { gap:10px; } .legend { flex:1; } .graph-controls { margin-left:auto; } #help { display:none; }
128
+ #decision { padding:10px 16px; gap:8px; flex-wrap:wrap; } #decision-message { min-width:120px; } #notice { bottom:16px; }
129
+ }
130
+ @media (prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
131
+ </style>
132
+ </head>
133
+ <body data-view="graph">
134
+ <header>
135
+ <div class="brand"><svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true"><path d="M6 7L20 6L14 21Z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="6" cy="7" r="3" fill="currentColor"/><circle cx="20" cy="6" r="3" fill="currentColor"/><circle cx="14" cy="21" r="3" fill="currentColor"/></svg>GraphEngine</div>
136
+ <div id="stats">Loading ideas โ€ฆ</div>
137
+ <span id="connection" role="status" data-state="connecting">Connecting โ€ฆ</span>
138
+ </header>
139
+ <form id="bar">
140
+ <div class="compose"><label for="text">Capture a new idea</label><textarea id="text" rows="1" placeholder="What is on your mind?" aria-description="Enter saves the idea. Shift and Enter inserts a new line." required></textarea></div>
141
+ <div class="source"><label for="source">Source</label><select id="source"><option value="human">Human</option><option value="agent">Agent</option><option value="research/subagent">Research</option></select></div>
142
+ <button id="ingest-button" class="primary" type="submit">Add idea</button>
143
+ </form>
144
+ <nav id="mobile-tabs" aria-label="View"><button type="button" data-view="graph" aria-pressed="true">Graph</button><button type="button" data-view="inbox" aria-pressed="false">Suggestions <span id="mobile-count"></span></button></nav>
145
+ <main id="main">
146
+ <section id="graphwrap" aria-label="Idea graph">
147
+ <div class="graph-toolbar"><h1>Your idea network</h1><div class="search"><label for="search" class="sr-only">Search ideas</label><input id="search" type="search" placeholder="Search ideas โ€ฆ" autocomplete="off" aria-controls="search-results" aria-expanded="false"><ul id="search-results" aria-label="Search results" hidden></ul></div></div>
148
+ <div id="canvas">
149
+ <div id="graph3d" hidden></div>
150
+ <svg id="graph" aria-label="Connections between your ideas"></svg>
151
+ <div id="graph-empty"><h2>Loading your graph โ€ฆ</h2><p>One moment, please.</p><button id="empty-action" hidden>Add your first idea</button></div>
152
+ <div id="tooltip" hidden></div>
153
+ </div>
154
+ <div class="graph-bottom"><div class="legend" aria-label="Edge types"><span><i class="line similar"></i>Similar</span><span><i class="line contradiction"></i>Contradiction</span><span><i class="line extends"></i>Extension</span><span><i class="line pending"></i>Suggestion</span></div><div class="graph-controls"><button id="graph-mode" aria-pressed="false" title="Toggle 3D view">3D</button><button id="zoom-out" aria-label="Zoom out">โˆ’</button><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Show all</button></div></div>
155
+ </section>
156
+ <aside id="inbox" aria-labelledby="inbox-title"><div class="inbox-heading"><h2 id="inbox-title">Review connections <span id="pending-count">0</span></h2><p>Which ideas belong together? You decide.</p><a class="review-link" href="/report">report</a>\n <a class="review-link" href="/review">Link aliases manually โ†—</a></div><div id="cards" aria-busy="true"></div></aside>
157
+ </main>
158
+ <div id="decision" hidden><span id="decision-message" role="status"></span><button id="undo" class="quiet">Undo</button><button id="dismiss-decision" class="quiet" aria-label="Dismiss notice">ร—</button></div>
159
+ <footer id="help"><span><kbd>I</kbd> New idea</span><span><kbd>/</kbd> Search</span><span><kbd>J</kbd> / <kbd>K</kbd> Pick suggestion</span><span><kbd>โ†ต</kbd> Accept</span><span><kbd>Space</kbd> Read idea</span><span><kbd>Esc</kbd> Close / clear selection</span></footer>
160
+ <dialog id="detail" aria-labelledby="detail-title"><div class="detail-header"><h2 id="detail-title">Idea</h2><button id="close-detail" class="quiet" aria-label="Close idea details" autofocus>Close ร—</button></div><div id="detail-meta"></div><p id="detail-text"></p><div id="detail-relations"></div></dialog>
161
+ <div id="notice" role="status" aria-live="polite" aria-atomic="true"></div>
162
+ <!-- Audit #13: CDN-Assets mit SRI-Hash (sha384, vom exakten Asset berechnet);
163
+ eine CDN-Kompromittierung kann kein Script mehr austauschen. -->
164
+ <script src="https://d3js.org/d3.v7.min.js" integrity="sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i" crossorigin="anonymous"></script>
165
+ <script src="https://unpkg.com/three@0.160.0/build/three.min.js" integrity="sha384-qOkzR5Ke/XkQxuGVJ9hpFEpDlcoLtWwVYhnJf06cLIZa2vaIptSqaubivErzmD5O" crossorigin="anonymous"></script>
166
+ <script src="https://unpkg.com/3d-force-graph@1.74.5/dist/3d-force-graph.min.js" integrity="sha384-5Q61EeUSxR75iiwp2gn9G61eb+HipUbZ+0ILqlmY+hkgFafqZL1ixKnIUDIYN+ZF" crossorigin="anonymous"></script>
167
+ <script src="app.js"></script>
168
+ </body>
169
+ </html>
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta charset="utf-8">
6
+ <title>GraphEngine BRAIN_REPORT</title>
7
+ <style>
8
+ :root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --dim:#8b949e;
9
+ --green:#3fb950; --red:#f85149; --blue:#58a6ff; --purple:#bc8cff; }
10
+ * { box-sizing: border-box; }
11
+ body { margin:0; background:var(--bg); color:var(--fg); font-family:ui-monospace,Menlo,monospace;
12
+ min-height:100vh; }
13
+ header { display:flex; align-items:center; gap:12px; padding:10px 14px;
14
+ border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--bg); }
15
+ header h1 { font-size:13px; margin:0; color:var(--dim); font-weight:normal; }
16
+ header a { color:var(--dim); font-size:12px; text-decoration:none; margin-left:auto; }
17
+ header a:hover { color:var(--fg); }
18
+ header button { background:var(--panel); border:1px solid var(--border); color:var(--fg);
19
+ font-family:inherit; font-size:12px; padding:6px 12px; border-radius:6px; cursor:pointer; }
20
+ header button:hover { border-color:var(--blue); }
21
+ main { max-width:900px; margin:0 auto; padding:18px 16px 60px; }
22
+ pre { white-space:pre-wrap; overflow-wrap:anywhere; line-height:1.55; font-size:13px; margin:0; }
23
+ pre .warn { color:var(--red); font-weight:bold; }
24
+ pre .ok { color:var(--green); }
25
+ pre .dim { color:var(--dim); }
26
+ pre .head { color:var(--purple); }
27
+ #error { color:var(--red); padding:20px; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <header>
32
+ <h1>BRAIN_REPORT <span id="meta"></span></h1>
33
+ <button id="copy">Copy markdown</button>
34
+ <a href="/">graph</a>
35
+ </header>
36
+ <main><pre id="out">loadingโ€ฆ</pre><div id="error" hidden></div></main>
37
+ <script src="/report.js"></script>
38
+ </body>
39
+ </html>
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /* BRAIN_REPORT viewer: fetches /api/report and renders the digest read-only.
3
+ The copy button hands the raw markdown to the clipboard (for pasting into
4
+ issues/PRs). No mutation endpoints are called from this page. */
5
+ (function () {
6
+ const out = document.getElementById("out");
7
+ const meta = document.getElementById("meta");
8
+ const err = document.getElementById("error");
9
+
10
+ function esc(s) {
11
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12
+ }
13
+
14
+ function render(md) {
15
+ // minimal, safe rendering: escape everything, then bold/highlight by marker
16
+ const lines = md.split("\n").map((line) => {
17
+ if (line.startsWith("# ")) return '<span class="head">' + esc(line) + "</span>";
18
+ if (line.startsWith("## ")) return '<span class="head">' + esc(line) + "</span>";
19
+ if (/โš |WARN/.test(line)) return '<span class="warn">' + esc(line) + "</span>";
20
+ if (/nothing queued|no pending|inert|omitted/.test(line))
21
+ return '<span class="dim">' + esc(line) + "</span>";
22
+ return esc(line);
23
+ });
24
+ out.innerHTML = lines.join("\n");
25
+ }
26
+
27
+ fetch("/api/report")
28
+ .then((r) => {
29
+ if (!r.ok) throw new Error("HTTP " + r.status);
30
+ return r.json();
31
+ })
32
+ .then((data) => {
33
+ meta.textContent = "ยท " + data.generated_at + " ยท " + data.nodes + " nodes / "
34
+ + data.edges + " live edges";
35
+ render(data.markdown);
36
+ })
37
+ .catch((e) => {
38
+ out.hidden = true;
39
+ err.hidden = false;
40
+ err.textContent = "failed to load report: " + e.message;
41
+ });
42
+
43
+ document.getElementById("copy").addEventListener("click", () => {
44
+ navigator.clipboard.writeText(out.textContent).then(() => {
45
+ const b = document.getElementById("copy");
46
+ b.textContent = "Copied!";
47
+ setTimeout(() => { b.textContent = "Copy markdown"; }, 1200);
48
+ });
49
+ });
50
+ })();
@@ -0,0 +1,104 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta charset="utf-8">
6
+ <title>GraphEngine Review</title>
7
+ <style>
8
+ :root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --dim:#8b949e;
9
+ --green:#3fb950; --red:#f85149; --blue:#58a6ff; --purple:#bc8cff; }
10
+ * { box-sizing: border-box; }
11
+ body { margin:0; background:var(--bg); color:var(--fg); font-family:ui-monospace,Menlo,monospace;
12
+ height:100vh; display:flex; flex-direction:column; overflow:hidden; }
13
+
14
+ header { display:flex; align-items:center; gap:12px; padding:10px 14px;
15
+ border-bottom:1px solid var(--border); flex:none; }
16
+ header h1 { font-size:13px; margin:0; color:var(--dim); font-weight:normal; }
17
+ header .count { color:var(--blue); }
18
+ header a { color:var(--dim); font-size:12px; text-decoration:none; }\n header a:first-of-type { margin-left:auto; }
19
+ header a:hover { color:var(--fg); }
20
+
21
+ #flash { position:fixed; top:52px; left:50%; transform:translateX(-50%);
22
+ background:var(--panel); border:1px solid var(--green); border-radius:6px;
23
+ padding:8px 18px; font-size:13px; opacity:0; transition:opacity .3s; z-index:10;
24
+ pointer-events:none; }
25
+
26
+ #main { flex:1; display:flex; min-height:0; }
27
+
28
+ /* Left column: search + node picker for same_as */
29
+ #side { width:400px; flex:none; border-right:1px solid var(--border);
30
+ display:flex; flex-direction:column; min-height:0; }
31
+ #side .pane { padding:12px; border-bottom:1px solid var(--border); }
32
+ #side h2 { margin:0 0 8px; font-size:11px; color:var(--dim); text-transform:uppercase; letter-spacing:.08em; }
33
+ #search { width:100%; background:var(--panel); border:1px solid var(--border); color:var(--fg);
34
+ padding:8px 12px; font-family:inherit; font-size:13px; border-radius:6px; outline:none; }
35
+ #search:focus { border-color:var(--blue); }
36
+ #hits { max-height:200px; overflow-y:auto; margin-top:6px; }
37
+ .hit { padding:6px 10px; font-size:12px; cursor:pointer; border-radius:6px; color:var(--dim); }
38
+ .hit:hover { background:var(--panel); color:var(--fg); }
39
+ .hit .id { color:var(--dim); font-size:10px; margin-right:6px; }
40
+ .hit.pickedA { background:#1c2128; color:var(--purple); }
41
+ .hit.pickedB { background:#1c2128; color:var(--green); }
42
+
43
+ #pickbar { display:flex; gap:8px; align-items:center; font-size:11px; color:var(--dim); margin-top:8px; }
44
+ #pickbar .slot { flex:1; background:var(--panel); border:1px dashed var(--border); border-radius:6px;
45
+ padding:6px 8px; min-height:34px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
46
+ #pickbar .slot.filled { border-style:solid; }
47
+ #pickbar .slot.a.filled { color:var(--purple); border-color:var(--purple); }
48
+ #pickbar .slot.b.filled { color:var(--green); border-color:var(--green); }
49
+ #linkbtn { background:#6e40c9; border:none; color:#fff; padding:8px 12px; font-family:inherit;
50
+ font-size:11px; border-radius:6px; cursor:pointer; }
51
+ #linkbtn:disabled { opacity:.4; cursor:default; }
52
+ .hint { font-size:10px; color:var(--dim); margin-top:6px; }
53
+
54
+ /* Right column: inbox */
55
+ #inbox { flex:1; overflow-y:auto; padding:12px 16px; }
56
+ .card { background:var(--panel); border:1px solid var(--border); border-radius:8px;
57
+ padding:14px; margin-bottom:10px; font-size:13px; }
58
+ .card.active { border-color:var(--blue); box-shadow:0 0 0 1px var(--blue); }
59
+ .kind { display:inline-block; font-size:11px; padding:1px 8px; border-radius:10px;
60
+ border:1px solid var(--border); color:var(--dim); margin-bottom:8px; }
61
+ .kind[data-kind="similar"] { color:var(--green); border-color:var(--green); }
62
+ .kind[data-kind="extends"] { color:var(--blue); border-color:var(--blue); }
63
+ .side-label { font-size:10px; color:var(--dim); margin:8px 0 2px; }
64
+ .nodebox { background:var(--bg); border-radius:6px; padding:8px 10px; font-size:12px;
65
+ max-height:88px; overflow-y:auto; }
66
+ .nodebox .id { color:var(--dim); font-size:10px; }
67
+ .nodebox.pickable { cursor:pointer; }
68
+ .nodebox.pickable:hover { outline:1px solid var(--purple); }
69
+ .actions { display:flex; gap:8px; margin-top:10px; }
70
+ .actions button { flex:1; border:none; padding:8px; font-family:inherit; font-size:12px;
71
+ border-radius:6px; cursor:pointer; }
72
+ .ok { background:#238636; color:#fff; }
73
+ .no { background:#21262d; color:var(--fg); border:1px solid var(--border) !important; }
74
+ .swap { background:none; color:var(--dim); border:1px solid var(--border); padding:4px 8px;
75
+ font-size:10px; border-radius:6px; cursor:pointer; margin-left:8px; }
76
+ </style>
77
+ </head>
78
+ <body>
79
+ <header>
80
+ <h1>GraphEngine Review ยท <span class="count" id="count">0 pending</span></h1>
81
+ <a href="/">\n <a href="/report">report</a>โ† Cockpit</a>
82
+ </header>
83
+ <div id="flash"></div>
84
+
85
+ <div id="main">
86
+ <div id="side">
87
+ <div class="pane">
88
+ <h2>Link same_as</h2>
89
+ <input id="search" placeholder="Search nodes โ€ฆ (s)">
90
+ <div id="hits"></div>
91
+ <div id="pickbar">
92
+ <div class="slot a" id="slotA">A: empty</div>
93
+ <div class="slot b" id="slotB">B: empty</div>
94
+ <button id="linkbtn" disabled>Link โŽ</button>
95
+ </div>
96
+ <div class="hint">Click a hit: 1st click โ†’ A, 2nd โ†’ B. x clears the selection.</div>
97
+ </div>
98
+ </div>
99
+ <div id="inbox"><h2 style="font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;margin:0 0 10px;">Pending Edges</h2><div id="cards"></div></div>
100
+ </div>
101
+
102
+ <script src="/review.js"></script>
103
+ </body>
104
+ </html>
@@ -0,0 +1,211 @@
1
+ // GraphEngine Review โ€” inbox work + same_as linking, keyboard-first.
2
+ //
3
+ // Security (Audit #11/#12/#14): alle dynamischen Werte (ids, kinds, Text)
4
+ // are strictly escaped; interaction runs via data attributes +
5
+ // Event-Delegation statt inline onclick-Attributen mit String-Interpolation.
6
+ // Ein manipuliertes Brain-Repo (geklontes Remote mit feindlichen ids/kinds)
7
+ // kann keinen Code mehr in die Seite injizieren.
8
+ const $ = id => document.getElementById(id);
9
+
10
+ let nodes = {}, pending = [], selectedId = null;
11
+ let pickA = null, pickB = null;
12
+
13
+ // Audit #47: die Selection war positional (Index in pending) und sprang nach
14
+ // jedem Refresh auf eine andere Edge. Jetzt wird die Edge-ID gemerkt; der
15
+ // Index wird nur noch als Fallback beim ersten Rendern hergeleitet.
16
+ function currentIndex() {
17
+ const i = pending.findIndex(e => e.id === selectedId);
18
+ return i >= 0 ? i : 0;
19
+ }
20
+
21
+ function short(id, n = 70) {
22
+ const t = (nodes[id] || { text: id }).text.replace(/\n/g, " ");
23
+ return t.length > n ? t.slice(0, n - 1) + "โ€ฆ" : t;
24
+ }
25
+
26
+ // Strict escaping for element content AND attribute context (audit #14:
27
+ // the old version escaped only & and < โ€” insufficient for attributes).
28
+ const esc = s => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;")
29
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
30
+
31
+ function flash(msg, ok = true) {
32
+ const f = $("flash");
33
+ f.textContent = msg;
34
+ f.style.borderColor = ok ? "var(--green)" : "var(--red)";
35
+ f.style.color = ok ? "var(--green)" : "var(--red)";
36
+ f.style.opacity = 1;
37
+ setTimeout(() => f.style.opacity = 0, 1800);
38
+ }
39
+
40
+ // Audit #30: a dead API previously left the page at "0 pending" forever โ€”
41
+ // refresh() now surfaces the failure and offers a retry.
42
+ async function refresh() {
43
+ let g;
44
+ try {
45
+ const r = await fetch("/api/graph");
46
+ if (!r.ok) throw new Error(`API ${r.status}`);
47
+ g = await r.json();
48
+ } catch (err) {
49
+ $("count").textContent = "API unreachable";
50
+ $("cards").innerHTML =
51
+ `<div style="color:var(--red);font-size:13px;">` +
52
+ `Failed to load graph (${esc(String(err.message || err))}). ` +
53
+ `<button class="ok" id="retryBtn">retry</button></div>`;
54
+ const btn = document.getElementById("retryBtn");
55
+ if (btn) btn.addEventListener("click", refresh);
56
+ return;
57
+ }
58
+ nodes = {};
59
+ g.nodes.forEach(n => nodes[n.id] = n);
60
+ pending = g.edges.filter(e => e.pending && !(e.invalidated ?? Boolean(e.valid_to)));
61
+ if (selectedId == null && pending.length) selectedId = pending[0].id;
62
+ if (selectedId != null && !pending.some(e => e.id === selectedId)) {
63
+ selectedId = pending.length ? pending[0].id : null;
64
+ }
65
+ $("count").textContent = `${pending.length} pending`;
66
+ renderCards();
67
+ }
68
+
69
+ // Audit #30: in-flight lock โ€” key-repeat or double-click spammed resolve POSTs
70
+ // (each POST commits; the spam created noisy history and raced the refresh).
71
+ let resolving = false;
72
+
73
+ // ---------- Inbox ----------
74
+ function renderCards() {
75
+ const box = $("cards");
76
+ if (!pending.length) {
77
+ box.innerHTML = `<div style="color:var(--dim);font-size:13px;">No open suggestions. ๐ŸŽ‰</div>`;
78
+ return;
79
+ }
80
+ const sel = currentIndex();
81
+ box.innerHTML = pending.map((e, i) => `
82
+ <div class="card ${i === sel ? "active" : ""}" data-i="${i}">
83
+ <span class="kind" data-kind="${esc(e.kind)}">${esc(e.kind === "fact" ? e.predicate || "fact" : e.kind)}</span>
84
+ <div class="side-label">A</div>
85
+ <div class="nodebox pickable" data-pick="${esc(e.source)}">
86
+ <span class="id">${esc(e.source)}</span><br>${esc(short(e.source))}</div>
87
+ <div class="side-label">B</div>
88
+ <div class="nodebox pickable" data-pick="${esc(e.target)}">
89
+ <span class="id">${esc(e.target)}</span><br>${esc(short(e.target))}</div>
90
+ ${e.kind === "fact" ? (e.evidence || []).slice(0, 3).map(item => `<p>${esc(item.text.slice(0, 240))}</p>`).join("") : ""}
91
+ <div class="actions">
92
+ <button class="ok" data-resolve="${esc(e.id)}" data-accept="1">โœ“ accept โŽ</button>
93
+ <button class="no" data-resolve="${esc(e.id)}" data-accept="0">โœ— reject esc</button>
94
+ </div>
95
+ </div>`).join("");
96
+ const active = box.querySelector(".card.active");
97
+ if (active) active.scrollIntoView({ block: "nearest" });
98
+ }
99
+
100
+ // Event-Delegation: kein inline onclick mit interpolierten ids mehr.
101
+ document.addEventListener("click", ev => {
102
+ const resolveBtn = ev.target.closest("[data-resolve]");
103
+ if (resolveBtn) {
104
+ resolve(resolveBtn.dataset.resolve, resolveBtn.dataset.accept === "1");
105
+ return;
106
+ }
107
+ const pickable = ev.target.closest("[data-pick]");
108
+ if (pickable) pickable.blur();
109
+ });
110
+
111
+ document.addEventListener("click", ev => {
112
+ const box = ev.target.closest(".nodebox.pickable[data-pick]");
113
+ if (box) pick(box.dataset.pick);
114
+ });
115
+
116
+ async function resolve(id, accept) {
117
+ if (resolving) return;
118
+ resolving = true;
119
+ try {
120
+ const r = await fetch(`/api/edge/${encodeURIComponent(id)}/${accept ? "accept" : "reject"}`, { method: "POST" });
121
+ if (!r.ok) return flash("Failed to resolve", false);
122
+ flash(accept ? "Edge accepted" : "Edge rejected");
123
+ await refresh();
124
+ // Audit #50: after accepting, focus landed on the next card's first button
125
+ // (the pickable nodebox) โ€” keyboard users expect the Accept button.
126
+ const active = document.querySelector(".card.active button.ok");
127
+ if (active) active.focus();
128
+ } catch (err) {
129
+ flash("Network error: " + String(err.message || err), false);
130
+ } finally {
131
+ resolving = false;
132
+ }
133
+ }
134
+
135
+ // ---------- same_as-Picker ----------
136
+ function pick(id) {
137
+ if (pickA === id || pickB === id) return; // never link a node to itself
138
+ if (!pickA) pickA = id;
139
+ else if (!pickB) pickB = id;
140
+ else { pickA = id; pickB = null; } // third click restarts the pick
141
+ renderPick();
142
+ }
143
+
144
+ window.pickFromCard = (id, el) => { pick(id); el.blur(); };
145
+
146
+ function renderPick() {
147
+ const a = $("slotA"), b = $("slotB"), btn = $("linkbtn");
148
+ a.textContent = "A: " + (pickA ? short(pickA, 40) : "empty");
149
+ b.textContent = "B: " + (pickB ? short(pickB, 40) : "empty");
150
+ a.classList.toggle("filled", !!pickA);
151
+ b.classList.toggle("filled", !!pickB);
152
+ btn.disabled = !(pickA && pickB);
153
+ btn.textContent = pickA && pickB
154
+ ? `${short(pickA, 12)} โ‡„ ${short(pickB, 12)} โŽ` : "link โŽ";
155
+ }
156
+
157
+ async function link() {
158
+ if (!(pickA && pickB)) return;
159
+ const r = await fetch("/api/edge", {
160
+ method: "POST", headers: { "Content-Type": "application/json" },
161
+ body: JSON.stringify({ source: pickA, target: pickB, kind: "same_as" }),
162
+ });
163
+ if (!r.ok) { const e = await r.json().catch(() => ({})); return flash(e.error || "Link failed", false); }
164
+ flash("same_as linked");
165
+ pickA = pickB = null;
166
+ renderPick();
167
+ await refresh();
168
+ }
169
+
170
+ // Search
171
+ $("search").addEventListener("input", () => {
172
+ const q = $("search").value.trim().toLowerCase();
173
+ const hits = $("hits");
174
+ if (!q) return hits.innerHTML = "";
175
+ const found = Object.values(nodes)
176
+ .filter(n => n.text.toLowerCase().includes(q)).slice(0, 8);
177
+ hits.innerHTML = found.map(n =>
178
+ `<div class="hit ${n.id === pickA ? "pickedA" : ""} ${n.id === pickB ? "pickedB" : ""}"
179
+ data-pick="${esc(n.id)}"><span class="id">${esc(n.id.slice(0, 8))}</span>${esc(short(n.id, 55))}</div>`).join("");
180
+ });
181
+
182
+ window.searchPick = id => { pick(id); $("search").value = ""; $("hits").innerHTML = ""; };
183
+
184
+ $("linkbtn").addEventListener("click", link);
185
+
186
+ // ---------- Keyboard ----------
187
+ document.addEventListener("keydown", e => {
188
+ const inField = ["INPUT", "TEXTAREA"].includes(document.activeElement.tagName);
189
+ // Audit #30: key-repeat fired Enter/Escape once per auto-repeat tick โ€”
190
+ // one held key spammed resolve POSTs. Auto-repeats are ignored.
191
+ if (e.repeat && (e.key === "Enter" || e.key === "Escape")) return;
192
+ // Audit #50: Ctrl/Meta/Alt chords must not trigger shortcuts (Ctrl+S hijacked).
193
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
194
+ switch (e.key) {
195
+ case "s": if (!inField) { e.preventDefault(); $("search").focus(); } break;
196
+ case "x": if (!inField && (pickA || pickB)) { pickA = pickB = null; renderPick(); } break;
197
+ case "j": if (!inField && pending.length) { selectedId = pending[Math.min(currentIndex() + 1, pending.length - 1)].id; renderCards(); } break;
198
+ case "k": if (!inField && pending.length) { selectedId = pending[Math.max(currentIndex() - 1, 0)].id; renderCards(); } break;
199
+ case "Enter":
200
+ if (inField) break;
201
+ if (pickA && pickB) link();
202
+ else if (pending[currentIndex()]) resolve(pending[currentIndex()].id, true);
203
+ break;
204
+ case "Escape":
205
+ if (inField) { document.activeElement.blur(); }
206
+ else if (pending[currentIndex()]) resolve(pending[currentIndex()].id, false);
207
+ break;
208
+ }
209
+ });
210
+
211
+ refresh();