import { z } from 'zod'; import { Result } from 'neverthrow'; import Database from 'better-sqlite3'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; type TraceMcpError = { code: 'PARSE_ERROR'; file: string; partial: boolean; message: string; } | { code: 'NOT_FOUND'; id: string; candidates?: string[]; } | { code: 'RESOLUTION_FAILED'; path: string; message: string; } | { code: 'TIMEOUT'; operation: string; ms: number; } | { code: 'SECURITY_VIOLATION'; detail: string; } | { code: 'PLUGIN_ERROR'; plugin: string; message: string; } | { code: 'DB_ERROR'; message: string; } | { code: 'CONFIG_ERROR'; message: string; } | { code: 'VALIDATION_ERROR'; message: string; details?: unknown; }; type TraceMcpResult = Result; declare const TraceMcpConfigSchema: z.ZodObject<{ root: z.ZodDefault; db: z.ZodPrefault; }, z.core.$strip>>; include: z.ZodDefault>; exclude: z.ZodDefault>; ignore: z.ZodPrefault>; patterns: z.ZodDefault>; }, z.core.$strip>>; frameworks: z.ZodOptional; timeout: z.ZodDefault; }, z.core.$strip>>; graceful_degradation: z.ZodDefault; }, z.core.$strip>>; }, z.core.$strip>>; ai: z.ZodOptional; provider: z.ZodDefault>; features: z.ZodPrefault; inference: z.ZodDefault; fast_inference: z.ZodDefault; }, z.core.$strip>>; base_url: z.ZodOptional; api_key: z.ZodOptional; inference_model: z.ZodOptional; fast_model: z.ZodOptional; embedding_model: z.ZodOptional; embedding_dimensions: z.ZodOptional; summarize_on_index: z.ZodDefault; summarize_batch_size: z.ZodDefault; summarize_kinds: z.ZodDefault>; summarizeFromDocstrings: z.ZodDefault; openaiExtraBody: z.ZodDefault>; concurrency: z.ZodDefault; reranker_model: z.ZodOptional; vertex_project: z.ZodOptional; vertex_location: z.ZodOptional; autoRebuildOnProviderMismatch: z.ZodDefault; }, z.core.$strip>>; plugins: z.ZodDefault>; security: z.ZodOptional>; max_file_size_bytes: z.ZodOptional; max_files: z.ZodOptional; }, z.core.$strip>>; predictive: z.ZodOptional; weights: z.ZodPrefault; fix_ratio: z.ZodDefault; complexity: z.ZodDefault; coupling: z.ZodDefault; pagerank: z.ZodDefault; authors: z.ZodDefault; }, z.core.$strip>>; tech_debt: z.ZodPrefault; coupling: z.ZodDefault; test_gap: z.ZodDefault; churn: z.ZodDefault; }, z.core.$strip>>; change_risk: z.ZodPrefault; complexity: z.ZodDefault; churn: z.ZodDefault; test_gap: z.ZodDefault; coupling: z.ZodDefault; }, z.core.$strip>>; }, z.core.$strip>>; cache_ttl_minutes: z.ZodDefault; git_since_days: z.ZodDefault; module_depth: z.ZodDefault; }, z.core.$strip>>; intent: z.ZodOptional; domain_hints: z.ZodOptional>>; custom_domains: z.ZodOptional; description: z.ZodOptional; path_patterns: z.ZodArray; }, z.core.$strip>>>; auto_classify_on_index: z.ZodDefault; classify_batch_size: z.ZodDefault; }, z.core.$strip>>; runtime: z.ZodOptional; otlp: z.ZodPrefault; host: z.ZodDefault; max_body_bytes: z.ZodDefault; }, z.core.$strip>>; retention: z.ZodPrefault; max_aggregate_age_days: z.ZodDefault; prune_interval: z.ZodDefault; }, z.core.$strip>>; mapping: z.ZodPrefault>; route_patterns: z.ZodDefault>; }, z.core.$strip>>; }, z.core.$strip>>; lsp: z.ZodOptional; servers: z.ZodPrefault>; initializationOptions: z.ZodOptional>; rootUri: z.ZodOptional; timeout_ms: z.ZodDefault; }, z.core.$strip>>>; auto_detect: z.ZodDefault; max_concurrent_servers: z.ZodDefault; enrichment_timeout_ms: z.ZodDefault; batch_size: z.ZodDefault; }, z.core.$strip>>; topology: z.ZodOptional; repos: z.ZodDefault>; auto_detect: z.ZodDefault; auto_discover: z.ZodDefault; contract_globs: z.ZodOptional>; }, z.core.$strip>>; indexer: z.ZodOptional; parallel_initial_index: z.ZodOptional; }, z.core.$strip>>; pipeline: z.ZodPrefault; }, z.core.$strip>>; vault: z.ZodPrefault; roots: z.ZodDefault>; extra_globs: z.ZodDefault>; }, z.core.$strip>>; decisions: z.ZodPrefault; reject_threshold: z.ZodDefault; }, z.core.$strip>>; memory: z.ZodPrefault; }, z.core.$strip>>; heat: z.ZodPrefault; halfLifeDays: z.ZodDefault; freshnessDays: z.ZodDefault; }, z.core.$strip>>; mining: z.ZodPrefault>; llm: z.ZodPrefault; minSessionLength: z.ZodDefault; maxSessions: z.ZodDefault; }, z.core.$strip>>; incrementalCursor: z.ZodDefault; }, z.core.$strip>>; memo: z.ZodPrefault; regenerateEveryN: z.ZodDefault; targetTokens: z.ZodDefault; maxBudgetTokens: z.ZodDefault; historyLimit: z.ZodDefault; autoRegenerate: z.ZodDefault; minTriggerIntervalSec: z.ZodDefault; }, z.core.$strip>>; consolidation: z.ZodOptional; defaultMaxDecisions: z.ZodDefault; defaultSameTypeOnly: z.ZodDefault; }, z.core.$strip>>; audit_log: z.ZodOptional; dir: z.ZodOptional; retentionDays: z.ZodDefault; }, z.core.$strip>>; weight_tuning: z.ZodOptional; min_events: z.ZodDefault; }, z.core.$strip>>; background: z.ZodPrefault; tickIntervalSec: z.ZodDefault; activityDebounceSec: z.ZodDefault; idleWindowSec: z.ZodDefault; coldThresholdSec: z.ZodDefault; mineMinIntervalSec: z.ZodDefault; clusterEveryNDecisions: z.ZodDefault; failureBackoffSec: z.ZodDefault; tuneCooldownSec: z.ZodDefault; tuneEveryNNewEvents: z.ZodDefault; }, z.core.$strip>>; }, z.core.$strip>>; quality_gates: z.ZodOptional; fail_on: z.ZodDefault>; rules: z.ZodPrefault; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_coupling_instability: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_circular_import_chains: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_dead_exports_percent: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_tech_debt_grade: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_security_critical_findings: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_antipattern_count: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; max_code_smell_count: z.ZodOptional; severity: z.ZodDefault>; scope: z.ZodOptional>; message: z.ZodOptional; }, z.core.$strip>>; }, z.core.$strip>>; }, z.core.$strip>>; telemetry: z.ZodOptional; max_rows: z.ZodDefault; observability: z.ZodPrefault; sink: z.ZodDefault>; sampleRate: z.ZodDefault; otlp: z.ZodPrefault; headers: z.ZodDefault>; serviceName: z.ZodDefault; maxQueuedSpans: z.ZodDefault; requestTimeoutMs: z.ZodDefault; }, z.core.$strip>>; langfuse: z.ZodPrefault; publicKey: z.ZodOptional; secretKey: z.ZodOptional; maxQueuedEvents: z.ZodDefault; requestTimeoutMs: z.ZodDefault; }, z.core.$strip>>; }, z.core.$strip>>; }, z.core.$strip>>; tools: z.ZodOptional; include: z.ZodOptional>; exclude: z.ZodOptional>; descriptions: z.ZodOptional]>>>; description_verbosity: z.ZodDefault>; instructions_verbosity: z.ZodDefault>; agent_behavior: z.ZodDefault>; meta_fields: z.ZodDefault>]>>; compact_schemas: z.ZodDefault; default_format: z.ZodDefault>; }, z.core.$strip>>; watch: z.ZodPrefault; debounceMs: z.ZodDefault; }, z.core.$strip>>; logging: z.ZodPrefault; path: z.ZodDefault; level: z.ZodDefault>; max_size_mb: z.ZodDefault; }, z.core.$strip>>; git: z.ZodPrefault; }, z.core.$strip>>; idle_timeout_minutes: z.ZodDefault; daemon_stability_seconds: z.ZodDefault; backend_swap_drain_ms: z.ZodDefault; auto_spawn_daemon: z.ZodDefault; daemon_spawn_timeout_seconds: z.ZodDefault; daemon_idle_exit_minutes: z.ZodDefault; hermes: z.ZodPrefault, z.ZodBoolean]>>; home_override: z.ZodOptional; profile: z.ZodOptional; }, z.core.$strip>>; children: z.ZodOptional>; }, z.core.$strip>; type TraceMcpConfig = z.infer; /** * Load config for a project. * Merge order: global defaults → per-project overrides → Zod schema defaults. * The `projectRoot` key in the global config (keyed by absolute path) is also checked. */ declare function loadConfig(searchFrom?: string): Promise>; declare function initializeDatabase(dbPath: string): Database.Database; interface NodeTypeDeclaration { name: string; } interface EdgeTypeDeclaration { name: string; category: string; directed?: boolean; description?: string; } interface RawSymbol { symbolId: string; name: string; kind: SymbolKind; fqn?: string; parentSymbolId?: string; signature?: string; byteStart: number; byteEnd: number; lineStart?: number; lineEnd?: number; metadata?: Record; } type SymbolKind = 'class' | 'method' | 'function' | 'constant' | 'property' | 'interface' | 'trait' | 'enum' | 'type' | 'variable' | 'enum_case' | 'namespace' | 'decorator' | 'heading' | 'tag'; /** How an edge was resolved during indexing — tiers from highest to lowest confidence */ type EdgeResolution = 'lsp_resolved' | 'ast_resolved' | 'ast_inferred' | 'text_matched'; interface RawEdge { sourceSymbolId?: string; sourceNodeType?: string; sourceRefId?: number; targetSymbolId?: string; targetNodeType?: string; targetRefId?: number; edgeType: string; resolved?: boolean; /** How this edge was resolved: ast_resolved (direct AST), ast_inferred (import graph), text_matched (heuristic) */ resolution?: EdgeResolution; metadata?: Record; } interface RawRoute { method: string; uri: string; name?: string; controllerSymbolId?: string; handler?: string; middleware?: string[]; fileId?: number; line?: number; metadata?: Record; } interface RawComponent { name: string; kind: 'page' | 'component' | 'layout' | 'context' | 'provider' | 'hook'; props?: Record; emits?: string[]; slots?: string[]; composables?: string[]; framework: string; } interface RawMigration { tableName: string; operation: 'create' | 'alter' | 'drop'; columns?: Record[]; indices?: Record[]; timestamp?: string; } interface RawOrmModel { name: string; orm: 'mongoose' | 'sequelize' | 'sqlalchemy' | 'django' | 'prisma' | 'typeorm' | 'drizzle'; collectionOrTable?: string; fields?: Record[]; options?: Record; metadata?: Record; } interface RawOrmAssociation { sourceModelName: string; targetModelName: string; kind: string; options?: Record; line?: number; } interface RawRnScreen { name: string; componentPath?: string; navigatorType?: 'stack' | 'tab' | 'drawer' | 'native-stack'; options?: Record; deepLink?: string; metadata?: Record; } interface FileParseResult { language?: string; frameworkRole?: string; status: 'ok' | 'partial' | 'error'; symbols: RawSymbol[]; edges?: RawEdge[]; routes?: RawRoute[]; components?: RawComponent[]; migrations?: RawMigration[]; ormModels?: RawOrmModel[]; ormAssociations?: RawOrmAssociation[]; rnScreens?: RawRnScreen[]; warnings?: string[]; metadata?: Record; } type PluginCategory = 'framework' | 'orm' | 'validation' | 'state' | 'api' | 'realtime' | 'testing' | 'tooling' | 'view'; interface PluginManifest { name: string; version: string; priority: number; dependencies?: string[]; category?: PluginCategory; } /** Detected runtime/language version from manifest files. */ interface DetectedVersion { runtime: string; version?: string; source: string; } /** Parsed dependency entry from any manifest. */ interface ParsedDependency { name: string; version?: string; dev?: boolean; } interface ProjectContext { rootPath: string; composerJson?: Record; packageJson?: Record; pyprojectToml?: Record; requirementsTxt?: string[]; goMod?: { module: string; goVersion?: string; deps: ParsedDependency[]; }; cargoToml?: { package?: Record; deps: ParsedDependency[]; }; gemfile?: { deps: ParsedDependency[]; }; pomXml?: { groupId?: string; artifactId?: string; version?: string; deps: ParsedDependency[]; }; buildGradle?: { deps: ParsedDependency[]; }; detectedVersions: DetectedVersion[]; allDependencies: ParsedDependency[]; configFiles: string[]; } interface LanguagePlugin { manifest: PluginManifest; supportedExtensions: string[]; supportedVersions?: string[]; extractSymbols(filePath: string, content: Buffer): TraceMcpResult | Promise>; } interface FrameworkPlugin { manifest: PluginManifest; detect(ctx: ProjectContext): boolean; registerSchema(): { nodeTypes?: NodeTypeDeclaration[]; edgeTypes?: EdgeTypeDeclaration[]; }; /** * May return either a synchronous `TraceMcpResult` or a `Promise` of one — * tree-sitter-based plugins (e.g. React) need async parser init on first * use, while regex-only plugins stay sync. The executor `await`s either. */ extractNodes?(filePath: string, content: Buffer, language: string): TraceMcpResult | Promise>; resolveEdges?(ctx: ResolveContext): TraceMcpResult; configure?(config: Record): void; } interface ResolveContext { rootPath: string; getAllFiles(): { id: number; path: string; language: string | null; }[]; getSymbolsByFile(fileId: number): { id: number; symbolId: string; name: string; kind: string; fqn: string | null; lineStart?: number | null; lineEnd?: number | null; metadata?: Record | null; }[]; getSymbolByFqn(fqn: string): { id: number; symbolId: string; } | undefined; getNodeId(nodeType: string, refId: number): number | undefined; createNodeIfNeeded(nodeType: string, refId: number): number; /** Read file content — uses Pass 1 cache when available, falls back to disk. */ readFile(relPath: string): string | undefined; /** * Set when the pipeline is doing a scoped (incremental) re-resolve. Plugins * MAY consult this to narrow their work to changed files. Plugins that * can't reason incrementally just ignore it and continue full-pass — * backward-compatible. */ changeScope?: ChangeScope; } /** * Resolver-scope hint shared between the pipeline and individual edge * resolvers. Same shape as the internal `ChangeScope` from pipeline-state.ts, * re-exported here so plugin authors don't need to reach into internal paths. */ interface ChangeScope { readonly changedFileIds: ReadonlySet; readonly newSymbolNames: ReadonlyMap>; readonly deletedSymbolNames: ReadonlyMap>; } /** Row type interfaces for all database tables. * Extracted from store.ts to break circular dependencies between Store and repository classes. */ interface FileRow { id: number; path: string; language: string | null; framework_role: string | null; status: string; content_hash: string | null; byte_length: number | null; indexed_at: string; metadata: string | null; workspace: string | null; gitignored: number; mtime_ms: number | null; } interface SymbolRow { id: number; file_id: number; symbol_id: string; name: string; kind: string; fqn: string | null; parent_id: number | null; signature: string | null; summary: string | null; byte_start: number; byte_end: number; line_start: number | null; line_end: number | null; metadata: string | null; } interface EdgeRow { id: number; source_node_id: number; target_node_id: number; edge_type_id: number; resolved: number; metadata: string | null; is_cross_ws: number; resolution_tier: string; /** * Numeric confidence in [0, 1]. Default seeded from resolution_tier: * lsp_resolved → 1.0, ast_resolved → 0.95, ast_inferred → 0.7, * text_matched → 0.4. Plugins MAY override on insert when they have a * better signal (e.g. spring DI through @Autowired metadata vs heuristic * receiver-type lookup). */ confidence: number; } interface RouteRow { id: number; method: string; uri: string; name: string | null; handler: string | null; controller_symbol_id: string | null; middleware: string | null; metadata: string | null; file_id: number | null; line: number | null; } interface MigrationRow { id: number; file_id: number; table_name: string; operation: string; columns: string | null; indices: string | null; timestamp: string | null; } interface ComponentRow { id: number; file_id: number; name: string; kind: string; props: string | null; emits: string | null; slots: string | null; composables: string | null; framework: string; } interface OrmModelRow { id: number; file_id: number; name: string; orm: string; collection_or_table: string | null; fields: string | null; options: string | null; metadata: string | null; } interface OrmAssociationRow { id: number; source_model_id: number; target_model_id: number | null; target_model_name: string | null; kind: string; options: string | null; file_id: number | null; line: number | null; } interface RnScreenRow { id: number; file_id: number; name: string; component_path: string | null; navigator_type: string | null; options: string | null; deep_link: string | null; metadata: string | null; } interface EnvVarRow$1 { id: number; file_id: number; key: string; value_type: string; value_format: string | null; comment: string | null; quoted: number; line: number | null; } interface SymbolWithFilePath extends SymbolRow { file_path: string; } interface EdgeTypeRow$1 { name: string; category: string; description: string; } interface IndexStats { totalFiles: number; totalSymbols: number; totalEdges: number; totalNodes: number; /** * HTTP method routes only (GET/POST/PUT/DELETE/PATCH/OPTIONS/HEAD). Excludes * synthetic resource routes (MCP TOOL/RESOURCE/PROMPT/JOB/SCHEMA/CLI) and * test-fixture routes (TEST/TEST_ROUTE/TEST_COMPONENT) — those used to inflate * this counter to meaningless levels for non-HTTP projects. */ totalRoutes: number; /** * Non-HTTP synthetic routes — MCP TOOL/RESOURCE/PROMPT/JOB, JSON SCHEMA, CLI * commands. Counted separately so `totalRoutes` stays a clean HTTP signal. */ totalResourceRoutes: number; /** * Routes registered by tests (method LIKE 'TEST%'). Excluded from `totalRoutes` * to prevent fixture bloat from drowning the real HTTP route count. */ totalTestFixtureRoutes: number; totalComponents: number; totalMigrations: number; partialFiles: number; errorFiles: number; } interface GraphSnapshotRow$1 { id: number; commit_hash: string | null; created_at: string; snapshot_type: string; file_path: string | null; data: string; } interface WorkspaceStats$1 { workspace: string; file_count: number; symbol_count: number; languages: string | null; } interface CrossWorkspaceEdge$1 { id: number; edge_type: string; source_workspace: string | null; source_path: string | null; source_symbol: string | null; source_kind: string | null; target_workspace: string | null; target_path: string | null; target_symbol: string | null; target_kind: string | null; } interface WorkspaceDependency$1 { from_workspace: string; to_workspace: string; edge_count: number; edge_types: string; } interface EnvVarRow { id: number; file_id: number; key: string; value_type: string; value_format: string | null; comment: string | null; quoted: number; line: number | null; } interface WorkspaceStats { workspace: string; file_count: number; symbol_count: number; languages: string | null; } interface CrossWorkspaceEdge { id: number; edge_type: string; source_workspace: string | null; source_path: string | null; source_symbol: string | null; source_kind: string | null; target_workspace: string | null; target_path: string | null; target_symbol: string | null; target_kind: string | null; } interface WorkspaceDependency { from_workspace: string; to_workspace: string; edge_count: number; edge_types: string; } interface GraphSnapshotRow { id: number; commit_hash: string | null; created_at: string; snapshot_type: string; file_path: string | null; data: string; } declare class AnalyticsRepository { private readonly db; constructor(db: Database.Database); insertEnvVar(fileId: number, entry: { key: string; valueType: string; valueFormat: string | null; comment: string | null; quoted: boolean; line: number; }): number; deleteEnvVarsByFile(fileId: number): void; getEnvVarsByFile(fileId: number): EnvVarRow[]; getAllEnvVars(): (EnvVarRow & { file_path: string; })[]; searchEnvVars(pattern: string): (EnvVarRow & { file_path: string; })[]; getWorkspaceStats(): WorkspaceStats[]; getCrossWorkspaceEdges(): CrossWorkspaceEdge[]; getWorkspaceDependencyGraph(): WorkspaceDependency[]; getWorkspaceExports(workspace: string): SymbolWithFilePath[]; getStats(): IndexStats; insertGraphSnapshot(snapshotType: string, data: Record, commitHash?: string, filePath?: string): number; getGraphSnapshots(snapshotType: string, options?: { filePath?: string; since?: string; limit?: number; }): GraphSnapshotRow[]; pruneGraphSnapshots(maxAge?: number): number; } declare class DomainRepository { private readonly db; constructor(db: Database.Database); insertRoute(route: RawRoute, fileId: number, createNode: (nodeType: string, refId: number) => number): number; getRouteByUriAndMethod(uri: string, method: string): RouteRow | undefined; getAllRoutes(): RouteRow[]; /** Rewrite a route's served URI in place (e.g. after composing a cross-file * FastAPI include_router mount prefix). */ updateRouteUri(id: number, uri: string): void; findRouteByPattern(uri: string, method: string): RouteRow | undefined; insertComponent(comp: RawComponent, fileId: number, createNode: (nodeType: string, refId: number) => number): number; getComponentByFileId(fileId: number): ComponentRow | undefined; getComponentByName(name: string): ComponentRow | undefined; getAllComponents(): ComponentRow[]; insertMigration(mig: RawMigration, fileId: number, createNode: (nodeType: string, refId: number) => number): number; getMigrationsByTable(tableName: string): MigrationRow[]; getAllMigrations(): MigrationRow[]; insertOrmModel(model: RawOrmModel, fileId: number, createNode: (nodeType: string, refId: number) => number): number; getOrmModelByName(name: string): OrmModelRow | undefined; getOrmModelsByOrm(orm: string): OrmModelRow[]; getAllOrmModels(): OrmModelRow[]; insertOrmAssociation(sourceModelId: number, targetModelId: number | null, targetModelName: string, kind: string, options?: Record, fileId?: number, line?: number): number; getAllOrmAssociations(fileIds?: number[]): OrmAssociationRow[]; getOrmAssociationsByModel(modelId: number): OrmAssociationRow[]; insertRnScreen(screen: RawRnScreen, fileId: number, createNode: (nodeType: string, refId: number) => number): number; getRnScreenByName(name: string): RnScreenRow | undefined; getAllRnScreens(): RnScreenRow[]; } declare class FileRepository { private readonly db; private readonly _stmts; constructor(db: Database.Database); insertFile(path: string, language: string | null, contentHash: string | null, byteLength: number | null, workspace: string | null, mtimeMs: number | null, createNode: (nodeType: string, refId: number) => number): number; getFile(path: string): FileRow | undefined; getFileById(id: number): FileRow | undefined; getAllFiles(): FileRow[]; updateFileWorkspace(fileId: number, workspace: string): void; getFilesByWorkspace(workspace: string): FileRow[]; updateFileHash(fileId: number, hash: string, byteLength: number, mtimeMs: number | null): void; updateFileMtime(fileId: number, mtimeMs: number | null): void; updateFileStatus(fileId: number, status: string, frameworkRole: string | null): void; updateFileGitignored(fileId: number, gitignored: boolean): void; deleteFile(fileId: number, deleteEdgesForFileNodes: (fileId: number) => void, deleteEntitiesByFile: (fileId: number) => void): void; deleteEntitiesByFile(fileId: number): void; getFilesByIds(ids: number[]): Map; getFilesByPaths(paths: string[]): Map; /** * Find every file row whose content_hash matches `hash`. Used by the * rename-detection pre-pass: when a "new" path on disk has the same content * hash as a known DB row whose old path no longer exists, that's a rename * and the existing symbols can be carried over instead of re-extracted. * graphify v0.7.0 made this work by removing path from the cache key — we * already key by content alone, this helper just exposes the lookup. */ findFilesByContentHash(hash: string): FileRow[]; /** * Atomically update a file row's path. Used for rename detection — we keep * the existing fileId so all foreign-key references (symbols, edges, nodes) * stay attached. ON CONFLICT(path) on the unique index is impossible by the * caller's contract: caller must verify the new path is free first. */ updateFilePath(fileId: number, newPath: string): void; } interface EdgeTypeRow { name: string; category: string; description: string; } declare class GraphRepository { private readonly db; private readonly _stmts; constructor(db: Database.Database); createNode(nodeType: string, refId: number): number; getNodeId(nodeType: string, refId: number): number | undefined; insertEdge(sourceNodeId: number, targetNodeId: number, edgeTypeName: string, resolved?: boolean, metadata?: Record, isCrossWs?: boolean, resolutionTier?: string): TraceMcpResult; deleteEdgesForFileNodes(fileId: number): void; deleteOutgoingImportEdges(fileId: number): void; deleteOutgoingEdgesForFileNodes(fileId: number): void; traverseEdges(startNodeId: number, direction: 'outgoing' | 'incoming', depth: number): EdgeRow[]; getEdgesByType(edgeTypeName: string): EdgeRow[]; getOutgoingEdges(nodeId: number): (EdgeRow & { edge_type_name: string; })[]; getIncomingEdges(nodeId: number): (EdgeRow & { edge_type_name: string; })[]; ensureEdgeType(name: string, category: string, description: string): void; getEdgeTypeName(edgeTypeId: number): string | undefined; getNodeRef(nodeId: number): { nodeType: string; refId: number; } | undefined; getNodeByNodeId(nodeId: number): { node_type: string; ref_id: number; } | undefined; getNodeIdsBatch(nodeType: string, refIds: number[]): Map; getNodeRefsBatch(nodeIds: number[]): Map; getEdgesForNodesBatch(nodeIds: number[]): Array; getEdgeTypes(): EdgeTypeRow[]; } declare class SymbolRepository { private readonly db; private readonly _stmts; constructor(db: Database.Database); insertSymbol(fileId: number, sym: RawSymbol, parentIdOverride: number | null | undefined, createNode: (nodeType: string, refId: number) => number): number; insertSymbols(fileId: number, symbols: RawSymbol[], insertSymbolFn: (fileId: number, sym: RawSymbol, parentIdOverride?: number | null) => number): number[]; deleteSymbolsByFile(fileId: number): void; getSymbolsByFile(fileId: number): SymbolRow[]; getSymbolBySymbolId(symbolId: string): SymbolRow | undefined; getSymbolByFqn(fqn: string): SymbolRow | undefined; getSymbolById(id: number): SymbolRow | undefined; getSymbolChildren(parentId: number): SymbolRow[]; getSymbolByName(name: string, kind?: string): SymbolRow | undefined; getExportedSymbols(filePattern?: string): SymbolWithFilePath[]; findImplementors(name: string): SymbolWithFilePath[]; getSymbolsWithHeritage(fileIds?: number[]): (SymbolRow & { file_path: string; })[]; getSymbolsByIds(ids: number[]): Map; findSymbolByRole(name: string, frameworkRole?: string): SymbolRow | undefined; updateSymbolSummary(symbolId: number, summary: string): void; countUnsummarizedSymbols(kinds: string[]): number; countUnembeddedSymbols(): number; getUnsummarizedSymbols(kinds: string[], limit: number): { id: number; name: string; fqn: string | null; kind: string; signature: string | null; file_path: string; byte_start: number; byte_end: number; }[]; } declare class Store { readonly db: Database.Database; readonly files: FileRepository; readonly symbols: SymbolRepository; readonly graph: GraphRepository; readonly domain: DomainRepository; readonly analytics: AnalyticsRepository; constructor(db: Database.Database); insertFile(path: string, language: string | null, contentHash: string | null, byteLength: number | null, workspace?: string | null, mtimeMs?: number | null): number; getFile(path: string): FileRow | undefined; getFileById(id: number): FileRow | undefined; getFilesByPaths(paths: string[]): Map; getAllFiles(): FileRow[]; updateFileWorkspace(fileId: number, workspace: string): void; getFilesByWorkspace(workspace: string): FileRow[]; updateFileHash(fileId: number, hash: string, byteLength: number, mtimeMs?: number | null): void; updateFileMtime(fileId: number, mtimeMs: number | null): void; /** Find files whose content_hash matches — used by rename detection. */ findFilesByContentHash(hash: string): FileRow[]; /** Atomically rename a file row, preserving its id so symbols / edges / nodes * keep pointing at it. Caller must ensure the new path is unused. */ updateFilePath(fileId: number, newPath: string): void; updateFileStatus(fileId: number, status: string, frameworkRole?: string): void; updateFileGitignored(fileId: number, gitignored: boolean): void; deleteFile(fileId: number): void; deleteEntitiesByFile(fileId: number): void; getFilesByIds(ids: number[]): Map; insertSymbol(fileId: number, sym: RawSymbol, parentIdOverride?: number | null): number; insertSymbols(fileId: number, syms: RawSymbol[]): number[]; deleteSymbolsByFile(fileId: number): void; getSymbolsByFile(fileId: number): SymbolRow[]; getSymbolsByFileIds(fileIds: number[]): SymbolRow[]; getSymbolBySymbolId(symbolId: string): SymbolRow | undefined; getSymbolByFqn(fqn: string): SymbolRow | undefined; getSymbolById(id: number): SymbolRow | undefined; /** * Count how many distinct symbols share this exact simple name. Used by * findReferences to detect when a name is too common for text_matched * edges to be trustworthy (graphify v0.5.5 / v0.6.3 phantom-god-node fix). * Cheap: indexed lookup, no body reads. */ countSymbolsByName(name: string): number; getSymbolByName(name: string, kind?: string): SymbolRow | undefined; getSymbolChildren(parentId: number): SymbolRow[]; getExportedSymbols(filePattern?: string): SymbolWithFilePath[]; findImplementors(name: string): SymbolWithFilePath[]; getSymbolsWithHeritage(fileIds?: number[]): (SymbolRow & { file_path: string; })[]; getSymbolsByIds(ids: number[]): Map; findSymbolByRole(name: string, frameworkRole?: string): SymbolRow | undefined; updateSymbolSummary(symbolId: number, summary: string): void; countUnsummarizedSymbols(kinds: string[]): number; countUnembeddedSymbols(): number; getUnsummarizedSymbols(kinds: string[], limit: number): { id: number; name: string; fqn: string | null; kind: string; signature: string | null; file_path: string; byte_start: number; byte_end: number; }[]; createNode(nodeType: string, refId: number): number; getNodeId(nodeType: string, refId: number): number | undefined; insertEdge(sourceNodeId: number, targetNodeId: number, edgeTypeName: string, resolved?: boolean, metadata?: Record, isCrossWs?: boolean, resolutionTier?: string): TraceMcpResult; deleteEdgesForFileNodes(fileId: number): void; deleteOutgoingImportEdges(fileId: number): void; deleteOutgoingEdgesForFileNodes(fileId: number): void; traverseEdges(startNodeId: number, direction: 'outgoing' | 'incoming', depth: number): EdgeRow[]; getEdgesByType(edgeTypeName: string): EdgeRow[]; getOutgoingEdges(nodeId: number): (EdgeRow & { edge_type_name: string; })[]; getIncomingEdges(nodeId: number): (EdgeRow & { edge_type_name: string; })[]; ensureEdgeType(name: string, category: string, description: string): void; getEdgeTypeName(edgeTypeId: number): string | undefined; getNodeRef(nodeId: number): { nodeType: string; refId: number; } | undefined; getNodeByNodeId(nodeId: number): { node_type: string; ref_id: number; } | undefined; getNodeIdsBatch(nodeType: string, refIds: number[]): Map; getNodeRefsBatch(nodeIds: number[]): Map; getEdgesForNodesBatch(nodeIds: number[]): Array; getEdgeTypes(): EdgeTypeRow$1[]; insertRoute(route: RawRoute, fileId: number): number; getRouteByUriAndMethod(uri: string, method: string): RouteRow | undefined; getAllRoutes(): RouteRow[]; updateRouteUri(id: number, uri: string): void; findRouteByPattern(uri: string, method: string): RouteRow | undefined; insertComponent(comp: RawComponent, fileId: number): number; getComponentByFileId(fileId: number): ComponentRow | undefined; getComponentByName(name: string): ComponentRow | undefined; getAllComponents(): ComponentRow[]; insertMigration(mig: RawMigration, fileId: number): number; getMigrationsByTable(tableName: string): MigrationRow[]; getAllMigrations(): MigrationRow[]; insertOrmModel(model: RawOrmModel, fileId: number): number; getOrmModelByName(name: string): OrmModelRow | undefined; getOrmModelsByOrm(orm: string): OrmModelRow[]; getAllOrmModels(): OrmModelRow[]; insertOrmAssociation(sourceModelId: number, targetModelId: number | null, targetModelName: string, kind: string, options?: Record, fileId?: number, line?: number): number; getAllOrmAssociations(fileIds?: number[]): OrmAssociationRow[]; getOrmAssociationsByModel(modelId: number): OrmAssociationRow[]; insertRnScreen(screen: RawRnScreen, fileId: number): number; getRnScreenByName(name: string): RnScreenRow | undefined; getAllRnScreens(): RnScreenRow[]; insertEnvVar(fileId: number, entry: { key: string; valueType: string; valueFormat: string | null; comment: string | null; quoted: boolean; line: number; }): number; deleteEnvVarsByFile(fileId: number): void; getEnvVarsByFile(fileId: number): EnvVarRow$1[]; getAllEnvVars(): (EnvVarRow$1 & { file_path: string; })[]; searchEnvVars(pattern: string): (EnvVarRow$1 & { file_path: string; })[]; getWorkspaceStats(): WorkspaceStats$1[]; getCrossWorkspaceEdges(): CrossWorkspaceEdge$1[]; getWorkspaceDependencyGraph(): WorkspaceDependency$1[]; getWorkspaceExports(workspace: string): SymbolWithFilePath[]; /** Read a single repo-metadata value. Returns null when the key is absent. */ getRepoMetadata(key: string): string | null; /** Upsert a repo-metadata value. */ setRepoMetadata(key: string, value: string): void; getStats(): IndexStats; insertGraphSnapshot(snapshotType: string, data: Record, commitHash?: string, filePath?: string): number; getGraphSnapshots(snapshotType: string, options?: { filePath?: string; since?: string; limit?: number; }): GraphSnapshotRow$1[]; pruneGraphSnapshots(maxAge?: number): number; } declare class PluginRegistry { private languagePlugins; private frameworkPlugins; private _extMap; private _activeFrameworkCache; registerLanguagePlugin(plugin: LanguagePlugin): void; registerFrameworkPlugin(plugin: FrameworkPlugin): void; /** Register the full set of built-in language and framework plugins. */ registerDefaults(): void; static createWithDefaults(): PluginRegistry; getLanguagePlugins(): LanguagePlugin[]; getActiveFrameworkPlugins(ctx: ProjectContext): TraceMcpResult; /** Clear per-pipeline-run caches. Call at start of each indexing run. */ clearCaches(): void; getAllFrameworkPlugins(): FrameworkPlugin[]; getLanguagePluginForFile(filePath: string): LanguagePlugin | undefined; /** * Resolve a language plugin from a `#!` shebang on the first line. * graphify v0.6.2 added the same fallback so extensionless scripts * (`bin/deploy`, `scripts/migrate`, etc.) actually get indexed instead * of dropping out silently. We map the interpreter to the existing * extension-based plugin lookup — so adding `.py` to a plugin * automatically enables it for Python shebangs too. */ getLanguagePluginByShebang(firstBytes: Buffer | string): LanguagePlugin | undefined; /** * Convenience that combines extension lookup with a shebang fallback. * Falls back to shebang only when the extension lookup fails — keeps * the fast path identical for the 99% of files with a normal extension. */ getLanguagePluginForFileWithFallback(filePath: string, firstBytes?: Buffer | string): LanguagePlugin | undefined; private getExtensionMap; private sortByPriority; private topologicalSort; } /** * Session journal — tracks tool calls, deduplicates queries, and flags zero-result repeats. * * Two-phase API: * 1. checkDuplicate(tool, params) — before executing, returns warning if duplicate * 2. record(tool, params, resultCount) — after executing, logs the result */ interface JournalEntry { tool: string; params_hash: string; params_summary: string; result_count: number; timestamp: number; /** Compact snapshot of the result (no source bodies) for dedup responses */ compact_result?: Record; /** Estimated token size of the full response */ result_tokens?: number; } /** * Returned by checkDuplicate when a previous result exists. * `action` tells the caller what to do: * - 'warn' → still execute, but prepend warning (search tools — results may differ) * - 'dedup' → skip execution, return compact_result instead (content-heavy tools) */ interface DuplicateInfo { action: 'warn' | 'dedup'; message: string; compact_result: Record | null; saved_tokens: number; } interface JournalSummary { total_entries: number; files_read: string[]; searches_with_zero_results: string[]; duplicate_queries: string[]; } interface PrefetchBoost { /** File path that was frequently accessed after get_task_context */ file: string; /** Number of times this follow-up pattern was observed */ frequency: number; } /** Structural landmark — a central symbol that should survive context compaction */ interface StructuralLandmark { symbol_id: string; name: string; kind: string; file: string; line: number | null; /** Why this symbol is a landmark */ reason: 'pagerank' | 'recently_edited'; /** PageRank score (only for pagerank landmarks) */ score?: number; } /** Provider callback that returns landmarks from the index */ type LandmarkProvider = () => StructuralLandmark[]; /** Structured snapshot for programmatic consumption */ interface SessionSnapshotStructured { duration_seconds: number; total_calls: number; files_explored: number; focus_files: Array<{ path: string; reads: number; last_tool: string; }>; edited_files: string[]; key_searches: Array<{ query: string; results: number; }>; dead_ends: Array<{ query: string; reason: string; }>; /** Structural landmarks: central + recently changed symbols */ landmarks?: StructuralLandmark[]; } /** Full snapshot result returned by getSnapshot() */ interface SessionSnapshot { snapshot: string; structured: SessionSnapshotStructured; estimated_tokens: number; } declare class SessionJournal { private entries; private filesRead; private zeroResultQueries; private allHashes; private dedupSavedTokens; /** Tracks timestamps of get_task_context calls for follow-up analysis */ private taskContextTimestamps; /** Path for periodic snapshot flushing (set via enablePeriodicSnapshot) */ private snapshotPath; private snapshotFlushInterval; /** Optional provider for structural landmarks (PageRank + recently edited symbols) */ private landmarkProvider; /** Hard cap on in-memory entries. A long-running daemon session can push * through tens of thousands of tool calls; without a cap the entries[] * array (plus allHashes / taskContextTimestamps) grows unbounded. When * exceeded we drop the oldest entries — periodic snapshots have already * persisted them to disk so nothing is lost for cross-session context. */ private static readonly MAX_ENTRIES; /** Drop the oldest 10% in one pass to amortise the splice cost. */ private static readonly DROP_BATCH; /** Tools whose results are content-heavy and safe to dedup (deterministic for same params) */ private static readonly DEDUP_TOOLS; /** * Check if this exact call was made before. Call BEFORE executing the tool. * Returns DuplicateInfo with action='dedup' for content-heavy tools (skip execution), * or action='warn' for search tools (still execute but warn). * Returns null for first-time calls. */ checkDuplicate(tool: string, params: Record): DuplicateInfo | null; /** * Record a tool call AFTER execution with the result count. * For dedup-able tools, also store a compact snapshot of the result. */ record(tool: string, params: Record, resultCount: number, opts?: { compactResult?: Record; resultTokens?: number; }): void; /** * Drop the oldest DROP_BATCH entries and rebuild secondary indexes from the * survivors. Cheaper than evicting one-by-one because we only walk the * survivors once. */ private evictOldest; /** * Enable periodic snapshot flushing to a file. * After enabling, every `interval` tool calls the snapshot is written to disk * so that the PreCompact hook can read it. */ enablePeriodicSnapshot(snapshotPath: string, interval?: number): void; /** * Set a provider for structural landmarks (PageRank top symbols + recently edited). * Called during snapshot generation to inject landmarks that survive context compaction. */ setLandmarkProvider(provider: LandmarkProvider): void; /** Record tokens saved by deduplication */ recordDedupSaving(tokens: number): void; /** Total tokens saved by dedup this session */ getDedupSavedTokens(): number; /** * Analyze what files/symbols agents typically request AFTER a get_task_context call. * Returns files that are frequently accessed as follow-ups, suggesting they should * be included in future task context results. * * Uses only data from the current session. Cross-session learning uses * persisted session summaries (see session-resume.ts). */ getPrefetchBoosts(): PrefetchBoost[]; private extractFileFromEntry; /** Get a summary of the session journal */ getSummary(): JournalSummary; /** Get all entries */ getEntries(): JournalEntry[]; /** * Detect wasteful usage patterns and return an optimization hint, or null if everything looks fine. * Called after each tool execution to provide real-time coaching. */ getOptimizationHint(currentTool: string, currentParams: Record): string | null; /** * Generate a compact session snapshot (~200 tokens) for context injection after compaction. * Returns both human-readable markdown and structured data. */ getSnapshot(opts?: { maxFiles?: number; maxSearches?: number; maxEdits?: number; includeNegativeEvidence?: boolean; }): SessionSnapshot; /** * Write current snapshot to a file so the PreCompact hook can read it. * Called periodically by the server (e.g. every 5 tool calls). */ flushSnapshotFile(snapshotPath: string): void; private extractFile; private isSearchTool; private buildSummary; private hash; /** Free session memory. Flushes final snapshot before clearing. */ dispose(): void; } /** * Journal broadcast helper. * * Converts SessionJournal entries into the `journal_entry` SSE event shape * expected by the Activity tab, and provides a snapshot builder for the * GET /api/projects/journal endpoint. */ /** * Shape passed to the `onJournalEntry` callback in installToolGate. * This is the raw data before the `type` discriminant is added. */ interface JournalEntryCallbackData { project: string; ts: number; tool: string; params_summary: string; result_count: number; result_tokens?: number; latency_ms?: number; is_error: boolean; session_id: string; } /** * Day-bucketed JSONL audit log for decision-store mutations. * * Optional, opt-in (`memory.audit_log.enabled`). When enabled, every * successful add/update/invalidate is mirrored as a one-line JSON entry * to `/YYYY-MM-DD.jsonl`. The audit write is best-effort and * synchronous — wrapped in try/catch inside the store so a failed audit * write never affects the SQLite mutation it shadows. * * Day-rollover is checked on each call (`new Date().toISOString().slice(0,10)`), * which keeps the logger stateless apart from the cached file handle / date. * Cheap enough to do per-call without booking dedicated rotation logic. */ type AuditOp = 'add' | 'update' | 'invalidate'; interface AuditEntry { op: AuditOp; decision_id: number; title?: string; type?: string; /** ISO timestamp; defaults to `new Date().toISOString()` when omitted. */ ts?: string; } interface AuditLogger { /** Append one entry to today's JSONL file. Throws only on programmer error. */ log(entry: AuditEntry): void; /** No-op for the file-based logger; reserved for future flushable backends. */ close(): void; } /** * Decision types — shared shape definitions for the decision store and its * helper modules (clusterer, confidence scorer, consolidator, tuner). * * Extracted from `decision-store.ts` to break import cycles: the helpers * needed these types as inputs while the store needed the helpers as * functions, producing circular imports. Splitting the type surface lets * both sides import from a single leaf module instead. * * `decision-store.ts` re-exports these names for backwards compatibility, * so external callers keep importing from `./decision-store.js`. */ type DecisionType = 'architecture_decision' | 'tech_choice' | 'bug_root_cause' | 'preference' | 'tradeoff' | 'discovery' | 'convention'; interface DecisionRow { id: number; /** Short title / summary */ title: string; /** Full content — the actual decision text, reasoning, context */ content: string; type: DecisionType; /** Project root this decision belongs to */ project_root: string; /** Optional: subproject name within the project (e.g., 'auth-api', 'user-service') */ service_name: string | null; /** Optional: symbol FQN this decision is about */ symbol_id: string | null; /** Optional: file path this decision is about */ file_path: string | null; /** Optional: tags for categorization (JSON array) */ tags: string | null; /** ISO timestamp when the decision became valid */ valid_from: string; /** ISO timestamp when the decision was invalidated (null = still active) */ valid_until: string | null; /** Session ID that produced this decision (for provenance) */ session_id: string | null; /** Source: 'manual' (user added), 'mined' (extracted from logs), 'auto' (hook-extracted) */ source: 'manual' | 'mined' | 'auto'; /** Confidence score 0..1 for mined decisions */ confidence: number; /** * Git branch the decision was captured on. NULL = branch-agnostic * (captured outside a git repo, on detached HEAD, or pre-feature legacy data). */ git_branch: string | null; /** * Memoir-style review status: * - `null` — auto-approved or legacy row (default; visible by default) * - `'pending'` — captured at borderline confidence; awaiting human review * - `'approved'` — explicitly approved by a human (visible by default) * - `'rejected'` — explicitly rejected by a human (hidden by default) */ review_status: 'pending' | 'approved' | 'rejected' | null; created_at: string; /** Unix millisecond timestamp of the last write (insert, update, or invalidate). */ updated_at: number | null; /** * Number of times this decision has been recalled by a read-side surface * (query_decisions, get_wake_up, get_decision_timeline). Drives the heat * term in `computeHeat`. Defaults to 0 for legacy rows. */ hit_count: number; /** * ISO timestamp of the most recent recall hit, or null when the decision * has never been recalled. Combined with `hit_count` to compute time-decay * heat in `computeHeat`. */ last_hit_at: string | null; } interface DecisionInput { title: string; content: string; type: DecisionType; project_root: string; /** Subproject name within the project (e.g., 'auth-api') */ service_name?: string; symbol_id?: string; file_path?: string; tags?: string[]; valid_from?: string; session_id?: string; source?: 'manual' | 'mined' | 'auto'; confidence?: number; /** * Git branch the decision was captured on. Omit / pass `null` for branch-agnostic * decisions (queryable from every branch). The capture path resolves this from * the project root automatically when omitted. */ git_branch?: string | null; /** * Memoir-style review status. Omit (or pass `null`) for auto-approved / * legacy rows; pass `'pending'` when capture confidence is borderline so * the row goes into the human review queue. */ review_status?: 'pending' | 'approved' | 'rejected' | null; } /** * Decision Consolidator (P2.2) — LLM-driven semantic dedup over the active * decision store. * * The L0 mining pipeline (regex + LLM) protects against exact-duplicate * captures via a sha256 dedup hash over title|content|type|symbol|file. Two * different phrasings of the same decision survive as separate rows. This * module pulls top-K similar candidates for each subject decision and asks * the LLM to decide store / merge / replace / drop — Tencent-style L1 * deduplication adapted to a knowledge graph rather than a chat session. * * Privacy * ─────── * The LLM input is a SANITISED projection of each decision: id + title + * type + first 300 chars of `content` + tags. Per-row PII fields like * `session_id`, `project_root`, `created_by`, and `git_branch` never leave * this process. * * Robustness * ────────── * - LLM output is parsed as strict JSON; malformed entries are dropped * individually. Whole-call failures (network, malformed top-level) fall * back to `{kind:'keep_separate'}` so the consolidator is non-destructive * on degraded LLM responses. * - Each returned `existing_id` is intersected with the candidate id set — * the LLM cannot invent ids. * - The verdict is shaped to match exactly one candidate; a subject + N * candidates produces up to N verdicts (often fewer when the LLM judges * most as `keep_separate` by omission). */ /** * What the LLM decided for a single (subject, candidate) pair. * * - `keep_separate` — they're genuinely different; no action. * - `merge_into_existing` — subject restates the candidate; merge content * into the existing row, invalidate the subject. * - `replace_existing` — subject is a clear refinement; invalidate the * existing, keep the subject as-is. * - `invalidate_existing` — the candidate is obsoleted by the subject and * the subject also lacks lasting value. Rare. */ type ConsolidationVerdict = { kind: 'keep_separate'; } | { kind: 'merge_into_existing'; existing_id: number; merged_content_hint?: string; } | { kind: 'replace_existing'; existing_id: number; } | { kind: 'invalidate_existing'; existing_id: number; }; /** * Decision Store — persistent knowledge graph for architectural decisions, * tech choices, bug root causes, and preferences. * * Stored in ~/.trace-mcp/decisions.db (separate from per-repo code DBs). * Each decision has temporal validity (valid_from / valid_until) and optional * linkage to code symbols/files, enabling code-aware memory queries. */ interface DecisionQuery { project_root?: string; /** Filter by subproject name within the project */ service_name?: string; type?: DecisionType; symbol_id?: string; file_path?: string; tag?: string; /** Only return decisions active at this timestamp (default: now) */ as_of?: string; /** Include invalidated decisions (default: false) */ include_invalidated?: boolean; /** Full-text search query */ search?: string; /** * Git-branch filter. Three modes: * - `'all'` — every branch (no filter) * - `string` (other) — only that branch + branch-agnostic (NULL) rows * - `null` — only branch-agnostic (NULL) rows * - omitted/`undefined` — no filter (back-compat: equivalent to `'all'`) * Callers that want "current branch + NULL" should resolve the branch first * (see `getCurrentBranch`) and pass the resolved name. */ git_branch?: string | null | 'all'; /** * Review-queue filter (memoir-style confidence tiers). * Default behaviour returns auto-approved (`NULL`) and explicitly-approved * rows so the review queue stays out of regular queries. * * - omitted — only `NULL` + `'approved'` rows * - `include_pending` — convenience flag; when true also returns `'pending'` * - `review_status` — restrict to that exact status (overrides default) */ include_pending?: boolean; review_status?: 'pending' | 'approved' | 'rejected'; /** * Result ordering. * - `'recency'` (default) — `valid_from DESC` (existing behaviour) * - `'created_at'` — `created_at DESC` * - `'heat'` — computed in JS via `computeHeat`; rows fetched * with a safety cap (limit * 3, capped at 500) * and sorted before truncation. Falls back to * recency when the heat subsystem is disabled. */ order_by?: 'recency' | 'heat' | 'created_at'; /** * Heat scoring overrides for `order_by='heat'`. Optional — defaults come * from `memory.heat.*` config or hard-coded defaults in `computeHeat`. */ heat_half_life_days?: number; heat_freshness_days?: number; /** * When `order_by='heat'` is requested but the heat subsystem is disabled * (config flag), callers can pass this flag to opt-out of the graceful * fallback and surface an explicit error. Reserved for future use. */ limit?: number; offset?: number; } interface DecisionTimelineEntry { id: number; title: string; type: DecisionType; valid_from: string; valid_until: string | null; is_active: boolean; } interface SessionChunkInput { session_id: string; project_root: string; chunk_index: number; role: 'user' | 'assistant'; content: string; timestamp: string; referenced_files?: string[]; } interface SessionSearchResult { chunk_id: number; session_id: string; role: string; content: string; timestamp: string; referenced_files: string | null; rank: number; } interface ClusterRow { id: number; project_root: string; service_name: string | null; title: string; summary: string; /** JSON array of tag strings (or null). */ tags: string | null; primary_type: DecisionType | null; decision_count: number; created_at: string; /** Unix millisecond timestamp of the last write. */ updated_at: number; } interface ClusterInput { project_root: string; service_name?: string | null; title: string; summary: string; tags?: string[]; primary_type?: DecisionType | null; decision_ids: number[]; } interface ClusterQuery { project_root?: string; service_name?: string; /** Full-text search across title + summary + tags (FTS5 with porter stemming). */ search?: string; /** * Sort order. * - 'decision_count' (default) — largest clusters first * - 'updated_at' — most-recently-rebuilt first * - 'title' — alphabetical */ order_by?: 'decision_count' | 'updated_at' | 'title'; limit?: number; offset?: number; } interface ProjectMemoRow { id: number; project_root: string; /** Null for project-wide memos; subproject name for per-service memos. */ service_name: string | null; memo_md: string; version: number; /** Identifier of the LLM that produced this memo (provider + model hint). */ model: string | null; created_at: string; updated_at: string; /** Highest decision.id at the time of generation — used to compute drift. */ last_decision_id: number | null; /** Decision count in scope when this memo was generated. */ decisions_at_generation: number; /** Cluster count in scope when this memo was generated. */ clusters_at_generation: number; /** Rough chars/4 token estimate of memo_md at write time. */ estimated_tokens: number; } /** * Persisted per-project background-scheduler bookkeeping. Restored on * daemon start so a restart does NOT re-run every stage on tick 1. */ interface SchedulerStateRow { project_root: string; last_mine_at: number | null; last_cluster_at: number | null; last_memo_at: number | null; last_tune_at: number | null; last_tune_event_count: number | null; consecutive_failures: number; updated_at: string; } declare class DecisionStore { readonly db: Database.Database; /** * Optional day-bucketed JSONL audit logger. When set, every successful * add/update/invalidate is mirrored to the configured directory. Audit * writes are best-effort — failures never bubble out of the mutation * methods. */ private auditLogger; /** * Bounded retention for `project_memos`: keep at most N rows per * (project_root, service_name) scope. The retention prune runs in the * same transaction as the INSERT inside `saveProjectMemo`, so the table * can never exceed `historyLimit * scopes` at rest. Defaults to 10 — the * historical UI default for `listProjectMemos`. */ private memoHistoryLimit; constructor(dbPath: string, opts?: { readonly?: boolean; auditLogger?: AuditLogger | null; memoHistoryLimit?: number; }); /** * Best-effort audit-log emit. Wrapped in try/catch so a failed JSONL * write never affects the underlying SQLite mutation. No-op when no * logger is configured. */ private auditEmit; private checkpointTimer; /** * Periodic `wal_checkpoint(TRUNCATE)` keeps `*-wal` from growing forever in * a long-running daemon. SQLite auto-checkpoints on the writer thread, but * only when the WAL crosses 1000 frames — heavy read traffic with light * writes can stall a checkpoint indefinitely. TRUNCATE truncates the WAL * back to zero after a successful checkpoint. */ private scheduleWalCheckpoint; private cancelWalCheckpoint; /** * Fix legacy schemas BEFORE DDL runs — prevents crashes when * CREATE INDEX references columns that don't exist in old tables. */ private preMigrate; private migrate; /** * Update mutable fields on an existing decision. * Returns the updated row, or undefined when the id does not exist. */ updateDecision(id: number, fields: Partial, 'tags'> & { /** Either a string[] (will be JSON-stringified) or a raw JSON string. */ tags?: string[] | string | null; }>): DecisionRow | undefined; close(): void; addDecision(input: DecisionInput): DecisionRow; addDecisions(inputs: DecisionInput[]): number; getDecision(id: number): DecisionRow | undefined; /** * Memoir-style review queue actions: stamp a decision as approved or * rejected (or back to pending). Returns true when a row was actually * updated. Backing for the `approve_decision` / `reject_decision` MCP * tools and the `/api/projects/decisions/:id/review` HTTP endpoint. */ setReviewStatus(id: number, status: 'pending' | 'approved' | 'rejected', opts?: { reviewer?: string | null; }): boolean; /** * Insert one row into decision_reviews for the given decision. Recomputes * confidence on the fly so the review log always carries the score the * scorer would assign right now — which is what the tuner needs to compare * against the human label. */ private insertReviewEvent; /** * Stream review events (approve/reject toggles) for the weight-tuner. When * `projectRoot` is set, results are filtered to reviews whose underlying * decision belongs to that project. Returns rows with signals already * parsed back into a structured object. */ listReviewEvents(opts?: { project_root?: string; limit?: number; }): Array<{ id: number; decision_id: number; action: 'approve' | 'reject'; signals: { has_code_ref: boolean; content_length: number; tag_count: number; type: string; has_service: boolean; }; confidence_at_decision: number; reviewed_at: string; reviewer: string | null; }>; /** * Count of rows currently in the review queue (`review_status = 'pending'`). * Used by the Memory Explorer Review tab badge and the wake-up surface. */ countPendingReviews(projectRoot?: string): number; invalidateDecision(id: number, validUntil?: string): boolean; deleteDecision(id: number): boolean; queryDecisions(query: DecisionQuery): DecisionRow[]; /** * Record one or more recall hits. Increments `hit_count` and stamps * `last_hit_at` to now() for every existing id in a single transaction. * Missing ids are silently ignored — callers don't care, and the * UPDATE's `WHERE id = ?` natively no-ops. Safe to call fire-and-forget. */ recordHits(decisionIds: number[]): void; /** * Compute the heat score for a single decision. Returns 0 when the * decision does not exist, so callers can treat this as a safe accessor. */ getHeat(id: number, params?: { halfLifeDays?: number; freshnessDays?: number; }): number; /** * Return the hottest N decisions (highest heat first). Filters mirror the * common query knobs: project_root, service_name, git_branch. Only active * (non-invalidated) and visible (NULL/approved) rows are considered. */ getHottest(opts: { project_root?: string; service_name?: string; git_branch?: string; limit?: number; halfLifeDays?: number; freshnessDays?: number; }): DecisionRow[]; getTimeline(opts: { project_root?: string; symbol_id?: string; file_path?: string; limit?: number; }): DecisionTimelineEntry[]; getStats(projectRoot?: string): { total: number; active: number; invalidated: number; by_type: Record; by_source: Record; }; isSessionMined(sessionPath: string): boolean; /** * @deprecated Use {@link updateSessionCursor} for incremental cursor-aware * mining. Retained for back-compat with callers that haven't migrated. * Internally writes cursor_offset=0, last_size=0, last_modified_ms=now — * which means the next {@link getSessionCursor} call will treat the file * as "grown" and re-read it in full. That's intentional fallback behaviour. */ markSessionMined(sessionPath: string, decisionsFound: number): void; getMinedSessionCount(): number; /** * Get the next-read cursor for a previously-mined session. * * Semantics: * - Row missing → `{ cursor: 0, reason: 'unmined' }` * - File shrank (current size < recorded last_size) → `{ cursor: 0, reason: 'restart_shrunk' }` * Indicates rotation/truncation; caller should restart from offset 0. * - File unchanged (same size + same mtime) → `null` (caller should skip) * - File grew → `{ cursor: cursor_offset, reason: 'incremental' }` * * `currentModifiedMs` is optional. When supplied, an unchanged-size + * unchanged-mtime pair returns `null`; when omitted, the unchanged-size * branch still skips. Pass it whenever you have it (you already `statSync`). */ getSessionCursor(sessionPath: string, currentSize: number, currentModifiedMs?: number): { cursor: number; reason: 'unmined' | 'restart_shrunk' | 'incremental'; } | null; /** * Atomically update the cursor after a successful mining pass. * Idempotent: re-running with identical inputs is a no-op write. */ updateSessionCursor(opts: { sessionPath: string; /** New byte offset (= total bytes consumed). */ cursor: number; /** Current file size on disk after the read. */ size: number; /** Current file mtime in ms after the read. */ modifiedMs: number; /** Delta of decisions extracted this pass. */ decisionsFound: number; }): void; /** Marker returned by {@link getProviderSessionCursor}. `null` means "no * changes since last pass — skip without iterating messages". */ getProviderSessionCursor(sessionKey: string, currentSeenSize?: number, currentSeenModifiedMs?: number): { lastTimestampMs: number; lastMessageId: string; reason: 'unmined' | 'restart_shrunk' | 'incremental'; } | null; /** Atomically update the provider-session cursor after a successful pass. * Idempotent: identical inputs are a no-op write. */ updateProviderSessionCursor(opts: { sessionKey: string; /** Highest message timestamp consumed this pass (ms since epoch). */ lastTimestampMs: number; /** Stable per-message id (string) — used as a tie-breaker when two * messages share a timestamp. Empty string is allowed. */ lastMessageId: string; /** Current SessionHandle.sizeBytes seen this pass — for rotation detection. * Pass 0 when the provider does not report a size. */ seenSize: number; /** Current SessionHandle.lastModifiedMs seen this pass. */ seenModifiedMs: number; /** Delta of decisions extracted this pass. */ decisionsFound: number; }): void; getCachedLlmExtraction(sessionId: string, contentSha: string, model: string): string | null; putCachedLlmExtraction(sessionId: string, contentSha: string, model: string, extractedJson: string): void; /** Get all decisions linked to a specific symbol */ getDecisionsForSymbol(symbolId: string, activeOnly?: boolean): DecisionRow[]; /** Get all decisions linked to a specific file */ getDecisionsForFile(filePath: string, activeOnly?: boolean): DecisionRow[]; /** Get decisions linked to any file matching a pattern (e.g., 'src/auth/%') */ getDecisionsForPath(pathPattern: string, activeOnly?: boolean): DecisionRow[]; /** Get all decisions for a specific subproject within a project */ getDecisionsForService(serviceName: string, projectRoot?: string, activeOnly?: boolean): DecisionRow[]; /** Get all distinct service names in a project */ getServiceNames(projectRoot: string): string[]; addSessionChunks(chunks: SessionChunkInput[]): number; isSessionIndexed(sessionId: string): boolean; searchSessions(query: string, opts?: { project_root?: string; limit?: number; }): SessionSearchResult[]; getSessionChunkCount(projectRoot?: string): number; getIndexedSessionIds(projectRoot?: string): string[]; /** * Insert a cluster + its membership rows in a single transaction. * `decision_count` is set from the input `decision_ids.length`. * Returns the inserted ClusterRow. */ createCluster(input: ClusterInput): ClusterRow; /** * Update an existing cluster's title/summary/tags/primary_type and * replace its membership list. Used when `build_decision_clusters` * merges a freshly-computed cluster into an existing row. */ updateCluster(id: number, input: { title?: string; summary?: string; tags?: string[]; primary_type?: DecisionType | null; decision_ids?: number[]; }): ClusterRow | undefined; /** Single-cluster lookup by id. */ getCluster(id: number): ClusterRow | undefined; /** * List clusters with optional project/service/search filters. * Default ordering is by `decision_count DESC` (largest first) so the * wake-up "topics" surface lands the biggest topics on top. */ listClusters(query?: ClusterQuery): ClusterRow[]; /** * Get the member decisions of a cluster. Respects active-only by default * (matches `query_decisions` defaults). `include_invalidated` returns * the full historical membership. */ getClusterDecisions(clusterId: number, opts?: { limit?: number; include_invalidated?: boolean; }): DecisionRow[]; /** * Find all clusters a decision belongs to. Used by surfaces that want * to annotate a decision with its parent topic(s). */ findClustersForDecision(decisionId: number): ClusterRow[]; /** * Delete a cluster. The `ON DELETE CASCADE` on * `decision_cluster_members.cluster_id` drops the membership rows * automatically. Returns true when a row was removed. */ deleteCluster(id: number): boolean; /** * Drop every cluster (and cascade-drop their membership rows) for a * scope. Used by `build_decision_clusters` when `force=true` to start * from a clean slate. Returns the number of cluster rows removed. */ deleteClustersForScope(opts: { project_root?: string; service_name?: string; }): number; /** Count clusters in a scope (used by tests + stats). */ countClusters(opts?: { project_root?: string; service_name?: string; }): number; /** * Insert a new project memo row. Computes `version` as `prev.version + 1` * when an earlier memo exists for the same (project_root, service_name) * scope, otherwise 1. Returns the new {id, version}. */ saveProjectMemo(input: { project_root: string; service_name?: string; memo_md: string; model?: string; last_decision_id?: number; decisions_at_generation: number; clusters_at_generation: number; estimated_tokens: number; }): { id: number; version: number; }; /** * Return the most-recent memo for a (project_root, service_name) scope, or * undefined when no memo has been generated. `service_name` semantics * mirror the decisions/clusters surfaces — omit for the project-wide memo, * supply a name for the per-service one. */ getLatestProjectMemo(opts: { project_root: string; service_name?: string; }): ProjectMemoRow | undefined; /** * List historical memos in a scope (most-recent first). Default limit 10. * Used by `get_project_memo` when `include_history=true`. */ listProjectMemos(opts: { project_root: string; service_name?: string; limit?: number; }): ProjectMemoRow[]; /** * Count of decisions whose id > the latest memo's `last_decision_id`, * scoped to the same project (+ optional service). Drives the auto- * regen threshold in `regenerate_project_memo`. When no prior memo * exists, returns the total active decision count in scope. */ countDecisionsSinceLastMemo(opts: { project_root: string; service_name?: string; }): number; /** Fetch the persisted scheduler state for a project, or undefined. */ getSchedulerState(projectRoot: string): SchedulerStateRow | undefined; /** * Upsert per-project scheduler state. Merge semantics: * - `undefined` fields PRESERVE the existing column value (no overwrite). * - `null` fields EXPLICITLY clear the column. * - `consecutive_failures` defaults to existing value on update, * or 0 on insert. * - `updated_at` is always stamped to ISO `now()`. * * Runs inside a transaction so the read-then-write merge is atomic. */ upsertSchedulerState(input: { project_root: string; last_mine_at?: number | null; last_cluster_at?: number | null; last_memo_at?: number | null; last_tune_at?: number | null; last_tune_event_count?: number | null; consecutive_failures?: number; }): void; /** * Return up to `topK` candidates similar to the given subject, sorted by * title trigram similarity (descending). Combines: * - FTS5 match on the subject's title words (OR-joined; quoted for * safety against FTS operators in titles) * - same-`type` filter when `same_type_only` * - trigram Jaccard floor (`min_title_similarity`) * - excludes the subject itself, optionally invalidated rows * * Returns [] when the subject has no FTS-extractable title words and * no candidate set can be produced cheaply. */ findSimilarDecisions(opts: { subject_id: number; topK?: number; min_title_similarity?: number; same_type_only?: boolean; project_root?: string; active_only?: boolean; }): DecisionRow[]; /** * Atomically apply one consolidation verdict. Returns whether any write * happened and which row ids were touched (useful for the MCP response * envelope and audit logs). * * Verdict semantics: * - `keep_separate` → no-op, applied=false * - `merge_into_existing` → update existing.content (concat unless * `merged_content` is provided), union tags, invalidate subject * - `replace_existing` → invalidate existing, subject untouched * - `invalidate_existing` → invalidate existing only * * Runs in a single transaction so a mid-flight failure rolls back * cleanly. Returns `applied:false` when either row is missing or already * invalidated (defensive — the LLM may pick a row that was just * invalidated by a prior verdict in the same batch). */ applyConsolidationVerdict(opts: { subject_id: number; verdict: ConsolidationVerdict; /** Optional: caller-supplied merged content. If absent, plain concat. */ merged_content?: string; }): { applied: boolean; affected_ids: number[]; }; } /** * Indexing progress tracking. * Shared mutable state object — pipelines write, MCP tools + CLI read. * Progress is also persisted to SQLite for cross-process access (CLI `status` command). */ type PipelinePhase = 'idle' | 'running' | 'completed' | 'skipped' | 'error'; type PipelineName = 'indexing' | 'summarization' | 'embedding'; interface PipelineProgress { phase: PipelinePhase; processed: number; total: number; startedAt: number; completedAt: number; error?: string; } interface PipelineProgressSnapshot extends PipelineProgress { percentage: number | null; elapsedMs: number; } interface ProgressSnapshot { indexing: PipelineProgressSnapshot; summarization: PipelineProgressSnapshot; embedding: PipelineProgressSnapshot; } type ProgressListener = (name: PipelineName, progress: PipelineProgress) => void; declare class ProgressState { indexing: PipelineProgress; summarization: PipelineProgress; embedding: PipelineProgress; private db; private listeners; constructor(db?: Database.Database); /** Register a callback for progress updates. Returns unsubscribe function. */ onUpdate(listener: ProgressListener): () => void; update(name: PipelineName, partial: Partial): void; snapshot(): ProgressSnapshot; private persist; private loadFromDb; } /** * Topology Store — manages the cross-service topology database (~/.trace-mcp/topology.db). * Separate from per-repo DBs. Stores services, API contracts, endpoints, events, and cross-service edges. */ interface ServiceRow { id: number; name: string; repo_root: string; db_path: string; service_type: string | null; detection_source: string | null; project_group: string | null; metadata: string | null; indexed_at: string; } interface ContractRow { id: number; service_id: number; contract_type: string; spec_path: string; version: string | null; content_hash: string | null; parsed_spec: string; indexed_at: string; } interface EndpointRow { id: number; contract_id: number; service_id: number; method: string | null; path: string; operation_id: string | null; request_schema: string | null; response_schema: string | null; metadata: string | null; } interface EventChannelRow { id: number; contract_id: number | null; service_id: number; channel_name: string; direction: string; payload_schema: string | null; metadata: string | null; } interface CrossServiceEdgeRow { id: number; source_service_id: number; target_service_id: number; edge_type: string; source_ref: string | null; target_ref: string | null; confidence: number; metadata: string | null; } interface SubprojectRow { id: number; name: string; repo_root: string; project_root: string; db_path: string | null; contract_paths: string | null; added_at: string; last_synced: string | null; metadata: string | null; } interface ClientCallRow { id: number; source_repo_id: number; target_repo_id: number | null; file_path: string; line: number | null; call_type: string; method: string | null; url_pattern: string; matched_endpoint_id: number | null; confidence: number; metadata: string | null; } interface ContractSnapshotRow { id: number; contract_id: number; service_id: number; version: string | null; spec_path: string; content_hash: string; endpoints_json: string; events_json: string; snapshot_at: string; } declare class TopologyStore { readonly db: Database.Database; constructor(dbPath: string, opts?: { readonly?: boolean; }); /** * Fix legacy schemas BEFORE DDL runs — prevents crashes when * CREATE INDEX references columns that don't exist in old tables. */ private preMigrate; private migrate; /** Run a migration block exactly once, tracked by key in topology_meta. */ private runOnce; close(): void; upsertService(input: { name: string; repoRoot: string; dbPath: string; serviceType?: string; detectionSource?: string; projectGroup?: string; metadata?: Record; }): number; getService(name: string): ServiceRow | undefined; getAllServices(): ServiceRow[]; updateServiceGroup(serviceId: number, projectGroup: string | null): void; getServicesWithEndpointCounts(projectRoot?: string): Array; deleteService(id: number): void; insertContract(serviceId: number, input: { contractType: string; specPath: string; version?: string; contentHash?: string; parsedSpec: string; }): number; getContractsByService(serviceId: number): ContractRow[]; deleteContractsByService(serviceId: number): void; insertEndpoints(contractId: number, serviceId: number, endpoints: Array<{ method?: string; path: string; operationId?: string; requestSchema?: string; responseSchema?: string; metadata?: Record; }>): void; getEndpointsByService(serviceId: number): EndpointRow[]; findEndpointByPath(pathQuery: string, method?: string): Array; getAllEndpoints(): Array; insertEventChannels(contractId: number | null, serviceId: number, channels: Array<{ channelName: string; direction: 'publish' | 'subscribe'; payloadSchema?: string; }>): void; getEventsByService(serviceId: number): EventChannelRow[]; matchProducersConsumers(): Array<{ channel: string; publishers: string[]; subscribers: string[]; }>; insertCrossServiceEdge(input: { sourceServiceId: number; targetServiceId: number; edgeType: string; sourceRef?: string; targetRef?: string; confidence?: number; metadata?: Record; }): number; getAllCrossServiceEdges(): Array; getEdgesBySource(serviceId: number): Array; getEdgesByTarget(serviceId: number): Array; getTopologyStats(): { services: number; contracts: number; endpoints: number; events: number; crossEdges: number; }; upsertSubproject(input: { name: string; repoRoot: string; projectRoot: string; dbPath?: string; contractPaths?: string[]; metadata?: Record; }): number; getSubproject(nameOrRoot: string, projectRoot?: string): SubprojectRow | undefined; getSubprojectsByProject(projectRoot: string): SubprojectRow[]; getAllSubprojects(): SubprojectRow[]; deleteSubproject(id: number): void; /** * Remove all topology data associated with a repo root: * subprojects (+ cascading client_calls), services (+ cascading contracts, * endpoints, events, edges, snapshots). * Returns counts of deleted rows for logging. */ removeByRepoRoot(repoRoot: string): { subprojects: number; services: number; }; updateSubprojectSyncTime(id: number): void; insertClientCalls(calls: Array<{ sourceRepoId: number; targetRepoId?: number; filePath: string; line?: number; callType: string; method?: string; urlPattern: string; matchedEndpointId?: number; confidence?: number; metadata?: Record; }>): void; deleteClientCallsByRepo(repoId: number): void; getClientCallsByEndpoint(endpointId: number): Array; getClientCallsByRepo(repoId: number): ClientCallRow[]; getClientCallsForTarget(targetRepoId: number): Array; /** Match unlinked client calls to known endpoints. Returns number of newly linked calls. */ linkClientCallsToEndpoints(): number; insertContractSnapshot(contractId: number, serviceId: number, input: { version: string | null; specPath: string; contentHash: string; endpointsJson: string; eventsJson: string; }): number; getContractSnapshots(contractId: number, limit?: number): ContractSnapshotRow[]; getLatestSnapshot(contractId: number): ContractSnapshotRow | undefined; getSnapshotsByService(serviceId: number, limit?: number): ContractSnapshotRow[]; getSubprojectStats(): { repos: number; clientCalls: number; linkedCalls: number; crossRepoEdges: number; }; } /** * R09 v2 — pipeline-lifecycle event shapes emitted by MCP tools * (embed_repo, snapshot_graph) and relayed by the daemon to the * existing /api/events SSE bus. The `project` field is stamped in by * cli.ts before broadcasting; tools omit it. The `type` strings are a * subset of the daemon-side `DaemonEvent` union (see src/cli.ts). * * Lives here (rather than in `./server.ts`) so `ServerContext` below can * reference it without forcing `tool-gate.ts` → `types.ts` → `server.ts` * → `tool-gate.ts` to close into an import cycle. `server.ts` re-exports * the name to preserve the public API. */ type PipelineLifecycleEvent = { type: 'embed_started'; total?: number; } | { type: 'embed_progress'; processed: number; total: number; } | { type: 'embed_completed'; duration_ms: number; embedded: number; } | { type: 'snapshot_created'; name: string; summary?: Record; }; interface ServerDeps { topoStore?: TopologyStore | null; decisionStore?: DecisionStore | null; /** * Optional per-session callback fired for every MCP tool call. Used by the * Electron app to stream live activity over the /api/events SSE bus. * cli.ts wires it to broadcastEvent. */ onJournalEntry?: (data: JournalEntryCallbackData) => void; /** Session ID associated with `onJournalEntry`. Required when the callback is set. */ sessionId?: string; /** * R09 v2 — optional callback for pipeline-lifecycle events emitted by * MCP tools (embed_repo, snapshot_graph). cli.ts wires it to * broadcastEvent, stamping the project root onto each event. */ onPipelineEvent?: (event: PipelineLifecycleEvent) => void; } /** * Handle returned by createServer() for proper lifecycle management. */ interface ServerHandle { server: McpServer; /** * SessionJournal of the MCP session this handle owns. Exposed so the daemon * HTTP layer can serve a snapshot of recent tool calls to the Electron app * (GET /api/projects/journal) without reaching into createServer internals. */ journal: SessionJournal; /** Flush session data and close owned resources. Safe to call multiple times. */ dispose: () => void; } declare function createServer(store: Store, registry: PluginRegistry, config: TraceMcpConfig, rootPath?: string, progress?: ProgressState, deps?: ServerDeps): ServerHandle; export { type FrameworkPlugin, type LanguagePlugin, type PluginManifest, PluginRegistry, Store, type TraceMcpConfig, type TraceMcpError, type TraceMcpResult, createServer, initializeDatabase, loadConfig };