/** * Local Backend (Multi-Repo) * * Provides tool implementations using local .gitnexus/ indexes. * Supports multiple indexed repositories via a global registry. * LadybugDB connections are opened lazily per repo on first query. */ import { type RegistryEntry } from '../../storage/repo-manager.js'; import { GroupService } from '../../core/group/service.js'; /** * Quick test-file detection for filtering impact results. * Matches common test file patterns across all supported languages. */ export declare function isTestFilePath(filePath: string): boolean; /** Valid LadybugDB node labels for safe Cypher query construction */ export declare const VALID_NODE_LABELS: Set; /** Valid relation types for impact analysis filtering */ export declare const VALID_RELATION_TYPES: Set; /** * Per-relation-type confidence floor for impact analysis. * * When the graph stores a relation with a confidence value, that stored * value is used as-is (it reflects resolution-tier accuracy from analysis * time). This map provides the floor for each edge type when no stored * confidence is available, and is also used for display / tooltip hints. * * Rationale: * CALLS / IMPORTS – direct, strongly-typed references → 0.9 * EXTENDS – class hierarchy, statically verifiable → 0.85 * IMPLEMENTS – interface contract, statically verifiable → 0.85 * METHOD_OVERRIDES – method override, statically verifiable → 0.85 * METHOD_IMPLEMENTS – interface method implementation, statically verifiable → 0.85 * HAS_METHOD – structural containment → 0.95 * HAS_PROPERTY – structural containment → 0.95 * ACCESSES – field read/write, may be indirect → 0.8 * CONTAINS – folder/file containment → 0.95 * (unknown type) – conservative fallback → 0.5 */ export declare const IMPACT_RELATION_CONFIDENCE: Readonly>; export interface CodebaseContext { projectName: string; stats: { fileCount: number; functionCount: number; communityCount: number; processCount: number; }; } interface RepoHandle { id: string; name: string; repoPath: string; storagePath: string; lbugPath: string; indexedAt: string; lastCommit: string; remoteUrl?: string; stats?: RegistryEntry['stats']; } /** * Resolve the git diff cwd for detect_changes, auto-detecting linked worktrees. * * When `launchCwd` is a linked worktree of the same canonical repository as * `repoPath` (i.e. `getGitRoot(launchCwd)` differs from `repoPath` but both * share the same `getCanonicalRepoRoot`), returns the worktree's git root so * that `git diff` sees the correct working directory and index. * * Returns `repoPath` unchanged in all other cases (non-worktree, git * unavailable, unrelated repo). * * Extracted as a module-level export so tests can pass any `launchCwd` instead * of relying on `process.cwd()`, which is fixed to the server launch directory * and cannot be changed mid-process. */ export declare function resolveWorktreeCwd(repoPath: string, launchCwd: string): string; /** * Length of the path-derived suffix appended to a colliding repo id. * Exported so tests can pin the suffix shape without re-deriving the * literal; see `assignRepoId()` and the hashed-id resolution tier (#1658). * * Note: base64url is an *encoding*, not a hash — it preserves byte order, so * two paths that share a long common prefix (sibling clones under one parent) * collapse to the same sliced suffix. `assignRepoId()` keeps the legacy * base64url suffix only for the first colliding duplicate (id compatibility) * and falls back to a content hash of the resolved path on a real collision * (#2054). */ export declare const REPO_ID_HASH_LENGTH = 6; /** * One repository entry as returned by {@link LocalBackend.listRepos} and in each * `list_repos` page. Named so the `listRepos`/`listReposPage` return types read * clearly instead of an opaque `Awaited>` expression. */ export interface RepoListing { name: string; path: string; indexedAt: string; lastCommit: string; remoteUrl?: string; stats?: any; staleness?: { commitsBehind: number; hint?: string; }; siblings?: Array<{ name: string; path: string; lastCommit: string; }>; } /** Continuation metadata for the paginated `list_repos` MCP tool (#2119). */ export interface ListReposPagination { /** Total repositories across all pages. */ total: number; /** Effective page size used (equals the requested limit; out-of-range is rejected, not clamped). */ limit: number; /** Offset this page started at. */ offset: number; /** Number of repositories actually returned in this page. */ returned: number; /** True when more repositories remain past this page. */ hasMore: boolean; /** Offset to request next; present only when `hasMore` is true. */ nextOffset?: number; } /** * Validate and normalise `list_repos` pagination arguments. * * @internal Exported for unit testing; not part of the public API surface. * * There is NO MCP-SDK-level enforcement of a tool's advertised `inputSchema` * (the SDK validates only the JSON-RPC envelope), and `callTool` is reachable * directly, so the backend is the real validation boundary. Malformed values — * non-number, `NaN`, non-integer, `limit < 1`, `limit > maxLimit`, or * `offset < 0` — are REJECTED with a clear error. `limit` is bounded but NOT * silently clamped: an over-max value throws (symmetric with the other bounds) * so a client never receives a smaller page than it asked for without knowing. * An omitted value (only `undefined`) falls back to the default. */ export declare function parseListReposPagination(params: { limit?: unknown; offset?: unknown; } | null | undefined, opts: { defaultLimit: number; maxLimit: number; }): { limit: number; offset: number; }; export declare class LocalBackend { private repos; private contextCache; private initializedRepos; private reinitPromises; private lastStalenessCheck; private groupToolSvc; /** * One-shot stderr warnings for sibling-clone drift, keyed by * `${repoId}|${cwdGitRoot}`. Without this guard every tool call * from inside a sibling clone would print the same warning, * making MCP stderr unreadable. */ private warnedSiblingDrift; /** * One-shot stderr warning for the VECTOR-extension fallback. Without this * guard the diagnostic would fire on every `semanticSearch()` call on * platforms where the extension is unsupported (e.g. Windows), making MCP * stderr noisy per DoD §2.8. */ private warnedVectorUnsupported; /** * Cross-repo group tools (CLI). Shares logic with MCP `group_*` handlers. */ getGroupService(): GroupService; /** Close all pooled LadybugDB connections (CLI one-shot; optional for long-lived MCP). */ dispose(): Promise; /** * Initialize from the global registry. * Returns true if at least one repo is available. */ init(): Promise; /** * Re-read the global registry and update the in-memory repo map. * New repos are added, existing repos are updated, removed repos are pruned. * LadybugDB connections for removed repos are NOT closed (they idle-timeout naturally). */ private refreshRepos; /** * Assign a collision-free in-memory id for a registered repo. * * - Unique name → the bare lowercased name. * - Duplicate name → a path-derived suffix. The *first* colliding clone keeps * the legacy `base64url(path)` suffix so ids generated before #2054 still * resolve (the #1658 hashed-id tier). base64url is an encoding, not a hash: * it preserves byte order, so sibling clones under one parent (e.g. * `.../REPO_2` and `.../REPO_3`) yield identical leading characters and thus * the same sliced suffix. Any further collision therefore falls back to a * content hash of the *resolved* path (order-insensitive), extended * deterministically until unique. * * `assigned` maps every id handed out in this refresh to its resolved path, * so a candidate is "free" when it is unused or already owned by this exact * path. This method records its own assignment into `assigned` before * returning, so the map-update is the function's invariant, not a caller * obligation. A returned id never overwrites a different path's handle (#2054). */ private assignRepoId; /** * Resolve which repo to use. * - If repoParam is given, match by name or path * - If only 1 repo, use it * - If 0 or multiple without param, throw with helpful message * * On a miss, re-reads the registry once in case a new repo was indexed * while the MCP server was running. */ resolveRepo(repoParam?: string): Promise; /** * Try to resolve a repo from the in-memory cache. Returns null on miss. * Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches * multiple handles by name and cwd cannot disambiguate (#1658). */ private resolveRepoFromCache; /** * Prefer the indexed repo whose path matches the git root of process.cwd(). * * In MCP stdio server mode, `process.cwd()` is the server's launch directory, * not the agent client's cwd. If the server was started from an unrelated * directory, `getGitRoot` returns null and duplicate-name resolution throws * {@link RegistryAmbiguousTargetError} — callers should pass an absolute path. */ private pickRepoHandleForCwd; private handleToRegistryEntry; /** * Ensure the LadybugDB pool is open for the *resolved* repo. * * Takes the `RepoHandle` the caller resolved — NOT a bare id — and keys the * pool (and the init/staleness/reinit maps) by the immutable `lbugPath`. Two * things matter for multi-clone correctness: (1) the handle is the one the * caller resolved, so a concurrent `refreshRepos` can't substitute a different * clone; (2) the pool key is the database path, so distinct clones never share * a pool entry even when their name-derived id transiently collides (#2067). */ private ensureInitialized; /** * Get context for a specific repo (or the single repo if only one). */ getContext(repoId?: string): CodebaseContext | null; /** * List all registered repos with their metadata. * Re-reads the global registry so newly indexed repos are discovered * without restarting the MCP server. * * Each entry includes: * - `staleness`: if the indexed clone's own HEAD has moved past * the recorded `lastCommit` (option D in the issue's fix list). * - `siblings`: other registered entries sharing the same * `remoteUrl` (option B's payoff: callers can see at a glance * that another clone of the same logical repo is registered). * - `remoteUrl`: the canonical origin URL recorded at index time. */ listRepos(): Promise; /** * Paginated view over {@link listRepos} for the `list_repos` MCP tool (#2119). * * `listRepos()` itself still returns the FULL array — its resource and CLI * consumers (`gitnexus://repos`, `gitnexus://setup`, startup logs) need every * entry, so pagination lives ONLY here, on the tool surface, to keep the * response under MCP/LLM token-truncation limits. * * Determinism: a single registry snapshot is taken per call, then sorted by * lower-cased name with the repository path as a tie-breaker. Sibling clones * share a name but never a path (#2054), so `(name, path)` is a total order — * paging never skips or duplicates an entry while the registry is unchanged. * Codepoint comparison (not `localeCompare`) keeps page boundaries stable * across machines/locales, matching the existing `refreshRepos` ordering. */ listReposPage(params?: { limit?: unknown; offset?: unknown; } | null): Promise<{ repositories: RepoListing[]; pagination: ListReposPagination; }>; /** * Best-effort sibling-clone drift warning. * * When the resolved index has a `remoteUrl` recorded and the caller's * `process.cwd()` is inside a *different* clone of the same repo, emit * one stderr line per (repo, cwd) pair so the operator knows the * graph may be stale relative to what's actually on disk under their * cwd. Silent on path matches and on repos without a remote URL. * * Limitation: in MCP stdio server mode `process.cwd()` is the * server's CWD at start time, *not* the agent client's CWD. The * warning therefore only fires when the MCP server itself was * launched from inside a sibling clone (typical for `npx gitnexus * serve` from a polecat workspace). Surfacing the client's CWD * would require a per-tool-call `cwd` parameter — out of scope for * the current MCP contract. * * Pure side-effect (stderr); never affects the returned handle. * After the first computation for a given (repo, cwd) pair the * result is cached so subsequent `resolveRepo()` calls don't * re-shell-out to git. */ private maybeWarnSiblingDrift; callTool(method: string, params: any): Promise; /** * Query tool — process-grouped search. * * 1. Hybrid search (BM25 + semantic) to find matching symbols * 2. Trace each match to its process(es) via STEP_IN_PROCESS * 3. Group by process, rank by aggregate relevance + internal cluster cohesion * 4. Return: { processes, process_symbols, definitions } */ private query; /** * BM25 keyword search helper - uses LadybugDB FTS for always-fresh results */ private bm25Search; /** * Semantic vector search helper */ private semanticSearch; executeCypher(repoName: string, query: string, params?: Record): Promise; private cypher; /** * Format raw Cypher result rows as a markdown table for LLM readability. * Falls back to raw result if rows aren't tabular objects. */ private formatCypherAsMarkdown; /** * Aggregate same-named clusters: group by heuristicLabel, sum symbols, * weighted-average cohesion, filter out tiny clusters (<5 symbols). * Raw communities stay intact in LadybugDB for Cypher queries. */ private aggregateClusters; private overview; /** * Patch the `type` field on candidates whose `labels(n)[0]` projection * came back empty — a known LadybugDB behaviour for several node types. * * Uses one scoped UNION query across the five priority labels rather * than per-candidate round-trips, so cost is a single DB call regardless * of how many candidates need enrichment. No-op when every candidate * already has a non-empty type. * * Failures are swallowed: label enrichment is an optimisation for * downstream scoring and #480 Class/Interface BFS seeding; if it fails * the symbol still resolves, just without the kind-priority bonus. */ private enrichCandidateLabels; /** * Score a symbol candidate for disambiguation ranking. * * Deterministic, no DB round-trip: * - base 0.50 * - +0.40 when file_path hint matches (substring, case-insensitive) * - +0.20 when kind hint exactly matches the candidate's kind * - when no kind hint, a small priority bonus (Class > Interface > * Function > Method > Constructor) to preserve the intuition that * class-level names are usually what the user wanted. * * Capped at 1.0. Intentionally simple and inspectable — a future v2 can * plug in BM25/embedding signals here without changing the surrounding * resolver shape. */ private scoreCandidate; /** * Shared symbol resolver used by `context` and `impact`. * * Returns one of: * - `{ kind: 'ok', symbol, resolvedLabel }` — single confident match * (either direct UID, only one candidate after filtering, Class/ * Constructor collapse, or a top-scoring candidate with a clear gap * to the runner-up). * - `{ kind: 'ambiguous', candidates }` — multiple viable matches, * sorted by score desc. Each candidate carries a relevance score. * - `{ kind: 'not_found' }` — no matches at all. * * Preserves the #480 Class/Constructor preference: when the only * ambiguity is between a Class and its own Constructor (same name, * same filePath), the Class wins silently. */ private resolveSymbolCandidates; /** * Context tool — 360-degree symbol view with categorized refs. * Disambiguation (ranked) when multiple symbols share a name. * UID-based direct lookup. No cluster in output. */ private context; private _contextImpl; /** * Legacy explore — kept for backwards compatibility with resources.ts. * Routes cluster/process types to direct graph queries. */ private explore; /** * Detect changes — git-diff based impact analysis. * Maps changed lines to indexed symbols, then finds affected processes. */ private detectChanges; /** * Rename tool — multi-file coordinated rename using graph + text search. * Graph refs are tagged "graph" (high confidence). * Additional refs found via text search are tagged "text_search" (lower confidence). */ private rename; private impact; private _impactImpl; /** * Shared BFS traversal for impact analysis (name-resolved or UID-resolved symbol). */ private _runImpactBFS; /** * UID-based impact for cross-repo fan-out. Same result shape as `impact`. * Returns null if the repo is unknown, the UID is missing, or analysis fails. */ impactByUid(repoId: string, uid: string, direction: string, opts: { maxDepth: number; relationTypes: string[]; minConfidence: number; includeTests: boolean; signal?: AbortSignal; }): Promise; private handleGroupTool; /** * Dispatch impact/query/context when `repo` is `@groupName` or `@groupName/memberPath` * (group mode — not the global indexed-repo `repo` parameter). */ private callToolAtGroupRepo; private groupList; private groupSync; /** * MCP resource body for `gitnexus://group/{name}/contracts` (Issue #794). */ readGroupContractsResource(groupName: string, filter: { type?: string; repo?: string; unmatchedOnly?: boolean; }): Promise; /** * MCP resource body for `gitnexus://group/{name}/status` (Issue #794). */ readGroupStatusResource(groupName: string): Promise; private static formatGroupResourcePayload; /** * Fetch Route nodes with their consumers in a single query. * Shared by routeMap and shapeCheck to avoid N+1 query patterns. */ private fetchRoutesWithConsumers; /** * Batch-fetch execution flows linked to a set of Route or Tool nodes. * Single query instead of N+1. */ private fetchLinkedFlowsBatch; private routeMap; private shapeCheck; private toolMap; private apiImpact; /** * Query clusters (communities) directly from graph. * Used by getClustersResource — avoids legacy overview() dispatch. */ queryClusters(repoName?: string, limit?: number): Promise<{ clusters: any[]; }>; /** * Query processes directly from graph. * Used by getProcessesResource — avoids legacy overview() dispatch. */ queryProcesses(repoName?: string, limit?: number): Promise<{ processes: any[]; }>; /** * Query cluster detail (members) directly from graph. * Used by getClusterDetailResource. */ queryClusterDetail(name: string, repoName?: string): Promise; /** * Query process detail (steps) directly from graph. * Used by getProcessDetailResource. */ queryProcessDetail(name: string, repoName?: string): Promise; disconnect(): Promise; } export {};