chore: prettier + eslint --fix + conflict resolution after rebase onto main

This commit is contained in:
sahal 2026-04-28 10:52:03 -07:00
parent 1cc6888e19
commit f5cbcb9d30
25 changed files with 286 additions and 179 deletions

View file

@ -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);

View file

@ -306,9 +306,11 @@ export const DropZone = ({ onServerConnect, onGroupSelect }: DropZoneProps) => {
<RepoLanding
repos={detectedRepos}
onSelectRepo={connectToRepo}
onSelectGroup={onGroupSelect
? (groupName) => onGroupSelect(groupName, detectedBackendUrl ?? undefined)
: undefined}
onSelectGroup={
onGroupSelect
? (groupName) => onGroupSelect(groupName, detectedBackendUrl ?? undefined)
: undefined
}
onAnalyzeComplete={connectToRepo}
/>
)}

View file

@ -157,7 +157,9 @@ const TreeItem = ({
)}
{/* Name */}
<span className={`truncate font-mono text-xs ${node.path.startsWith('repo::') ? 'font-semibold' : ''}`}>
<span
className={`truncate font-mono text-xs ${node.path.startsWith('repo::') ? 'font-semibold' : ''}`}
>
{node.name}
</span>
</button>

View file

@ -103,7 +103,9 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
}
const node = nodeById.get(nodeId);
setHoveredNodeName(node ? node.properties.name : null);
const repo = node ? (node.properties as Record<string, unknown>)._repo as string | undefined : undefined;
const repo = node
? ((node.properties as Record<string, unknown>)._repo as string | undefined)
: undefined;
setHoveredNodeRepo(repo ?? null);
},
[graph, nodeById],

View file

@ -228,7 +228,9 @@ export const Header = ({
{g}
</span>
{g === projectName && (
<span className="shrink-0 font-mono text-[10px] text-amber-400">active</span>
<span className="shrink-0 font-mono text-[10px] text-amber-400">
active
</span>
)}
</button>
))

View file

@ -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({
</h3>
</div>
<div className="mt-1 flex items-center gap-3 pl-6">
{repoCount > 0 && (
<span className="text-xs text-text-muted">{repoCount} repos</span>
)}
{repoCount > 0 && <span className="text-xs text-text-muted">{repoCount} repos</span>}
{lastSync && (
<span className="text-xs text-text-muted">
synced {formatRelativeTime(lastSync)}
</span>
<span className="text-xs text-text-muted">synced {formatRelativeTime(lastSync)}</span>
)}
</div>
</div>
@ -172,7 +173,7 @@ export const RepoLanding = ({
}, [activeTab, groups, groupStatuses]);
return (
<div className="relative max-h-[80vh] animate-fade-in overflow-y-auto rounded-3xl bg-surface p-7 shadow-2xl shadow-black/60 ring-1 ring-border-subtle">
<div className="relative max-h-[80vh] animate-fade-in overflow-y-auto rounded-3xl bg-surface p-7 shadow-2xl ring-1 shadow-black/60 ring-border-subtle">
{/* Ambient glows */}
<div className="pointer-events-none absolute -top-28 -right-28 h-72 w-72 rounded-full bg-accent/6 blur-3xl" />
<div className="pointer-events-none absolute -bottom-24 -left-24 h-56 w-56 rounded-full bg-node-function/6 blur-3xl" />
@ -255,7 +256,9 @@ export const RepoLanding = ({
<FolderOpen className="mx-auto mb-3 h-8 w-8 text-text-muted/50" />
<p className="text-sm text-text-muted">No groups configured yet.</p>
<p className="mt-1 text-xs text-text-muted/70">
Run <code className="rounded bg-void px-1.5 py-0.5">gitnexus group auto-discover</code> to create one.
Run{' '}
<code className="rounded bg-void px-1.5 py-0.5">gitnexus group auto-discover</code>{' '}
to create one.
</p>
</div>
) : (

View file

@ -184,7 +184,7 @@ export const knowledgeGraphToGraphology = (
const nodePositions = new Map<string, { x: number; y: number }>();
// 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<string, unknown>)._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,

View file

@ -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<string, unknown>): GraphNode {
function makeNode(
id: string,
label: string,
name: string,
props?: Record<string, unknown>,
): 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);
});
});

View file

@ -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`);
}
}
}

View file

@ -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<ExtractedContract[]> {
private async extractProviders(dbExecutor: CypherExecutor | null): Promise<ExtractedContract[]> {
if (!this.ownPackageName || !dbExecutor) return [];
let rows: Record<string, unknown>[];

View file

@ -51,6 +51,7 @@ export interface GroupToolPort {
include_content?: boolean;
},
): Promise<unknown>;
impact(repo: GroupRepoHandle, params: Record<string, unknown>): Promise<unknown>;
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<string>([sourceRepo.name]);
const findConnections = async (
repoName: string,
currentDepth: number,
): Promise<void> => {
const findConnections = async (repoName: string, currentDepth: number): Promise<void> => {
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<string, string> = {};
const packages: Record<string, Record<string, string>> = {};
@ -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);

View file

@ -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<string, string>();
const packageMap = new Map<string, string>();
if (config.detect.shared_libs) {
const hasExplicitPackages =
Object.keys(config.packages).length > 0 &&

View file

@ -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<LocalBackend['impact']>[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<string, unknown>) : {};
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<string, unknown>): Promise<unknown> {
await this.refreshRepos();
return this.getGroupService().groupStatus(params);
}
private async groupGraph(params: Record<string, unknown>): Promise<unknown> {

View file

@ -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 "@<groupName>" syntax instead';
}
const repoName = parsed.repoName;

View file

@ -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' },

View file

@ -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}`,

View file

@ -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({

View file

@ -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

View file

@ -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<GroupRepoHandle> => {
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<string, string>,
registry: ContractRegistry,
) {
async function writeFixture(repos: Record<string, string>, 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<string, GroupRepoHandle> = {
'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);

View file

@ -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',
]);

View file

@ -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'));
});

View file

@ -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 () => {

View file

@ -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 () => {

9
package-lock.json generated
View file

@ -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",

View file

@ -26,5 +26,8 @@
"prettier --write"
],
"*.{js,jsx,mjs,json,css,yml,yaml}": "prettier --write"
},
"dependencies": {
"jsonc-parser": "^3.3.1"
}
}