From f5cbcb9d30424b5d88be30adc17c27238ee8d112 Mon Sep 17 00:00:00 2001 From: sahal Date: Tue, 28 Apr 2026 10:52:03 -0700 Subject: [PATCH] chore: prettier + eslint --fix + conflict resolution after rebase onto main --- gitnexus-web/src/App.tsx | 9 +- gitnexus-web/src/components/DropZone.tsx | 8 +- gitnexus-web/src/components/FileTreePanel.tsx | 4 +- gitnexus-web/src/components/GraphCanvas.tsx | 4 +- gitnexus-web/src/components/Header.tsx | 4 +- gitnexus-web/src/components/RepoLanding.tsx | 21 +-- gitnexus-web/src/lib/graph-adapter.ts | 7 +- gitnexus-web/test/unit/group-features.test.ts | 33 ++--- gitnexus/src/cli/group.ts | 4 +- .../group/extractors/code-dep-extractor.ts | 4 +- gitnexus/src/core/group/service.ts | 29 ++-- gitnexus/src/core/group/sync.ts | 2 +- gitnexus/src/mcp/local/local-backend.ts | 22 +-- gitnexus/src/mcp/resources.ts | 5 +- gitnexus/src/mcp/tools.ts | 10 +- gitnexus/src/server/api.ts | 13 +- .../integration/group/auto-discover.test.ts | 21 ++- .../integration/group/code-dep-sync.test.ts | 4 +- .../integration/group/group-graph.test.ts | 86 ++++++++--- gitnexus/test/unit/cli-group.test.ts | 138 ++++++++++++------ gitnexus/test/unit/cli-status.test.ts | 4 +- gitnexus/test/unit/cli-tool.test.ts | 16 +- .../test/unit/group/import-scanner.test.ts | 5 +- package-lock.json | 9 ++ package.json | 3 + 25 files changed, 286 insertions(+), 179 deletions(-) diff --git a/gitnexus-web/src/App.tsx b/gitnexus-web/src/App.tsx index afcdf6525..7279aaf7b 100644 --- a/gitnexus-web/src/App.tsx +++ b/gitnexus-web/src/App.tsx @@ -211,7 +211,14 @@ const AppContent = () => { setProgress(null); }, ERROR_RESET_DELAY_MS); }); - }, [handleServerConnect, setProgress, setViewMode, setServerBaseUrl, setAvailableRepos, connectToGroup]); + }, [ + handleServerConnect, + setProgress, + setViewMode, + setServerBaseUrl, + setAvailableRepos, + connectToGroup, + ]); const handleFocusNode = useCallback((nodeId: string) => { graphCanvasRef.current?.focusNode(nodeId); diff --git a/gitnexus-web/src/components/DropZone.tsx b/gitnexus-web/src/components/DropZone.tsx index ba7101c82..bdf80dedd 100644 --- a/gitnexus-web/src/components/DropZone.tsx +++ b/gitnexus-web/src/components/DropZone.tsx @@ -306,9 +306,11 @@ export const DropZone = ({ onServerConnect, onGroupSelect }: DropZoneProps) => { onGroupSelect(groupName, detectedBackendUrl ?? undefined) - : undefined} + onSelectGroup={ + onGroupSelect + ? (groupName) => onGroupSelect(groupName, detectedBackendUrl ?? undefined) + : undefined + } onAnalyzeComplete={connectToRepo} /> )} diff --git a/gitnexus-web/src/components/FileTreePanel.tsx b/gitnexus-web/src/components/FileTreePanel.tsx index 54fee08e2..bd592a60e 100644 --- a/gitnexus-web/src/components/FileTreePanel.tsx +++ b/gitnexus-web/src/components/FileTreePanel.tsx @@ -157,7 +157,9 @@ const TreeItem = ({ )} {/* Name */} - + {node.name} diff --git a/gitnexus-web/src/components/GraphCanvas.tsx b/gitnexus-web/src/components/GraphCanvas.tsx index eb11ad771..811c5f718 100644 --- a/gitnexus-web/src/components/GraphCanvas.tsx +++ b/gitnexus-web/src/components/GraphCanvas.tsx @@ -103,7 +103,9 @@ export const GraphCanvas = forwardRef((_, ref) => { } const node = nodeById.get(nodeId); setHoveredNodeName(node ? node.properties.name : null); - const repo = node ? (node.properties as Record)._repo as string | undefined : undefined; + const repo = node + ? ((node.properties as Record)._repo as string | undefined) + : undefined; setHoveredNodeRepo(repo ?? null); }, [graph, nodeById], diff --git a/gitnexus-web/src/components/Header.tsx b/gitnexus-web/src/components/Header.tsx index 7f611cf33..9e18e1700 100644 --- a/gitnexus-web/src/components/Header.tsx +++ b/gitnexus-web/src/components/Header.tsx @@ -228,7 +228,9 @@ export const Header = ({ {g} {g === projectName && ( - active + + active + )} )) diff --git a/gitnexus-web/src/components/RepoLanding.tsx b/gitnexus-web/src/components/RepoLanding.tsx index 9b47c7ffb..0ae0636c7 100644 --- a/gitnexus-web/src/components/RepoLanding.tsx +++ b/gitnexus-web/src/components/RepoLanding.tsx @@ -15,7 +15,12 @@ import { Sparkles, ArrowRight, GitBranch, FileCode, Layers, FolderOpen } from '@/lib/lucide-icons'; import { useState, useEffect } from 'react'; import { RepoAnalyzer } from './RepoAnalyzer'; -import { fetchGroups, fetchGroupStatus, type BackendRepo, type GroupStatus } from '../services/backend-client'; +import { + fetchGroups, + fetchGroupStatus, + type BackendRepo, + type GroupStatus, +} from '../services/backend-client'; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -113,13 +118,9 @@ function GroupCard({
- {repoCount > 0 && ( - {repoCount} repos - )} + {repoCount > 0 && {repoCount} repos} {lastSync && ( - - synced {formatRelativeTime(lastSync)} - + synced {formatRelativeTime(lastSync)} )}
@@ -172,7 +173,7 @@ export const RepoLanding = ({ }, [activeTab, groups, groupStatuses]); return ( -
+
{/* Ambient glows */}
@@ -255,7 +256,9 @@ export const RepoLanding = ({

No groups configured yet.

- Run gitnexus group auto-discover to create one. + Run{' '} + gitnexus group auto-discover{' '} + to create one.

) : ( diff --git a/gitnexus-web/src/lib/graph-adapter.ts b/gitnexus-web/src/lib/graph-adapter.ts index d45cf6b4c..3c512bba9 100644 --- a/gitnexus-web/src/lib/graph-adapter.ts +++ b/gitnexus-web/src/lib/graph-adapter.ts @@ -184,7 +184,7 @@ export const knowledgeGraphToGraphology = ( const nodePositions = new Map(); // Helper to get repo name from a node - const getNodeRepo = (node: typeof knowledgeGraph.nodes[0]): string | undefined => { + const getNodeRepo = (node: (typeof knowledgeGraph.nodes)[0]): string | undefined => { const repo = (node.properties as Record)._repo as string | undefined; if (repo) return repo; const sep = node.id.indexOf('::'); @@ -217,9 +217,8 @@ export const knowledgeGraphToGraphology = ( const scaledSize = getScaledNodeSize(baseSize, nodeCount); // In multi-repo mode, color File/Folder nodes by repo for visual distinction - const structColor = isMultiRepo && repoCenter - ? repoCenter.color - : NODE_COLORS[node.label] || '#9ca3af'; + const structColor = + isMultiRepo && repoCenter ? repoCenter.color : NODE_COLORS[node.label] || '#9ca3af'; graph.addNode(node.id, { x, diff --git a/gitnexus-web/test/unit/group-features.test.ts b/gitnexus-web/test/unit/group-features.test.ts index cac766cc3..76f3dbf30 100644 --- a/gitnexus-web/test/unit/group-features.test.ts +++ b/gitnexus-web/test/unit/group-features.test.ts @@ -11,10 +11,7 @@ import { getRepoColor, type EdgeType, } from '../../src/lib/constants'; -import { - knowledgeGraphToGraphology, - type SigmaNodeAttributes, -} from '../../src/lib/graph-adapter'; +import { knowledgeGraphToGraphology, type SigmaNodeAttributes } from '../../src/lib/graph-adapter'; import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; import type { KnowledgeGraph } from '../../src/core/graph/types'; @@ -65,7 +62,12 @@ describe('getRepoColor', () => { // ── Graph adapter tests ──────────────────────────────────────────────────── -function makeNode(id: string, label: string, name: string, props?: Record): GraphNode { +function makeNode( + id: string, + label: string, + name: string, + props?: Record, +): GraphNode { return { id, label: label as GraphNode['label'], @@ -92,8 +94,12 @@ function makeGraph(nodes: GraphNode[], relationships: GraphRelationship[]): Know return { nodes, relationships, - get nodeCount() { return nodes.length; }, - get relationshipCount() { return relationships.length; }, + get nodeCount() { + return nodes.length; + }, + get relationshipCount() { + return relationships.length; + }, addNode: () => {}, addRelationship: () => {}, }; @@ -144,9 +150,7 @@ describe('knowledgeGraphToGraphology - multi-repo support', () => { makeNode('repoA::fn1', 'Function', 'funcA', { _repo: 'repoA' }), makeNode('repoB::fn1', 'Function', 'funcB', { _repo: 'repoB' }), ]; - const rels = [ - makeRel('repoB::fn1', 'repoA::fn1', 'CROSS_REPO_IMPORT'), - ]; + const rels = [makeRel('repoB::fn1', 'repoA::fn1', 'CROSS_REPO_IMPORT')]; const graph = knowledgeGraphToGraphology(makeGraph(nodes, rels)); // Edge should exist @@ -173,10 +177,7 @@ describe('knowledgeGraphToGraphology - multi-repo support', () => { }); it('works correctly in single-repo mode (no repoName assigned)', () => { - const nodes = [ - makeNode('fn1', 'Function', 'funcA'), - makeNode('fn2', 'Function', 'funcB'), - ]; + const nodes = [makeNode('fn1', 'Function', 'funcA'), makeNode('fn2', 'Function', 'funcB')]; const graph = knowledgeGraphToGraphology(makeGraph(nodes, [])); const attr = graph.getNodeAttributes('fn1') as SigmaNodeAttributes; @@ -195,9 +196,7 @@ describe('knowledgeGraphToGraphology - multi-repo support', () => { const posB = graph.getNodeAttributes('repoB::folder1') as SigmaNodeAttributes; // Different repos should be positioned in different regions - const distance = Math.sqrt( - Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2), - ); + const distance = Math.sqrt(Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2)); expect(distance).toBeGreaterThan(0); }); }); diff --git a/gitnexus/src/cli/group.ts b/gitnexus/src/cli/group.ts index 13a70a11f..cb666a949 100644 --- a/gitnexus/src/cli/group.ts +++ b/gitnexus/src/cli/group.ts @@ -58,9 +58,7 @@ export function registerGroupCommands(program: Command): void { } } if (result.synced) { - console.log( - `\nSync: ${result.contracts} contracts, ${result.crossLinks} cross-links`, - ); + console.log(`\nSync: ${result.contracts} contracts, ${result.crossLinks} cross-links`); } } } diff --git a/gitnexus/src/core/group/extractors/code-dep-extractor.ts b/gitnexus/src/core/group/extractors/code-dep-extractor.ts index 1ce7e19bc..1166d0a15 100644 --- a/gitnexus/src/core/group/extractors/code-dep-extractor.ts +++ b/gitnexus/src/core/group/extractors/code-dep-extractor.ts @@ -47,9 +47,7 @@ export class CodeDepExtractor implements ContractExtractor { /** * Extract provider contracts: exported symbols from this repo that other repos can import. */ - private async extractProviders( - dbExecutor: CypherExecutor | null, - ): Promise { + private async extractProviders(dbExecutor: CypherExecutor | null): Promise { if (!this.ownPackageName || !dbExecutor) return []; let rows: Record[]; diff --git a/gitnexus/src/core/group/service.ts b/gitnexus/src/core/group/service.ts index c371ff41c..e482974a9 100644 --- a/gitnexus/src/core/group/service.ts +++ b/gitnexus/src/core/group/service.ts @@ -51,6 +51,7 @@ export interface GroupToolPort { include_content?: boolean; }, ): Promise; + impact(repo: GroupRepoHandle, params: Record): Promise; impactByUid( repoId: string, uid: string, @@ -519,8 +520,7 @@ export class GroupService { const symbol = String(params.symbol ?? '').trim(); const repoParam = typeof params.repo === 'string' ? params.repo.trim() : undefined; const depth = typeof params.depth === 'number' ? Math.min(params.depth, 2) : 1; - const direction = - typeof params.direction === 'string' ? params.direction : 'both'; + const direction = typeof params.direction === 'string' ? params.direction : 'both'; if (!name || !symbol) return { error: 'name and symbol are required' }; @@ -579,23 +579,22 @@ export class GroupService { // Find cross-repo connections via CrossLinks const crossConnections: Array<{ direction: 'outgoing' | 'incoming'; - link: typeof registry.crossLinks[0]; + link: (typeof registry.crossLinks)[0]; remoteRepo: string; remoteContext: unknown; }> = []; const visited = new Set([sourceRepo.name]); - const findConnections = async ( - repoName: string, - currentDepth: number, - ): Promise => { + const findConnections = async (repoName: string, currentDepth: number): Promise => { if (currentDepth > depth) return; for (const link of registry.crossLinks) { - const isFrom = link.from.repo === repoName || + const isFrom = + link.from.repo === repoName || Object.entries(config.repos).some(([gp, rn]) => gp === link.from.repo && rn === repoName); - const isTo = link.to.repo === repoName || + const isTo = + link.to.repo === repoName || Object.entries(config.repos).some(([gp, rn]) => gp === link.to.repo && rn === repoName); let remoteRepoGroupPath: string | null = null; @@ -646,8 +645,9 @@ export class GroupService { }; // Find connections from source repo - const sourceGroupPath = Object.entries(config.repos) - .find(([, rn]) => rn === sourceRepo!.name)?.[0] || sourceRepo.name; + const sourceGroupPath = + Object.entries(config.repos).find(([, rn]) => rn === sourceRepo!.name)?.[0] || + sourceRepo.name; await findConnections(sourceGroupPath, 1); return { @@ -680,7 +680,8 @@ export class GroupService { const force = Boolean(params.force); const skipSync = Boolean(params.skipSync); - if (!directory && repoPaths.length === 0) return { error: 'directory or repoPaths is required' }; + if (!directory && repoPaths.length === 0) + return { error: 'directory or repoPaths is required' }; const repos: Record = {}; const packages: Record> = {}; @@ -704,7 +705,9 @@ export class GroupService { // Not indexed } if (!metaExists) { - return { error: `Repo at ${resolvedPath} is not indexed. Run 'gitnexus analyze' there first.` }; + return { + error: `Repo at ${resolvedPath} is not indexed. Run 'gitnexus analyze' there first.`, + }; } const dirName = path.basename(resolvedPath); diff --git a/gitnexus/src/core/group/sync.ts b/gitnexus/src/core/group/sync.ts index 20cf09650..dc01f9539 100644 --- a/gitnexus/src/core/group/sync.ts +++ b/gitnexus/src/core/group/sync.ts @@ -100,7 +100,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis const openPoolIds: string[] = []; // Build package map for code-dep extraction (auto-discover when config.packages is empty) - let packageMap = new Map(); + const packageMap = new Map(); if (config.detect.shared_libs) { const hasExplicitPackages = Object.keys(config.packages).length > 0 && diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 71e74a524..2ba17a09f 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -28,7 +28,6 @@ import { type RegistryEntry, } from '../../storage/repo-manager.js'; import { GroupService, type GroupToolPort } from '../../core/group/service.js'; -import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js'; import { collectBestChunks } from '../../core/embeddings/types.js'; import { rankExactEmbeddingRows, @@ -231,6 +230,7 @@ export class LocalBackend { const port: GroupToolPort = { resolveRepo: (p) => this.resolveRepo(p), query: (r, p) => this.query(r as RepoHandle, p), + impact: (r, p) => this.impact(r as RepoHandle, p as Parameters[1]), impactByUid: (id, uid, d, o) => this.impactByUid(id, uid, d, o), context: (r, p) => this.context(r as RepoHandle, p), }; @@ -639,15 +639,6 @@ export class LocalBackend { return this.handleGroupTool(method, params || {}); } - const p = params && typeof params === 'object' ? (params as Record) : {}; - if ( - (method === 'impact' || method === 'query' || method === 'context') && - typeof p.repo === 'string' && - p.repo.startsWith('@') - ) { - return this.callToolAtGroupRepo(method, p); - } - // Resolve repo from optional param (re-reads registry on miss) const repo = await this.resolveRepo((params as { repo?: string } | undefined)?.repo); @@ -3061,14 +3052,9 @@ export class LocalBackend { return this.getGroupService().groupQuery(params); } - private static formatGroupResourcePayload(raw: unknown): string { - if (raw && typeof raw === 'object' && 'error' in raw) { - const err = (raw as { error?: unknown }).error; - if (typeof err === 'string' && err.length > 0) { - return `error: ${err}`; - } - } - return JSON.stringify(raw, null, 2); + private async groupStatus(params: Record): Promise { + await this.refreshRepos(); + return this.getGroupService().groupStatus(params); } private async groupGraph(params: Record): Promise { diff --git a/gitnexus/src/mcp/resources.ts b/gitnexus/src/mcp/resources.ts index 88e7a99cc..b07ac4295 100644 --- a/gitnexus/src/mcp/resources.ts +++ b/gitnexus/src/mcp/resources.ts @@ -238,10 +238,7 @@ export async function readResource(uri: string, backend: LocalBackend): Promise< } if (parsed.kind === 'group') { - if (parsed.resourceType === 'contracts') { - return backend.readGroupContractsResource(parsed.groupName, parsed.contractsFilter); - } - return backend.readGroupStatusResource(parsed.groupName); + return 'error: group resources are not supported in this build; use repo "@" syntax instead'; } const repoName = parsed.repoName; diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index 255948c0b..c0dc71e12 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -563,7 +563,10 @@ WHEN TO USE: After group_sync, to explore how a symbol in one repo connects to s properties: { name: { type: 'string', description: 'Group name' }, symbol: { type: 'string', description: 'Symbol name to search for' }, - repo: { type: 'string', description: 'Repo containing the symbol (optional, searches all if omitted)' }, + repo: { + type: 'string', + description: 'Repo containing the symbol (optional, searches all if omitted)', + }, depth: { type: 'number', description: 'Cross-repo traversal depth (default: 1, max: 2)' }, direction: { type: 'string', @@ -582,7 +585,10 @@ WHEN TO USE: When a user wants to see a combined knowledge graph from multiple i inputSchema: { type: 'object', properties: { - directory: { type: 'string', description: 'Parent directory containing repos (alternative to repoPaths)' }, + directory: { + type: 'string', + description: 'Parent directory containing repos (alternative to repoPaths)', + }, repoPaths: { type: 'array', items: { type: 'string' }, diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index 175423a8c..fd5995688 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -746,7 +746,12 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => const allNodes: GraphNode[] = []; const allRelationships: GraphRelationship[] = []; - const repoSummaries: Array<{ name: string; groupPath: string; nodeCount: number; edgeCount: number }> = []; + const repoSummaries: Array<{ + name: string; + groupPath: string; + nodeCount: number; + edgeCount: number; + }> = []; // Load graph from each repo and namespace node IDs for (const [groupPath, registryName] of Object.entries(config.repos)) { @@ -794,10 +799,8 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => // Add cross-repo edges from contracts.json if (registry) { for (const link of registry.crossLinks) { - const fromRepoName = - config.repos[link.from.repo] || link.from.repo; - const toRepoName = - config.repos[link.to.repo] || link.to.repo; + const fromRepoName = config.repos[link.from.repo] || link.from.repo; + const toRepoName = config.repos[link.to.repo] || link.to.repo; allRelationships.push({ id: `cross::${link.contractId}::${fromRepoName}::${toRepoName}`, diff --git a/gitnexus/test/integration/group/auto-discover.test.ts b/gitnexus/test/integration/group/auto-discover.test.ts index 2a9cb2c19..87515197a 100644 --- a/gitnexus/test/integration/group/auto-discover.test.ts +++ b/gitnexus/test/integration/group/auto-discover.test.ts @@ -8,7 +8,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; -import { GroupService, type GroupToolPort, type GroupRepoHandle } from '../../../src/core/group/service.js'; +import { + GroupService, + type GroupToolPort, + type GroupRepoHandle, +} from '../../../src/core/group/service.js'; import { listGroups } from '../../../src/core/group/storage.js'; describe('Group auto-discover integration', () => { @@ -97,7 +101,10 @@ console.log(formatDate(new Date()));`, } // Try matching by path for (const handle of Object.values(repoHandles)) { - if (nameOrPath && (handle.repoPath === nameOrPath || handle.repoPath.includes(nameOrPath))) { + if ( + nameOrPath && + (handle.repoPath === nameOrPath || handle.repoPath.includes(nameOrPath)) + ) { return handle; } } @@ -180,7 +187,10 @@ console.log(formatDate(new Date()));`, // Add a non-indexed directory const nonIndexed = path.join(tmpDir, 'repos', 'not-indexed'); fs.mkdirSync(nonIndexed, { recursive: true }); - fs.writeFileSync(path.join(nonIndexed, 'package.json'), JSON.stringify({ name: 'not-indexed' })); + fs.writeFileSync( + path.join(nonIndexed, 'package.json'), + JSON.stringify({ name: 'not-indexed' }), + ); // No .gitnexus/meta.json const service = new GroupService(makeMockPort()); @@ -225,10 +235,7 @@ console.log(formatDate(new Date()));`, it('returns an error when one of the explicit repoPaths is not indexed', async () => { const unindexed = path.join(tmpDir, 'repos', 'unindexed'); fs.mkdirSync(unindexed, { recursive: true }); - fs.writeFileSync( - path.join(unindexed, 'package.json'), - JSON.stringify({ name: 'unindexed' }), - ); + fs.writeFileSync(path.join(unindexed, 'package.json'), JSON.stringify({ name: 'unindexed' })); const service = new GroupService(makeMockPort()); const result = (await service.groupDiscover({ diff --git a/gitnexus/test/integration/group/code-dep-sync.test.ts b/gitnexus/test/integration/group/code-dep-sync.test.ts index 0a8499209..d3f0aba9c 100644 --- a/gitnexus/test/integration/group/code-dep-sync.test.ts +++ b/gitnexus/test/integration/group/code-dep-sync.test.ts @@ -185,9 +185,7 @@ export function getVersion() { return VERSION; }`, } // Verify from/to directions - const formatDateLink = result.crossLinks.find((l) => - l.contractId.includes('formatDate'), - ); + const formatDateLink = result.crossLinks.find((l) => l.contractId.includes('formatDate')); expect(formatDateLink).toBeDefined(); expect(formatDateLink!.from.repo).toBe('apps/web'); // consumer expect(formatDateLink!.to.repo).toBe('libs/shared'); // provider diff --git a/gitnexus/test/integration/group/group-graph.test.ts b/gitnexus/test/integration/group/group-graph.test.ts index d855626ae..3fd69f201 100644 --- a/gitnexus/test/integration/group/group-graph.test.ts +++ b/gitnexus/test/integration/group/group-graph.test.ts @@ -133,7 +133,13 @@ describe('Group graph traversal integration', () => { repos: { 'libs/shared': 'shared-utils', 'apps/web': 'web-app' }, links: [], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: true, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: true, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }; fs.writeFileSync(path.join(groupDir, 'group.yaml'), yaml.dump(config), 'utf-8'); @@ -154,19 +160,27 @@ describe('Group graph traversal integration', () => { return { resolveRepo: async (nameOrPath?: string): Promise => { if (nameOrPath === 'shared-utils') { - return { id: 'shared-utils', name: 'shared-utils', repoPath: '/mock/shared', storagePath: '/mock/shared/.gitnexus' }; + return { + id: 'shared-utils', + name: 'shared-utils', + repoPath: '/mock/shared', + storagePath: '/mock/shared/.gitnexus', + }; } if (nameOrPath === 'web-app') { - return { id: 'web-app', name: 'web-app', repoPath: '/mock/web', storagePath: '/mock/web/.gitnexus' }; + return { + id: 'web-app', + name: 'web-app', + repoPath: '/mock/web', + storagePath: '/mock/web/.gitnexus', + }; } throw new Error(`Repo not found: ${nameOrPath}`); }, query: async (_repo, params) => { const queryText = (params as { query: string }).query; return { - processes: [ - { name: `process-${queryText}`, summary: `Mock process for ${queryText}` }, - ], + processes: [{ name: `process-${queryText}`, summary: `Mock process for ${queryText}` }], }; }, }; @@ -195,9 +209,7 @@ describe('Group graph traversal integration', () => { // shared-utils is the provider; apps/web is the consumer // Direction from shared-utils perspective: incoming (apps/web imports from us) - const conn = result.crossConnections.find((c) => - c.contractId.includes('formatDate'), - ); + const conn = result.crossConnections.find((c) => c.contractId.includes('formatDate')); expect(conn).toBeDefined(); expect(conn!.contractType).toBe('lib'); expect(conn!.confidence).toBe(1.0); @@ -269,10 +281,7 @@ describe('Group graph traversal integration', () => { }); describe('depth traversal', () => { - async function writeFixture( - repos: Record, - registry: ContractRegistry, - ) { + async function writeFixture(repos: Record, registry: ContractRegistry) { const { createRequire } = await import('node:module'); const _require = createRequire(import.meta.url); const yaml = _require('js-yaml') as typeof import('js-yaml'); @@ -303,9 +312,7 @@ describe('Group graph traversal integration', () => { return handles[name]; }, query: async (_repo, params) => ({ - processes: [ - { name: `proc-${(params as { query: string }).query}`, summary: 'mock' }, - ], + processes: [{ name: `proc-${(params as { query: string }).query}`, summary: 'mock' }], }), }; } @@ -364,9 +371,24 @@ describe('Group graph traversal integration', () => { }; const chainHandles: Record = { - 'web-app': { id: 'web-app', name: 'web-app', repoPath: '/mock/web', storagePath: '/mock/web/.gitnexus' }, - 'shared-utils': { id: 'shared-utils', name: 'shared-utils', repoPath: '/mock/shared', storagePath: '/mock/shared/.gitnexus' }, - 'deep-lib': { id: 'deep-lib', name: 'deep-lib', repoPath: '/mock/deep', storagePath: '/mock/deep/.gitnexus' }, + 'web-app': { + id: 'web-app', + name: 'web-app', + repoPath: '/mock/web', + storagePath: '/mock/web/.gitnexus', + }, + 'shared-utils': { + id: 'shared-utils', + name: 'shared-utils', + repoPath: '/mock/shared', + storagePath: '/mock/shared/.gitnexus', + }, + 'deep-lib': { + id: 'deep-lib', + name: 'deep-lib', + repoPath: '/mock/deep', + storagePath: '/mock/deep/.gitnexus', + }, }; beforeEach(async () => { @@ -431,7 +453,11 @@ describe('Group graph traversal integration', () => { contracts: [], crossLinks: [ { - from: { repo: 'apps/a', symbolUid: 'uid-a', symbolRef: { filePath: 'a.ts', name: 'a' } }, + from: { + repo: 'apps/a', + symbolUid: 'uid-a', + symbolRef: { filePath: 'a.ts', name: 'a' }, + }, to: { repo: 'apps/b', symbolUid: 'uid-b', symbolRef: { filePath: 'b.ts', name: 'b' } }, type: 'lib', contractId: 'lib::a-to-b', @@ -439,7 +465,11 @@ describe('Group graph traversal integration', () => { confidence: 1.0, }, { - from: { repo: 'apps/b', symbolUid: 'uid-b', symbolRef: { filePath: 'b.ts', name: 'b' } }, + from: { + repo: 'apps/b', + symbolUid: 'uid-b', + symbolRef: { filePath: 'b.ts', name: 'b' }, + }, to: { repo: 'apps/a', symbolUid: 'uid-a', symbolRef: { filePath: 'a.ts', name: 'a' } }, type: 'lib', contractId: 'lib::b-to-a', @@ -452,8 +482,18 @@ describe('Group graph traversal integration', () => { await writeFixture({ 'apps/a': 'repo-a', 'apps/b': 'repo-b' }, cycleRegistry); const cyclePort = makeHandlePort({ - 'repo-a': { id: 'repo-a', name: 'repo-a', repoPath: '/mock/a', storagePath: '/mock/a/.gitnexus' }, - 'repo-b': { id: 'repo-b', name: 'repo-b', repoPath: '/mock/b', storagePath: '/mock/b/.gitnexus' }, + 'repo-a': { + id: 'repo-a', + name: 'repo-a', + repoPath: '/mock/a', + storagePath: '/mock/a/.gitnexus', + }, + 'repo-b': { + id: 'repo-b', + name: 'repo-b', + repoPath: '/mock/b', + storagePath: '/mock/b/.gitnexus', + }, }); const service = new GroupService(cyclePort); diff --git a/gitnexus/test/unit/cli-group.test.ts b/gitnexus/test/unit/cli-group.test.ts index 72814be81..aa85314a3 100644 --- a/gitnexus/test/unit/cli-group.test.ts +++ b/gitnexus/test/unit/cli-group.test.ts @@ -82,9 +82,7 @@ describe('group CLI', () => { await runCommand(['group', 'create', 'team-a']); expect(mockCreateGroupDir).toHaveBeenCalledWith('/home/.gitnexus', 'team-a', undefined); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('Created group "team-a"'), - ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Created group "team-a"')); }); it('forwards --force to createGroupDir', async () => { @@ -125,20 +123,24 @@ describe('group CLI', () => { expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Discovering indexed repos')); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Repos (2)')); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('shared-utils (@test/shared)')); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('5 contracts, 3 cross-links'), - ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('5 contracts, 3 cross-links')); expect(mockBackend.dispose).toHaveBeenCalled(); }); it('prints JSON when --json is set', async () => { - const payload = { group: 'workspace', groupDir: '/g', repoCount: 0, repos: [], packageMappings: {} }; + const payload = { + group: 'workspace', + groupDir: '/g', + repoCount: 0, + repos: [], + packageMappings: {}, + }; mockGroupService.groupDiscover.mockResolvedValueOnce(payload); await runCommand(['group', 'auto-discover', '/repos', '--json']); - const jsonCall = logSpy.mock.calls.find((c) => - typeof c[0] === 'string' && c[0].trim().startsWith('{'), + const jsonCall = logSpy.mock.calls.find( + (c) => typeof c[0] === 'string' && c[0].trim().startsWith('{'), ); expect(jsonCall).toBeDefined(); expect(JSON.parse(jsonCall![0] as string)).toEqual(payload); @@ -189,7 +191,13 @@ describe('group CLI', () => { repos: {}, links: [], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: false, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }); @@ -214,7 +222,13 @@ describe('group CLI', () => { repos: { 'apps/web': 'web-app', 'libs/shared': 'shared-utils' }, links: [], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: false, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }); @@ -234,15 +248,19 @@ describe('group CLI', () => { repos: { 'libs/shared': 'shared-utils' }, links: [], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: false, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }); await runCommand(['group', 'remove', 'team-a', 'apps/web']); - expect(errSpy).toHaveBeenCalledWith( - expect.stringContaining('not found in group "team-a"'), - ); + expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('not found in group "team-a"')); expect(process.exitCode).toBe(1); expect(mockFsWriteFile).not.toHaveBeenCalled(); }); @@ -264,9 +282,7 @@ describe('group CLI', () => { await runCommand(['group', 'list']); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('No groups configured'), - ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No groups configured')); }); it('with name: prints repos and manifest links', async () => { @@ -276,10 +292,22 @@ describe('group CLI', () => { description: 'A team group', repos: { 'apps/web': 'web-app' }, links: [ - { from: 'apps/web', to: 'services/api', type: 'http', contract: 'GET::/x', role: 'consumer' }, + { + from: 'apps/web', + to: 'services/api', + type: 'http', + contract: 'GET::/x', + role: 'consumer', + }, ], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: false, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }); @@ -306,8 +334,18 @@ describe('group CLI', () => { }); mockGroupService.groupStatus.mockResolvedValueOnce({ repos: { - 'apps/web': { indexStale: false, contractsStale: false, missing: false, commitsBehind: 0 }, - 'libs/shared': { indexStale: true, contractsStale: true, missing: false, commitsBehind: 3 }, + 'apps/web': { + indexStale: false, + contractsStale: false, + missing: false, + commitsBehind: 0, + }, + 'libs/shared': { + indexStale: true, + contractsStale: true, + missing: false, + commitsBehind: 3, + }, 'legacy/old': { indexStale: false, contractsStale: false, missing: true }, }, missingRepos: ['legacy/old'], @@ -345,16 +383,18 @@ describe('group CLI', () => { repos: { 'apps/web': 'web-app' }, links: [], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: false, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }); mockSyncGroup.mockResolvedValueOnce({ contracts: [{ id: 1 }, { id: 2 }], - crossLinks: [ - { matchType: 'exact' }, - { matchType: 'exact' }, - { matchType: 'manifest' }, - ], + crossLinks: [{ matchType: 'exact' }, { matchType: 'exact' }, { matchType: 'manifest' }], unmatched: [{ id: 3 }], missingRepos: [], repoSnapshots: {}, @@ -381,7 +421,13 @@ describe('group CLI', () => { repos: {}, links: [], packages: {}, - detect: { http: false, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + detect: { + http: false, + grpc: false, + topics: false, + shared_libs: false, + embedding_fallback: false, + }, matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, }); const result = { @@ -395,8 +441,8 @@ describe('group CLI', () => { await runCommand(['group', 'sync', 'team-a', '--json']); - const jsonCall = logSpy.mock.calls.find((c) => - typeof c[0] === 'string' && c[0].trim().startsWith('{'), + const jsonCall = logSpy.mock.calls.find( + (c) => typeof c[0] === 'string' && c[0].trim().startsWith('{'), ); expect(jsonCall).toBeDefined(); }); @@ -427,10 +473,16 @@ describe('group CLI', () => { }); await runCommand([ - 'group', 'graph', 'team-a', 'mySymbol', - '--repo', 'web-app', - '--depth', '2', - '--direction', 'both', + 'group', + 'graph', + 'team-a', + 'mySymbol', + '--repo', + 'web-app', + '--depth', + '2', + '--direction', + 'both', ]); expect(mockGroupService.groupGraph).toHaveBeenCalledWith({ @@ -493,7 +545,10 @@ describe('group CLI', () => { { summary: 'flow A', _repo: 'apps/web', _rrf_score: 0.0164 }, { name: 'flow B', _repo: 'libs/shared', _rrf_score: 0.0161 }, ], - per_repo: [{ repo: 'apps/web', count: 1 }, { repo: 'libs/shared', count: 1 }], + per_repo: [ + { repo: 'apps/web', count: 1 }, + { repo: 'libs/shared', count: 1 }, + ], }); await runCommand(['group', 'query', 'team-a', 'auth', '--limit', '10', '--subgroup', 'apps']); @@ -513,9 +568,7 @@ describe('group CLI', () => { await runCommand(['group', 'query', 'team-a', 'nothing']); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('No matching execution flows'), - ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No matching execution flows')); }); }); @@ -563,9 +616,12 @@ describe('group CLI', () => { mockGroupService.groupContracts.mockResolvedValueOnce({ contracts: [], crossLinks: [] }); await runCommand([ - 'group', 'contracts', 'team-a', + 'group', + 'contracts', + 'team-a', '--unmatched', - '--repo', 'apps/web', + '--repo', + 'apps/web', '--json', ]); diff --git a/gitnexus/test/unit/cli-status.test.ts b/gitnexus/test/unit/cli-status.test.ts index e1a800af3..e23bd1e1b 100644 --- a/gitnexus/test/unit/cli-status.test.ts +++ b/gitnexus/test/unit/cli-status.test.ts @@ -47,9 +47,7 @@ describe('statusCommand', () => { const { statusCommand } = await import('../../src/cli/status.js'); await statusCommand(); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('stale KuzuDB index'), - ); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('stale KuzuDB index')); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('rebuilds the index')); }); diff --git a/gitnexus/test/unit/cli-tool.test.ts b/gitnexus/test/unit/cli-tool.test.ts index de5c0d82e..83011cf47 100644 --- a/gitnexus/test/unit/cli-tool.test.ts +++ b/gitnexus/test/unit/cli-tool.test.ts @@ -79,9 +79,7 @@ describe('CLI tool commands', () => { it('errors when both name and uid are empty', async () => { const { contextCommand } = await import('../../src/cli/tool.js'); await expect(contextCommand('')).rejects.toThrow('__exit__'); - expect(errSpy).toHaveBeenCalledWith( - expect.stringContaining('gitnexus context'), - ); + expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('gitnexus context')); }); it('accepts uid without a name', async () => { @@ -117,9 +115,7 @@ describe('CLI tool commands', () => { it('errors when target is empty', async () => { const { impactCommand } = await import('../../src/cli/tool.js'); await expect(impactCommand('')).rejects.toThrow('__exit__'); - expect(errSpy).toHaveBeenCalledWith( - expect.stringContaining('gitnexus impact'), - ); + expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('gitnexus impact')); }); it('defaults direction to upstream and passes depth as integer', async () => { @@ -168,9 +164,7 @@ describe('CLI tool commands', () => { it('errors when query is empty', async () => { const { cypherCommand } = await import('../../src/cli/tool.js'); await expect(cypherCommand('')).rejects.toThrow('__exit__'); - expect(errSpy).toHaveBeenCalledWith( - expect.stringContaining('gitnexus cypher'), - ); + expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('gitnexus cypher')); }); it('forwards --repo option', async () => { @@ -192,9 +186,7 @@ describe('CLI tool commands', () => { const { queryCommand } = await import('../../src/cli/tool.js'); await expect(queryCommand('x')).rejects.toThrow('__exit__'); - expect(errSpy).toHaveBeenCalledWith( - expect.stringContaining('No indexed repositories found'), - ); + expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('No indexed repositories found')); }); it('reuses the same backend across calls', async () => { diff --git a/gitnexus/test/unit/group/import-scanner.test.ts b/gitnexus/test/unit/group/import-scanner.test.ts index f38e4d9a6..26b001bb6 100644 --- a/gitnexus/test/unit/group/import-scanner.test.ts +++ b/gitnexus/test/unit/group/import-scanner.test.ts @@ -220,10 +220,7 @@ import { Logger } from '@acme/shared/logging'; const results = await scanRepoForImports(tmpDir, targets); expect(results).toHaveLength(2); - expect(results.map((r) => r.filePath).sort()).toEqual([ - 'src/app.ts', - 'src/utils/helper.ts', - ]); + expect(results.map((r) => r.filePath).sort()).toEqual(['src/app.ts', 'src/utils/helper.ts']); }); it('ignores node_modules', async () => { diff --git a/package-lock.json b/package-lock.json index 65c0870ed..892d4e4fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "gitnexus-monorepo", + "dependencies": { + "jsonc-parser": "^3.3.1" + }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", @@ -1927,6 +1930,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index f2ac47785..31a7911ec 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "prettier --write" ], "*.{js,jsx,mjs,json,css,yml,yaml}": "prettier --write" + }, + "dependencies": { + "jsonc-parser": "^3.3.1" } }