Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7828aee
feat: add Mnem knowledge-graph 3D viewer
gitricko Aug 3, 2026
63662a5
fix(knowledge-graph): keep edges visible when importance filter changes
gitricko Aug 4, 2026
2456402
docs(knowledge-graph): add design document + regeneration workflow
gitricko Aug 4, 2026
82054b8
feat(skills): add mnemon-graph-export skill + wiki cross-reference
gitricko Aug 4, 2026
95131fc
refactor(knowledge-graph): decouple data from viewer (portable design)
gitricko Aug 4, 2026
867bd84
fix(knowledge-graph): make file:// double-click work (user-reported)
gitricko Aug 4, 2026
34ec402
feat(knowledge-graph): index.html forwards to viewer (root URL works)
gitricko Aug 4, 2026
4fde818
feat: implement canvas-based auto-force layout and smart camera posit…
gitricko Aug 5, 2026
ab0d4e9
feat: implement canvas-based auto-force layout and smart camera posit…
gitricko Aug 5, 2026
03a774a
fix(knowledge-graph): repair auto-layout — fatal SyntaxError + duplic…
gitricko Aug 6, 2026
f28926e
docs(knowledge-graph): document auto-layout pitfalls (SyntaxError cha…
gitricko Aug 6, 2026
1693a17
chore(knowledge-graph): drop mnemon-viz.html vis.js fallback
gitricko Aug 6, 2026
f95cb9d
docs(knowledge-graph): switch viewer port 8123 -> 8130
gitricko Aug 6, 2026
0143d7a
chore(knowledge-graph): refactor viewer tooling into skill folder (mo…
gitricko Sep 6, 2026
a572045
merge: resolve main conflicts (MEMORY.md path filter + INDEX.md mnemo…
gitricko Sep 6, 2026
1c1d35d
fix: tooltip unclickable + canvas cursor missing + click positioning
gitricko Sep 6, 2026
cb8fa46
feat(knowledge-graph): hoverable category pills + untrack generated e…
gitricko Sep 6, 2026
7198d4a
fix(knowledge-graph): resolve Greptile/Copilot review findings (secur…
gitricko Sep 6, 2026
83a0099
fix(knowledge-graph): snap repulsion force value to slider 10-step (G…
gitricko Sep 6, 2026
9b1cf4b
revert: restore MEMORY.md to main version — exclude from this PR
gitricko Sep 6, 2026
335e337
chore(knowledge-graph): untrack .cache/fg2.js (build cache, gitignored)
gitricko Sep 6, 2026
11a867d
seed: add mnemon-graph-export skill entry for cross-spawn persistence
gitricko Sep 6, 2026
7991297
docs(memory): expand CI path-filter entry with runtime/infrastructure…
gitricko Sep 6, 2026
1be79f7
fix(knowledge-graph): escape nodeLabel in graph (Greptile P1 XSS)
gitricko Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/memories/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ SKILL-LOADING RULE: Before ANY GitHub/Git operation in a Codespace, ALWAYS load
§
WIKI-SKILL SYNC RULE: When updating a skill in `.devcontainer/skills/`, always check if any wiki article in `.devcontainer/wiki/` references the same topic and needs a corresponding update (or cross-reference). User explicitly asked: "check whether the change in this skill deserve some changes in some LM wiki that we have stored." Wiki = reference knowledge; skill = procedural. They should stay in sync on the same topic.
§
CI path-filter (user-validated): .devcontainer/memories/** and .devcontainer/skills/** = CONTENT -> 30s lint-check only. Only boot scripts/devcontainer.json/workflows = infrastructure -> full-build. Never move markdown content into infrastructure. Self-check Persistence = 9a+9b only.
CI path-filter (user-validated): .devcontainer/skills/** and .devcontainer/memories/** are CONTENT -> runtime group, 30s lint-check only. Only boot scripts/.devcontainer.json/workflows = infrastructure -> full-build. Never promote content dirs into infrastructure. Mnemon seed import parses `imported` field (not 'added') from mnemon import JSON. PR-ONLY POLICY: Never commit changes to .devcontainer/memories/MEMORY.md or USER.md on a PR branch — memory-automation artifacts create diffs-vs-main that must be reverted.
§
Mnemon persistence: fresh Codespace spawns re-seed from .devcontainer/mnemon/seed.json (imported by start-hermes.sh every spawn). The LIVE Mnemon DB is ephemeral and does NOT survive a rebuild. To persist a memory/skill across spawns, write a seed.json entry, not just mnemon_remember. Validate: `mnemon import --dry-run .devcontainer/mnemon/seed.json` -> 'validation passed'.
§
Expand Down
22 changes: 22 additions & 0 deletions .devcontainer/mnemon/seed.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,28 @@
"pixelflux"
],
"source": "agent"
},
{
"content": "Skill: mnemon-graph-export \u2014 Export Mnemon knowledge graph to 3D viewer. Location: .devcontainer/skills/mnemon-graph-export/. Trigger: \"export mnemon graph\", \"regenerate knowledge graph\", \"update 3D viewer\", \"new graph from mnemon\". Usage: cd .devcontainer/skills/mnemon-graph-export/scripts && python3 export_graph.py (fresh graph.json + graph-data.js) && python3 -m http.server 8130 (serve viewer at localhost:8130 or file:// mnemon-graph.html). Viewer loads data at runtime \u2014 data refresh never needs rebuild; only template.html changes require python3 build.py (vendors 3d-force-graph@1.80 with SRI pin). Wiki: .devcontainer/wiki/mnemon-graph-viewer.md.",
"category": "fact",
"importance": 4,
"tags": [
"skill",
"knowledge-graph",
"mnemon",
"export",
"viewer",
"codespace"
],
"entities": [
"mnemon-graph-export",
"export_graph.py",
"build.py",
"mnemon-graph.html",
"graph.json",
"graph-data.js"
],
"source": "agent"
}
]
}
143 changes: 143 additions & 0 deletions .devcontainer/skills/mnemon-graph-export/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
name: mnemon-graph-export
description: "Use when exporting/regenerating the Mnemon knowledge graph."
---

# Mnemon Knowledge-Graph Export (3D viewer regeneration)

Regenerate the 3D knowledge-graph viewer from the latest Mnemon data. The tool
lives in `.devcontainer/skills/mnemon-graph-export/scripts/`; the pipeline is
`export_graph.py -> graph.json (+graph-data.js)`, and the STATIC viewer
(`mnemon-graph.html`) loads the data on open — **data refresh never rebuilds
the viewer**. It works both by double-clicking the HTML (file://, via
`graph-data.js`) and over http (fetch). Plus Mnemon's own `viz` command for
the vis.js fallback. Design rationale: see
`scripts/DESIGN.md`; wiki reference:
`.devcontainer/wiki/mnemon-graph-viewer.md`.

## Trigger

User says anything like: "export mnemon graph", "regenerate/show my knowledge
graph", "update the 3D viewer", "new graph from mnemon".

## Steps (verified end-to-end 2026-08)

```bash
cd .devcontainer/skills/mnemon-graph-export/scripts

# 1) Fresh snapshot from the live DB (read-only SQLite) -> graph.json
# (+ graph-data.js, the file://-safe sibling — keep both in sync)
python3 export_graph.py
# -> "Exported N nodes, M edges -> graph.json (+graph-data.js)"

# 2) DONE — the viewer is a fixed asset; it loads the data on open.
# Double-click mnemon-graph.html (file://, uses graph-data.js) or serve:
python3 -m http.server 8130 --bind 0.0.0.0 # then http://localhost:8130/ (index.html forwards)

# 3) Commit the refreshed data (viewer only changes when template.html does)
git add graph.json graph-data.js
git -c commit.gpgsign=false commit -m "chore(knowledge-graph): refresh graph from latest mnemon export"
```

Rebuild the viewer ONLY when the template (`template.html`) changes — re-vendors
the fg2 library into `mnemon-graph.html`; does not touch data:

```bash
python3 build.py # first run fetches 3d-force-graph into .cache/
```

Non-default DB/store: `python3 export_graph.py <path-to-db> -o out.json`.
Point the viewer at any JSON with `mnemon-graph.html?data=other.json`.

## Verification (MANDATORY — the user's standard)

Static greps / `node --check` are NOT verification. Prove it renders:

1. **file:// mode** (the user-reported failure): copy `mnemon-graph.html` +
`graph-data.js` to a fresh dir, open the HTML via `file://` in a headless
browser — subtitle must read "N memories, M connections" with NO server.

Verified headless invocation (chromium from playwright cache, no module
needed — full render incl. WebGL):
```bash
CHROME=~/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome
$CHROME --headless=new --no-sandbox --use-angle=swiftshader \
--enable-unsafe-swiftshader --virtual-time-budget=15000 \
--dump-dom file:///abs/path/mnemon-graph.html > dom.html 2>/dev/null
grep -oE 'id="subtitle">[^<]*' dom.html # must be "N memories, M connections"
grep -c '<canvas' dom.html # must be >=1
grep -o 'class="nl"' dom.html | wc -l # must equal N (label pills == nodes)
```
For http mode: serve a dir containing ONLY `mnemon-graph.html` +
`graph.json` (no graph-data.js) and dump-dom the http:// URL instead.
2. **http mode**: serve a dir with viewer + `graph.json` (no `graph-data.js`),
open `/mnemon-graph.html` — same subtitle (fetch path).
3. Assert category label pills == node count (`.nl` elements in `#labels`).
4. Move the importance slider to 5: node count drops to importance-5 nodes,
edges stay visible between remaining nodes (the linkVisibility bug).
5. Prove portability: swap the data (or use `?data=other.json`) and confirm
the subtitle changes with NO rebuild.

If any check fails, debug the viewer (see pitfalls), never ship unverified.

## Pitfalls (all hit and fixed; do not re-derive)

1. **file:// fetch blocked** (user-reported "Cannot load graph.json"): browsers
block `fetch()` from `file://`. The viewer loads data via a
`<script src="graph-data.js">` tag (allowed from file://), falling back to
fetch. ALWAYS regenerate `graph-data.js` together with `graph.json`
(`export_graph.py` writes both).
2. **Serving**: `index.html` is a meta-refresh forwarder to
`mnemon-graph.html`, so `http://host:8130/` just works — no need to know
the artifact filename. Never serve the TEMPLATE (`template.html`) as the
root: it still has unsubstituted markers and renders blank.
3. **Never inline a separate three.js copy** next to the fg2 bundle: fatal
"Multiple instances of Three.js" crash, `ForceGraph3D` undefined. The bundle
embeds its own Three r183 (which ships no UMD build anyway).
4. **No `.autoRotate()`** on the vendored fg2 fork — it throws mid-`build()`
and silently kills everything after it. Use manual orbit in the rAF loop
(`spinAngle += 0.0008; camera.position.x = r*sin(a); camera.position.z =
r*cos(a); camera.lookAt(0,0,0)`), gated by a `spin` flag.
5. **linkVisibility endpoints**: after the engine settles, `l.source`/
`l.target` are node OBJECTS, not string ids. Predicate must accept both:
`typeof l.source === 'object' ? l.source : nodes.find(x => x.id ===
l.source)`. The id-only version hides ALL edges on the first slider move.
6. **`graph2ScreenCoords(x,y,z)` returns `{x,y}` only** (no z field) — any
depth-culling on `p.z` hides every label. Labels are never culled (known
limitation).
7. **Slider must be integer**: `min="1" max="5" step="1"` — data importance is
integer (currently 2-5); `step="0.1"` showed "2.6" style floats.
8. **Repo is PUBLIC** — graph.json embeds memory content. Review the export for
secrets/PII before committing (content mirrors committed wiki + seed.json,
but re-scan anyway).
9. **Generated data files should be gitignored**: `graph.json` and `graph-data.js`
are refreshed by `export_graph.py` on every regeneration. Add them to
`.gitignore` to avoid commit noise and conflicts — users run the export once
after clone to get a local graph.
10. **Dense graphs squish together**: with many edges (e.g. 25 nodes / 372 edges),
the default d3 force parameters pull everything into a tight ball. The fix
is exposing force controls in the UI: link distance, charge strength,
charge distanceMin, plus a "Reheat simulation" button calling
`d3ReheatSimulation()`. This lets you tune spread per dataset without
rebuilds. See `references/force-controls.md` for the implementation.
11. Browser caching: after rebuild + copy, hard-reload or cache-bust
(`?v=N`); the page may otherwise serve a stale artifact.
12. **Leading-dot chain after `;` = silent total failure**: applying forces
with `...cameraPosition(...);\n.d3Force('link').distance(x)` is a JS
SyntaxError (`.d3Force` has no receiver) — the ENTIRE app `<script>`
dies: no auto-layout, no labels, no visible error. User just sees
"no change". Force application MUST be separate statements:
`Graph.d3Force('link').distance(x);`. Always `node --check` the
extracted app script after editing the template.
13. **Duplicate function declarations shadow each other**: two
`computeAutoForces()` definitions (one added in a new section, one left
in the old build section) — JS hoisting makes the LAST one win. After
editing, `grep -c "function computeAutoForces"` must be 1.
14. **Bubble size**: fg2 sphere radius = `Math.cbrt(nodeVal) * nodeRelSize`
(verified in bundle). nodeRelSize 12 with val up to 19 → radius ~21
units = giant bubbles that read as "zoom too big". Use nodeRelSize(3)
and keep effToVal small (3..19): radius ~8 units max.
15. **Auto-rotate orbit center**: spinCam must orbit the graph CLUSTER
center (`Graph.getGraphBbox()` midpoint), NOT the origin — otherwise
auto-rotate swings the framed view off-center and the graph appears to
drift/cluster. Camera must lookAt(tx,ty,tz).
7 changes: 7 additions & 0 deletions .devcontainer/skills/mnemon-graph-export/scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# generated data — refreshed by export_graph.py; do not commit
graph.json
graph-data.js

# build cache (fetched 3d-force-graph bundle) — re-fetched on demand by build.py
.cache/
__pycache__/
Loading
Loading