From 54a83546c8fcbbd9e6d9f91f2e872ecd65ff72e9 Mon Sep 17 00:00:00 2001 From: prosdev Date: Mon, 30 Mar 2026 20:04:53 -0700 Subject: [PATCH 1/2] fix(mcp): fix index check and remove dead metrics module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP install/start checked for a local `vectors` directory (LanceDB artifact) that no longer exists with Antfly. Switch to metadata.json. Remove the entire metrics module (MetricsStore, analytics, collector, schema, types, service) — it was write-only with no consumers after removing `dev stats` and the dashboard. Drops better-sqlite3 native dependency (-36 transitive packages, -2400 lines). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/commands/index.ts | 32 +- packages/cli/src/commands/mcp.ts | 18 +- packages/cli/src/commands/storage.ts | 6 +- packages/core/package.json | 2 - packages/core/src/events/types.ts | 3 - packages/core/src/index.ts | 1 - .../src/metrics/__tests__/analytics.test.ts | 287 ------------ .../core/src/metrics/__tests__/store.test.ts | 296 ------------ packages/core/src/metrics/analytics.ts | 208 --------- packages/core/src/metrics/collector.ts | 105 ----- packages/core/src/metrics/index.ts | 32 -- packages/core/src/metrics/schema.ts | 99 ----- packages/core/src/metrics/store.ts | 420 ------------------ packages/core/src/metrics/types.ts | 146 ------ .../services/__tests__/health-service.test.ts | 65 +-- .../__tests__/metrics-service.test.ts | 272 ------------ packages/core/src/services/health-service.ts | 45 +- packages/core/src/services/index.ts | 1 - packages/core/src/services/metrics-service.ts | 169 ------- packages/core/src/storage/path.ts | 2 - packages/dev-agent/package.json | 1 - pnpm-lock.yaml | 234 +--------- 22 files changed, 24 insertions(+), 2420 deletions(-) delete mode 100644 packages/core/src/metrics/__tests__/analytics.test.ts delete mode 100644 packages/core/src/metrics/__tests__/store.test.ts delete mode 100644 packages/core/src/metrics/analytics.ts delete mode 100644 packages/core/src/metrics/collector.ts delete mode 100644 packages/core/src/metrics/index.ts delete mode 100644 packages/core/src/metrics/schema.ts delete mode 100644 packages/core/src/metrics/store.ts delete mode 100644 packages/core/src/metrics/types.ts delete mode 100644 packages/core/src/services/__tests__/metrics-service.test.ts delete mode 100644 packages/core/src/services/metrics-service.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 7b30440..59d2250 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,11 +1,8 @@ -import { join, resolve } from 'node:path'; +import { resolve } from 'node:path'; import { - AsyncEventBus, ensureStorageDirectory, getStorageFilePaths, getStoragePath, - type IndexUpdatedEvent, - MetricsStore, RepositoryIndexer, updateIndexedStats, } from '@prosdevlab/dev-agent-core'; @@ -72,29 +69,13 @@ export const indexCommand = new Command('index') await ensureStorageDirectory(storagePath); const filePaths = getStorageFilePaths(storagePath); - // Create event bus for metrics - const eventBus = new AsyncEventBus(); - const metricsDbPath = join(storagePath, 'metrics.db'); - const metricsStore = new MetricsStore(metricsDbPath); - - eventBus.on('index.updated', async (event) => { - try { - metricsStore.recordSnapshot(event.stats, event.isIncremental ? 'update' : 'index'); - } catch { - // Metrics are non-critical — don't fail indexing - } + const indexer = new RepositoryIndexer({ + repositoryPath: resolvedRepoPath, + vectorStorePath: filePaths.vectors, + excludePatterns: config.repository?.excludePatterns || config.excludePatterns, + languages: config.repository?.languages || config.languages, }); - const indexer = new RepositoryIndexer( - { - repositoryPath: resolvedRepoPath, - vectorStorePath: filePaths.vectors, - excludePatterns: config.repository?.excludePatterns || config.excludePatterns, - languages: config.repository?.languages || config.languages, - }, - eventBus - ); - await indexer.initialize(); const indexLogger = createIndexLogger(options.verbose); @@ -149,7 +130,6 @@ export const indexCommand = new Command('index') // Finalize await indexer.close(); - metricsStore.close(); await updateIndexedStats(storagePath, { files: stats.filesScanned, diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts index 2fafc1b..b3553e4 100644 --- a/packages/cli/src/commands/mcp.ts +++ b/packages/cli/src/commands/mcp.ts @@ -70,13 +70,13 @@ Available Tools (6): try { // Check if repository is indexed const storagePath = await getStoragePath(repositoryPath); - const { vectors, watcherSnapshot } = getStorageFilePaths(storagePath); + const { vectors, metadata, watcherSnapshot } = getStorageFilePaths(storagePath); - const vectorsExist = await fs - .access(vectors) + const isIndexed = await fs + .access(metadata) .then(() => true) .catch(() => false); - if (!vectorsExist) { + if (!isIndexed) { logger.error(`Repository not indexed. Run: ${chalk.yellow('dev index')}`); process.exit(1); } @@ -205,15 +205,15 @@ Available Tools (6): const spinner = ora(`Installing dev-agent MCP server in ${targetIDE}...`).start(); try { - // Check if repository is indexed + // Check if repository is indexed (metadata.json is written at index time) const storagePath = await getStoragePath(repositoryPath); - const { vectors } = getStorageFilePaths(storagePath); + const { metadata } = getStorageFilePaths(storagePath); - const vectorsExist = await fs - .access(vectors) + const isIndexed = await fs + .access(metadata) .then(() => true) .catch(() => false); - if (!vectorsExist) { + if (!isIndexed) { spinner.fail(`Repository not indexed. Run: ${chalk.yellow('dev index')}`); process.exit(1); } diff --git a/packages/cli/src/commands/storage.ts b/packages/cli/src/commands/storage.ts index 6fd6d5f..1cc8f76 100644 --- a/packages/cli/src/commands/storage.ts +++ b/packages/cli/src/commands/storage.ts @@ -99,11 +99,9 @@ Storage Location: Each repository gets its own subdirectory based on path hash What's Stored: - • vectors.lance/ Vector embeddings for semantic search - • indexer-state.json Repository indexing state - • github-state.json GitHub issues/PRs state • metadata.json Repository metadata - • metrics.db Historical metrics (SQLite) + + Vector data is stored in Antfly (local search backend). ` ); diff --git a/packages/core/package.json b/packages/core/package.json index c9134cd..2bfd684 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,6 @@ "test:watch": "vitest" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "tree-sitter-wasms": "^0.1.13", @@ -40,7 +39,6 @@ "@antfly/sdk": "0.0.14", "@prosdevlab/dev-agent-types": "workspace:*", "@prosdevlab/kero": "workspace:*", - "better-sqlite3": "^12.5.0", "globby": "^16.0.0", "remark": "^15.0.1", "remark-parse": "^11.0.0", diff --git a/packages/core/src/events/types.ts b/packages/core/src/events/types.ts index eebe446..f41e68c 100644 --- a/packages/core/src/events/types.ts +++ b/packages/core/src/events/types.ts @@ -6,7 +6,6 @@ */ import type { DetailedIndexStats } from '../indexer/types.js'; -import type { CodeMetadata } from '../metrics/types.js'; /** * Event handler function type @@ -148,8 +147,6 @@ export interface IndexUpdatedEvent { stats: DetailedIndexStats; /** Whether this was an incremental update (vs full index) */ isIncremental?: boolean; - /** Per-file code metadata for metrics storage */ - codeMetadata?: CodeMetadata[]; } export interface IndexErrorEvent { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ad7b746..72386ff 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,7 +5,6 @@ export * from './context'; export * from './events'; export * from './indexer'; export * from './map'; -export * from './metrics'; export * from './observability'; export * from './scanner'; export * from './services'; diff --git a/packages/core/src/metrics/__tests__/analytics.test.ts b/packages/core/src/metrics/__tests__/analytics.test.ts deleted file mode 100644 index c0c7f18..0000000 --- a/packages/core/src/metrics/__tests__/analytics.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Tests for Metrics Analytics - */ - -import * as fs from 'node:fs'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createDetailedIndexStats } from '../../indexer/__tests__/test-factories.js'; -import { - getConcentratedOwnership, - getFileMetrics, - getFileTrend, - getLargestFiles, - getMostActive, - getSnapshotSummary, -} from '../analytics.js'; -import { MetricsStore } from '../store.js'; -import type { CodeMetadata } from '../types.js'; - -describe('Metrics Analytics', () => { - let tempDbPath: string; - let store: MetricsStore; - let snapshotId: string; - - beforeEach(() => { - // Create temp database - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'analytics-test-')); - tempDbPath = path.join(tempDir, 'test-metrics.db'); - store = new MetricsStore(tempDbPath); - - // Create a snapshot - const stats = createDetailedIndexStats({ - repositoryPath: '/test/repo', - filesScanned: 5, - documentsIndexed: 10, - }); - snapshotId = store.recordSnapshot(stats, 'index'); - - // Add code metadata for testing - const metadata: CodeMetadata[] = [ - { - filePath: 'src/very-active.ts', - commitCount: 100, - authorCount: 1, - linesOfCode: 2000, - numFunctions: 50, - numImports: 20, - }, - { - filePath: 'src/medium-active.ts', - commitCount: 30, - authorCount: 3, - linesOfCode: 500, - numFunctions: 15, - numImports: 10, - }, - { - filePath: 'src/low-active.ts', - commitCount: 5, - authorCount: 5, - linesOfCode: 100, - numFunctions: 5, - numImports: 3, - }, - ]; - - store.appendCodeMetadata(snapshotId, metadata); - }); - - afterEach(() => { - store.close(); - if (fs.existsSync(tempDbPath)) { - fs.unlinkSync(tempDbPath); - const tempDir = path.dirname(tempDbPath); - fs.rmSync(tempDir, { recursive: true }); - } - }); - - describe('getFileMetrics', () => { - it('should return files with classified metrics', () => { - const metrics = getFileMetrics(store, snapshotId); - - expect(metrics.length).toBe(3); - expect(metrics[0].filePath).toBeDefined(); - expect(metrics[0].activity).toBeDefined(); - expect(metrics[0].size).toBeDefined(); - expect(metrics[0].ownership).toBeDefined(); - }); - - it('should classify activity levels correctly', () => { - const metrics = getFileMetrics(store, snapshotId); - - // Find by file path - const veryActive = metrics.find((m) => m.filePath === 'src/very-active.ts'); - const mediumActive = metrics.find((m) => m.filePath === 'src/medium-active.ts'); - const lowActive = metrics.find((m) => m.filePath === 'src/low-active.ts'); - - // 100 commits = very-high - expect(veryActive?.activity).toBe('very-high'); - expect(veryActive?.commitCount).toBe(100); - - // 30 commits = medium - expect(mediumActive?.activity).toBe('medium'); - expect(mediumActive?.commitCount).toBe(30); - - // 5 commits = low - expect(lowActive?.activity).toBe('low'); - expect(lowActive?.commitCount).toBe(5); - }); - - it('should classify size correctly', () => { - const metrics = getFileMetrics(store, snapshotId); - - const veryActive = metrics.find((m) => m.filePath === 'src/very-active.ts'); - const mediumActive = metrics.find((m) => m.filePath === 'src/medium-active.ts'); - const lowActive = metrics.find((m) => m.filePath === 'src/low-active.ts'); - - // 2000 LOC = very-large - expect(veryActive?.size).toBe('very-large'); - - // 500 LOC = medium - expect(mediumActive?.size).toBe('medium'); - - // 100 LOC = small - expect(lowActive?.size).toBe('small'); - }); - - it('should classify ownership correctly', () => { - const metrics = getFileMetrics(store, snapshotId); - - const veryActive = metrics.find((m) => m.filePath === 'src/very-active.ts'); - const mediumActive = metrics.find((m) => m.filePath === 'src/medium-active.ts'); - const lowActive = metrics.find((m) => m.filePath === 'src/low-active.ts'); - - // 1 author = single - expect(veryActive?.ownership).toBe('single'); - expect(veryActive?.authorCount).toBe(1); - - // 3 authors = small-team - expect(mediumActive?.ownership).toBe('small-team'); - - // 5 authors = small-team - expect(lowActive?.ownership).toBe('small-team'); - }); - - it('should respect limit parameter', () => { - const metrics = getFileMetrics(store, snapshotId, { limit: 2 }); - expect(metrics.length).toBe(2); - }); - - it('should return empty array for non-existent snapshot', () => { - const metrics = getFileMetrics(store, 'non-existent-id'); - expect(metrics.length).toBe(0); - }); - }); - - describe('getMostActive', () => { - it('should return files sorted by activity', () => { - const active = getMostActive(store, snapshotId, 10); - - expect(active.length).toBe(3); - // Should be sorted by commit count descending - expect(active[0].commitCount).toBeGreaterThanOrEqual(active[1].commitCount); - expect(active[1].commitCount).toBeGreaterThanOrEqual(active[2].commitCount); - }); - }); - - describe('getLargestFiles', () => { - it('should return files sorted by size', () => { - const largest = getLargestFiles(store, snapshotId, 10); - - expect(largest.length).toBe(3); - // Should be sorted by LOC descending - expect(largest[0].linesOfCode).toBeGreaterThanOrEqual(largest[1].linesOfCode); - expect(largest[1].linesOfCode).toBeGreaterThanOrEqual(largest[2].linesOfCode); - }); - }); - - describe('getConcentratedOwnership', () => { - it('should return files with single or pair ownership', () => { - const concentrated = getConcentratedOwnership(store, snapshotId, 10); - - expect(concentrated.length).toBeGreaterThan(0); - // All should have single or pair ownership - for (const file of concentrated) { - expect(['single', 'pair']).toContain(file.ownership); - } - }); - }); - - describe('getFileTrend', () => { - it('should return file metadata across snapshots', async () => { - // Wait to ensure different timestamp - await new Promise((resolve) => setTimeout(resolve, 10)); - - // Create a second snapshot - const stats2 = createDetailedIndexStats({ - repositoryPath: '/test/repo', - filesScanned: 5, - }); - const snapshotId2 = store.recordSnapshot(stats2, 'update'); - - // Add updated metadata - const updatedMetadata: CodeMetadata[] = [ - { - filePath: 'src/very-active.ts', - commitCount: 110, // Increased - authorCount: 2, // More authors - linesOfCode: 2100, // More LOC - numFunctions: 52, - numImports: 22, - }, - ]; - store.appendCodeMetadata(snapshotId2, updatedMetadata); - - const trend = getFileTrend(store, 'src/very-active.ts', 10); - - expect(trend.length).toBe(2); - // Most recent first - expect(trend[0].commitCount).toBe(110); - expect(trend[1].commitCount).toBe(100); - }); - - it('should return empty array for non-existent file', () => { - const trend = getFileTrend(store, 'src/non-existent.ts', 10); - expect(trend.length).toBe(0); - }); - }); - - describe('getSnapshotSummary', () => { - it('should calculate summary statistics', () => { - const summary = getSnapshotSummary(store, snapshotId); - - expect(summary).toBeDefined(); - expect(summary?.totalFiles).toBe(3); - expect(summary?.totalLOC).toBe(2600); // 2000 + 500 + 100 - expect(summary?.totalFunctions).toBe(70); // 50 + 15 + 5 - expect(summary?.avgLOC).toBe(867); // 2600 / 3, rounded - }); - - it('should categorize files by activity', () => { - const summary = getSnapshotSummary(store, snapshotId); - - expect(summary).toBeDefined(); - // Should have activity metrics - expect(summary?.veryActiveFiles).toBe(1); // very-active.ts has 100 commits - expect(summary?.highActivityFiles).toBe(1); // Only 1 file >= 50 commits - expect(summary?.veryActivePercent).toBeGreaterThan(0); - }); - - it('should categorize files by size', () => { - const summary = getSnapshotSummary(store, snapshotId); - - expect(summary).toBeDefined(); - // Should have size metrics - expect(summary?.veryLargeFiles).toBe(1); // very-active.ts has 2000 LOC - expect(summary?.largeFiles).toBe(1); // Only 1 file >= 1000 LOC - expect(summary?.veryLargePercent).toBeGreaterThan(0); - }); - - it('should categorize files by ownership', () => { - const summary = getSnapshotSummary(store, snapshotId); - - expect(summary).toBeDefined(); - // Should have ownership metrics - expect(summary?.singleAuthorFiles).toBe(1); // very-active.ts has 1 author - expect(summary?.pairAuthorFiles).toBe(0); // No files with exactly 2 authors - expect(summary?.singleAuthorPercent).toBeGreaterThan(0); - }); - - it('should return null for non-existent snapshot', () => { - const summary = getSnapshotSummary(store, 'non-existent-id'); - expect(summary).toBeNull(); - }); - - it('should return null for snapshot with no metadata', () => { - const stats = createDetailedIndexStats({ - repositoryPath: '/test/repo2', - }); - const emptySnapshotId = store.recordSnapshot(stats, 'index'); - - const summary = getSnapshotSummary(store, emptySnapshotId); - expect(summary).toBeNull(); - }); - }); -}); diff --git a/packages/core/src/metrics/__tests__/store.test.ts b/packages/core/src/metrics/__tests__/store.test.ts deleted file mode 100644 index abc39e8..0000000 --- a/packages/core/src/metrics/__tests__/store.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Tests for MetricsStore - */ - -import * as fs from 'node:fs'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { createDetailedIndexStats } from '../../indexer/__tests__/test-factories.js'; -import { MetricsStore } from '../store.js'; - -describe('MetricsStore', () => { - let tempDbPath: string; - let store: MetricsStore; - - beforeEach(() => { - // Create temp database path - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'metrics-test-')); - tempDbPath = path.join(tempDir, 'test-metrics.db'); - store = new MetricsStore(tempDbPath); - }); - - afterEach(() => { - // Clean up - store.close(); - if (fs.existsSync(tempDbPath)) { - fs.unlinkSync(tempDbPath); - const tempDir = path.dirname(tempDbPath); - fs.rmSync(tempDir, { recursive: true }); - } - }); - - describe('recordSnapshot', () => { - it('should record a snapshot successfully', () => { - const stats = createDetailedIndexStats({ - repositoryPath: '/test/repo', - filesScanned: 10, - documentsIndexed: 20, - vectorsStored: 20, - duration: 1000, - }); - - const id = store.recordSnapshot(stats, 'index'); - - expect(id).toBeTruthy(); - expect(typeof id).toBe('string'); - }); - - it('should generate unique IDs for each snapshot', () => { - const stats = createDetailedIndexStats(); - - const id1 = store.recordSnapshot(stats, 'index'); - const id2 = store.recordSnapshot(stats, 'update'); - - expect(id1).not.toBe(id2); - }); - - it('should store both index and update triggers', () => { - const stats = createDetailedIndexStats(); - - const indexId = store.recordSnapshot(stats, 'index'); - const updateId = store.recordSnapshot(stats, 'update'); - - const indexSnapshot = store.getSnapshot(indexId); - const updateSnapshot = store.getSnapshot(updateId); - - expect(indexSnapshot?.trigger).toBe('index'); - expect(updateSnapshot?.trigger).toBe('update'); - }); - }); - - describe('getSnapshot', () => { - it('should retrieve a snapshot by ID', () => { - const stats = createDetailedIndexStats({ - repositoryPath: '/test/repo', - filesScanned: 10, - documentsIndexed: 20, - }); - - const id = store.recordSnapshot(stats, 'index'); - const snapshot = store.getSnapshot(id); - - expect(snapshot).toBeTruthy(); - expect(snapshot?.id).toBe(id); - expect(snapshot?.repositoryPath).toBe('/test/repo'); - expect(snapshot?.stats.filesScanned).toBe(10); - expect(snapshot?.stats.documentsIndexed).toBe(20); - expect(snapshot?.trigger).toBe('index'); - }); - - it('should return null for non-existent ID', () => { - const snapshot = store.getSnapshot('non-existent-id'); - expect(snapshot).toBeNull(); - }); - }); - - describe('getSnapshots', () => { - beforeEach(() => { - // Seed with multiple snapshots - const repo1 = '/test/repo1'; - const repo2 = '/test/repo2'; - - store.recordSnapshot(createDetailedIndexStats({ repositoryPath: repo1 }), 'index'); - store.recordSnapshot(createDetailedIndexStats({ repositoryPath: repo1 }), 'update'); - store.recordSnapshot(createDetailedIndexStats({ repositoryPath: repo2 }), 'index'); - }); - - it('should retrieve all snapshots with default limit', () => { - const snapshots = store.getSnapshots({}); - expect(snapshots.length).toBe(3); - }); - - it('should filter by repository path', () => { - const snapshots = store.getSnapshots({ repositoryPath: '/test/repo1' }); - expect(snapshots.length).toBe(2); - expect(snapshots.every((s) => s.repositoryPath === '/test/repo1')).toBe(true); - }); - - it('should filter by trigger type', () => { - const snapshots = store.getSnapshots({ trigger: 'index' }); - expect(snapshots.length).toBe(2); - expect(snapshots.every((s) => s.trigger === 'index')).toBe(true); - }); - - it('should respect limit parameter', () => { - const snapshots = store.getSnapshots({ limit: 2 }); - expect(snapshots.length).toBe(2); - }); - - it('should return snapshots in descending timestamp order', () => { - const snapshots = store.getSnapshots({}); - expect(snapshots.length).toBeGreaterThan(1); - - for (let i = 1; i < snapshots.length; i++) { - expect(snapshots[i - 1].timestamp.getTime()).toBeGreaterThanOrEqual( - snapshots[i].timestamp.getTime() - ); - } - }); - - it('should filter by since date', () => { - const now = new Date(); - const oneHourAgo = new Date(now.getTime() - 3600000); - - const snapshots = store.getSnapshots({ since: oneHourAgo }); - expect(snapshots.length).toBeGreaterThan(0); - }); - - it('should filter by until date', () => { - const futureDate = new Date(Date.now() + 3600000); - const snapshots = store.getSnapshots({ until: futureDate }); - expect(snapshots.length).toBe(3); - }); - }); - - describe('getLatestSnapshot', () => { - it('should return the most recent snapshot', () => { - const stats1 = createDetailedIndexStats({ filesScanned: 10 }); - const stats2 = createDetailedIndexStats({ filesScanned: 20 }); - - // Use explicit timestamps to ensure deterministic ordering - store.recordSnapshot(stats1, 'index', new Date('2024-01-01T10:00:00Z')); - const latestId = store.recordSnapshot(stats2, 'update', new Date('2024-01-01T11:00:00Z')); - - const latest = store.getLatestSnapshot(); - expect(latest?.id).toBe(latestId); - expect(latest?.stats.filesScanned).toBe(20); - }); - - it('should filter by repository path', () => { - store.recordSnapshot( - createDetailedIndexStats({ repositoryPath: '/repo1' }), - 'index', - new Date('2024-01-01T10:00:00Z') - ); - const repo2Id = store.recordSnapshot( - createDetailedIndexStats({ repositoryPath: '/repo2' }), - 'index', - new Date('2024-01-01T11:00:00Z') - ); - - const latest = store.getLatestSnapshot('/repo2'); - expect(latest?.id).toBe(repo2Id); - }); - - it('should return null when no snapshots exist', () => { - const latest = store.getLatestSnapshot(); - expect(latest).toBeNull(); - }); - }); - - describe('getCount', () => { - it('should return correct count of all snapshots', () => { - store.recordSnapshot(createDetailedIndexStats(), 'index'); - store.recordSnapshot(createDetailedIndexStats(), 'update'); - store.recordSnapshot(createDetailedIndexStats(), 'index'); - - expect(store.getCount()).toBe(3); - }); - - it('should return correct count filtered by repository path', () => { - store.recordSnapshot(createDetailedIndexStats({ repositoryPath: '/repo1' }), 'index'); - store.recordSnapshot(createDetailedIndexStats({ repositoryPath: '/repo1' }), 'update'); - store.recordSnapshot(createDetailedIndexStats({ repositoryPath: '/repo2' }), 'index'); - - expect(store.getCount('/repo1')).toBe(2); - expect(store.getCount('/repo2')).toBe(1); - }); - - it('should return 0 for empty database', () => { - expect(store.getCount()).toBe(0); - }); - }); - - describe('pruneOldSnapshots', () => { - it('should delete snapshots older than retention period', async () => { - // Record a snapshot - store.recordSnapshot(createDetailedIndexStats(), 'index'); - - // Wait 2ms to ensure the snapshot is in the past - await new Promise((resolve) => setTimeout(resolve, 2)); - - // Prune snapshots older than 0 days (should delete all) - const deleted = store.pruneOldSnapshots(0); - expect(deleted).toBeGreaterThan(0); - expect(store.getCount()).toBe(0); - }); - - it('should not delete recent snapshots', () => { - store.recordSnapshot(createDetailedIndexStats(), 'index'); - store.recordSnapshot(createDetailedIndexStats(), 'update'); - - // Prune snapshots older than 90 days (should delete none) - const deleted = store.pruneOldSnapshots(90); - expect(deleted).toBe(0); - expect(store.getCount()).toBe(2); - }); - - it('should return 0 when no snapshots to prune', () => { - const deleted = store.pruneOldSnapshots(30); - expect(deleted).toBe(0); - }); - }); - - describe('close', () => { - it('should close database without error', () => { - expect(() => store.close()).not.toThrow(); - }); - - it('should not throw when closed multiple times', () => { - store.close(); - expect(() => store.close()).not.toThrow(); - }); - }); - - describe('logger integration', () => { - it('should work without a logger', () => { - const storeWithoutLogger = new MetricsStore(tempDbPath); - const stats = createDetailedIndexStats(); - - expect(() => { - storeWithoutLogger.recordSnapshot(stats, 'index'); - }).not.toThrow(); - - storeWithoutLogger.close(); - }); - - it('should call logger methods when provided', () => { - const mockLogger = { - trace: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - success: vi.fn(), - fatal: vi.fn(), - child: vi.fn(), - startTimer: vi.fn(), - isLevelEnabled: vi.fn(), - level: 'info' as const, - }; - - const tempDbPath2 = path.join(path.dirname(tempDbPath), 'test-metrics-2.db'); - const storeWithLogger = new MetricsStore(tempDbPath2, mockLogger); - const stats = createDetailedIndexStats(); - - storeWithLogger.recordSnapshot(stats, 'index'); - - expect(mockLogger.info).toHaveBeenCalled(); - expect(mockLogger.debug).toHaveBeenCalled(); - - storeWithLogger.close(); - fs.unlinkSync(tempDbPath2); - }); - }); -}); diff --git a/packages/core/src/metrics/analytics.ts b/packages/core/src/metrics/analytics.ts deleted file mode 100644 index 9f91639..0000000 --- a/packages/core/src/metrics/analytics.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Metrics Analytics - * - * Factual metrics about repository files. - * No "risk scores" - just observable data for developers to interpret. - */ - -import type { MetricsStore } from './store.js'; -import type { CodeMetadata } from './types.js'; - -/** - * File metrics with activity classification - */ -export interface FileMetrics { - filePath: string; - activity: 'very-high' | 'high' | 'medium' | 'low' | 'minimal'; - commitCount: number; - size: 'very-large' | 'large' | 'medium' | 'small' | 'tiny'; - linesOfCode: number; - ownership: 'single' | 'pair' | 'small-team' | 'shared'; - authorCount: number; - lastModified?: Date; - numFunctions: number; - numImports: number; -} - -/** - * Classify activity level based on commit count - */ -function classifyActivity(commits: number): FileMetrics['activity'] { - if (commits >= 100) return 'very-high'; - if (commits >= 50) return 'high'; - if (commits >= 20) return 'medium'; - if (commits >= 5) return 'low'; - return 'minimal'; -} - -/** - * Classify size based on lines of code - */ -function classifySize(loc: number): FileMetrics['size'] { - if (loc >= 2000) return 'very-large'; - if (loc >= 1000) return 'large'; - if (loc >= 500) return 'medium'; - if (loc >= 100) return 'small'; - return 'tiny'; -} - -/** - * Classify ownership based on author count - */ -function classifyOwnership(authors: number): FileMetrics['ownership'] { - if (authors === 1) return 'single'; - if (authors === 2) return 'pair'; - if (authors <= 5) return 'small-team'; - return 'shared'; -} - -/** - * Get file metrics from a snapshot - * - * Returns factual metrics about files without judgment. - * Developers can filter/sort based on what matters to them. - * - * @param store - MetricsStore instance - * @param snapshotId - Snapshot ID to analyze - * @param options - Query options - * @returns Array of file metrics - */ -export function getFileMetrics( - store: MetricsStore, - snapshotId: string, - options?: { - sortBy?: 'activity' | 'size' | 'ownership'; - limit?: number; - } -): FileMetrics[] { - const sortBy = options?.sortBy || 'activity'; - const limit = options?.limit || 100; - - // Map sortBy to MetricsStore query format - const sortMapping = { - activity: 'commits_desc' as const, - size: 'lines_desc' as const, - ownership: 'risk_desc' as const, // Risk formula weights single authors - }; - - const metadata = store.getCodeMetadata({ - snapshotId, - sortBy: sortMapping[sortBy], - limit, - }); - - return metadata.map((m) => ({ - filePath: m.filePath, - activity: classifyActivity(m.commitCount || 0), - commitCount: m.commitCount || 0, - size: classifySize(m.linesOfCode), - linesOfCode: m.linesOfCode, - ownership: classifyOwnership(m.authorCount || 1), - authorCount: m.authorCount || 1, - lastModified: m.lastModified, - numFunctions: m.numFunctions, - numImports: m.numImports, - })); -} - -/** - * Get most active files (by commit count) - */ -export function getMostActive(store: MetricsStore, snapshotId: string, limit = 10): FileMetrics[] { - return getFileMetrics(store, snapshotId, { sortBy: 'activity', limit }); -} - -/** - * Get largest files (by LOC) - */ -export function getLargestFiles( - store: MetricsStore, - snapshotId: string, - limit = 10 -): FileMetrics[] { - return getFileMetrics(store, snapshotId, { sortBy: 'size', limit }); -} - -/** - * Get files with concentrated ownership (single/pair authors) - */ -export function getConcentratedOwnership( - store: MetricsStore, - snapshotId: string, - limit = 10 -): FileMetrics[] { - const all = getFileMetrics(store, snapshotId, { sortBy: 'ownership', limit: 1000 }); - return all.filter((m) => m.ownership === 'single' || m.ownership === 'pair').slice(0, limit); -} - -/** - * Get trend for a specific file across snapshots - * - * Shows how a file's metrics have changed over time. - * - * @param store - MetricsStore instance - * @param filePath - File path to analyze - * @param limit - Number of snapshots to analyze (default: 10) - * @returns Array of metadata ordered by time (newest first) - */ -export function getFileTrend(store: MetricsStore, filePath: string, limit = 10): CodeMetadata[] { - return store.getCodeMetadataForFile(filePath, limit); -} - -/** - * Get summary statistics for a snapshot - * - * Provides aggregate metrics for all files in a snapshot. - * - * @param store - MetricsStore instance - * @param snapshotId - Snapshot ID to analyze - * @returns Summary statistics - */ -export function getSnapshotSummary(store: MetricsStore, snapshotId: string) { - const metadata = store.getCodeMetadata({ - snapshotId, - limit: 10000, // Get all files - }); - - if (metadata.length === 0) { - return null; - } - - const totalLOC = metadata.reduce((sum, m) => sum + m.linesOfCode, 0); - const totalFunctions = metadata.reduce((sum, m) => sum + m.numFunctions, 0); - const avgLOC = Math.round(totalLOC / metadata.length); - - // Activity distribution - const veryActiveFiles = metadata.filter((m) => (m.commitCount || 0) >= 100).length; - const highActivityFiles = metadata.filter((m) => (m.commitCount || 0) >= 50).length; - - // Size distribution - const veryLargeFiles = metadata.filter((m) => m.linesOfCode >= 2000).length; - const largeFiles = metadata.filter((m) => m.linesOfCode >= 1000).length; - - // Ownership distribution - const singleAuthorFiles = metadata.filter((m) => (m.authorCount || 1) === 1).length; - const pairAuthorFiles = metadata.filter((m) => (m.authorCount || 1) === 2).length; - - return { - totalFiles: metadata.length, - totalLOC, - totalFunctions, - avgLOC, - - // Activity metrics - veryActiveFiles, - highActivityFiles, - veryActivePercent: Math.round((veryActiveFiles / metadata.length) * 100), - - // Size metrics - veryLargeFiles, - largeFiles, - veryLargePercent: Math.round((veryLargeFiles / metadata.length) * 100), - - // Ownership metrics - singleAuthorFiles, - pairAuthorFiles, - singleAuthorPercent: Math.round((singleAuthorFiles / metadata.length) * 100), - }; -} diff --git a/packages/core/src/metrics/collector.ts b/packages/core/src/metrics/collector.ts deleted file mode 100644 index 0dcbfed..0000000 --- a/packages/core/src/metrics/collector.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Metrics Collector - * - * Builds CodeMetadata from scanner results and change frequency data. - */ - -import { calculateChangeFrequency } from '../indexer/utils/change-frequency.js'; -import type { Document } from '../scanner/types.js'; -import type { CodeMetadata } from './types.js'; - -/** - * Count lines of code in a file - */ -async function countFileLines(repositoryPath: string, filePath: string): Promise { - const fs = await import('node:fs/promises'); - const path = await import('node:path'); - - try { - const fullPath = path.join(repositoryPath, filePath); - const content = await fs.readFile(fullPath, 'utf-8'); - return content.split('\n').length; - } catch { - // File doesn't exist or can't be read - return 0 - return 0; - } -} - -/** - * Build code metadata from indexer state - * - * Combines data from: - * - Scanner results (documents, imports) - * - Git history (change frequency) - calculated on-demand - * - * @param repositoryPath - Repository path - * @param documents - Scanned documents - * @returns Code metadata array - */ -export async function buildCodeMetadata( - repositoryPath: string, - documents: Document[] -): Promise { - // Calculate change frequency using git log - const changeFreq = await calculateChangeFrequency({ - repositoryPath, - maxCommits: 1000, - }).catch(() => new Map()); - - // Group documents by file - const fileToDocuments = new Map(); - for (const doc of documents) { - const filePath = doc.metadata.file; - const existing = fileToDocuments.get(filePath) || []; - existing.push(doc); - fileToDocuments.set(filePath, existing); - } - - // Build metadata for each file - process in parallel for speed - const CONCURRENCY = 50; // Read 50 files at a time - const fileEntries = Array.from(fileToDocuments.entries()); - const batches: Array<[string, Document[]][]> = []; - - // Create batches - for (let i = 0; i < fileEntries.length; i += CONCURRENCY) { - batches.push(fileEntries.slice(i, i + CONCURRENCY)); - } - - const metadata: CodeMetadata[] = []; - - // Process each batch in parallel - for (const batch of batches) { - const batchResults = await Promise.all( - batch.map(async ([filePath, docs]) => { - const freq = changeFreq.get(filePath); - - // Count actual lines of code from the file on disk - const linesOfCode = await countFileLines(repositoryPath, filePath); - - // Count unique imports across all documents in this file - const allImports = new Set(); - for (const doc of docs) { - if (doc.metadata.imports) { - for (const imp of doc.metadata.imports) { - allImports.add(imp); - } - } - } - - return { - filePath, - commitCount: freq?.commitCount, - lastModified: freq?.lastModified, - authorCount: freq?.authorCount, - linesOfCode, - numFunctions: docs.length, // Each document is a function/component - numImports: allImports.size, - }; - }) - ); - - metadata.push(...batchResults); - } - - return metadata; -} diff --git a/packages/core/src/metrics/index.ts b/packages/core/src/metrics/index.ts deleted file mode 100644 index a6e191d..0000000 --- a/packages/core/src/metrics/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Metrics Module - * - * Provides persistent storage for repository metrics and snapshots. - */ - -export { - type FileMetrics, - getConcentratedOwnership, - getFileMetrics, - getFileTrend, - getLargestFiles, - getMostActive, - getSnapshotSummary, -} from './analytics.js'; -export { buildCodeMetadata } from './collector.js'; -export { initializeDatabase, METRICS_SCHEMA_V1 } from './schema.js'; -export { MetricsStore } from './store.js'; -export type { - CodeMetadata, - CodeMetadataQuery, - Hotspot, - MetricsConfig, - Snapshot, - SnapshotQuery, -} from './types.js'; -export { - CodeMetadataSchema, - DEFAULT_METRICS_CONFIG, - HotspotSchema, - SnapshotQuerySchema, -} from './types.js'; diff --git a/packages/core/src/metrics/schema.ts b/packages/core/src/metrics/schema.ts deleted file mode 100644 index 82fcc6a..0000000 --- a/packages/core/src/metrics/schema.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Metrics Database Schema - * - * SQLite schema definitions for metrics storage. - */ - -import type Database from 'better-sqlite3'; - -/** - * Schema version 1: Core snapshots table - * - * Design philosophy: - * - Single table for MVP (snapshots) - * - JSON storage for flexibility (no schema migrations needed) - * - Denormalized fields for fast queries - * - Future tables can be added without breaking this - */ -export const METRICS_SCHEMA_V1 = ` - -- Core snapshots table - CREATE TABLE IF NOT EXISTS snapshots ( - id TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL, - repository_path TEXT NOT NULL, - stats TEXT NOT NULL, -- JSON serialized DetailedIndexStats - - -- Denormalized for fast queries (avoid parsing JSON) - trigger TEXT CHECK(trigger IN ('index', 'update')), - total_files INTEGER, - total_documents INTEGER, - total_vectors INTEGER, - duration_ms INTEGER, - - created_at INTEGER NOT NULL - ); - - -- Index for time-based queries (most common) - CREATE INDEX IF NOT EXISTS idx_snapshots_timestamp - ON snapshots(timestamp DESC); - - -- Index for repository-specific queries - CREATE INDEX IF NOT EXISTS idx_snapshots_repo - ON snapshots(repository_path, timestamp DESC); - - -- Index for filtering by trigger type - CREATE INDEX IF NOT EXISTS idx_snapshots_trigger - ON snapshots(trigger, timestamp DESC); - - -- Code metadata table (per-file metrics for hotspot detection) - CREATE TABLE IF NOT EXISTS code_metadata ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - snapshot_id TEXT NOT NULL, - file_path TEXT NOT NULL, - - -- Data we have or can easily get: - commit_count INTEGER, -- From change frequency - last_modified INTEGER, -- From change frequency (timestamp) - author_count INTEGER, -- From change frequency - lines_of_code INTEGER, -- Count lines during scan - num_functions INTEGER, -- From document count - num_imports INTEGER, -- From DocumentMetadata.imports - - -- Calculated risk score - risk_score REAL, -- (commit_count * lines_of_code) / max(author_count, 1) - - FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE, - UNIQUE (snapshot_id, file_path) - ); - - -- Index for querying by snapshot - CREATE INDEX IF NOT EXISTS idx_code_metadata_snapshot - ON code_metadata(snapshot_id); - - -- Index for finding hotspots (highest risk files) - CREATE INDEX IF NOT EXISTS idx_code_metadata_risk - ON code_metadata(risk_score DESC); - - -- Index for file-specific queries - CREATE INDEX IF NOT EXISTS idx_code_metadata_file - ON code_metadata(file_path); -`; - -/** - * Initialize database with schema and optimizations - */ -export function initializeDatabase(db: Database.Database): void { - // Enable WAL (Write-Ahead Logging) mode for better concurrency - // This allows readers and writers to operate concurrently - db.pragma('journal_mode = WAL'); - - // Use NORMAL synchronous mode for better performance - // Still safe with WAL mode enabled - db.pragma('synchronous = NORMAL'); - - // Enable foreign keys - db.pragma('foreign_keys = ON'); - - // Create schema - db.exec(METRICS_SCHEMA_V1); -} diff --git a/packages/core/src/metrics/store.ts b/packages/core/src/metrics/store.ts deleted file mode 100644 index 9b6b5cd..0000000 --- a/packages/core/src/metrics/store.ts +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Metrics Store - * - * SQLite-based storage for repository metrics and snapshots. - * Provides automatic persistence via event bus integration. - */ - -import * as crypto from 'node:crypto'; -import type { Logger } from '@prosdevlab/kero'; -import Database from 'better-sqlite3'; -import type { DetailedIndexStats } from '../indexer/types.js'; -import { initializeDatabase } from './schema.js'; -import { - type CodeMetadata, - type CodeMetadataQuery, - type Snapshot, - type SnapshotQuery, - SnapshotQuerySchema, -} from './types.js'; - -/** - * Metrics Store Class - * - * Stores snapshots of repository statistics over time. - * Designed to work with event bus for automatic persistence. - */ -export class MetricsStore { - private db: Database.Database; - - constructor( - dbPath: string, - private logger?: Logger - ) { - try { - this.db = new Database(dbPath); - initializeDatabase(this.db); - this.logger?.info({ dbPath }, 'Metrics store initialized'); - } catch (error) { - this.logger?.error({ error }, 'Failed to initialize metrics DB'); - throw error; - } - } - - /** - * Record a snapshot - * - * @param stats - Repository statistics to record - * @param trigger - What triggered this snapshot ('index' or 'update') - * @param customTimestamp - Optional timestamp (for testing) - * @returns Snapshot ID - * @throws Error if database write fails - */ - recordSnapshot( - stats: DetailedIndexStats, - trigger: 'index' | 'update', - customTimestamp?: Date - ): string { - const id = crypto.randomUUID(); - const timestamp = customTimestamp ? customTimestamp.getTime() : Date.now(); - - try { - this.db - .prepare( - ` - INSERT INTO snapshots - (id, timestamp, repository_path, stats, trigger, - total_files, total_documents, total_vectors, duration_ms, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ` - ) - .run( - id, - timestamp, - stats.repositoryPath, - JSON.stringify(stats), - trigger, - stats.filesScanned, - stats.documentsIndexed, - stats.vectorsStored, - stats.duration, - timestamp - ); - - this.logger?.debug( - { - id, - trigger, - files: stats.filesScanned, - documents: stats.documentsIndexed, - }, - 'Recorded snapshot' - ); - - return id; - } catch (error) { - this.logger?.error({ error }, 'Failed to record snapshot'); - throw error; - } - } - - /** - * Query snapshots with filters - * - * @param query - Query parameters (since, until, limit, etc.) - * @returns Array of snapshots matching the query - */ - getSnapshots(query: SnapshotQuery): Snapshot[] { - // Validate query with Zod - const validated = SnapshotQuerySchema.parse(query); - const { since, until, limit, repositoryPath, trigger } = validated; - - let sql = 'SELECT * FROM snapshots WHERE 1=1'; - const params: unknown[] = []; - - if (since) { - sql += ' AND timestamp >= ?'; - params.push(since.getTime()); - } - - if (until) { - sql += ' AND timestamp <= ?'; - params.push(until.getTime()); - } - - if (repositoryPath) { - sql += ' AND repository_path = ?'; - params.push(repositoryPath); - } - - if (trigger) { - sql += ' AND trigger = ?'; - params.push(trigger); - } - - sql += ' ORDER BY timestamp DESC LIMIT ?'; - params.push(limit); - - const rows = this.db.prepare(sql).all(...params) as Array<{ - id: string; - timestamp: number; - repository_path: string; - stats: string; - trigger: 'index' | 'update'; - }>; - - return rows.map((row) => ({ - id: row.id, - timestamp: new Date(row.timestamp), - repositoryPath: row.repository_path, - stats: JSON.parse(row.stats) as DetailedIndexStats, - trigger: row.trigger, - })); - } - - /** - * Get the latest snapshot - * - * @param repositoryPath - Optional repository path filter - * @returns Latest snapshot or null if none exist - */ - getLatestSnapshot(repositoryPath?: string): Snapshot | null { - const snapshots = this.getSnapshots({ limit: 1, repositoryPath }); - return snapshots[0] || null; - } - - /** - * Get count of snapshots - * - * @param repositoryPath - Optional repository path filter - * @returns Total number of snapshots - */ - getCount(repositoryPath?: string): number { - let sql = 'SELECT COUNT(*) as count FROM snapshots'; - const params: unknown[] = []; - - if (repositoryPath) { - sql += ' WHERE repository_path = ?'; - params.push(repositoryPath); - } - - const result = this.db.prepare(sql).get(...params) as { count: number }; - return result.count; - } - - /** - * Get a specific snapshot by ID - * - * @param id - Snapshot ID - * @returns Snapshot or null if not found - */ - getSnapshot(id: string): Snapshot | null { - const row = this.db.prepare('SELECT * FROM snapshots WHERE id = ?').get(id) as - | { - id: string; - timestamp: number; - repository_path: string; - stats: string; - trigger: 'index' | 'update'; - } - | undefined; - - if (!row) return null; - - return { - id: row.id, - timestamp: new Date(row.timestamp), - repositoryPath: row.repository_path, - stats: JSON.parse(row.stats) as DetailedIndexStats, - trigger: row.trigger, - }; - } - - /** - * Delete old snapshots based on retention policy - * - * @param retentionDays - Number of days to keep - * @returns Number of snapshots deleted - */ - pruneOldSnapshots(retentionDays: number): number { - const cutoff = Date.now() - retentionDays * 86400000; - - const result = this.db.prepare('DELETE FROM snapshots WHERE timestamp < ?').run(cutoff); - - if (result.changes > 0) { - this.logger?.info( - { - deleted: result.changes, - retentionDays, - }, - 'Pruned old snapshots' - ); - } - - return result.changes; - } - - /** - * Calculate risk score for a file - * Formula: (commit_count * lines_of_code) / max(author_count, 1) - * - * Rationale: - * - High commit count = frequently changed (more bugs) - * - High LOC = more complex (harder to maintain) - * - Low author count = knowledge concentrated (bus factor risk) - */ - private calculateRiskScore(metadata: CodeMetadata): number { - const commitCount = metadata.commitCount || 0; - const authorCount = Math.max(metadata.authorCount || 1, 1); - const linesOfCode = metadata.linesOfCode; - - return (commitCount * linesOfCode) / authorCount; - } - - /** - * Append code metadata for a snapshot - * - * @param snapshotId - Snapshot ID to associate metadata with - * @param metadata - Array of file metadata to store - * @returns Number of records inserted - */ - appendCodeMetadata(snapshotId: string, metadata: CodeMetadata[]): number { - if (metadata.length === 0) return 0; - - const stmt = this.db.prepare(` - INSERT INTO code_metadata - (snapshot_id, file_path, commit_count, last_modified, author_count, - lines_of_code, num_functions, num_imports, risk_score) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - const insert = this.db.transaction((items: CodeMetadata[]) => { - for (const item of items) { - const riskScore = this.calculateRiskScore(item); - stmt.run( - snapshotId, - item.filePath, - item.commitCount || null, - item.lastModified ? item.lastModified.getTime() : null, - item.authorCount || null, - item.linesOfCode, - item.numFunctions, - item.numImports, - riskScore - ); - } - }); - - try { - insert(metadata); - this.logger?.debug({ snapshotId, count: metadata.length }, 'Appended code metadata'); - return metadata.length; - } catch (error) { - this.logger?.error({ error, snapshotId }, 'Failed to append code metadata'); - throw error; - } - } - - /** - * Get code metadata for a snapshot - * - * @param query - Query parameters - * @returns Array of code metadata - */ - getCodeMetadata(query: CodeMetadataQuery): CodeMetadata[] { - let sql = 'SELECT * FROM code_metadata WHERE snapshot_id = ?'; - const params: unknown[] = [query.snapshotId]; - - if (query.minRiskScore !== undefined) { - sql += ' AND risk_score >= ?'; - params.push(query.minRiskScore); - } - - // Sort order - const sortBy = query.sortBy || 'risk_desc'; - switch (sortBy) { - case 'risk_desc': - sql += ' ORDER BY risk_score DESC'; - break; - case 'risk_asc': - sql += ' ORDER BY risk_score ASC'; - break; - case 'lines_desc': - sql += ' ORDER BY lines_of_code DESC'; - break; - case 'commits_desc': - sql += ' ORDER BY commit_count DESC'; - break; - } - - sql += ' LIMIT ?'; - params.push(query.limit || 100); - - const rows = this.db.prepare(sql).all(...params) as Array<{ - file_path: string; - commit_count: number | null; - last_modified: number | null; - author_count: number | null; - lines_of_code: number; - num_functions: number; - num_imports: number; - risk_score: number; - }>; - - return rows.map((row) => ({ - filePath: row.file_path, - commitCount: row.commit_count || undefined, - lastModified: row.last_modified ? new Date(row.last_modified) : undefined, - authorCount: row.author_count || undefined, - linesOfCode: row.lines_of_code, - numFunctions: row.num_functions, - numImports: row.num_imports, - riskScore: row.risk_score, - })); - } - - /** - * Get code metadata for a specific file across snapshots - * - * @param filePath - File path to query - * @param limit - Maximum number of snapshots to return (default: 10) - * @returns Array of code metadata ordered by snapshot timestamp (newest first) - */ - getCodeMetadataForFile(filePath: string, limit = 10): CodeMetadata[] { - const sql = ` - SELECT cm.*, s.timestamp - FROM code_metadata cm - JOIN snapshots s ON cm.snapshot_id = s.id - WHERE cm.file_path = ? - ORDER BY s.timestamp DESC - LIMIT ? - `; - - const rows = this.db.prepare(sql).all(filePath, limit) as Array<{ - file_path: string; - commit_count: number | null; - last_modified: number | null; - author_count: number | null; - lines_of_code: number; - num_functions: number; - num_imports: number; - risk_score: number; - }>; - - return rows.map((row) => ({ - filePath: row.file_path, - commitCount: row.commit_count || undefined, - lastModified: row.last_modified ? new Date(row.last_modified) : undefined, - authorCount: row.author_count || undefined, - linesOfCode: row.lines_of_code, - numFunctions: row.num_functions, - numImports: row.num_imports, - riskScore: row.risk_score, - })); - } - - /** - * Get count of code metadata records for a snapshot - * - * @param snapshotId - Snapshot ID - * @returns Total number of code metadata records - */ - getCodeMetadataCount(snapshotId: string): number { - const result = this.db - .prepare('SELECT COUNT(*) as count FROM code_metadata WHERE snapshot_id = ?') - .get(snapshotId) as { count: number }; - return result.count; - } - - /** - * Close the database connection - */ - close(): void { - try { - this.db?.close(); - this.logger?.debug({}, 'Metrics store closed'); - } catch (error) { - this.logger?.error({ error }, 'Failed to close metrics store'); - } - } -} diff --git a/packages/core/src/metrics/types.ts b/packages/core/src/metrics/types.ts deleted file mode 100644 index eebfdb7..0000000 --- a/packages/core/src/metrics/types.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Metrics Store Types - * - * Type definitions for the metrics storage system. - */ - -import { z } from 'zod'; -import type { DetailedIndexStats } from '../indexer/types.js'; - -/** - * A single metrics snapshot - */ -export interface Snapshot { - id: string; - timestamp: Date; - repositoryPath: string; - stats: DetailedIndexStats; - trigger: 'index' | 'update'; -} - -/** - * Query parameters for retrieving snapshots - */ -export interface SnapshotQuery { - /** Start date (inclusive) */ - since?: Date; - - /** End date (inclusive) */ - until?: Date; - - /** Maximum number of results (default: 100, max: 1000) */ - limit?: number; - - /** Filter by repository path */ - repositoryPath?: string; - - /** Filter by trigger type */ - trigger?: 'index' | 'update'; -} - -/** - * Zod schema for validating snapshot queries - */ -export const SnapshotQuerySchema = z.object({ - since: z.coerce.date().optional(), - until: z.coerce.date().optional(), - limit: z.number().int().positive().max(1000).default(100), - repositoryPath: z.string().optional(), - trigger: z.enum(['index', 'update']).optional(), -}); - -/** - * Metrics store configuration - */ -export interface MetricsConfig { - /** Enable metrics collection (default: true) */ - enabled?: boolean; - - /** Retention period in days (default: 90) */ - retentionDays?: number; - - /** Maximum database size in MB (default: 100) */ - maxSizeMB?: number; -} - -/** - * Default metrics configuration - */ -export const DEFAULT_METRICS_CONFIG: Required = { - enabled: true, - retentionDays: 90, - maxSizeMB: 100, -}; - -/** - * Per-file code metadata for hotspot detection - */ -export interface CodeMetadata { - filePath: string; - commitCount?: number; - lastModified?: Date; - authorCount?: number; - linesOfCode: number; - numFunctions: number; - numImports: number; - riskScore?: number; -} - -/** - * Zod schema for code metadata - */ -export const CodeMetadataSchema = z.object({ - filePath: z.string().min(1), - commitCount: z.number().int().nonnegative().optional(), - lastModified: z.coerce.date().optional(), - authorCount: z.number().int().positive().optional(), - linesOfCode: z.number().int().nonnegative(), - numFunctions: z.number().int().nonnegative(), - numImports: z.number().int().nonnegative(), - riskScore: z.number().nonnegative().optional(), -}); - -/** - * Query parameters for retrieving code metadata - */ -export interface CodeMetadataQuery { - /** Snapshot ID to query */ - snapshotId: string; - - /** Minimum risk score threshold */ - minRiskScore?: number; - - /** Maximum number of results (default: 100) */ - limit?: number; - - /** Sort order (default: 'risk_desc') */ - sortBy?: 'risk_desc' | 'risk_asc' | 'lines_desc' | 'commits_desc'; -} - -/** - * Hotspot detection result - */ -export interface Hotspot { - filePath: string; - riskScore: number; - commitCount: number; - authorCount: number; - linesOfCode: number; - numFunctions: number; - lastModified?: Date; - reason: string; // Human-readable explanation -} - -/** - * Zod schema for hotspot results - */ -export const HotspotSchema = z.object({ - filePath: z.string(), - riskScore: z.number().nonnegative(), - commitCount: z.number().int().nonnegative(), - authorCount: z.number().int().positive(), - linesOfCode: z.number().int().nonnegative(), - numFunctions: z.number().int().nonnegative(), - lastModified: z.coerce.date().optional(), - reason: z.string(), -}); diff --git a/packages/core/src/services/__tests__/health-service.test.ts b/packages/core/src/services/__tests__/health-service.test.ts index f19d160..8d1c58c 100644 --- a/packages/core/src/services/__tests__/health-service.test.ts +++ b/packages/core/src/services/__tests__/health-service.test.ts @@ -4,14 +4,12 @@ import { describe, expect, it, vi } from 'vitest'; import type { RepositoryIndexer } from '../../indexer/index.js'; -import type { MetricsStore } from '../../metrics/store.js'; import type { VectorStorage } from '../../vector/index.js'; import { HealthService } from '../health-service.js'; describe('HealthService', () => { describe('check', () => { it('should return healthy status when all checks pass', async () => { - // Create mock components const mockIndexer: RepositoryIndexer = { initialize: vi.fn().mockResolvedValue(undefined), getStats: vi.fn().mockResolvedValue({ @@ -27,18 +25,11 @@ describe('HealthService', () => { close: vi.fn().mockResolvedValue(undefined), } as unknown as VectorStorage; - const mockMetricsStore: MetricsStore = { - getCount: vi.fn().mockReturnValue(10), - close: vi.fn(), - } as unknown as MetricsStore; - - // Inject mock factories const service = new HealthService( { repositoryPath: '/test/repo' }, { createIndexer: vi.fn().mockResolvedValue(mockIndexer), createVectorStorage: vi.fn().mockResolvedValue(mockVectorStorage), - createMetricsStore: vi.fn().mockReturnValue(mockMetricsStore), } ); @@ -47,44 +38,9 @@ describe('HealthService', () => { expect(result.status).toBe('healthy'); expect(result.checks.indexer.status).toBe('ok'); expect(result.checks.vectorStorage.status).toBe('ok'); - expect(result.checks.metricsStore.status).toBe('ok'); expect(result.timestamp).toBeInstanceOf(Date); }); - it('should return degraded status when metrics check has warning', async () => { - const mockIndexer: RepositoryIndexer = { - initialize: vi.fn().mockResolvedValue(undefined), - getStats: vi.fn().mockResolvedValue({ endTime: new Date() }), - close: vi.fn().mockResolvedValue(undefined), - } as unknown as RepositoryIndexer; - - const mockVectorStorage: VectorStorage = { - initialize: vi.fn().mockResolvedValue(undefined), - close: vi.fn().mockResolvedValue(undefined), - } as unknown as VectorStorage; - - const mockMetricsStore: MetricsStore = { - getCount: vi.fn().mockImplementation(() => { - throw new Error('Metrics unavailable'); - }), - close: vi.fn(), - } as unknown as MetricsStore; - - const service = new HealthService( - { repositoryPath: '/test/repo' }, - { - createIndexer: vi.fn().mockResolvedValue(mockIndexer), - createVectorStorage: vi.fn().mockResolvedValue(mockVectorStorage), - createMetricsStore: vi.fn().mockReturnValue(mockMetricsStore), - } - ); - - const result = await service.check(); - - expect(result.status).toBe('degraded'); - expect(result.checks.metricsStore.status).toBe('warning'); - }); - it('should return unhealthy status when indexer check fails', async () => { const mockIndexer: RepositoryIndexer = { initialize: vi.fn().mockRejectedValue(new Error('Indexer error')), @@ -96,17 +52,11 @@ describe('HealthService', () => { close: vi.fn().mockResolvedValue(undefined), } as unknown as VectorStorage; - const mockMetricsStore: MetricsStore = { - getCount: vi.fn().mockReturnValue(10), - close: vi.fn(), - } as unknown as MetricsStore; - const service = new HealthService( { repositoryPath: '/test/repo' }, { createIndexer: vi.fn().mockResolvedValue(mockIndexer), createVectorStorage: vi.fn().mockResolvedValue(mockVectorStorage), - createMetricsStore: vi.fn().mockReturnValue(mockMetricsStore), } ); @@ -129,17 +79,11 @@ describe('HealthService', () => { close: vi.fn().mockResolvedValue(undefined), } as unknown as VectorStorage; - const mockMetricsStore: MetricsStore = { - getCount: vi.fn().mockReturnValue(10), - close: vi.fn(), - } as unknown as MetricsStore; - const service = new HealthService( { repositoryPath: '/test/repo' }, { createIndexer: vi.fn().mockResolvedValue(mockIndexer), createVectorStorage: vi.fn().mockResolvedValue(mockVectorStorage), - createMetricsStore: vi.fn().mockReturnValue(mockMetricsStore), } ); @@ -161,17 +105,11 @@ describe('HealthService', () => { close: vi.fn().mockResolvedValue(undefined), } as unknown as VectorStorage; - const mockMetricsStore: MetricsStore = { - getCount: vi.fn().mockReturnValue(10), - close: vi.fn(), - } as unknown as MetricsStore; - const service = new HealthService( { repositoryPath: '/test/repo' }, { createIndexer: vi.fn().mockResolvedValue(mockIndexer), createVectorStorage: vi.fn().mockResolvedValue(mockVectorStorage), - createMetricsStore: vi.fn().mockReturnValue(mockMetricsStore), } ); @@ -179,8 +117,7 @@ describe('HealthService', () => { await service.check(); const duration = Date.now() - startTime; - // If checks were sequential with 10ms delays, would take 30ms+ - // Parallel should complete much faster + // Parallel should complete fast expect(duration).toBeLessThan(100); }); }); diff --git a/packages/core/src/services/__tests__/metrics-service.test.ts b/packages/core/src/services/__tests__/metrics-service.test.ts deleted file mode 100644 index 7bcd649..0000000 --- a/packages/core/src/services/__tests__/metrics-service.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -/** - * Tests for MetricsService - */ - -import { describe, expect, it, vi } from 'vitest'; -import type { FileMetrics } from '../../metrics/analytics.js'; -import type { MetricsStore } from '../../metrics/store.js'; -import type { CodeMetadata, Snapshot } from '../../metrics/types.js'; -import { MetricsService } from '../metrics-service.js'; - -vi.mock('../../storage/path.js', () => ({ - getStoragePath: vi.fn().mockResolvedValue('/mock/storage'), - getStorageFilePaths: vi.fn().mockReturnValue({ - vectors: '/mock/storage/vectors', - indexerState: '/mock/storage/indexer-state.json', - metrics: '/mock/storage/metrics.db', - }), -})); - -describe('MetricsService', () => { - const mockSnapshot: Snapshot = { - id: 'snapshot-1', - repositoryPath: '/test/repo', - timestamp: new Date('2024-01-01T00:00:00Z'), - trigger: 'index', - stats: { - filesScanned: 100, - documentsIndexed: 250, - documentsExtracted: 250, - vectorsStored: 250, - repositoryPath: '/test/repo', - startTime: new Date('2024-01-01T00:00:00Z'), - endTime: new Date('2024-01-01T00:01:00Z'), - duration: 60000, - errors: [], - }, - }; - - describe('getMostActive', () => { - it('should return most active files', async () => { - const mockMetrics: FileMetrics[] = [ - { - filePath: 'src/file1.ts', - activity: 'high', - commitCount: 50, - size: 'medium', - linesOfCode: 200, - ownership: 'small-team', - authorCount: 3, - lastModified: new Date(), - numFunctions: 10, - numImports: 5, - }, - ]; - - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(mockSnapshot), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - // Mock the analytics function - const analytics = await import('../../metrics/analytics.js'); - vi.spyOn(analytics, 'getMostActive').mockReturnValue(mockMetrics); - - const result = await service.getMostActive(10); - - expect(result).toEqual(mockMetrics); - expect(mockStore.close).toHaveBeenCalledOnce(); - }); - - it('should return empty array when no snapshot exists', async () => { - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(null), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const result = await service.getMostActive(); - - expect(result).toEqual([]); - expect(mockStore.close).toHaveBeenCalledOnce(); - }); - }); - - describe('getLargestFiles', () => { - it('should return largest files', async () => { - const mockMetrics: FileMetrics[] = [ - { - filePath: 'src/large.ts', - activity: 'low', - commitCount: 10, - size: 'large', - linesOfCode: 1000, - ownership: 'shared', - authorCount: 5, - lastModified: new Date(), - numFunctions: 50, - numImports: 20, - }, - ]; - - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(mockSnapshot), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const analytics = await import('../../metrics/analytics.js'); - vi.spyOn(analytics, 'getLargestFiles').mockReturnValue(mockMetrics); - - const result = await service.getLargestFiles(10); - - expect(result).toEqual(mockMetrics); - }); - }); - - describe('getConcentratedOwnership', () => { - it('should return files with concentrated ownership', async () => { - const mockMetrics: FileMetrics[] = [ - { - filePath: 'src/solo.ts', - activity: 'medium', - commitCount: 30, - size: 'medium', - linesOfCode: 500, - ownership: 'single', - authorCount: 1, - lastModified: new Date(), - numFunctions: 20, - numImports: 8, - }, - ]; - - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(mockSnapshot), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const analytics = await import('../../metrics/analytics.js'); - vi.spyOn(analytics, 'getConcentratedOwnership').mockReturnValue(mockMetrics); - - const result = await service.getConcentratedOwnership(10); - - expect(result).toEqual(mockMetrics); - }); - }); - - describe('getFileTrend', () => { - it('should return file trend history', async () => { - const mockTrend: CodeMetadata[] = [ - { - filePath: 'src/file.ts', - commitCount: 10, - lastModified: new Date('2024-01-01'), - authorCount: 3, - linesOfCode: 200, - numFunctions: 5, - numImports: 3, - }, - ]; - - const mockStore: MetricsStore = { - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const analytics = await import('../../metrics/analytics.js'); - vi.spyOn(analytics, 'getFileTrend').mockReturnValue(mockTrend); - - const result = await service.getFileTrend('src/file.ts', 10); - - expect(result).toEqual(mockTrend); - }); - }); - - describe('getSummary', () => { - it('should return snapshot summary', async () => { - const mockSummary = { - totalFiles: 100, - totalLOC: 10000, - totalFunctions: 500, - avgLOC: 100, - veryActiveFiles: 5, - highActivityFiles: 10, - veryActivePercent: 5, - veryLargeFiles: 3, - largeFiles: 8, - veryLargePercent: 3, - singleAuthorFiles: 20, - pairAuthorFiles: 15, - singleAuthorPercent: 20, - }; - - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(mockSnapshot), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const analytics = await import('../../metrics/analytics.js'); - vi.spyOn(analytics, 'getSnapshotSummary').mockReturnValue(mockSummary); - - const result = await service.getSummary(); - - expect(result).toEqual(mockSummary); - }); - - it('should return null when no snapshot exists', async () => { - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(null), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const result = await service.getSummary(); - - expect(result).toBeNull(); - }); - }); - - describe('getSnapshots', () => { - it('should query snapshots', async () => { - const mockStore: MetricsStore = { - getSnapshots: vi.fn().mockReturnValue([mockSnapshot]), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const result = await service.getSnapshots({ limit: 10 }); - - expect(result).toEqual([mockSnapshot]); - expect(mockStore.getSnapshots).toHaveBeenCalledWith({ - limit: 10, - repositoryPath: '/test/repo', - }); - }); - }); - - describe('getLatestSnapshot', () => { - it('should return latest snapshot', async () => { - const mockStore: MetricsStore = { - getLatestSnapshot: vi.fn().mockReturnValue(mockSnapshot), - close: vi.fn(), - } as unknown as MetricsStore; - - const mockFactory = vi.fn().mockReturnValue(mockStore); - const service = new MetricsService({ repositoryPath: '/test/repo' }, mockFactory); - - const result = await service.getLatestSnapshot(); - - expect(result).toEqual(mockSnapshot); - }); - }); -}); diff --git a/packages/core/src/services/health-service.ts b/packages/core/src/services/health-service.ts index 8db6f0a..c0372b9 100644 --- a/packages/core/src/services/health-service.ts +++ b/packages/core/src/services/health-service.ts @@ -7,7 +7,6 @@ import type { Logger } from '@prosdevlab/kero'; import type { RepositoryIndexer } from '../indexer/index.js'; -import type { MetricsStore } from '../metrics/store.js'; import type { VectorStorage } from '../vector/index.js'; export interface ComponentHealth { @@ -22,7 +21,6 @@ export interface HealthCheckResult { checks: { indexer: ComponentHealth; vectorStorage: ComponentHealth; - metricsStore: ComponentHealth; }; } @@ -52,13 +50,12 @@ export interface VectorStorageFactoryConfig { export interface HealthServiceFactories { createIndexer?: (config: IndexerFactoryConfig) => Promise; createVectorStorage?: (config: VectorStorageFactoryConfig) => Promise; - createMetricsStore?: (path: string, logger?: Logger) => MetricsStore; } /** * Service for checking component health * - * Runs health checks on indexer, vector storage, and metrics store. + * Runs health checks on indexer and vector storage. * Returns structured health information. */ export class HealthService { @@ -75,8 +72,6 @@ export class HealthService { createIndexer: factories?.createIndexer || this.defaultIndexerFactory.bind(this), createVectorStorage: factories?.createVectorStorage || this.defaultVectorStorageFactory.bind(this), - createMetricsStore: - factories?.createMetricsStore || this.defaultMetricsStoreFactory.bind(this), }; } @@ -103,11 +98,6 @@ export class HealthService { }); } - private defaultMetricsStoreFactory(path: string, logger?: Logger): MetricsStore { - const { MetricsStore: Store } = require('../metrics/store.js'); - return new Store(path, logger); - } - /** * Run comprehensive health checks * @@ -121,15 +111,14 @@ export class HealthService { const filePaths = getStorageFilePaths(storagePath); // Run checks in parallel - const [indexer, vectorStorage, metricsStore] = await Promise.all([ + const [indexer, vectorStorage] = await Promise.all([ this.checkIndexer(filePaths), this.checkVectorStorage(filePaths), - this.checkMetricsStore(filePaths), ]); // Determine overall status - const hasError = [indexer, vectorStorage, metricsStore].some((c) => c.status === 'error'); - const hasWarning = [indexer, vectorStorage, metricsStore].some((c) => c.status === 'warning'); + const hasError = [indexer, vectorStorage].some((c) => c.status === 'error'); + const hasWarning = [indexer, vectorStorage].some((c) => c.status === 'warning'); const overallStatus = hasError ? 'unhealthy' : hasWarning ? 'degraded' : 'healthy'; @@ -139,14 +128,12 @@ export class HealthService { checks: { indexer, vectorStorage, - metricsStore, }, }; } private async checkIndexer(filePaths: { vectors: string; - indexerState: string; [key: string]: string; }): Promise { try { @@ -208,28 +195,4 @@ export class HealthService { }; } } - - private async checkMetricsStore(filePaths: { - metrics: string; - [key: string]: string; - }): Promise { - try { - const metricsStore = this.factories.createMetricsStore(filePaths.metrics, this.logger); - const count = metricsStore.getCount(); - metricsStore.close(); - - return { - status: 'ok', - details: { - snapshotCount: count, - }, - }; - } catch (error) { - // Metrics is optional, so warning instead of error - return { - status: 'warning', - message: error instanceof Error ? error.message : 'Metrics store unavailable', - }; - } - } } diff --git a/packages/core/src/services/index.ts b/packages/core/src/services/index.ts index 03aee53..1d10e53 100644 --- a/packages/core/src/services/index.ts +++ b/packages/core/src/services/index.ts @@ -12,7 +12,6 @@ export { HealthService, type HealthServiceConfig, } from './health-service.js'; -export { MetricsService, type MetricsServiceConfig } from './metrics-service.js'; export { type ErrorHandlingComparison, type ErrorHandlingPattern, diff --git a/packages/core/src/services/metrics-service.ts b/packages/core/src/services/metrics-service.ts deleted file mode 100644 index 9d9c902..0000000 --- a/packages/core/src/services/metrics-service.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Metrics Service - * - * Shared service for querying analytics and metrics. - * Used by both MCP adapters and Dashboard API routes. - */ - -import type { Logger } from '@prosdevlab/kero'; -import type { FileMetrics } from '../metrics/analytics.js'; -import type { MetricsStore } from '../metrics/store.js'; -import type { CodeMetadata, Snapshot, SnapshotQuery } from '../metrics/types.js'; - -export interface MetricsServiceConfig { - repositoryPath: string; - logger?: Logger; -} - -/** - * Factory function for creating MetricsStore instances - */ -export type MetricsStoreFactory = (path: string, logger?: Logger) => MetricsStore; - -/** - * Service for querying metrics and analytics - * - * Encapsulates metrics store access and analytics queries. - * Ensures consistent behavior across MCP and Dashboard. - */ -export class MetricsService { - private repositoryPath: string; - private logger?: Logger; - private createStore: MetricsStoreFactory; - - constructor(config: MetricsServiceConfig, createStore?: MetricsStoreFactory) { - this.repositoryPath = config.repositoryPath; - this.logger = config.logger; - - // Use provided factory or default implementation - this.createStore = createStore || this.defaultStoreFactory.bind(this); - } - - /** - * Default factory that creates a real MetricsStore - */ - private defaultStoreFactory(path: string, logger?: Logger): MetricsStore { - const { MetricsStore: Store } = require('../metrics/store.js'); - return new Store(path, logger); - } - - /** - * Get metrics store for this repository - */ - private async getStore(): Promise { - const { getStoragePath, getStorageFilePaths } = await import('../storage/path.js'); - const storagePath = await getStoragePath(this.repositoryPath); - const filePaths = getStorageFilePaths(storagePath); - return this.createStore(filePaths.metrics, this.logger); - } - - /** - * Get most active files by commit count - */ - async getMostActive(limit = 10): Promise { - const store = await this.getStore(); - try { - const { getMostActive } = await import('../metrics/analytics.js'); - const latest = store.getLatestSnapshot(this.repositoryPath); - if (!latest) { - return []; - } - return getMostActive(store, latest.id, limit); - } finally { - store.close(); - } - } - - /** - * Get largest files by LOC - */ - async getLargestFiles(limit = 10): Promise { - const store = await this.getStore(); - try { - const { getLargestFiles } = await import('../metrics/analytics.js'); - const latest = store.getLatestSnapshot(this.repositoryPath); - if (!latest) { - return []; - } - return getLargestFiles(store, latest.id, limit); - } finally { - store.close(); - } - } - - /** - * Get files with concentrated ownership - */ - async getConcentratedOwnership(limit = 10): Promise { - const store = await this.getStore(); - try { - const { getConcentratedOwnership } = await import('../metrics/analytics.js'); - const latest = store.getLatestSnapshot(this.repositoryPath); - if (!latest) { - return []; - } - return getConcentratedOwnership(store, latest.id, limit); - } finally { - store.close(); - } - } - - /** - * Get file trend history - */ - async getFileTrend(filePath: string, limit = 10): Promise { - const store = await this.getStore(); - try { - const { getFileTrend } = await import('../metrics/analytics.js'); - return getFileTrend(store, filePath, limit); - } finally { - store.close(); - } - } - - /** - * Get snapshot summary statistics - */ - async getSummary(): Promise | null> { - const store = await this.getStore(); - try { - const { getSnapshotSummary } = await import('../metrics/analytics.js'); - const latest = store.getLatestSnapshot(this.repositoryPath); - if (!latest) { - return null; - } - return getSnapshotSummary(store, latest.id); - } finally { - store.close(); - } - } - - /** - * Query historical snapshots - */ - async getSnapshots(query: SnapshotQuery): Promise { - const store = await this.getStore(); - try { - return store.getSnapshots({ - ...query, - repositoryPath: query.repositoryPath || this.repositoryPath, - }); - } finally { - store.close(); - } - } - - /** - * Get latest snapshot - */ - async getLatestSnapshot(): Promise { - const store = await this.getStore(); - try { - return store.getLatestSnapshot(this.repositoryPath); - } finally { - store.close(); - } - } -} diff --git a/packages/core/src/storage/path.ts b/packages/core/src/storage/path.ts index ba89bd2..f673f21 100644 --- a/packages/core/src/storage/path.ts +++ b/packages/core/src/storage/path.ts @@ -104,7 +104,6 @@ export async function ensureStorageDirectory(storagePath: string): Promise export function getStorageFilePaths(storagePath: string): { vectors: string; metadata: string; - metrics: string; watcherSnapshot: string; /** @deprecated Removed in Phase 2 — only used for migration cleanup */ indexerState: string; @@ -114,7 +113,6 @@ export function getStorageFilePaths(storagePath: string): { return { vectors: path.join(storagePath, 'vectors'), metadata: path.join(storagePath, 'metadata.json'), - metrics: path.join(storagePath, 'metrics.db'), watcherSnapshot: path.join(storagePath, 'watcher-snapshot'), // Legacy paths — kept for migration cleanup only indexerState: path.join(storagePath, 'indexer-state.json'), diff --git a/packages/dev-agent/package.json b/packages/dev-agent/package.json index 6f81d6d..9dc5842 100644 --- a/packages/dev-agent/package.json +++ b/packages/dev-agent/package.json @@ -44,7 +44,6 @@ }, "dependencies": { "@parcel/watcher": "^2.5.6", - "better-sqlite3": "^12.5.0", "ts-morph": "^27.0.2", "web-tree-sitter": "^0.25.10" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf0f504..2f3a802 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,9 +90,6 @@ importers: '@prosdevlab/kero': specifier: workspace:* version: link:../logger - better-sqlite3: - specifier: ^12.5.0 - version: 12.5.0 globby: specifier: ^16.0.0 version: 16.0.0 @@ -118,9 +115,6 @@ importers: specifier: ^4.1.13 version: 4.1.13 devDependencies: - '@types/better-sqlite3': - specifier: ^7.6.13 - version: 7.6.13 '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -139,9 +133,6 @@ importers: '@parcel/watcher': specifier: ^2.5.6 version: 2.5.6 - better-sqlite3: - specifier: ^12.5.0 - version: 12.5.0 ts-morph: specifier: ^27.0.2 version: 27.0.2 @@ -1845,12 +1836,6 @@ packages: resolution: {integrity: sha512-Kgq5yXTvnUnvlhob0xJpOH4na9PWtuFhHSf94MpDwnENWgiFeJKDNANQV2MT1WpXZYkK2WSWfVYKhVkR7bc8TA==} dev: true - /@types/better-sqlite3@7.6.13: - resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} - dependencies: - '@types/node': 22.19.1 - dev: true - /@types/chai@5.2.3: resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} dependencies: @@ -2202,10 +2187,6 @@ packages: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} dev: false - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -2213,42 +2194,12 @@ packages: is-windows: 1.0.2 dev: true - /better-sqlite3@12.5.0: - resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==} - engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} - requiresBuild: true - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - dev: false - - /bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false - /braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} dependencies: fill-range: 7.1.1 - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false - /bundle-require@5.1.0(esbuild@0.27.0): resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2328,10 +2279,6 @@ packages: readdirp: 4.1.2 dev: true - /chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - dev: false - /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2493,13 +2440,6 @@ packages: character-entities: 2.0.2 dev: false - /decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dependencies: - mimic-response: 3.1.0 - dev: false - /deep-eql@4.1.4: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} @@ -2512,11 +2452,6 @@ packages: engines: {node: '>=6'} dev: true - /deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: false - /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2563,12 +2498,6 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - /end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - dependencies: - once: 1.4.0 - dev: false - /enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -2728,11 +2657,6 @@ packages: strip-final-newline: 3.0.0 dev: true - /expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - dev: false - /expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -2780,10 +2704,6 @@ packages: dependencies: picomatch: 4.0.3 - /file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - /fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2815,10 +2735,6 @@ packages: rollup: 4.52.4 dev: true - /fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - dev: false - /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2879,10 +2795,6 @@ packages: split2: 4.2.0 dev: true - /github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - dev: false - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2956,10 +2868,6 @@ packages: safer-buffer: 2.1.2 dev: true - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false - /ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2982,14 +2890,6 @@ packages: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: false - /ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3538,11 +3438,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - /mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - dev: false - /minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -3552,10 +3447,7 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - /mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - dev: false + dev: true /mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -3588,17 +3480,6 @@ packages: hasBin: true dev: true - /napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - dev: false - - /node-abi@3.85.0: - resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} - engines: {node: '>=10'} - dependencies: - semver: 7.7.3 - dev: false - /node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -3614,12 +3495,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: false - /onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3842,25 +3717,6 @@ packages: source-map-js: 1.2.1 dev: true - /prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.85.0 - pump: 3.0.3 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - dev: false - /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -3876,13 +3732,6 @@ packages: react-is: 18.3.1 dev: true - /pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - dev: false - /quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} dev: true @@ -3890,16 +3739,6 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - /rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - dev: false - /react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} dev: true @@ -3914,15 +3753,6 @@ packages: strip-bom: 3.0.0 dev: true - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: false - /readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -4026,10 +3856,6 @@ packages: dependencies: queue-microtask: 1.2.3 - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true @@ -4038,6 +3864,7 @@ packages: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + dev: true /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -4059,18 +3886,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - /simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - dev: false - - /simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - dev: false - /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4153,12 +3968,6 @@ packages: strip-ansi: 7.1.2 dev: true - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: false - /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4181,11 +3990,6 @@ packages: engines: {node: '>=12'} dev: true - /strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - dev: false - /strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} dependencies: @@ -4213,26 +4017,6 @@ packages: has-flag: 4.0.0 dev: true - /tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.3 - tar-stream: 2.2.0 - dev: false - - /tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false - /term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -4388,12 +4172,6 @@ packages: - yaml dev: true - /tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - dependencies: - safe-buffer: 5.2.1 - dev: false - /turbo-darwin-64@2.5.8: resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==} cpu: [x64] @@ -4531,10 +4309,6 @@ packages: engines: {node: '>= 4.0.0'} dev: true - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false - /vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} dependencies: @@ -4951,10 +4725,6 @@ packages: strip-ansi: 7.1.2 dev: false - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false - /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} From 95c4238dfef16b726f917b3ff1890e59b9da9806 Mon Sep 17 00:00:00 2001 From: prosdev Date: Mon, 30 Mar 2026 20:06:10 -0700 Subject: [PATCH 2/2] chore: add changeset and release notes for v0.10.2 Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/fix-mcp-install-remove-metrics.md | 5 +++++ website/content/latest-version.ts | 8 ++++---- website/content/updates/index.mdx | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-mcp-install-remove-metrics.md diff --git a/.changeset/fix-mcp-install-remove-metrics.md b/.changeset/fix-mcp-install-remove-metrics.md new file mode 100644 index 0000000..ca35859 --- /dev/null +++ b/.changeset/fix-mcp-install-remove-metrics.md @@ -0,0 +1,5 @@ +--- +"@prosdevlab/dev-agent": patch +--- + +Fix `dev mcp install` failing with "Repository not indexed" after successful indexing. Remove dead metrics module and better-sqlite3 dependency (-36 packages, -2400 lines). diff --git a/website/content/latest-version.ts b/website/content/latest-version.ts index e8e3df3..1969c5c 100644 --- a/website/content/latest-version.ts +++ b/website/content/latest-version.ts @@ -4,10 +4,10 @@ */ export const latestVersion = { - version: '0.10.1', - title: 'Tool Refinements & Docs Cleanup', + version: '0.10.2', + title: 'MCP Install Fix & Dependency Cleanup', date: 'March 30, 2026', summary: - 'Renamed dev_inspect → dev_patterns, merged dev explore into dev search, fixed search thresholds, expanded scanner exclusions.', - link: '/updates#v0101--tool-refinements--docs-cleanup', + 'Fixed dev mcp install check, removed dead metrics module and better-sqlite3 dependency.', + link: '/updates#v0102--mcp-install-fix--dependency-cleanup', } as const; diff --git a/website/content/updates/index.mdx b/website/content/updates/index.mdx index 3043fc7..939eeb6 100644 --- a/website/content/updates/index.mdx +++ b/website/content/updates/index.mdx @@ -9,6 +9,20 @@ What's new in dev-agent. We ship improvements regularly to help AI assistants un --- +## v0.10.2 — MCP Install Fix & Dependency Cleanup + +*March 30, 2026* + +**Fixed `dev mcp install` and removed dead metrics module.** + +- Fixed `dev mcp install` failing with "Repository not indexed" after successful indexing (was checking for stale LanceDB `vectors` directory instead of `metadata.json`) +- Removed dead metrics module — MetricsStore, analytics, collector were write-only with no consumers +- Removed `better-sqlite3` native dependency (-36 transitive packages) +- Updated storage help text to reflect actual Antfly-based storage +- 1,568 tests passing, 0 failures + +--- + ## v0.10.1 — Tool Refinements & Docs Cleanup *March 30, 2026*