Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .changeset/huge-release-v0-8-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
"@lytics/dev-agent": minor
"@lytics/dev-agent-cli": minor
"@lytics/dev-agent-core": minor
"@lytics/dev-agent-mcp": minor
"@lytics/dev-agent-subagents": minor
"@lytics/dev-agent-types": minor
---

## πŸŽ‰ v0.8.0 - Major Feature Release

This release includes 33 commits with significant new features, performance improvements, and architectural enhancements.

### πŸš€ Major Features

- **`dev map` command** - Visualize codebase structure with component counts, exports, and hot paths (224x performance improvement!)
- **`dev activity` command** - Show most active files with commit counts, recency, and complexity
- **`dev owners` command** - Developer specialization breakdown with file-level ownership
- **Author contribution indexing** - Indexed during `dev index` for 35x faster ownership queries
- **Service layer architecture** - 7 services with dependency injection for better testability
- **MetricsStore with SQLite** - Persistent code analytics with `file_authors` table
- **Code metadata system** - Factual metrics replacing risk scoring
- **Change frequency analysis** - Git activity tracking and hotspot identification
- **Stats comparison & export** - Historical metrics analysis

### 🎨 CLI/UX Improvements

- **Compact table format** for metrics commands with factual summaries
- **Top-level commands** - `dev activity` and `dev owners` (refactored from `dev metrics`)
- Enhanced `dev stats` output with 10x performance boost
- Enhanced `dev git stats` with clean, scannable format
- Enhanced `dev compact`, `dev clean`, and MCP command outputs
- Modernized CLI with compact, user-friendly formatting
- Comprehensive help text with examples and use cases
- Visual indicators (πŸ”₯ for hotspots, ✏️ for activity)
- GitHub handle resolution for developer identification

### πŸ—οΈ Architecture & Quality

- Service-oriented architecture with dependency injection
- Circular dependency resolution via shared types package
- Complete Zod validation across all 9 MCP adapters and external boundaries
- Kero logger integration throughout
- SearchService refactor for better code reuse
- Improved error handling and messaging

### ⚑ Performance Optimizations

- **`dev map`**: 224x speedup (103s β†’ 0.46s)
- Added `getAll()` method for fast scans without semantic search
- Added `skipEmbedder` option for read-only operations
- Added `getBasicStats()` to avoid expensive git enrichment
- **`dev owners`**: 35x speedup (17.5s β†’ 0.5s)
- Batched git operations during indexing (1 call vs N file calls)
- Author contributions stored in `file_authors` table
- Offline capability - no git access needed after indexing
- **`dev stats`**: 10x speedup via direct JSON reads

### πŸ› Bug Fixes

- Fixed component count overflow in map generation (2.4B β†’ 3.7K)
- Fixed detailed stats persistence in indexer
- Fixed ENOBUFS issues

### πŸ“š Documentation

- Updated website for v0.7.0 features
- TypeScript standards with Zod validation examples
- Workflow documentation with commit checkpoints
- Enhanced CLI help text across all commands

### πŸ§ͺ Testing

- All 1,918 tests passing
- Added comprehensive test coverage for new features
- Mock updates for new `getAll()` method

This release represents a significant step forward in usability, performance, and code quality. Special thanks to all contributors!

6 changes: 4 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import chalk from 'chalk';
import { Command } from 'commander';
import { activityCommand } from './commands/activity.js';
import { cleanCommand } from './commands/clean.js';
import { compactCommand } from './commands/compact.js';
import { dashboardCommand } from './commands/dashboard.js';
Expand All @@ -12,7 +13,7 @@ import { indexCommand } from './commands/index.js';
import { initCommand } from './commands/init.js';
import { mapCommand } from './commands/map.js';
import { mcpCommand } from './commands/mcp.js';
import { metricsCommand } from './commands/metrics.js';
import { ownersCommand } from './commands/owners.js';
import { planCommand } from './commands/plan.js';
import { searchCommand } from './commands/search.js';
import { statsCommand } from './commands/stats.js';
Expand Down Expand Up @@ -41,7 +42,8 @@ program.addCommand(gitCommand);
program.addCommand(mapCommand);
program.addCommand(updateCommand);
program.addCommand(statsCommand);
program.addCommand(metricsCommand);
program.addCommand(ownersCommand);
program.addCommand(activityCommand);
program.addCommand(dashboardCommand);
program.addCommand(compactCommand);
program.addCommand(cleanCommand);
Expand Down
169 changes: 169 additions & 0 deletions packages/cli/src/commands/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Activity command - Show most active files by commit frequency
*/

import * as path from 'node:path';
import {
type FileMetrics,
getMostActive,
getStoragePath,
MetricsStore,
} from '@lytics/dev-agent-core';
import chalk from 'chalk';
import { Command } from 'commander';
import { loadConfig } from '../utils/config.js';
import { logger } from '../utils/logger.js';

/**
* Format relative time (e.g., "2 days ago", "today")
*/
function formatRelativeTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

if (diffDays === 0) return 'today';
if (diffDays === 1) return 'yesterday';
if (diffDays < 7) return `${diffDays}d ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
return `${Math.floor(diffDays / 365)}y ago`;
}

/**
* Format files as a compact table
*/
function formatFileMetricsTable(files: FileMetrics[]): string {
if (files.length === 0) return '';

// Calculate column widths
const maxPathLen = Math.max(...files.map((f) => f.filePath.length), 40);
const pathWidth = Math.min(maxPathLen, 55);

// Header
let output = chalk.bold(
`${'FILE'.padEnd(pathWidth)} ${'COMMITS'.padStart(7)} ${'LOC'.padStart(6)} ${'AUTHORS'.padStart(7)} ${'LAST CHANGE'}\n`
);

// Separator line
output += chalk.dim(`${'─'.repeat(pathWidth + 2 + 7 + 2 + 6 + 2 + 7 + 2 + 12)}\n`);

// Rows
for (const file of files) {
// Truncate path if too long
let displayPath = file.filePath;
if (displayPath.length > pathWidth) {
displayPath = `...${displayPath.slice(-(pathWidth - 3))}`;
}
displayPath = displayPath.padEnd(pathWidth);

const commits = String(file.commitCount).padStart(7);
const loc = String(file.linesOfCode).padStart(6);

// Author count with emoji
const authorIcon = file.authorCount === 1 ? ' πŸ‘€' : file.authorCount === 2 ? ' πŸ‘₯' : 'πŸ‘₯πŸ‘₯';
const authors = `${String(file.authorCount).padStart(5)}${authorIcon}`;

// Relative time
const lastChange = file.lastModified ? formatRelativeTime(file.lastModified) : 'unknown';

output += `${chalk.dim(displayPath)} ${chalk.cyan(commits)} ${chalk.yellow(loc)} ${chalk.green(authors)} ${chalk.gray(lastChange)}\n`;
}

return output;
}

/**
* Generate summary insights
*/
function generateActivitySummary(files: FileMetrics[]): string[] {
const insights: string[] = [];
const highChurn = files.filter((f) => f.commitCount >= 10).length;
const singleAuthor = files.filter((f) => f.authorCount === 1).length;

if (highChurn > 0) {
insights.push(`${highChurn} file${highChurn > 1 ? 's' : ''} changed 10+ times this month`);
}
if (singleAuthor > 0 && singleAuthor === files.length) {
insights.push(`All files have single author`);
} else if (singleAuthor > files.length / 2) {
insights.push(`${singleAuthor}/${files.length} files have single author`);
}

return insights;
}

/**
* Activity command - Show most active files
*/
export const activityCommand = new Command('activity')
.description('Show most active files by commit frequency')
.option('-n, --limit <number>', 'Number of files to show', '10')
.option('--json', 'Output as JSON', false)
.action(async (options) => {
try {
const config = await loadConfig();
if (!config) {
logger.error('No config found. Run "dev init" first.');
process.exit(1);
}

const repositoryPath = config.repository?.path || config.repositoryPath || process.cwd();
const storagePath = await getStoragePath(path.resolve(repositoryPath));
const metricsDbPath = path.join(storagePath, 'metrics.db');

const store = new MetricsStore(metricsDbPath);
const latestSnapshot = store.getLatestSnapshot(path.resolve(repositoryPath));

if (!latestSnapshot) {
logger.warn('No metrics found. Index your repository first with "dev index".');
store.close();
process.exit(0);
}

const limit = Number.parseInt(options.limit, 10);
const files = getMostActive(store, latestSnapshot.id, limit);

// Get total count for context
const allFiles = store.getCodeMetadata({ snapshotId: latestSnapshot.id, limit: 10000 });
const totalWithActivity = allFiles.filter((f) => (f.commitCount || 0) >= 5).length;

store.close();

if (files.length === 0) {
logger.warn('No file metrics available.');
process.exit(0);
}

// JSON output for programmatic use
if (options.json) {
console.log(JSON.stringify({ files, totalWithActivity }, null, 2));
return;
}

// Human-readable table output
console.log('');
console.log(
chalk.bold.cyan(`πŸ“Š Most Active Files (${totalWithActivity} total with 5+ commits)`)
);
console.log('');
console.log(formatFileMetricsTable(files));
console.log('');

// Add summary
const summary = generateActivitySummary(files);
if (summary.length > 0) {
console.log(chalk.dim('Summary:'));
for (const insight of summary) {
console.log(chalk.dim(` β€’ ${insight}`));
}
}

console.log('');
} catch (error) {
logger.error(
`Failed to get activity metrics: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
});
9 changes: 7 additions & 2 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { join, resolve } from 'node:path';
import {
AsyncEventBus,
ensureStorageDirectory,
Expand Down Expand Up @@ -67,7 +67,7 @@ export const indexCommand = new Command('index')
const spinner = ora('Checking prerequisites...').start();

try {
const resolvedRepoPath = repositoryPath;
const resolvedRepoPath = resolve(repositoryPath);

// Check prerequisites upfront
const isGitRepo = isGitRepository(resolvedRepoPath);
Expand Down Expand Up @@ -139,6 +139,11 @@ export const indexCommand = new Command('index')
if (event.codeMetadata && event.codeMetadata.length > 0) {
metricsStore.appendCodeMetadata(snapshotId, event.codeMetadata);
}

// Store file author contributions if available
if (event.authorContributions && event.authorContributions.size > 0) {
metricsStore.appendFileAuthors(snapshotId, event.authorContributions);
}
} catch (error) {
// Log error but don't fail indexing - metrics are non-critical
logger.error(
Expand Down
Loading