diff --git a/src/services/graph-visualize-html.ts b/src/services/graph-visualize-html.ts
index 6356c79..4aaf1d5 100644
--- a/src/services/graph-visualize-html.ts
+++ b/src/services/graph-visualize-html.ts
@@ -281,16 +281,20 @@ export async function buildInteractiveGraphHtml(opts: InteractiveHtmlOptions): P
? `${files.length} files · ${fileEdges.length} edges · ${sym.symbolCount ?? 0} symbols · ${sym.edgeCount ?? 0} calls`
: `${files.length} files · ${fileEdges.length} edges`;
+ // Use function replacers for asset/data injection: string replacements
+ // would interpret `$&`, `$'`, `` $` ``, and `$$` in the replacement, which
+ // corrupts vendored bundles (cytoscape.min.js and dagre.min.js both contain
+ // a regex-escape idiom of the form `\$&`).
const html = assets.template
.replace(/\{\{TITLE\}\}/g, escapeHtml(`${opts.projectName} — SocratiCode graph`))
.replace(/\{\{PROJECT_NAME\}\}/g, escapeHtml(opts.projectName))
.replace(/\{\{STATS\}\}/g, escapeHtml(statsLine))
- .replace("{{STYLES}}", assets.styles)
- .replace("{{CYTOSCAPE}}", assets.cytoscape)
- .replace("{{DAGRE}}", assets.dagre)
- .replace("{{CYTOSCAPE_DAGRE}}", assets.cytoscapeDagre)
- .replace("{{DATA_JSON}}", toEmbeddedJson(data))
- .replace("{{APP}}", assets.app);
+ .replace("{{STYLES}}", () => assets.styles)
+ .replace("{{CYTOSCAPE}}", () => assets.cytoscape)
+ .replace("{{DAGRE}}", () => assets.dagre)
+ .replace("{{CYTOSCAPE_DAGRE}}", () => assets.cytoscapeDagre)
+ .replace("{{DATA_JSON}}", () => toEmbeddedJson(data))
+ .replace("{{APP}}", () => assets.app);
logger.info("Built interactive graph HTML", {
project: opts.projectName,
diff --git a/tests/unit/graph-visualize-html.test.ts b/tests/unit/graph-visualize-html.test.ts
index 3e5abc0..b5da649 100644
--- a/tests/unit/graph-visualize-html.test.ts
+++ b/tests/unit/graph-visualize-html.test.ts
@@ -105,6 +105,20 @@ describe("graph-visualize-html", () => {
expect(html).toContain('"symbolMode":"capped"');
});
+ it("inlines vendored Cytoscape/Dagre bundles byte-for-byte (no $-token interpretation)", async () => {
+ // Regression for a `String.prototype.replace(string, string)` pitfall: a `$&`
+ // inside the replacement is interpreted as the match, corrupting any asset
+ // (or embedded data) that contains `$&`, `$'`, `` $` ``, or `$$`.
+ const { html } = await buildInteractiveGraphHtml({
+ projectPath: "/proj", projectName: "bytes", projectId: "p1", graph: SAMPLE_GRAPH,
+ });
+ // If the bug returns, `\$&` becomes `\{{CYTOSCAPE}}` / `\{{DAGRE}}` in the output.
+ expect(html.includes("\\{{CYTOSCAPE}}")).toBe(false);
+ expect(html.includes("\\{{DAGRE}}")).toBe(false);
+ // And the original token must survive verbatim from both bundles.
+ expect(html.includes("\\$&")).toBe(true);
+ });
+
it("marks edges that belong to a cycle as cyclic:true", async () => {
const cyclic: CodeGraph = {
nodes: [