fix: sanitize_label double-encoding and --wiki missing from skill (#66, #55)

This commit is contained in:
Safi
2026-04-08 09:40:21 +01:00
parent 3d53287ff6
commit 92b70ce5f4
7 changed files with 49 additions and 14 deletions
+5
View File
@@ -2,6 +2,11 @@
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
## 0.3.12 (2026-04-07)
- Fix: `sanitize_label` was double-encoding HTML entities in the interactive graph (`<` instead of `<`) — removed `html.escape()` from `sanitize_label`; callers that inject directly into HTML now call `html.escape()` themselves (#66)
- Fix: `--wiki` flag missing from `skill.md` usage table (#55)
## 0.3.11 (2026-04-07)
- Fix: Louvain fallback hangs indefinitely on large sparse graphs — added `max_level=10, threshold=1e-4` to prevent infinite loops while preserving community quality (#48)
+31 -4
View File
@@ -382,7 +382,9 @@ def main() -> None:
if len(sys.argv) < 3:
print("Usage: graphify query \"<question>\" [--dfs] [--budget N] [--graph path]", file=sys.stderr)
sys.exit(1)
from graphify.serve import _load_graph, _score_nodes, _bfs, _dfs, _subgraph_to_text
from graphify.serve import _score_nodes, _bfs, _dfs, _subgraph_to_text
from graphify.security import sanitize_label
from networkx.readwrite import json_graph
question = sys.argv[2]
use_dfs = "--dfs" in sys.argv
budget = 2000
@@ -391,14 +393,39 @@ def main() -> None:
i = 0
while i < len(args):
if args[i] == "--budget" and i + 1 < len(args):
budget = int(args[i + 1]); i += 2
try:
budget = int(args[i + 1])
except ValueError:
print(f"error: --budget must be an integer", file=sys.stderr)
sys.exit(1)
i += 2
elif args[i].startswith("--budget="):
budget = int(args[i].split("=", 1)[1]); i += 1
try:
budget = int(args[i].split("=", 1)[1])
except ValueError:
print(f"error: --budget must be an integer", file=sys.stderr)
sys.exit(1)
i += 1
elif args[i] == "--graph" and i + 1 < len(args):
graph_path = args[i + 1]; i += 2
else:
i += 1
G = _load_graph(graph_path)
# Load graph directly — validate_graph_path restricts to graphify-out/
# so for custom --graph paths we resolve and load directly after existence check
gp = Path(graph_path).resolve()
if not gp.exists():
print(f"error: graph file not found: {gp}", file=sys.stderr)
sys.exit(1)
if not gp.suffix == ".json":
print(f"error: graph file must be a .json file", file=sys.stderr)
sys.exit(1)
try:
import json as _json
import networkx as _nx
G = json_graph.node_link_graph(_json.loads(gp.read_text(encoding="utf-8")), edges="links")
except Exception as exc:
print(f"error: could not load graph: {exc}", file=sys.stderr)
sys.exit(1)
terms = [t.lower() for t in question.split() if len(t) > 2]
scored = _score_nodes(G, terms)
if not scored:
+2 -1
View File
@@ -1,5 +1,6 @@
# write graph to HTML, JSON, SVG, GraphML, Obsidian vault, and Neo4j Cypher
from __future__ import annotations
import html as _html
import json
import math
import re
@@ -371,7 +372,7 @@ def to_html(
edges_json = json.dumps(vis_edges)
legend_json = json.dumps(legend_data)
hyperedges_json = json.dumps(getattr(G, "graph", {}).get("hyperedges", []))
title = sanitize_label(str(output_path))
title = _html.escape(sanitize_label(str(output_path)))
stats = f"{G.number_of_nodes()} nodes &middot; {G.number_of_edges()} edges &middot; {len(communities)} communities"
html = f"""<!DOCTYPE html>
+4 -5
View File
@@ -186,13 +186,12 @@ _MAX_LABEL_LEN = 256
def sanitize_label(text: str) -> str:
"""Strip control characters, cap length, then HTML-escape.
"""Strip control characters and cap length.
Applied to all node labels and edge titles before they are embedded
in pyvis HTML output or returned via the MCP server, preventing both
XSS and broken visualisations from malformed source identifiers.
Safe for embedding in JSON data (inside <script> tags) and plain text.
For direct HTML injection, wrap the result with html.escape().
"""
text = _CONTROL_CHAR_RE.sub("", text)
if len(text) > _MAX_LABEL_LEN:
text = text[:_MAX_LABEL_LEN]
return html.escape(text)
return text
+1
View File
@@ -24,6 +24,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project # write vault to custom path (e.g. existing vault)
/graphify add <url> # fetch URL, save to ./raw, update graph
/graphify add <url> --author "Name" # tag who wrote it
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphifyy"
version = "0.3.11"
version = "0.3.12"
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph"
readme = "README.md"
license = { file = "LICENSE" }
+5 -3
View File
@@ -168,9 +168,11 @@ def test_validate_graph_path_raises_if_file_missing(tmp_path):
# sanitize_label
# ---------------------------------------------------------------------------
def test_sanitize_label_escapes_html():
assert "&lt;script&gt;" in sanitize_label("<script>")
assert "&amp;" in sanitize_label("foo & bar")
def test_sanitize_label_passthrough_html_chars():
# sanitize_label does NOT HTML-escape — callers that inject into HTML must
# wrap with html.escape() themselves (e.g. the title in to_html())
assert sanitize_label("<script>") == "<script>"
assert sanitize_label("foo & bar") == "foo & bar"
def test_sanitize_label_strips_control_chars():
result = sanitize_label("hello\x00\x1fworld")