Date: 2026-05-24
Branch: codex/hardening
Source: Codebase audit of graphiq v3.3.0 (crates: graphiq-core, graphiq-cli, graphiq-mcp, graphiq-bench)
Goal: Zero-friction, always-ready indexing layer that agents plug into without manual index management, stale DB conflicts, or setup confusion. Multiple concurrent sessions with isolated state. Search results surfaced in the most agent-consumable format possible.
GraphIQ has proven search quality (+48% NDCG, +128% MRR). The next inflection is reliability and usability: making the tool disappear into the agent workflow so the user never thinks about indexing, stale databases, or which tool to call. This roadmap covers 6 phases, each independently shippable with tests.
Problem: Users (and agents) encounter stale or wrong indexes. Opening a project from a different path, switching branches, or having old .graphiq/graphiq.db data causes search to silently degrade or fail. The user has to manually run doctor, figure out what's wrong, and reindex.
Files touched: crates/graphiq-core/src/manifest.rs, crates/graphiq-mcp/src/main.rs, crates/graphiq-core/src/index.rs
Currently check_artifact_freshness is only called in cmd_search (CLI) and tool_doctor (MCP). The MCP server's tool_search and background_warm don't check staleness before serving results.
Changes:
- In
tool_search(MCPmain.rs:1105): before running search, compare manifest freshness hash against current DB state. If cruncher is stale, trigger a silent rebuild of the cruncher index (not a full reindex — justbuild_cruncher_index). Surface a note in the response:"mode: GraphWalk (cruncher rebuilt)". - Add
SearchResult::staleness_note: Option<String>to communicate this transparently. - In
background_warm(MCPmain.rs:556): after prewarm, check manifest. If stale, rebuild cruncher before marking ready.
Test: Unit test that inserts symbols, builds manifest, adds more symbols, then calls search and verifies the cruncher was silently rebuilt and the note is present.
The indexer already skips files whose content hash hasn't changed (index.rs:144-153). But the manifest freshness check (manifest.rs:82-86) only compares symbol/edge/file counts, not actual content hashes. Adding/removing one symbol flips counts but so does adding a file with 0 symbols — both look the same.
Changes:
- Add
content_hash: String(SHA-256 of all file content hashes concatenated) toFreshnessHash. - Compute it during indexing: hash all
files.content_hashvalues in sorted order. is_stale_vscompares this hash instead of counts. This detects branch switches, file edits that don't change symbol count, etc.- Fall back gracefully: if manifest has no
content_hashfield (old manifests), use count-based comparison.
Test: Index a project, change one file's contents without changing symbol count, verify is_stale_vs returns true with content hash but false with count-only.
The MCP server already detects stored project_root mismatches (main.rs:273-293). But it only logs and uses the stored root — it doesn't reindex. If the stored root no longer matches the filesystem (user moved the project, or is working in a different checkout), search returns stale data.
Changes:
- In
resolve_project_root: if the storedproject_rootdoesn't match the candidate and both exist, compare their content hash against the manifest. If they differ, nuke the DB and reindex from the new root. - Add a
--force-reindexCLI flag that explicitly clears the DB before indexing. - In MCP: on
initialize, if the DB has data but the project root differs, automatically clear and reindex. Surface this in the_meta.readyfield.
Test: Index project at path A, move to path B, start MCP server pointing at B, verify it auto-reindexes.
Problem: Multiple agents sharing one .graphiq/graphiq.db creates race conditions. The watcher's indexing bool (main.rs:517) is not a real lock — two concurrent do_index calls could interleave writes. The --ephemeral flag exists but isn't wired to any auto-session hook.
Files touched: crates/graphiq-mcp/src/main.rs, crates/graphiq-core/src/db.rs
Changes:
- Add
--session-id <ID>flag tographiq-mcp. When set, creates an ephemeral temp DB at$TMPDIR/graphiq-<session-id>/graphiq.dbinstead of.graphiq/graphiq.db. - The Signet plugin manifest already has
prompt:contribute:user-prompt-submit. Add a Signet hook that detects when an agent session starts, auto-launchesgraphiq-mcp --session-id <session-id> --project <path>, and the agent's MCP config points to this ephemeral instance. - On session end (SIGTERM/SIGINT), clean up the temp directory (already done for
--ephemeral, extend to--session-id). - Shared (non-session) mode still uses
.graphiq/graphiq.dband is the default.
Test: Launch two MCP servers with different --session-id values pointing at the same project. Verify each has its own DB. Verify search in one doesn't affect the other. Verify cleanup on exit.
Changes:
- Replace the
indexing: boolguard inServerStatewith a properRwLock-style concurrency model:- Searches read from
CruncherIndex(which isClone-cheap after initial build). - Indexing holds an exclusive write lock, during which searches return a "reindexing in progress" response.
- Searches read from
- In practice: wrap
cruncher_indexin aRwLock<Option<CruncherIndex>>separate from theMutex<ServerState>. Search acquires read lock, indexing acquires write lock. - This means searches can run concurrently with each other, and only block during cruncher rebuilds.
Test: Spawn 10 concurrent search goroutines while triggering a reindex. Verify no panics, no corrupted results, and that searches during reindex return a retry-able response.
Changes:
- The current watcher (
main.rs:418-540) debounces by 2 seconds but doesn't batch multiple file changes. If 20 files change in a git checkout, it may trigger 5-10 reindexes. - Add a merge window: accumulate file change events for a configurable period (default 5s), then do a single reindex pass for all changed files.
- Use the existing incremental reindex logic (content-hash-based skip) so unchanged files aren't re-parsed.
Test: Touch 10 files simultaneously, verify only one reindex is triggered within the merge window.
Problem: Agents using GraphIQ often need to follow up a search with a context call to understand what a symbol does. The search result could include more structural context inline, reducing round trips and giving agents better first-pass understanding.
Files touched: crates/graphiq-mcp/src/main.rs (tool output formatting), crates/graphiq-core/src/search.rs
Currently each search result shows: rank, score, file:line, kind::name, signature, and optionally calls/callers. This is good but missing:
- Subsystem membership (which module/subsystem does this symbol belong to?)
- Structural role (hub, bridge, leaf, entry point?)
- Evidence edges (what kinds of edges connect to it — calls, imports, implements?)
Changes:
- In
tool_searchoutput: add aroleline showing structural role tags from therolesmodule. - Add a
subsystemline if subsystems have been computed (fromsubsystemstable). - Add
edges: N incoming, M outgoingsummary. - Keep the format compact — one or two extra lines per result, not a wall of text.
Example output:
#1 [8.52] src/auth.rs:142 function::authenticateUser (24L) [hub]
fn authenticateUser(token: string): Promise<User>
edges: 12 incoming, 8 outgoing subsystem: auth
calls:
-> verifyToken
<- handleLogin
Test: Index a project with subsystems, search, verify the enriched format includes role and subsystem info.
The MCP tool descriptions are already good, but agents sometimes use search when they should use context, or blast when they should use search.
Changes:
- Add a
guidancefield to thesearchtool response when results are ambiguous (multiple symbols match):"2 symbols named 'handle'. Use context with qualified name for specific result." - Add tool usage hints to
briefingoutput: when to use each tool, with examples. - In
tool_blast: if the symbol has a very large blast radius (>50 dependents), suggest narrowing withtopologyinstead.
Test: Search for an ambiguous name, verify guidance is included. Search for an unambiguous name, verify no guidance noise.
Add a new tool or mode that lets agents ask structural questions directly, without needing to translate them into symbol-name queries.
Changes:
- Enhance
interrogatetool (already exists in MCPtools_list) to accept structured queries like{"pattern": "hub_symbols", "subsystem": "auth"}or{"pattern": "error_handlers"}. - Wire
interrogateto the existingrolesandmotifsmodules: hub symbols, bridge symbols, entry points, error handlers, validators. - Return results in the same format as
searchfor consistency.
Test: Interrogate for "entry points", verify the response lists symbols with entry_point role.
Problem: graphiq setup writes configs for 9 harnesses but has several reliability issues: no PATH verification, partial failure handling, and a SQL injection risk in cmd_roles.
Files touched: crates/graphiq-cli/src/main.rs, install.sh
Changes:
- In
cmd_setup: before writing any config, verifygraphiq-mcpis on PATH by runningwhich graphiq-mcp. If not found, print a clear error with install instructions and abort. - Add a
--checkflag that validates the current setup without modifying anything.
Test: Run setup with graphiq-mcp not on PATH, verify clear error. Run with it on PATH, verify success.
Currently, if writing the Claude Desktop config fails, the function returns early and doesn't configure the remaining harnesses.
Changes:
- Each harness config write should be independent — a failure in one should not prevent the others from being attempted.
- Collect all successes and failures, print a summary at the end.
- Add a
--dry-runflag that shows what would be written without writing.
Test: Mock a write failure for one harness, verify others are still configured.
Current code (main.rs:572-576):
let query = format!("... WHERE subsystem_id = {} ... LIMIT {}", sub_id, top);sub_id is a usize (safe), but the raw format!() pattern is a ticking time bomb if the API ever widens.
Changes:
- Use parameterized queries with
params![sub_id as i64, top as i64]. - Audit all other raw SQL format strings in the CLI for the same pattern.
Test: Existing tests pass. Add a test that verifies parameterized query behavior.
Problem: The Signet plugin manifest (signet-plugin/manifest.json) declares capabilities but the integration is shallow — it doesn't leverage Signet's session hooks for auto-indexing, doesn't report index health to the Signet dashboard, and doesn't participate in Signet's memory system.
Files touched: signet-plugin/manifest.json, signet-plugin/integration.json, new signet-plugin/hooks/
Changes:
- Add
session:startandsession:endhook handlers to the Signet plugin. - On
session:start: detect the project root from the session context, check if.graphiq/graphiq.dbexists and is fresh, trigger a background index if needed, and register the MCP tools for that session. - On
session:end: clean up ephemeral databases if session-scoped. - This means agents get GraphIQ context automatically without any manual setup step.
Test: Start a Signet session in a project directory, verify GraphIQ auto-indexes and tools are available.
Changes:
- Add a
dashboardPanelsentry to the manifest that shows index status (file count, symbol count, freshness, search mode) in the Signet dashboard. - Report staleness events as Signet notifications: "GraphIQ index stale — 3 files changed since last index".
Test: Verify dashboard panel appears in Signet UI with correct data.
Changes:
- When GraphIQ computes structural roles, motifs, or subsystems, emit them as Signet knowledge graph entities. Example:
entity:graphiq_subsystem_auth { symbols: 42, cohesion: 0.87 }. - This lets other Signet plugins and agents query the codebase structure without calling GraphIQ directly.
- Keep this opt-in via a
--emit-knowledgeflag or manifest config.
Test: Index a project with knowledge emission enabled, verify entities appear in Signet's knowledge graph.
Problem: Each search tool is better at different tasks, but the distinctions aren't surfaced to agents effectively. The query family classification is internal and invisible to the agent.
Files touched: crates/graphiq-core/src/query_family.rs, crates/graphiq-mcp/src/main.rs
Changes:
- Include the detected
query_familyin every search response so agents learn which queries work best. - Add a
query_familysummary to thebriefingtool output explaining the 8 families and what they're good for. - Example:
"query family: NaturalDescriptive — works well for 'how does authentication work', 'error handling middleware'".
Test: Search with different query styles, verify correct family classification in response.
Currently results are a flat ranked list. For exploratory queries, this means the top 10 results might all be from the same file.
Changes:
- After ranking, cluster results by file or subsystem. Show at most 3 results per cluster in the top-K, then backfill from remaining clusters.
- This gives agents broader coverage of the codebase per query.
- Add
--clusterflag (default on for MCP, off for CLI) to enable/disable.
Test: Search in a project where one file has 10 matching symbols, verify results are spread across files.
Changes:
- In
tool_context: currently returns the full source. For large symbols (>50 lines), return a smart excerpt: signature + first 10 lines + last 5 lines, with a"showing 15 of 150 lines"note. - Add
context_lines: usizeparameter to include N lines of surrounding context from the file (lines before/after the symbol). - This reduces token waste for agents working within context windows.
Test: Query context for a 200-line function, verify smart excerpt. Query with context_lines: 5, verify surrounding code.
Phase 1 (auto-index + staleness) ← start here, unblocks everything
├── 1A: proactive staleness guard
├── 1B: content-hash freshness
└── 1C: project root mismatch fix
Phase 2 (isolation & concurrency) ← enables multi-agent
├── 2A: session-scoped ephemeral DB
├── 2B: reader-writer lock
└── 2C: watcher debouncing
Phase 3 (search surfacing) ← improves agent effectiveness
├── 3A: enriched result format
├── 3B: tool routing guidance
└── 3C: structural search mode
Phase 4 (install hardening) ← independent, ship anytime
├── 4A: PATH verification
├── 4B: partial failure recovery
└── 4C: SQL injection fix
Phase 5 (Signet integration) ← depends on Phase 1 + 2
├── 5A: session lifecycle hooks
├── 5B: index health reporting
└── 5C: memory cross-pollination
Phase 6 (search methodology) ← independent, ship anytime
├── 6A: query family transparency
├── 6B: search result clustering
└── 6C: context window optimization
Every phase ships with:
cargo test— all existing + new tests passcargo fmt --all -- --check— no formatting regressions- Live smoke test — index
graphiqitself, search, verify results - MCP smoke test — start
graphiq-mcp, call each tool via JSON-RPC, verify responses - No performance regression —
cargo benchruns within 5% of baseline
| File | Role | Phases |
|---|---|---|
crates/graphiq-core/src/manifest.rs |
Artifact freshness tracking | 1A, 1B |
crates/graphiq-core/src/index.rs |
Indexing pipeline | 1B, 1C, 2C |
crates/graphiq-core/src/db.rs |
SQLite storage layer | 1C, 2B |
crates/graphiq-core/src/search.rs |
Search engine orchestration | 3A, 6A |
crates/graphiq-core/src/cache.rs |
In-memory LRU cache | 2B |
crates/graphiq-core/src/cruncher.rs |
CruncherIndex | 1A, 2B |
crates/graphiq-core/src/query_family.rs |
Query classification | 6A |
crates/graphiq-mcp/src/main.rs |
MCP server (14 tools) | 1A, 2A, 2B, 2C, 3A, 3B, 3C, 6B, 6C |
crates/graphiq-cli/src/main.rs |
CLI commands | 1C, 4A, 4B, 4C |
signet-plugin/manifest.json |
Signet plugin config | 5A, 5B, 5C |
signet-plugin/integration.json |
Signet version compat | 5A |