From 939a0bde22db6bccf63606d058ffa817a7c19a23 Mon Sep 17 00:00:00 2001 From: Mir Sameer Date: Fri, 19 Jun 2026 13:05:39 -0700 Subject: [PATCH] Add Fleet Galaxy dashboard view Signed-off-by: Mir Sameer --- README.md | 4 +- docs/agent-guide.md | 2 +- docs/research-notes.md | 4 +- src/dashboard/server.ts | 114 ++++++++++++++++++++++++++++++++++++++-- tests/dashboard.test.ts | 87 +++++++++++++++++++----------- 5 files changed, 170 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3a250ce..9dd0f28 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ RepoLens MCP is an original TypeScript implementation built around fast local ve - **Persistent local config**: `config set auto-index incremental` and `config set auto-sync true` store defaults for MCP startup indexing, live-session syncing, root, database path, max file size, labels, and graph-package bootstrap without requiring shell env vars. - **Watch mode**: keep an indexed graph fresh during active coding with polling-based incremental refreshes, optionally skipping unchanged git polls with `--git-aware`. - **Portable graph and report artifacts**: export self-contained HTML graph snapshots, architecture reports, and compressed `.rlgz` graph packages from the CLI; first index can bootstrap a missing database from `.repolens/graph.rlgz`. -- **Operational dashboard**: browse graph previews, structural filters, references, semantic/vector search, schema counts, relationship patterns, label property hints, fleet service links, dead-code candidates, review signals, and report links without a frontend build. +- **Operational dashboard**: browse graph previews, Fleet Galaxy cross-repo maps, structural filters, references, semantic/vector search, schema counts, relationship patterns, label property hints, fleet service links, dead-code candidates, review signals, and report links without a frontend build. - **Graph communities**: detects functional modules from weighted relationships, not just folder names. - **Code-aware search ranking**: uses SQLite FTS5 BM25 ranking with indexed camelCase and snake_case term expansion, so `create order` can find `createOrder` without scanning files. - **Reference lookup**: finds exact indexed identifier references and labels definition lines for language-server-style navigation without requiring an external LSP process. @@ -62,7 +62,7 @@ node --experimental-sqlite dist/src/cli.js serve Then open `http://127.0.0.1:9749`. -The dashboard includes code search, reference lookup, semantic/vector search, graph search, graph schema tables with relationship patterns and label property hints, fleet service links, hotspot and boundary summaries, git-history signals, dead-code candidates, and one-click Markdown/HTML architecture reports. +The dashboard includes code search, reference lookup, semantic/vector search, graph search, graph schema tables with relationship patterns and label property hints, Fleet Galaxy cross-repo graphing, fleet service links, hotspot and boundary summaries, git-history signals, dead-code candidates, and one-click Markdown/HTML architecture reports. From a local clone, the installer runs the same build and Codex checks: diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 6edb52d..dd535b2 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -34,7 +34,7 @@ On Windows PowerShell, the local installer mirrors the shell installer: ## Before Editing 1. Run `index` or `benchmark` for the target repository. -2. Use `architecture`, `schema`, `communities`, or `fleet-summary` to understand the project shape. `schema` includes relationship patterns and label property hints for safer graph queries. +2. Use `architecture`, `schema`, `communities`, `fleet-summary`, or `fleet-graph` to understand the project shape. `schema` includes relationship patterns and label property hints for safer graph queries. 3. Use `search`, `symbols`, `references`, `trace`, `cycles`, and `context-pack` for focused code context. 4. Use `changes` after edits to map uncommitted files back to graph impact, including per-file blast radius, relationship counts, and risk reasons. diff --git a/docs/research-notes.md b/docs/research-notes.md index 57206a8..f79955c 100644 --- a/docs/research-notes.md +++ b/docs/research-notes.md @@ -29,7 +29,7 @@ RepoLens MCP is not a fork or a drop-in static C replacement. It is an original - Incremental indexing skips unchanged files, prunes removed files, and avoids call-edge rebuilds when there is no repository delta. - Watch mode keeps the graph fresh with polling-based incremental refreshes while preserving deterministic CLI behavior for tests and automation; git-aware auto-sync skips unchanged HEAD/status polls during long-running MCP sessions. - Browser dashboard without a bundler so the project is easy to build and inspect. -- Dashboard APIs expose architecture, fleet summaries, graph schema relationship/property hints, graph search, semantic search, local vector search, reference lookup, read-only graph queries, source snippets, import-resolved dependency cycles, dead-code candidates, graph previews, code search, and live Markdown/HTML architecture reports from the same local server. +- Dashboard APIs expose architecture, fleet summaries, Fleet Galaxy cross-repo graph data, graph schema relationship/property hints, graph search, semantic search, local vector search, reference lookup, read-only graph queries, source snippets, import-resolved dependency cycles, dead-code candidates, graph previews, code search, and live Markdown/HTML architecture reports from the same local server. - Self-contained graph and architecture report exports for sharing HTML or Markdown artifacts without running a server, plus compressed checksummed `.rlgz` graph packages for reusing a SQLite graph without reindexing. A successful index can write a fresh package with `--write-package`, and a missing database can bootstrap from `.repolens/graph.rlgz` before the incremental pass. - CI runs explicit test-skip governance, type-check, tests, production dependency audit, package dry-run, package contents gating, installer dry-run auditing, CycloneDX SBOM generation, self-indexing, and architecture output; separate workflows cover OpenSSF Scorecard and release build-provenance attestations. - `llms.txt`, `docs/agent-guide.md`, and `docs/BENCHMARK.md` provide concise agent-facing operating instructions, sanitized validation evidence, and local-data boundaries in the npm package. @@ -42,7 +42,7 @@ RepoLens MCP is not a fork or a drop-in static C replacement. It is an original - Codex setup is explicit and reviewable through `doctor` and `install-codex --dry-run`, with safeguards around existing unmanaged MCP entries. - Multi-agent setup is explicit, reversible, and reviewable through `agent-setup`, `install-agents --dry-run`, and `uninstall-agents --dry-run`, generating project-local instructions and config snippets for Codex, Claude, Gemini, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro. - Built-in ADR memory, not just structural graph search. -- Dashboard API and HTML are included in the same binary entrypoint, avoiding a separate frontend build while still exposing graph exploration, fleet service links, schema counts, relationship patterns, label property hints, review signals, dead-code samples, and report links. +- Dashboard API and HTML are included in the same binary entrypoint, avoiding a separate frontend build while still exposing graph exploration, Fleet Galaxy cross-repo graphing, fleet service links, schema counts, relationship patterns, label property hints, review signals, dead-code samples, and report links. - Swift extraction and big-repo validation now cover a mixed mobile/web monorepo, not only TypeScript services. - Structural graph search, BM25 source search with code-aware token expansion, reference lookup, typed inheritance/implementation/use edges, conservative data-flow edges, persistent startup config, redacted secret scanning, context packs for agents, multi-agent setup guidance, graph package bootstrap, optional startup auto-indexing, git-aware auto-sync, repeatable benchmark output, import-resolved local file edges, multi-ecosystem package/dependency nodes, resolved lockfile dependency nodes, project inventory/status, fleet summaries with cross-project service links, cross-repo graphing, runtime trace ingestion, Docker/Kubernetes infrastructure nodes, channel/event edges, first-class HTTP call nodes, GraphQL/gRPC/tRPC/OpenAPI protocol nodes, route-call edges, deterministic graph communities, dependency-free semantic search, persisted local vector search, read-only Cypher-like graph queries, graph schema relationship/property summaries, dependency-cycle detection, dead-code candidates, git-history hotspots, git-diff impact mapping with per-file blast radius, watch indexing, and portable graph/package exports are first-class workflows. - Indexing now writes local `SIMILAR_TO` and `SEMANTICALLY_RELATED` edges plus deterministic symbol vectors without external embeddings or network calls. diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index 7d99ef5..d7872e1 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -1,5 +1,5 @@ import http from "node:http"; -import { architectureReport, findCommunities, findDeadCode, findDependencyCycles, findReferences, fleetSummary, getArchitecture, getCodeSnippet, getGraphSchema, graphSnapshot, queryGraph, searchCode, searchGraph, semanticSearch, searchSymbols, vectorSearch } from "../core/api.js"; +import { architectureReport, findCommunities, findDeadCode, findDependencyCycles, findReferences, fleetGraph, fleetSummary, getArchitecture, getCodeSnippet, getGraphSchema, graphSnapshot, queryGraph, searchCode, searchGraph, semanticSearch, searchSymbols, vectorSearch } from "../core/api.js"; export interface DashboardOptions { dbPath?: string; @@ -52,6 +52,15 @@ export async function serveDashboard(options: DashboardOptions): PromiseGraph Preview +
+

Fleet Galaxy

+
Loading indexed project graph.
+ +
+ Project + Dependency + Route + Language + Cross-repo call +
+

Languages

@@ -307,7 +328,11 @@ function dashboardHtml(): string { const cypherQuery = document.querySelector('#cypher-query'); const canvas = document.querySelector('#graph-canvas'); const ctx = canvas.getContext('2d'); + const fleetCanvas = document.querySelector('#fleet-canvas'); + const fleetCtx = fleetCanvas.getContext('2d'); + const fleetGalaxySummary = document.querySelector('#fleet-galaxy-summary'); let graphState = { nodes: [], edges: [] }; + let fleetState = { nodes: [], edges: [] }; function metric(label, value) { return '
' + fmt.format(value) + '' + label + '
'; } function item(text, cls='') { return '
' + text + '
'; } @@ -321,13 +346,14 @@ function dashboardHtml(): string { return url.toString(); } async function load() { - const [arch, schema, graph, dead, communityRows, fleet] = await Promise.all([ + const [arch, schema, graph, dead, communityRows, fleet, fleetGalaxy] = await Promise.all([ fetch('/api/architecture').then(r => r.json()), fetch('/api/schema').then(r => r.json()), fetch('/api/graph?limit=500').then(r => r.json()), fetch('/api/dead-code?limit=8').then(r => r.json()), fetch('/api/communities?limit=8&minSize=4').then(r => r.json()), - fetch('/api/fleet?limit=20').then(r => r.json()) + fetch('/api/fleet?limit=20').then(r => r.json()), + fetch('/api/fleet-graph?limit=20&maxNodes=500&maxEdges=1000').then(r => r.json()) ]); indexed.textContent = 'Indexed ' + new Date(arch.indexedAt).toLocaleString(); metrics.innerHTML = metric('files', arch.totals.files) + metric('symbols', arch.totals.symbols) + metric('edges', arch.totals.edges) + metric('lines', arch.totals.lines); @@ -345,8 +371,12 @@ function dashboardHtml(): string { cycles.innerHTML = arch.dependencyCycles.length ? arch.dependencyCycles.map(c => item('' + escapeHtml(c.clusters.join(' -> ')) + '
' + fmt.format(c.edges) + ' internal cycle edges
' + escapeHtml(c.recommendation) + '
')).join('') : item('No cross-cluster dependency cycles found.'); communities.innerHTML = communityRows.length ? communityRows.map(c => item('' + escapeHtml(c.label) + '
' + fmt.format(c.members) + ' members - cohesion ' + c.cohesion.toFixed(2) + ' - ' + fmt.format(c.internalEdges) + ' internal edges
' + escapeHtml(c.files.slice(0, 4).join(' | ')) + '
' + escapeHtml(c.representativeSymbols.slice(0, 4).map(s => s.name).join(', ')) + '
')).join('') : item('No graph communities found.'); graphState = prepareGraph(graph); + fleetState = prepareFleetGraph(fleetGalaxy); + fleetGalaxySummary.textContent = fmt.format(fleetGalaxy.totals.projectNodes) + ' projects, ' + fmt.format(fleetGalaxy.totals.graphNodes) + ' nodes, ' + fmt.format(fleetGalaxy.totals.graphEdges) + ' edges, ' + fmt.format(fleetGalaxy.totals.crossRepoEdges) + ' cross-repo links.'; resizeGraph(); + resizeFleetGraph(); drawGraph(); + drawFleetGraph(); } async function doSearch() { const q = search.value.trim(); @@ -397,6 +427,12 @@ function dashboardHtml(): string { canvas.height = Math.max(320, Math.floor(rect.height * devicePixelRatio)); ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); } + function resizeFleetGraph() { + const rect = fleetCanvas.getBoundingClientRect(); + fleetCanvas.width = Math.max(320, Math.floor(rect.width * devicePixelRatio)); + fleetCanvas.height = Math.max(320, Math.floor(rect.height * devicePixelRatio)); + fleetCtx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); + } function drawGraph() { const w = canvas.clientWidth, h = canvas.clientHeight; for (let step = 0; step < 2; step += 1) { @@ -425,13 +461,83 @@ function dashboardHtml(): string { } requestAnimationFrame(drawGraph); } + function prepareFleetGraph(graph) { + const nodes = graph.nodes.map(node => ({ ...node, x: 0, y: 0 })); + const byId = new Map(nodes.map(node => [node.id, node])); + const projects = nodes.filter(node => node.group === 'project'); + const cx = 360, cy = 210; + const ring = Math.max(120, Math.min(230, projects.length * 34)); + projects.forEach((node, index) => { + const angle = projects.length === 1 ? -Math.PI / 2 : (Math.PI * 2 * index) / projects.length - Math.PI / 2; + node.x = cx + Math.cos(angle) * ring; + node.y = cy + Math.sin(angle) * ring * 0.62; + }); + const satellites = new Map(); + for (const edge of graph.edges) { + const source = byId.get(edge.source), target = byId.get(edge.target); + if (!source || !target) continue; + const project = source.group === 'project' ? source : target.group === 'project' ? target : null; + const other = project === source ? target : source; + if (project && other.group !== 'project') { + const list = satellites.get(project.id) || []; + if (!list.includes(other)) list.push(other); + satellites.set(project.id, list); + } + } + for (const [projectId, list] of satellites.entries()) { + const project = byId.get(projectId); + if (!project) continue; + list.forEach((node, index) => { + if (node.x || node.y) return; + const angle = (Math.PI * 2 * index) / Math.max(1, list.length); + const radius = node.group === 'language' ? 42 : node.group === 'route' ? 66 : 88; + node.x = project.x + Math.cos(angle) * radius; + node.y = project.y + Math.sin(angle) * radius; + }); + } + nodes.forEach((node, index) => { + if (!node.x && !node.y) { + const angle = (Math.PI * 2 * index) / Math.max(1, nodes.length); + node.x = cx + Math.cos(angle) * 260; + node.y = cy + Math.sin(angle) * 160; + } + }); + return { nodes, edges: graph.edges.map(edge => ({ ...edge, sourceNode: byId.get(edge.source), targetNode: byId.get(edge.target) })).filter(edge => edge.sourceNode && edge.targetNode) }; + } + function drawFleetGraph() { + const w = fleetCanvas.clientWidth, h = fleetCanvas.clientHeight; + fleetCtx.clearRect(0, 0, w, h); + const scaleX = w / 720, scaleY = h / 430; + const edgeColors = { CROSS_REPO_HTTP_CALLS:'#be123c', ROUTE_OVERLAP:'#b45309', SHARES_DEPENDENCY:'#7c3aed', DEPENDS_ON:'#64748b', USES_LANGUAGE:'#0f766e', PROVIDES_ROUTE:'#2563eb', CALLS_ENDPOINT:'#0891b2' }; + for (const edge of fleetState.edges) { + fleetCtx.strokeStyle = edgeColors[edge.type] || 'rgba(71,85,105,.22)'; + fleetCtx.lineWidth = edge.type === 'CROSS_REPO_HTTP_CALLS' ? 2.2 : 1; + fleetCtx.beginPath(); + fleetCtx.moveTo(edge.sourceNode.x * scaleX, edge.sourceNode.y * scaleY); + fleetCtx.lineTo(edge.targetNode.x * scaleX, edge.targetNode.y * scaleY); + fleetCtx.stroke(); + } + const palette = { project:'#111827', language:'#0f766e', dependency:'#6d28d9', route:'#be123c' }; + for (const node of fleetState.nodes) { + const x = node.x * scaleX, y = node.y * scaleY; + fleetCtx.fillStyle = palette[node.group] || '#64748b'; + fleetCtx.beginPath(); + fleetCtx.arc(x, y, node.group === 'project' ? 9 : 5, 0, Math.PI * 2); + fleetCtx.fill(); + if (node.group === 'project') { + fleetCtx.fillStyle = '#1f2937'; + fleetCtx.font = '12px Inter, system-ui, sans-serif'; + fleetCtx.fillText(node.label.slice(0, 28), x + 12, y + 4); + } + } + } search.addEventListener('input', () => { clearTimeout(window.__t); window.__t = setTimeout(doSearch, 120); }); referencesRun.addEventListener('click', doReferences); semanticRun.addEventListener('click', doSemanticSearch); vectorRun.addEventListener('click', doVectorSearch); graphRun.addEventListener('click', doGraphSearch); cypherRun.addEventListener('click', doCypherQuery); - window.addEventListener('resize', resizeGraph); + window.addEventListener('resize', () => { resizeGraph(); resizeFleetGraph(); drawFleetGraph(); }); load().catch(err => { document.body.innerHTML = '
' + escapeHtml(err.stack || err) + '
'; }); diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts index 3a0dec0..d78a9dd 100644 --- a/tests/dashboard.test.ts +++ b/tests/dashboard.test.ts @@ -4,53 +4,76 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { indexRepository } from "../src/core/indexer.js"; +import { runIndex } from "../src/core/api.js"; import { serveDashboard } from "../src/dashboard/server.js"; const fixture = path.join(process.cwd(), "tests", "fixtures", "sample-repo"); test("dashboard serves graph, query, search, and report endpoints", async (t) => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "repolens-dashboard-")); - const dbPath = path.join(tmp, "memory.db"); - await indexRepository({ root: fixture, dbPath }); - - let server: http.Server; + const previousCatalog = process.env.REPOLENS_CATALOG; try { - server = await serveDashboard({ dbPath, port: 0 }); - } catch (error) { - if (isListenPermissionError(error)) { - t.skip("local sandbox does not permit binding a dashboard smoke-test port"); - return; + process.env.REPOLENS_CATALOG = path.join(tmp, "projects.json"); + const dbPath = path.join(tmp, "memory.db"); + await Promise.all([ + runIndex({ root: fixture, dbPath, runLabel: "dashboard-service-a" }), + runIndex({ root: fixture, dbPath: path.join(tmp, "service-b.db"), runLabel: "dashboard-service-b" }) + ]); + + let server: http.Server; + try { + server = await serveDashboard({ dbPath, port: 0 }); + } catch (error) { + if (isListenPermissionError(error)) { + t.skip("local sandbox does not permit binding a dashboard smoke-test port"); + return; + } + throw error; } - throw error; - } - try { - const baseUrl = dashboardUrl(server); - const page = await fetchText(`${baseUrl}/`); - assert.match(page, /RepoLens MCP/); + try { + const baseUrl = dashboardUrl(server); + const page = await fetchText(`${baseUrl}/`); + assert.match(page, /RepoLens MCP/); + assert.match(page, /Fleet Galaxy/); - const schema = await fetchJson<{ totals: { files: number; symbols: number; edges: number } }>(`${baseUrl}/api/schema`); - assert.equal(schema.totals.files, 22); - assert.ok(schema.totals.symbols > 0); - assert.ok(schema.totals.edges > 0); + const schema = await fetchJson<{ totals: { files: number; symbols: number; edges: number } }>(`${baseUrl}/api/schema`); + assert.equal(schema.totals.files, 22); + assert.ok(schema.totals.symbols > 0); + assert.ok(schema.totals.edges > 0); - const graph = await fetchJson<{ nodes: unknown[]; edges: unknown[] }>(`${baseUrl}/api/graph?limit=25`); - assert.ok(graph.nodes.length > 0); - assert.ok(graph.edges.length > 0); + const graph = await fetchJson<{ nodes: unknown[]; edges: unknown[] }>(`${baseUrl}/api/graph?limit=25`); + assert.ok(graph.nodes.length > 0); + assert.ok(graph.edges.length > 0); - const search = await fetchJson<{ code: unknown[]; symbols: Array<{ name: string }> }>(`${baseUrl}/api/search?q=createOrder`); - assert.ok(search.symbols.some((symbol) => symbol.name === "createOrder")); + const fleetGraph = await fetchJson<{ totals: { projectNodes: number; graphNodes: number; crossRepoEdges: number }; nodes: unknown[]; edges: unknown[] }>( + `${baseUrl}/api/fleet-graph?limit=20&maxNodes=200&maxEdges=500` + ); + assert.equal(fleetGraph.totals.projectNodes, 2); + assert.ok(fleetGraph.totals.graphNodes >= 2); + assert.ok(fleetGraph.totals.crossRepoEdges >= 1); + assert.ok(fleetGraph.nodes.length >= 2); + assert.ok(fleetGraph.edges.length >= 1); - const query = await fetchJson<{ rows: Array> }>( - `${baseUrl}/api/query-graph?q=${encodeURIComponent("MATCH (f:Function) RETURN f.name,f.filePath LIMIT 3")}` - ); - assert.ok(query.rows.length > 0); + const search = await fetchJson<{ code: unknown[]; symbols: Array<{ name: string }> }>(`${baseUrl}/api/search?q=createOrder`); + assert.ok(search.symbols.some((symbol) => symbol.name === "createOrder")); - const report = await fetchText(`${baseUrl}/api/report?format=markdown&graphLimit=25`); - assert.match(report, /# RepoLens Architecture Report/); + const query = await fetchJson<{ rows: Array> }>( + `${baseUrl}/api/query-graph?q=${encodeURIComponent("MATCH (f:Function) RETURN f.name,f.filePath LIMIT 3")}` + ); + assert.ok(query.rows.length > 0); + + const report = await fetchText(`${baseUrl}/api/report?format=markdown&graphLimit=25`); + assert.match(report, /# RepoLens Architecture Report/); + } finally { + await closeServer(server); + } } finally { - await closeServer(server); + if (previousCatalog === undefined) { + delete process.env.REPOLENS_CATALOG; + } else { + process.env.REPOLENS_CATALOG = previousCatalog; + } } });