diff --git a/README.md b/README.md index b79e315..3e760a6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ > **Alpha software — API is stabilizing but may change** > > codedb works and is used daily in production AI workflows, but: -> - **Language support** — Zig, Python, TypeScript/JavaScript, Rust, Go, PHP, Ruby, HCL, R, Dart/Flutter +> - **Parser support** — Zig, C/C++, Python, TypeScript/JavaScript, Rust, Go, PHP, Ruby, HCL, R, Dart/Flutter +> - **Lightweight outline support** — Java, Kotlin, Svelte, Vue, Astro, shell, CSS/SCSS, SQL, protobuf, Fortran, LLVM IR, MLIR, and TableGen > - **No auth** — HTTP server binds to localhost only > - **Snapshot format** may change between versions > - **MCP protocol** is JSON-RPC 2.0 over stdio (stable) @@ -53,7 +54,7 @@ | Auto-registration in Claude, Codex, Gemini, Cursor | | | Polling file watcher with filtered directory walker | | | Portable snapshot for instant MCP startup | | -| Singleton MCP with PID lock + 10min idle timeout | | +| Singleton MCP with PID lock + 1h idle timeout | | | Sensitive file blocking (.env, credentials, keys) | | | Codesigned + notarized macOS binaries | | | SHA256 checksum verification in installer | | @@ -137,25 +138,30 @@ codedb hot # recently modified files ### `codedb_remote` — Cloud Intelligence -Query any public GitHub repo without cloning it. Powered by `codedb.codegraff.com`. +Query any public GitHub repo without cloning it. The default backend uses `codedb.codegraff.com`; `backend="wiki"` uses `api.wiki.codes` for code intelligence plus dependency/security artifacts. ``` -# Get the file tree of an external repo +# Get the file tree of an external repo via the default backend codedb_remote repo="vercel/next.js" action="tree" # Search for code in a dependency codedb_remote repo="justrach/merjs" action="search" query="handleRequest" -# Get symbol outlines -codedb_remote repo="justrach/merjs" action="outline" +# Exact symbol lookup through api.wiki.codes +codedb_remote repo="justrach/codedb" backend="wiki" action="symbol" query="buildSnapshot" -# Get repo metadata -codedb_remote repo="justrach/merjs" action="meta" +# Check dependency CVE evidence; scope can be runtime or all +codedb_remote repo="axios/axios" backend="wiki" action="cves" scope="runtime" + +# Raw wiki slugs are accepted for repos that are indexed that way +codedb_remote repo="chromium" backend="wiki" action="policy" ``` -**Actions:** `tree`, `outline`, `search`, `meta` +**Default actions:** `tree`, `outline`, `search`, `meta` + +**Wiki actions:** `tree`, `outline`, `search`, `symbol`, `policy`, `deps`, `score`, `cves`, `commits`, `branches`, `dep-history` -**Note:** This tool calls `codedb.codegraff.com` via HTTPS. No API key required. The service must be available for this tool to work. +**Note:** This tool calls remote HTTPS services. No API key required. The selected service must be available for this tool to work. ### CLI Commands diff --git a/docs/architecture.md b/docs/architecture.md index 8bbb5d4..a102c3f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -136,7 +136,7 @@ JSON-RPC 2.0 over stdio with Content-Length framing. Implements the Model Contex | `codedb_status` | Index status | | `codedb_snapshot` | Full snapshot | | `codedb_bundle` | Batch multiple queries (max 20 ops) | -| `codedb_remote` | Query GitHub repos via codedb.codegraff.com | +| `codedb_remote` | Query GitHub repos via codedb.codegraff.com or api.wiki.codes | | `codedb_projects` | List locally indexed projects | | `codedb_index` | Index a local folder | **Safety:** path validation, oversized message handling (drains >1MB lines instead of killing the loop). diff --git a/scripts/compare-bench.py b/scripts/compare-bench.py index ffa317f..36eb878 100644 --- a/scripts/compare-bench.py +++ b/scripts/compare-bench.py @@ -28,18 +28,28 @@ def pct_change(base_ns: int, head_ns: int) -> float: return ((head_ns - base_ns) / base_ns) * 100.0 -def render_markdown(rows: list[tuple[str, int, int, float]], threshold_pct: float) -> str: +def status_for(delta_pct: float, abs_delta_ns: int, threshold_pct: float, min_abs_ns: int) -> str: + if delta_pct <= threshold_pct: + return "OK" + if abs_delta_ns <= min_abs_ns: + return "NOISE" + return "FAIL" + + +def render_markdown(rows: list[tuple[str, int, int, float, int]], threshold_pct: float, min_abs_ns: int) -> str: lines = [ "## Benchmark Regression Report", "", - f"Threshold: {threshold_pct:.2f}%", + f"Thresholds: {threshold_pct:.2f}% and {min_abs_ns:,} ns absolute delta", + "", + "`NOISE` means the percentage threshold was exceeded, but the absolute delta was too small to fail CI.", "", - "| Tool | Base (ns) | Head (ns) | Delta | Status |", - "| --- | ---: | ---: | ---: | --- |", + "| Tool | Base (ns) | Head (ns) | Delta | Abs Delta (ns) | Status |", + "| --- | ---: | ---: | ---: | ---: | --- |", ] - for tool, base_ns, head_ns, delta in rows: - status = "FAIL" if delta > threshold_pct else "OK" - lines.append(f"| `{tool}` | {base_ns} | {head_ns} | {delta:+.2f}% | {status} |") + for tool, base_ns, head_ns, delta, abs_delta in rows: + status = status_for(delta, abs_delta, threshold_pct, min_abs_ns) + lines.append(f"| `{tool}` | {base_ns} | {head_ns} | {delta:+.2f}% | {abs_delta:+d} | {status} |") return "\n".join(lines) + "\n" @@ -57,7 +67,7 @@ def main() -> int: return 1 common = sorted(set(base) & set(head)) - rows: list[tuple[str, int, int, float]] = [] + rows: list[tuple[str, int, int, float, int]] = [] failures: list[str] = [] for tool in common: @@ -65,13 +75,13 @@ def main() -> int: head_ns = int(head[tool]["avg_latency_ns"]) delta = pct_change(base_ns, head_ns) abs_delta = head_ns - base_ns - rows.append((tool, base_ns, head_ns, delta)) + rows.append((tool, base_ns, head_ns, delta, abs_delta)) # Only flag as regression if BOTH percentage AND absolute delta exceed thresholds # This prevents false positives on fast tools where CI noise dominates if delta > args.threshold_pct and abs_delta > args.min_abs_ns: - failures.append(f"{tool} regressed by {delta:.2f}%") + failures.append(f"{tool} regressed by {delta:.2f}% ({abs_delta:+d} ns)") - report = render_markdown(rows, args.threshold_pct) + report = render_markdown(rows, args.threshold_pct, args.min_abs_ns) sys.stdout.write(report) if args.markdown_out: diff --git a/scripts/test_compare_bench.py b/scripts/test_compare_bench.py new file mode 100644 index 0000000..8aedeb9 --- /dev/null +++ b/scripts/test_compare_bench.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import importlib.util +from pathlib import Path +import unittest + + +SCRIPT = Path(__file__).with_name("compare-bench.py") +spec = importlib.util.spec_from_file_location("compare_bench", SCRIPT) +assert spec is not None +compare_bench = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(compare_bench) + + +class CompareBenchTests(unittest.TestCase): + def test_small_absolute_regression_is_noise(self) -> None: + self.assertEqual(compare_bench.status_for(22.54, 11_399, 10.0, 50_000), "NOISE") + + def test_large_absolute_regression_fails(self) -> None: + self.assertEqual(compare_bench.status_for(12.0, 75_000, 10.0, 50_000), "FAIL") + + def test_report_explains_noise_status(self) -> None: + report = compare_bench.render_markdown( + [("codedb_read", 50_580, 61_979, 22.54, 11_399)], + 10.0, + 50_000, + ) + self.assertIn("50,000 ns absolute delta", report) + self.assertIn("| `codedb_read` | 50580 | 61979 | +22.54% | +11399 | NOISE |", report) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/explore.zig b/src/explore.zig index f0c0564..3a9388f 100644 --- a/src/explore.zig +++ b/src/explore.zig @@ -100,12 +100,30 @@ pub const Language = enum(u8) { yaml, unknown, dart, + java, + kotlin, + svelte, + vue, + astro, + shell, + css, + scss, + sql, + protobuf, + fortran, + llvm_ir, + mlir, + tablegen, }; pub fn detectLanguage(path: []const u8) Language { if (std.mem.endsWith(u8, path, ".zig")) return .zig; if (std.mem.endsWith(u8, path, ".c") or std.mem.endsWith(u8, path, ".h")) return .c; - if (std.mem.endsWith(u8, path, ".cpp") or std.mem.endsWith(u8, path, ".hpp")) return .cpp; + if (std.mem.endsWith(u8, path, ".cpp") or std.mem.endsWith(u8, path, ".hpp") or + std.mem.endsWith(u8, path, ".cc") or std.mem.endsWith(u8, path, ".hh") or + std.mem.endsWith(u8, path, ".cxx") or std.mem.endsWith(u8, path, ".hxx") or + std.mem.endsWith(u8, path, ".mm")) + return .cpp; if (std.mem.endsWith(u8, path, ".py")) return .python; if (std.mem.endsWith(u8, path, ".js") or std.mem.endsWith(u8, path, ".jsx")) return .javascript; if (std.mem.endsWith(u8, path, ".ts") or std.mem.endsWith(u8, path, ".tsx")) return .typescript; @@ -119,6 +137,20 @@ pub fn detectLanguage(path: []const u8) Language { if (std.mem.endsWith(u8, path, ".json")) return .json; if (std.mem.endsWith(u8, path, ".yaml") or std.mem.endsWith(u8, path, ".yml")) return .yaml; if (std.mem.endsWith(u8, path, ".dart")) return .dart; + if (std.mem.endsWith(u8, path, ".java")) return .java; + if (std.mem.endsWith(u8, path, ".kt")) return .kotlin; + if (std.mem.endsWith(u8, path, ".svelte")) return .svelte; + if (std.mem.endsWith(u8, path, ".vue")) return .vue; + if (std.mem.endsWith(u8, path, ".astro")) return .astro; + if (std.mem.endsWith(u8, path, ".sh")) return .shell; + if (std.mem.endsWith(u8, path, ".css")) return .css; + if (std.mem.endsWith(u8, path, ".scss")) return .scss; + if (std.mem.endsWith(u8, path, ".sql")) return .sql; + if (std.mem.endsWith(u8, path, ".proto")) return .protobuf; + if (std.mem.endsWith(u8, path, ".f90")) return .fortran; + if (std.mem.endsWith(u8, path, ".ll")) return .llvm_ir; + if (std.mem.endsWith(u8, path, ".mlir")) return .mlir; + if (std.mem.endsWith(u8, path, ".td")) return .tablegen; return .unknown; } @@ -609,7 +641,12 @@ pub const Explorer = struct { outline.language == .cpp or outline.language == .typescript or outline.language == .javascript or outline.language == .rust or outline.language == .go_lang or outline.language == .php or - outline.language == .dart; + outline.language == .dart or outline.language == .java or + outline.language == .kotlin or outline.language == .svelte or + outline.language == .vue or outline.language == .astro or + outline.language == .css or outline.language == .scss or + outline.language == .protobuf or outline.language == .mlir or + outline.language == .tablegen; for (outline.symbols.items) |*sym| { // Skip single-line kinds @@ -834,7 +871,12 @@ pub const Explorer = struct { outline.language == .go_lang or outline.language == .c or outline.language == .cpp or outline.language == .rust or outline.language == .zig or outline.language == .hcl or - outline.language == .dart) + outline.language == .dart or outline.language == .java or + outline.language == .kotlin or outline.language == .svelte or + outline.language == .vue or outline.language == .astro or + outline.language == .css or outline.language == .scss or + outline.language == .protobuf or outline.language == .mlir or + outline.language == .tablegen) { if (in_block_comment) { if (std.mem.indexOf(u8, trimmed, "*/")) |close_pos| { @@ -862,6 +904,8 @@ pub const Explorer = struct { try parser.parsePythonLine(trimmed, line_num, &outline); } else if (outline.language == .typescript or outline.language == .javascript) { try parser.parseTsLine(trimmed, line_num, &outline); + } else if (outline.language == .c or outline.language == .cpp) { + try parser.parseCLine(trimmed, line_num, &outline); } else if (outline.language == .rust) { try parser.parseRustLine(trimmed, line_num, &outline, prev_line_trimmed); } else if (outline.language == .php) { @@ -896,6 +940,28 @@ pub const Explorer = struct { try parser.parseHclLine(trimmed, line_num, &outline); } else if (outline.language == .r) { try parser.parseRLine(trimmed, line_num, &outline); + } else if (outline.language == .java) { + try parser.parseJavaLine(trimmed, line_num, &outline); + } else if (outline.language == .kotlin) { + try parser.parseKotlinLine(trimmed, line_num, &outline); + } else if (outline.language == .svelte or outline.language == .vue or outline.language == .astro) { + try parser.parseComponentLine(trimmed, line_num, &outline); + } else if (outline.language == .shell) { + try parser.parseShellLine(trimmed, line_num, &outline); + } else if (outline.language == .css or outline.language == .scss) { + try parser.parseStyleLine(trimmed, line_num, &outline); + } else if (outline.language == .sql) { + try parser.parseSqlLine(trimmed, line_num, &outline); + } else if (outline.language == .protobuf) { + try parser.parseProtoLine(trimmed, line_num, &outline); + } else if (outline.language == .fortran) { + try parser.parseFortranLine(trimmed, line_num, &outline); + } else if (outline.language == .llvm_ir) { + try parser.parseLlvmIrLine(trimmed, line_num, &outline); + } else if (outline.language == .mlir) { + try parser.parseMlirLine(trimmed, line_num, &outline); + } else if (outline.language == .tablegen) { + try parser.parseTableGenLine(trimmed, line_num, &outline); } prev_line_trimmed = trimmed; @@ -1256,22 +1322,63 @@ pub const Explorer = struct { var result_list: std.ArrayList(SymbolResult) = .empty; errdefer result_list.deinit(allocator); - // Scan outlines for all symbols by name (catches all kinds including imports). + // Track (path, line_start) pairs already appended. symbol_index can be + // incomplete after fast-snapshot restore (outlines are populated before + // rebuildSymbolIndexFor runs on every file), so we must still fall + // through to the outline scan — and dedupe against what the index + // already supplied. Keys are ":" allocated from the caller + // allocator, freed at end of call. + var seen = std.StringHashMap(void).init(allocator); + defer { + var sit = seen.keyIterator(); + while (sit.next()) |k| allocator.free(k.*); + seen.deinit(); + } + + if (self.symbol_index.get(name)) |locs| { + for (locs.items) |loc| { + var detail: ?[]const u8 = null; + if (self.outlines.getPtr(loc.path)) |outline| { + for (outline.symbols.items) |sym| { + if (sym.line_start == loc.line_start and std.mem.eql(u8, sym.name, name)) { + detail = if (sym.detail) |d| try allocator.dupe(u8, d) else null; + break; + } + } + } + try result_list.append(allocator, .{ + .path = try allocator.dupe(u8, loc.path), + .symbol = .{ + .name = try allocator.dupe(u8, name), + .kind = loc.kind, + .line_start = loc.line_start, + .line_end = loc.line_end, + .detail = detail, + }, + }); + const key = try std.fmt.allocPrint(allocator, "{s}:{d}", .{ loc.path, loc.line_start }); + seen.put(key, {}) catch allocator.free(key); + } + } + + // Safety scan: append any outline symbols the index missed. var iter = self.outlines.iterator(); while (iter.next()) |entry| { for (entry.value_ptr.symbols.items) |sym| { - if (std.mem.eql(u8, sym.name, name)) { - try result_list.append(allocator, .{ - .path = try allocator.dupe(u8, entry.key_ptr.*), - .symbol = .{ - .name = try allocator.dupe(u8, sym.name), - .kind = sym.kind, - .line_start = sym.line_start, - .line_end = sym.line_end, - .detail = if (sym.detail) |d| try allocator.dupe(u8, d) else null, - }, - }); - } + if (!std.mem.eql(u8, sym.name, name)) continue; + var key_buf: [std.fs.max_path_bytes + 32]u8 = undefined; + const key = std.fmt.bufPrint(&key_buf, "{s}:{d}", .{ entry.key_ptr.*, sym.line_start }) catch continue; + if (seen.contains(key)) continue; + try result_list.append(allocator, .{ + .path = try allocator.dupe(u8, entry.key_ptr.*), + .symbol = .{ + .name = try allocator.dupe(u8, sym.name), + .kind = sym.kind, + .line_start = sym.line_start, + .line_end = sym.line_end, + .detail = if (sym.detail) |d| try allocator.dupe(u8, d) else null, + }, + }); } } return result_list.toOwnedSlice(allocator); @@ -1797,8 +1904,19 @@ pub const Explorer = struct { } fn parseTsLine(self: *Explorer, line: []const u8, line_num: u32, outline: *FileOutline) !void { const a = self.allocator; - if (containsAny(line, &.{ "function ", "const ", "export function ", "export const " })) { - const kind: SymbolKind = if (std.mem.indexOf(u8, line, "function") != null) .function else .constant; + if (containsAny(line, &.{ "function ", "const ", "let ", "var ", "class ", "interface ", "enum ", "type " })) { + const kind: SymbolKind = if (std.mem.indexOf(u8, line, "function") != null) + .function + else if (std.mem.indexOf(u8, line, "class ") != null) + .class_def + else if (std.mem.indexOf(u8, line, "interface ") != null) + .interface_def + else if (std.mem.indexOf(u8, line, "enum ") != null) + .enum_def + else if (std.mem.indexOf(u8, line, "type ") != null) + .type_alias + else + .constant; const trimmed = skipKeywords(line); if (extractIdent(trimmed)) |name| { const name_copy = try a.dupe(u8, name); @@ -1831,6 +1949,254 @@ pub const Explorer = struct { } } + fn parseJavaLine(self: *Explorer, raw_line: []const u8, line_num: u32, outline: *FileOutline) !void { + const a = self.allocator; + const line = stripLineComment(raw_line); + if (line.len == 0 or startsWith(line, "@")) return; + + if (parseDelimitedImport(line, "import ", ";")) |imp| { + try appendImportSymbol(a, outline, imp, line_num, line); + return; + } + + if (extractIdentAfterKeyword(line, "record ")) |name| { + try appendOutlineSymbol(a, outline, name, .class_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "class ")) |name| { + try appendOutlineSymbol(a, outline, name, .class_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "interface ")) |name| { + try appendOutlineSymbol(a, outline, name, .interface_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "enum ")) |name| { + try appendOutlineSymbol(a, outline, name, .enum_def, line_num, line); + } else if (extractJvmMethodName(line)) |name| { + try appendOutlineSymbol(a, outline, name, .method, line_num, line); + } + } + + fn parseKotlinLine(self: *Explorer, raw_line: []const u8, line_num: u32, outline: *FileOutline) !void { + const a = self.allocator; + const line = stripLineComment(raw_line); + if (line.len == 0 or startsWith(line, "@")) return; + + if (parseDelimitedImport(line, "import ", "")) |imp| { + try appendImportSymbol(a, outline, imp, line_num, line); + return; + } + + if (extractIdentAfterKeyword(line, "enum class ")) |name| { + try appendOutlineSymbol(a, outline, name, .enum_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "interface ")) |name| { + try appendOutlineSymbol(a, outline, name, .interface_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "class ")) |name| { + try appendOutlineSymbol(a, outline, name, .class_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "object ")) |name| { + try appendOutlineSymbol(a, outline, name, .class_def, line_num, line); + } else if (extractIdentAfterKeyword(line, "fun ")) |name| { + try appendOutlineSymbol(a, outline, name, .function, line_num, line); + } else if (extractIdentAfterKeyword(line, "val ")) |name| { + try appendOutlineSymbol(a, outline, name, .constant, line_num, line); + } else if (extractIdentAfterKeyword(line, "var ")) |name| { + try appendOutlineSymbol(a, outline, name, .variable, line_num, line); + } + } + + fn parseComponentLine(self: *Explorer, raw_line: []const u8, line_num: u32, outline: *FileOutline) !void { + const line = std.mem.trim(u8, raw_line, " \t"); + if (line.len == 0 or startsWith(line, "", .svelte)); + try testing.expect(isCommentOrBlank(" ", .vue)); + try testing.expect(isCommentOrBlank(" ", .astro)); + try testing.expect(isCommentOrBlank(" # shell comment", .shell)); + try testing.expect(isCommentOrBlank(" /* css block comment */", .css)); + try testing.expect(isCommentOrBlank(" // scss line comment", .scss)); + try testing.expect(isCommentOrBlank(" -- sql comment", .sql)); + try testing.expect(isCommentOrBlank(" // proto comment", .protobuf)); + try testing.expect(isCommentOrBlank(" ! fortran comment", .fortran)); + try testing.expect(isCommentOrBlank(" ; llvm ir comment", .llvm_ir)); + try testing.expect(isCommentOrBlank(" // mlir comment", .mlir)); + try testing.expect(isCommentOrBlank(" // tablegen comment", .tablegen)); + try testing.expect(!isCommentOrBlank(" SELECT * FROM users;", .sql)); +} + test "isCommentOrBlank: tabs and mixed whitespace" { try testing.expect(isCommentOrBlank("\t\t// tabbed comment", .zig)); try testing.expect(isCommentOrBlank(" \t \t ", .zig)); @@ -2142,6 +2168,11 @@ test "detectLanguage: all supported extensions" { try testing.expect(explore.detectLanguage("util.h") == .c); try testing.expect(explore.detectLanguage("app.cpp") == .cpp); try testing.expect(explore.detectLanguage("app.hpp") == .cpp); + try testing.expect(explore.detectLanguage("app.cc") == .cpp); + try testing.expect(explore.detectLanguage("app.hh") == .cpp); + try testing.expect(explore.detectLanguage("app.cxx") == .cpp); + try testing.expect(explore.detectLanguage("app.hxx") == .cpp); + try testing.expect(explore.detectLanguage("bridge.mm") == .cpp); try testing.expect(explore.detectLanguage("script.py") == .python); try testing.expect(explore.detectLanguage("app.js") == .javascript); try testing.expect(explore.detectLanguage("comp.jsx") == .javascript); @@ -2154,6 +2185,20 @@ test "detectLanguage: all supported extensions" { try testing.expect(explore.detectLanguage("pkg.json") == .json); try testing.expect(explore.detectLanguage("config.yaml") == .yaml); try testing.expect(explore.detectLanguage("config.yml") == .yaml); + try testing.expect(explore.detectLanguage("Main.java") == .java); + try testing.expect(explore.detectLanguage("App.kt") == .kotlin); + try testing.expect(explore.detectLanguage("Widget.svelte") == .svelte); + try testing.expect(explore.detectLanguage("Widget.vue") == .vue); + try testing.expect(explore.detectLanguage("Page.astro") == .astro); + try testing.expect(explore.detectLanguage("bootstrap.sh") == .shell); + try testing.expect(explore.detectLanguage("styles.css") == .css); + try testing.expect(explore.detectLanguage("styles.scss") == .scss); + try testing.expect(explore.detectLanguage("schema.sql") == .sql); + try testing.expect(explore.detectLanguage("service.proto") == .protobuf); + try testing.expect(explore.detectLanguage("solver.f90") == .fortran); + try testing.expect(explore.detectLanguage("module.ll") == .llvm_ir); + try testing.expect(explore.detectLanguage("dialect.mlir") == .mlir); + try testing.expect(explore.detectLanguage("records.td") == .tablegen); try testing.expect(explore.detectLanguage("Makefile") == .unknown); try testing.expect(explore.detectLanguage("no_ext") == .unknown); } @@ -5091,9 +5136,14 @@ test "issue-116: getGitHead returns valid SHA for git repos" { } } -test "issue-148: idle timeout is 10 minutes" { +test "issue-148: idle timeout is 1 hour" { + const mcp = @import("mcp.zig"); + try testing.expectEqual(@as(i64, 60 * 60 * 1000), mcp.idle_timeout_ms); +} + +test "issue-148: dead MCP clients are polled every second" { const mcp = @import("mcp.zig"); - try testing.expectEqual(@as(i64, 10 * 60 * 1000), mcp.idle_timeout_ms); + try testing.expectEqual(@as(u64, 1000), mcp.dead_client_poll_ms); } test "issue-148: POLLHUP detects closed pipe" { @@ -5159,31 +5209,31 @@ test "issue-148: idle watchdog respects activity timestamp" { // Set activity to "just now" mcp.last_activity.store(cio.milliTimestamp(), .release); - // With 10-minute timeout, checking now should NOT trigger exit + // With 1-hour timeout, checking now should NOT trigger exit const last = mcp.last_activity.load(.acquire); const now = cio.milliTimestamp(); try testing.expect(now - last < mcp.idle_timeout_ms); } -test "issue-148: MCP session survives 2-minute idle" { +test "issue-148: MCP session survives 30-minute idle" { const mcp = @import("mcp.zig"); - // With the old 2-min timeout, an activity 3 minutes ago would trigger exit. - // With the new 10-min timeout, it should be fine. - const three_min_ago = cio.milliTimestamp() - (3 * 60 * 1000); + // With the old 10-min timeout, an activity 30 minutes ago would trigger exit. + // With the new 1-hour timeout, it should be fine. + const thirty_min_ago = cio.milliTimestamp() - (30 * 60 * 1000); // Save and restore const saved = mcp.last_activity.load(.acquire); defer mcp.last_activity.store(saved, .release); - mcp.last_activity.store(three_min_ago, .release); + mcp.last_activity.store(thirty_min_ago, .release); const last = mcp.last_activity.load(.acquire); const now = cio.milliTimestamp(); - // Should NOT exceed 10-minute timeout + // Should NOT exceed 1-hour timeout try testing.expect(now - last < mcp.idle_timeout_ms); - // Should have exceeded old 2-minute timeout - try testing.expect(now - last > 2 * 60 * 1000); + // Should have exceeded old 10-minute timeout + try testing.expect(now - last > 10 * 60 * 1000); } test "issue-148: open pipe does not trigger HUP" { @@ -5225,8 +5275,8 @@ test "issue-148: codedb mcp exits when stdin is closed" { child.stdin = null; } - // Wait up to 15 seconds for the process to exit - // (watchdog polls every 10s, so it should detect POLLHUP within ~10s) + // Wait for the process to exit. The main read loop exits on stdin EOF; + // the watchdog also polls dead clients every second as a backup. const start = cio.milliTimestamp(); const term = child.wait(io) catch { // If wait fails, the process is stuck — test fails @@ -5242,8 +5292,8 @@ test "issue-148: codedb mcp exits when stdin is closed" { else => {}, } - // Should exit within 15 seconds (10s poll interval + margin) - try testing.expect(elapsed < 15_000); + // Should exit promptly after stdin closes. + try testing.expect(elapsed < 5_000); } const MmapTrigramIndex = @import("index.zig").MmapTrigramIndex; @@ -6181,6 +6231,458 @@ test "issue-215: detectLanguage handles .r and .R" { try testing.expectEqual(Language.r, explore.detectLanguage("analysis.R")); } +test "issue-319: C parser extracts includes macros types and functions" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var explorer = Explorer.init(alloc); + + try explorer.indexFile("src/core.c", + \\#include + \\#include "local.h" + \\#define MAX_SIZE 64 + \\#define SQUARE(x) ((x) * (x)) + \\struct Worker { + \\ int id; + \\}; + \\enum Mode { + \\ MODE_A, + \\}; + \\union Value { + \\ int i; + \\}; + \\typedef unsigned long size_alias_t; + \\static inline const char *worker_name(const struct Worker *worker) { + \\ return "worker"; + \\} + \\void *alloc_item(size_t size) + \\{ + \\ return malloc(size); + \\} + ); + + const outline = try explorer.getOutline("src/core.c", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.c, outline.language); + try testing.expectEqual(@as(usize, 2), outline.imports.items.len); + try testing.expectEqualStrings("stdio.h", outline.imports.items[0]); + try testing.expectEqualStrings("local.h", outline.imports.items[1]); + + const max_size = try explorer.findAllSymbols("MAX_SIZE", alloc); + defer alloc.free(max_size); + try testing.expectEqual(@as(usize, 1), max_size.len); + try testing.expectEqual(SymbolKind.macro_def, max_size[0].symbol.kind); + + const square = try explorer.findAllSymbols("SQUARE", alloc); + defer alloc.free(square); + try testing.expectEqual(@as(usize, 1), square.len); + try testing.expectEqual(SymbolKind.macro_def, square[0].symbol.kind); + + const worker = try explorer.findAllSymbols("Worker", alloc); + defer alloc.free(worker); + try testing.expectEqual(@as(usize, 1), worker.len); + try testing.expectEqual(SymbolKind.struct_def, worker[0].symbol.kind); + + const mode = try explorer.findAllSymbols("Mode", alloc); + defer alloc.free(mode); + try testing.expectEqual(@as(usize, 1), mode.len); + try testing.expectEqual(SymbolKind.enum_def, mode[0].symbol.kind); + + const value = try explorer.findAllSymbols("Value", alloc); + defer alloc.free(value); + try testing.expectEqual(@as(usize, 1), value.len); + try testing.expectEqual(SymbolKind.union_def, value[0].symbol.kind); + + const alias = try explorer.findAllSymbols("size_alias_t", alloc); + defer alloc.free(alias); + try testing.expectEqual(@as(usize, 1), alias.len); + try testing.expectEqual(SymbolKind.type_alias, alias[0].symbol.kind); + + const worker_name = try explorer.findAllSymbols("worker_name", alloc); + defer alloc.free(worker_name); + try testing.expectEqual(@as(usize, 1), worker_name.len); + try testing.expectEqual(SymbolKind.function, worker_name[0].symbol.kind); + + const alloc_item = try explorer.findAllSymbols("alloc_item", alloc); + defer alloc.free(alloc_item); + try testing.expectEqual(@as(usize, 1), alloc_item.len); + try testing.expectEqual(SymbolKind.function, alloc_item[0].symbol.kind); +} + +test "issue-319: C parser avoids comments strings prototypes and macro calls" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var explorer = Explorer.init(alloc); + + try explorer.indexFile("src/noise.c", + \\// int fake_comment(void) { + \\/* int fake_block(void) { */ + \\const char *s = "int fake_string(void) {"; + \\typedef int (*handler_fn)(int); + \\int prototype_only(void); + \\EXPORT_SYMBOL(real_function); + \\if (real_function()) { + \\} + \\int real_function(void) { + \\ return 1; + \\} + ); + + const real = try explorer.findAllSymbols("real_function", alloc); + defer alloc.free(real); + try testing.expectEqual(@as(usize, 1), real.len); + try testing.expectEqual(SymbolKind.function, real[0].symbol.kind); + + const fake_comment = try explorer.findAllSymbols("fake_comment", alloc); + defer alloc.free(fake_comment); + try testing.expectEqual(@as(usize, 0), fake_comment.len); + + const fake_block = try explorer.findAllSymbols("fake_block", alloc); + defer alloc.free(fake_block); + try testing.expectEqual(@as(usize, 0), fake_block.len); + + const fake_string = try explorer.findAllSymbols("fake_string", alloc); + defer alloc.free(fake_string); + try testing.expectEqual(@as(usize, 0), fake_string.len); + + const prototype = try explorer.findAllSymbols("prototype_only", alloc); + defer alloc.free(prototype); + try testing.expectEqual(@as(usize, 0), prototype.len); + + const handler = try explorer.findAllSymbols("handler_fn", alloc); + defer alloc.free(handler); + try testing.expectEqual(@as(usize, 0), handler.len); +} + +test "issue-321: common detected extensions produce outlines" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var explorer = Explorer.init(alloc); + + try explorer.indexFile("src/math.cc", + \\#include + \\class Calculator { + \\public: + \\ int add(int a, int b) { + \\ return a + b; + \\ } + \\}; + \\int free_add(int a, int b) { + \\ return a + b; + \\} + ); + try explorer.indexFile("src/Bridge.mm", + \\#import "Bridge.h" + \\@interface BrowserController + \\- (void)loadPage:(NSString *)url; + \\@end + \\@implementation BrowserController + \\- (void)loadPage:(NSString *)url { } + \\@end + \\class BrowserBridge { + \\}; + \\int bridge_main(void) { + \\ return 0; + \\} + ); + try explorer.indexFile("src/App.java", + \\package demo; + \\import java.util.List; + \\public class Worker { + \\ public void run() {} + \\} + \\interface RunnableThing {} + \\enum Mode { A } + \\record Pair(int left, int right) {} + ); + try explorer.indexFile("src/App.kt", + \\package demo + \\import kotlinx.coroutines.runBlocking + \\data class User(val name: String) + \\interface Repo + \\enum class KotlinMode { A } + \\fun loadUser(): User = User("a") + \\val answer = 42 + ); + try explorer.indexFile("src/Widget.svelte", + \\ + \\.card { color: red; } + ); + try explorer.indexFile("src/View.vue", + \\ + ); + try explorer.indexFile("src/Page.astro", + \\--- + \\import Layout from '../layouts/Layout.astro'; + \\const title = 'Home'; + \\--- + ); + try explorer.indexFile("scripts/build.sh", + \\source ./env.sh + \\function build_app() { + \\} + \\deploy_app() { + \\} + \\BUILD_MODE=release + ); + try explorer.indexFile("styles/app.css", + \\:root { + \\ --brand: red; + \\} + \\.button { + \\ color: var(--brand); + \\} + \\@keyframes fade {} + ); + try explorer.indexFile("styles/app.scss", + \\$gap: 8px; + \\@mixin center {} + \\.panel {} + ); + try explorer.indexFile("db/schema.sql", + \\CREATE TABLE users (id integer); + \\CREATE OR REPLACE FUNCTION do_thing() RETURNS void AS $$ SELECT 1; $$ LANGUAGE sql; + \\CREATE INDEX idx_users_id ON users(id); + ); + try explorer.indexFile("api/service.proto", + \\syntax = "proto3"; + \\import "google/protobuf/timestamp.proto"; + \\message User {} + \\enum Status { STATUS_OK = 0; } + \\service UserService { + \\ rpc GetUser (User) returns (User); + \\} + ); + try explorer.indexFile("math/solver.f90", + \\module solver + \\use mathlib + \\type :: Particle + \\end type + \\subroutine step() + \\end subroutine + \\function energy() + \\end function + ); + try explorer.indexFile("ir/module.ll", + \\%Pair = type { i32, i32 } + \\@global_value = global i32 0 + \\define i32 @main() { + \\ ret i32 0 + \\} + ); + try explorer.indexFile("ir/dialect.mlir", + \\module @kernel_mod { + \\ func.func @kernel() { + \\ return + \\ } + \\} + ); + try explorer.indexFile("llvm/records.td", + \\include "Base.td" + \\class Register; + \\multiclass Pat; + \\def R0 : Register<"r0">; + \\defm ADD : Pat<"add">; + \\let Namespace = "Toy"; + ); + + const cc_outline = try explorer.getOutline("src/math.cc", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.cpp, cc_outline.language); + try expectOutlineImport(&cc_outline, "vector"); + try expectOutlineSymbol(&cc_outline, "Calculator", .class_def); + try expectOutlineSymbol(&cc_outline, "add", .function); + try expectOutlineSymbol(&cc_outline, "free_add", .function); + + const mm_outline = try explorer.getOutline("src/Bridge.mm", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.cpp, mm_outline.language); + try expectOutlineImport(&mm_outline, "Bridge.h"); + try expectOutlineSymbol(&mm_outline, "BrowserController", .class_def); + try expectOutlineSymbol(&mm_outline, "loadPage", .method); + try expectOutlineSymbol(&mm_outline, "BrowserBridge", .class_def); + try expectOutlineSymbol(&mm_outline, "bridge_main", .function); + + const java_outline = try explorer.getOutline("src/App.java", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.java, java_outline.language); + try expectOutlineImport(&java_outline, "java.util.List"); + try expectOutlineSymbol(&java_outline, "Worker", .class_def); + try expectOutlineSymbol(&java_outline, "run", .method); + try expectOutlineSymbol(&java_outline, "RunnableThing", .interface_def); + try expectOutlineSymbol(&java_outline, "Mode", .enum_def); + try expectOutlineSymbol(&java_outline, "Pair", .class_def); + + const kt_outline = try explorer.getOutline("src/App.kt", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.kotlin, kt_outline.language); + try expectOutlineImport(&kt_outline, "kotlinx.coroutines.runBlocking"); + try expectOutlineSymbol(&kt_outline, "User", .class_def); + try expectOutlineSymbol(&kt_outline, "Repo", .interface_def); + try expectOutlineSymbol(&kt_outline, "KotlinMode", .enum_def); + try expectOutlineSymbol(&kt_outline, "loadUser", .function); + try expectOutlineSymbol(&kt_outline, "answer", .constant); + + const svelte_outline = try explorer.getOutline("src/Widget.svelte", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.svelte, svelte_outline.language); + try expectOutlineImport(&svelte_outline, "./Thing.svelte"); + try expectOutlineSymbol(&svelte_outline, "title", .constant); + try expectOutlineSymbol(&svelte_outline, "renderTitle", .function); + try expectOutlineSymbol(&svelte_outline, ".card", .class_def); + + const vue_outline = try explorer.getOutline("src/View.vue", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.vue, vue_outline.language); + try expectOutlineImport(&vue_outline, "./Child.vue"); + try expectOutlineSymbol(&vue_outline, "count", .constant); + try expectOutlineSymbol(&vue_outline, "inc", .function); + + const astro_outline = try explorer.getOutline("src/Page.astro", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.astro, astro_outline.language); + try expectOutlineImport(&astro_outline, "../layouts/Layout.astro"); + try expectOutlineSymbol(&astro_outline, "title", .constant); + + const shell_outline = try explorer.getOutline("scripts/build.sh", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.shell, shell_outline.language); + try expectOutlineImport(&shell_outline, "./env.sh"); + try expectOutlineSymbol(&shell_outline, "build_app", .function); + try expectOutlineSymbol(&shell_outline, "deploy_app", .function); + try expectOutlineSymbol(&shell_outline, "BUILD_MODE", .variable); + + const css_outline = try explorer.getOutline("styles/app.css", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.css, css_outline.language); + try expectOutlineSymbol(&css_outline, "--brand", .constant); + try expectOutlineSymbol(&css_outline, ".button", .class_def); + try expectOutlineSymbol(&css_outline, "fade", .function); + + const scss_outline = try explorer.getOutline("styles/app.scss", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.scss, scss_outline.language); + try expectOutlineSymbol(&scss_outline, "$gap", .constant); + try expectOutlineSymbol(&scss_outline, "center", .function); + try expectOutlineSymbol(&scss_outline, ".panel", .class_def); + + const sql_outline = try explorer.getOutline("db/schema.sql", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.sql, sql_outline.language); + try expectOutlineSymbol(&sql_outline, "users", .struct_def); + try expectOutlineSymbol(&sql_outline, "do_thing", .function); + try expectOutlineSymbol(&sql_outline, "idx_users_id", .constant); + + const proto_outline = try explorer.getOutline("api/service.proto", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.protobuf, proto_outline.language); + try expectOutlineImport(&proto_outline, "google/protobuf/timestamp.proto"); + try expectOutlineSymbol(&proto_outline, "User", .struct_def); + try expectOutlineSymbol(&proto_outline, "Status", .enum_def); + try expectOutlineSymbol(&proto_outline, "UserService", .interface_def); + try expectOutlineSymbol(&proto_outline, "GetUser", .method); + + const fortran_outline = try explorer.getOutline("math/solver.f90", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.fortran, fortran_outline.language); + try expectOutlineImport(&fortran_outline, "mathlib"); + try expectOutlineSymbol(&fortran_outline, "solver", .class_def); + try expectOutlineSymbol(&fortran_outline, "Particle", .struct_def); + try expectOutlineSymbol(&fortran_outline, "step", .function); + try expectOutlineSymbol(&fortran_outline, "energy", .function); + + const llvm_outline = try explorer.getOutline("ir/module.ll", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.llvm_ir, llvm_outline.language); + try expectOutlineSymbol(&llvm_outline, "Pair", .type_alias); + try expectOutlineSymbol(&llvm_outline, "global_value", .variable); + try expectOutlineSymbol(&llvm_outline, "main", .function); + + const mlir_outline = try explorer.getOutline("ir/dialect.mlir", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.mlir, mlir_outline.language); + try expectOutlineSymbol(&mlir_outline, "kernel_mod", .class_def); + try expectOutlineSymbol(&mlir_outline, "kernel", .function); + + const td_outline = try explorer.getOutline("llvm/records.td", alloc) orelse return error.TestUnexpectedResult; + try testing.expectEqual(Language.tablegen, td_outline.language); + try expectOutlineImport(&td_outline, "Base.td"); + try expectOutlineSymbol(&td_outline, "Register", .class_def); + try expectOutlineSymbol(&td_outline, "Pat", .class_def); + try expectOutlineSymbol(&td_outline, "R0", .constant); + try expectOutlineSymbol(&td_outline, "ADD", .constant); + try expectOutlineSymbol(&td_outline, "Namespace", .variable); + + const worker = try explorer.findAllSymbols("Worker", alloc); + defer alloc.free(worker); + try testing.expectEqual(@as(usize, 1), worker.len); + try testing.expectEqual(SymbolKind.class_def, worker[0].symbol.kind); + + const run = try explorer.findAllSymbols("run", alloc); + defer alloc.free(run); + try testing.expectEqual(@as(usize, 1), run.len); + try testing.expectEqual(SymbolKind.method, run[0].symbol.kind); + + const user = try explorer.findAllSymbols("User", alloc); + defer alloc.free(user); + try testing.expect(user.len >= 2); + + const load_user = try explorer.findAllSymbols("loadUser", alloc); + defer alloc.free(load_user); + try testing.expectEqual(@as(usize, 1), load_user.len); + try testing.expectEqual(SymbolKind.function, load_user[0].symbol.kind); + + const title = try explorer.findAllSymbols("title", alloc); + defer alloc.free(title); + try testing.expect(title.len >= 2); + + const build_app = try explorer.findAllSymbols("build_app", alloc); + defer alloc.free(build_app); + try testing.expectEqual(@as(usize, 1), build_app.len); + try testing.expectEqual(SymbolKind.function, build_app[0].symbol.kind); + + const button = try explorer.findAllSymbols(".button", alloc); + defer alloc.free(button); + try testing.expectEqual(@as(usize, 1), button.len); + + const users = try explorer.findAllSymbols("users", alloc); + defer alloc.free(users); + try testing.expectEqual(@as(usize, 1), users.len); + try testing.expectEqual(SymbolKind.struct_def, users[0].symbol.kind); + + const user_service = try explorer.findAllSymbols("UserService", alloc); + defer alloc.free(user_service); + try testing.expectEqual(@as(usize, 1), user_service.len); + try testing.expectEqual(SymbolKind.interface_def, user_service[0].symbol.kind); + + const particle = try explorer.findAllSymbols("Particle", alloc); + defer alloc.free(particle); + try testing.expectEqual(@as(usize, 1), particle.len); + try testing.expectEqual(SymbolKind.struct_def, particle[0].symbol.kind); + + const main_sym = try explorer.findAllSymbols("main", alloc); + defer alloc.free(main_sym); + try testing.expectEqual(@as(usize, 1), main_sym.len); + try testing.expectEqual(SymbolKind.function, main_sym[0].symbol.kind); + + const kernel = try explorer.findAllSymbols("kernel", alloc); + defer alloc.free(kernel); + try testing.expectEqual(@as(usize, 1), kernel.len); + try testing.expectEqual(SymbolKind.function, kernel[0].symbol.kind); + + const r0 = try explorer.findAllSymbols("R0", alloc); + defer alloc.free(r0); + try testing.expectEqual(@as(usize, 1), r0.len); +} + +fn expectOutlineSymbol(outline: *const explore.FileOutline, name: []const u8, kind: SymbolKind) !void { + for (outline.symbols.items) |sym| { + if (std.mem.eql(u8, sym.name, name) and sym.kind == kind) return; + } + return error.TestUnexpectedResult; +} + +fn expectOutlineImport(outline: *const explore.FileOutline, import_path: []const u8) !void { + for (outline.imports.items) |imp| { + if (std.mem.eql(u8, imp, import_path)) return; + } + return error.TestUnexpectedResult; +} + test "issue-179: Python inline docstring does not leak symbols" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit();