Skip to content

Commit 4f8b371

Browse files
author
Jeremy Stover
committed
feat: expand MCP tools and Codex integration
1 parent b416a69 commit 4f8b371

15 files changed

Lines changed: 1655 additions & 414 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## [0.9.0] - 2026-03-12
4+
5+
### Added
6+
- Expanded MCP coverage with first-class `trace`, `get_module`, `dead_code_audit`, and `index_status` tools.
7+
- Added richer MCP index operations for stats, checks, updates, prune, and export workflows.
8+
- Added explicit search diagnostics for semantic index usage, fallback behavior, and index health.
9+
10+
### Changed
11+
- Updated Codex MCP setup to use `config.toml` for global and project-scoped configuration instead of legacy `mcp.json`.
12+
- Improved dead-code auditing heuristics for NestJS-style framework-managed symbols.
13+
- Improved server status reporting to distinguish live in-memory layer state from on-disk cache/index state.
14+
15+
### Fixed
16+
- Fixed incremental index/cache metadata inconsistencies that could leave stale stats and incomplete retained summaries.
17+
- Fixed trace resolution by symbol name and file path when the symbol index is stored as `symbol_index.jsonl.gz`.
18+
- Fixed module query resolution for normal repository paths.
19+
- Fixed symbol-index detection for compressed index artifacts.
20+
- Hardened drift tests to create valid local git repos even when commit signing is enabled globally.
21+
322
## [0.8.1] - 2026-03-07
423

524
### 🧸 Teddy, Day 2 — Electric Boogaloo

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "semfora-engine"
3-
version = "0.8.1"
3+
version = "0.9.0"
44
edition = "2021"
55
description = "Semantic code analyzer with TOON output"
66
license = "Apache-2.0"

src/cache/mod.rs

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,10 @@ impl CacheDir {
406406
}
407407

408408
/// Read a symbol's raw content from the consolidated file, falling back to individual shards
409-
pub fn read_symbol_raw(&self, symbol_hash: &str) -> std::result::Result<String, crate::error::McpDiffError> {
409+
pub fn read_symbol_raw(
410+
&self,
411+
symbol_hash: &str,
412+
) -> std::result::Result<String, crate::error::McpDiffError> {
410413
// Try consolidated file first
411414
let idx_path = self.symbols_idx_path();
412415
if idx_path.exists() {
@@ -418,8 +421,10 @@ impl CacheDir {
418421
// Fall back to individual shard files
419422
let path = self.symbol_path(symbol_hash);
420423
if path.exists() {
421-
return std::fs::read_to_string(&path).map_err(|e| crate::error::McpDiffError::GitError {
422-
message: format!("Failed to read symbol file: {}", e),
424+
return std::fs::read_to_string(&path).map_err(|e| {
425+
crate::error::McpDiffError::GitError {
426+
message: format!("Failed to read symbol file: {}", e),
427+
}
423428
});
424429
}
425430

@@ -430,40 +435,49 @@ impl CacheDir {
430435

431436
/// Load the symbols.idx into a HashMap (last entry per hash wins, deduplicating stale entries).
432437
/// Cached via OnceCell so the index is only parsed once per CacheDir lifetime.
433-
fn load_symbol_dat_index(&self) -> std::result::Result<&std::collections::HashMap<String, (u64, usize)>, crate::error::McpDiffError> {
434-
self.symbol_dat_index.get_or_try_init(|| {
435-
use std::io::BufRead;
436-
437-
let idx_path = self.symbols_idx_path();
438-
let idx_file = std::fs::File::open(&idx_path).map_err(|e| crate::error::McpDiffError::GitError {
439-
message: format!("Failed to open symbols.idx: {}", e),
440-
})?;
438+
fn load_symbol_dat_index(
439+
&self,
440+
) -> std::result::Result<
441+
&std::collections::HashMap<String, (u64, usize)>,
442+
crate::error::McpDiffError,
443+
> {
444+
self.symbol_dat_index
445+
.get_or_try_init(|| {
446+
use std::io::BufRead;
447+
448+
let idx_path = self.symbols_idx_path();
449+
let idx_file = std::fs::File::open(&idx_path).map_err(|e| {
450+
crate::error::McpDiffError::GitError {
451+
message: format!("Failed to open symbols.idx: {}", e),
452+
}
453+
})?;
441454

442-
let reader = std::io::BufReader::new(idx_file);
443-
let mut map = std::collections::HashMap::new();
455+
let reader = std::io::BufReader::new(idx_file);
456+
let mut map = std::collections::HashMap::new();
444457

445-
for line in reader.lines() {
446-
let line = line.map_err(|e| crate::error::McpDiffError::GitError {
447-
message: format!("Failed to read index line: {}", e),
448-
})?;
449-
let trimmed = line.trim();
450-
if trimmed.is_empty() {
451-
continue;
452-
}
453-
if let Ok(entry) = serde_json::from_str::<serde_json::Value>(trimmed) {
454-
if let (Some(h), Some(o), Some(l)) = (
455-
entry.get("h").and_then(|v| v.as_str()),
456-
entry.get("o").and_then(|v| v.as_u64()),
457-
entry.get("l").and_then(|v| v.as_u64()),
458-
) {
459-
// Last entry wins — deduplicates stale entries from incremental appends
460-
map.insert(h.to_string(), (o, l as usize));
458+
for line in reader.lines() {
459+
let line = line.map_err(|e| crate::error::McpDiffError::GitError {
460+
message: format!("Failed to read index line: {}", e),
461+
})?;
462+
let trimmed = line.trim();
463+
if trimmed.is_empty() {
464+
continue;
465+
}
466+
if let Ok(entry) = serde_json::from_str::<serde_json::Value>(trimmed) {
467+
if let (Some(h), Some(o), Some(l)) = (
468+
entry.get("h").and_then(|v| v.as_str()),
469+
entry.get("o").and_then(|v| v.as_u64()),
470+
entry.get("l").and_then(|v| v.as_u64()),
471+
) {
472+
// Last entry wins — deduplicates stale entries from incremental appends
473+
map.insert(h.to_string(), (o, l as usize));
474+
}
461475
}
462476
}
463-
}
464477

465-
Ok(map)
466-
}).map_err(|e: crate::error::McpDiffError| e)
478+
Ok(map)
479+
})
480+
.map_err(|e: crate::error::McpDiffError| e)
467481
}
468482

469483
/// Invalidate the cached symbol dat index (call after writing new entries)
@@ -473,35 +487,48 @@ impl CacheDir {
473487

474488
/// Read a symbol from the consolidated .dat file using the cached HashMap index.
475489
/// O(1) lookup per read instead of O(n) linear scan.
476-
fn read_symbol_from_dat(&self, symbol_hash: &str) -> std::result::Result<String, crate::error::McpDiffError> {
490+
fn read_symbol_from_dat(
491+
&self,
492+
symbol_hash: &str,
493+
) -> std::result::Result<String, crate::error::McpDiffError> {
477494
use std::io::{Read, Seek, SeekFrom};
478495

479496
let index = self.load_symbol_dat_index()?;
480497

481-
let &(offset, len) = index.get(symbol_hash).ok_or_else(|| {
482-
crate::error::McpDiffError::FileNotFound {
483-
path: format!("symbol:{} (not in index)", symbol_hash),
484-
}
485-
})?;
498+
let &(offset, len) =
499+
index
500+
.get(symbol_hash)
501+
.ok_or_else(|| crate::error::McpDiffError::FileNotFound {
502+
path: format!("symbol:{} (not in index)", symbol_hash),
503+
})?;
486504

487505
let mut dat_file = std::fs::File::open(self.symbols_dat_path()).map_err(|e| {
488-
crate::error::McpDiffError::GitError { message: format!("Failed to open symbols.dat: {}", e) }
506+
crate::error::McpDiffError::GitError {
507+
message: format!("Failed to open symbols.dat: {}", e),
508+
}
489509
})?;
490510
dat_file.seek(SeekFrom::Start(offset)).map_err(|e| {
491-
crate::error::McpDiffError::GitError { message: format!("Failed to seek in symbols.dat: {}", e) }
511+
crate::error::McpDiffError::GitError {
512+
message: format!("Failed to seek in symbols.dat: {}", e),
513+
}
492514
})?;
493515
let mut buf = vec![0u8; len];
494-
dat_file.read_exact(&mut buf).map_err(|e| {
495-
crate::error::McpDiffError::GitError { message: format!("Failed to read from symbols.dat: {}", e) }
496-
})?;
516+
dat_file
517+
.read_exact(&mut buf)
518+
.map_err(|e| crate::error::McpDiffError::GitError {
519+
message: format!("Failed to read from symbols.dat: {}", e),
520+
})?;
497521

498-
String::from_utf8(buf).map_err(|e| {
499-
crate::error::McpDiffError::GitError { message: format!("Invalid UTF-8 in symbol: {}", e) }
522+
String::from_utf8(buf).map_err(|e| crate::error::McpDiffError::GitError {
523+
message: format!("Invalid UTF-8 in symbol: {}", e),
500524
})
501525
}
502526

503527
/// Read a symbol and parse it as CachedContent, with fallback to individual shards
504-
pub fn read_symbol_cached(&self, symbol_hash: &str) -> crate::error::Result<crate::commands::toon_parser::CachedContent> {
528+
pub fn read_symbol_cached(
529+
&self,
530+
symbol_hash: &str,
531+
) -> crate::error::Result<crate::commands::toon_parser::CachedContent> {
505532
let raw = self.read_symbol_raw(symbol_hash)?;
506533
let is_json = raw.trim_start().starts_with('{');
507534
let json = if is_json {
@@ -1012,6 +1039,7 @@ impl CacheDir {
10121039
/// Check if symbol index exists
10131040
pub fn has_symbol_index(&self) -> bool {
10141041
self.symbol_index_path().exists()
1042+
|| self.symbol_index_path().with_extension("jsonl.gz").exists()
10151043
}
10161044

10171045
/// Path to the function signature index file (JSONL format)
@@ -1072,10 +1100,10 @@ impl CacheDir {
10721100
file_path: &str,
10731101
new_entries: Vec<SymbolIndexEntry>,
10741102
) -> Result<()> {
1075-
use std::io::{BufRead, Write};
10761103
use flate2::read::GzDecoder;
10771104
use flate2::write::GzEncoder;
10781105
use flate2::Compression;
1106+
use std::io::{BufRead, Write};
10791107

10801108
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
10811109
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1192,8 +1220,8 @@ impl CacheDir {
11921220
risk_filter: Option<&str>,
11931221
limit: usize,
11941222
) -> Result<Vec<SymbolIndexEntry>> {
1195-
use std::io::BufRead;
11961223
use flate2::read::GzDecoder;
1224+
use std::io::BufRead;
11971225

11981226
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
11991227
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1279,8 +1307,8 @@ impl CacheDir {
12791307
risk_filter: Option<&str>,
12801308
limit: usize,
12811309
) -> Result<Vec<SymbolIndexEntry>> {
1282-
use std::io::BufRead;
12831310
use flate2::read::GzDecoder;
1311+
use std::io::BufRead;
12841312

12851313
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
12861314
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1344,8 +1372,8 @@ impl CacheDir {
13441372
/// Returns all entries from the symbol index without filtering.
13451373
/// Use this for batch analysis operations.
13461374
pub fn load_all_symbol_entries(&self) -> Result<Vec<SymbolIndexEntry>> {
1347-
use std::io::BufRead;
13481375
use flate2::read::GzDecoder;
1376+
use std::io::BufRead;
13491377

13501378
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
13511379
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1392,8 +1420,8 @@ impl CacheDir {
13921420
where
13931421
F: Fn(&SymbolIndexEntry) -> bool,
13941422
{
1395-
use std::io::BufRead;
13961423
use flate2::read::GzDecoder;
1424+
use std::io::BufRead;
13971425

13981426
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
13991427
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1439,8 +1467,8 @@ impl CacheDir {
14391467
where
14401468
F: Fn(&SymbolIndexEntry) -> bool,
14411469
{
1442-
use std::io::BufRead;
14431470
use flate2::read::GzDecoder;
1471+
use std::io::BufRead;
14441472

14451473
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
14461474
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1481,9 +1509,11 @@ impl CacheDir {
14811509

14821510
/// Build a HashMap from symbol hash to (name, risk) by streaming the index.
14831511
/// Used for call graph resolution without loading the full index.
1484-
pub fn build_symbol_name_map(&self) -> Result<std::collections::HashMap<String, (String, String)>> {
1485-
use std::io::BufRead;
1512+
pub fn build_symbol_name_map(
1513+
&self,
1514+
) -> Result<std::collections::HashMap<String, (String, String)>> {
14861515
use flate2::read::GzDecoder;
1516+
use std::io::BufRead;
14871517

14881518
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
14891519
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -1512,7 +1542,10 @@ impl CacheDir {
15121542
Err(_) => continue,
15131543
};
15141544

1515-
map.insert(entry.hash.clone(), (entry.symbol.clone(), entry.risk.clone()));
1545+
map.insert(
1546+
entry.hash.clone(),
1547+
(entry.symbol.clone(), entry.risk.clone()),
1548+
);
15161549
}
15171550

15181551
Ok(map)
@@ -2165,8 +2198,8 @@ impl CacheDir {
21652198

21662199
// Get unique files from symbol index (streaming — only collect file paths, not full entries)
21672200
let unique_files: HashSet<String> = {
2168-
use std::io::BufRead;
21692201
use flate2::read::GzDecoder;
2202+
use std::io::BufRead;
21702203

21712204
// Try .jsonl.gz first (compressed), fall back to .jsonl (uncompressed)
21722205
let compressed_path = self.symbol_index_path().with_extension("jsonl.gz");
@@ -2187,7 +2220,9 @@ impl CacheDir {
21872220
if let Some(reader) = reader {
21882221
for line in reader.lines() {
21892222
let line = line?;
2190-
if line.trim().is_empty() { continue; }
2223+
if line.trim().is_empty() {
2224+
continue;
2225+
}
21912226
// Quick JSON extraction of just the "f" field
21922227
if let Ok(entry) = serde_json::from_str::<SymbolIndexEntry>(&line) {
21932228
files.insert(entry.file);

0 commit comments

Comments
 (0)