Navigate the graph_os knowledge graph before editing load-bearing code. Use when tracing dependencies, planning a rename, auditing API surface, or answering "what breaks if I change this?". Pairs with codebase-explorer — graph-explorer wins for symbol-precise queries, codebase-explorer wins for conceptual code-reading.
Installs into .claude/skills of the current project.
Are you the author of Graph Explorer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kouroshez-graph-explorer)
---
name: graph-explorer
tier: exploration
domain: [universal]
description: Navigate the graph_os knowledge graph before editing load-bearing code. Use when tracing dependencies, planning a rename, auditing API surface, or answering "what breaks if I change this?". Pairs with codebase-explorer — graph-explorer wins for symbol-precise queries, codebase-explorer wins for conceptual code-reading.
last_reviewed: "2026-05-11"
---
# graph-explorer
Purpose: Load the graph_os toolset (`cos_graph_*` MCP tools) and
use it deliberately before any non-trivial code edit. The graph is the
third retrieval layer (CLAUDE.md Three-Layer Retrieval) — use it when
tree-grep or past-memory searches return noise.
Read when: Editing `src/core/**` or `src/cli/**`, producing a rename plan,
auditing API/MCP contracts, answering "what depends on X".
Skip when: You already know the blast radius, or the change is a
self-contained one-file edit with no callers.
## Decision ladder
1. **Need to know "what calls this?"** → `cos_graph_references(uid)`.
2. **Need surrounding context before editing?** → `cos_graph_context(uid_or_name, depth=1)` (Implementer pre-impl step).
3. **Planning a refactor?** → `cos_graph_impact(uid, depth=3)` (Analyst dependency map) groups edges by risk tier.
4. **Renaming a symbol?** → `cos_graph_rename_plan(uid, new_name)` before any `Edit` — returns call-sites, doc refs, tests, string literals.
5. **API / contract audit?** → `cos_graph_contracts(kinds=["http","mcp"])` (Documenter + Deployer).
6. **Tracing a fault?** → `cos_graph_trace(entry_uid)` (Debugger fault isolation step).
7. **"Is anything similar?"** → `cos_graph_similar(uid, top_k=5)`.
8. **Shortest dependency path?** → `cos_graph_path(source, target)`.
9. **Need a diagram?** → `cos_graph_export(format="mermaid", root_uid=...)`.
10. **Pre-commit self-review?** → `cos_graph_detect_changes(files=[...])` — call BEFORE `make verify`.
> **Method blast-radius — use `impact`, not bare `references(kinds="calls")`.**
> Static AST cannot resolve instance-method calls on locally-typed
> receivers (`backend.upsert_node(...)` — the type of `backend` is
> unknown), so `references(method, kinds="calls")` under-reports those
> call sites. `cos_graph_impact` compensates by traversing
> method→class→`constructs`, so "what breaks if I change this method?"
> stays complete. Treat `references(calls)` as a lower bound for methods.
> **Function blast-radius — import bindings are first-class (TASK-402).**
> Every `from M import name` site emits an `import_` node which the
> `link_import_bindings` pass binds to the real symbol (exactly-one
> match; ambiguous → skipped, never guessed). `references(function)`
> with default kinds therefore returns BOTH direct `calls` edges AND
> `imports` edges whose `import_` source node carries the caller's
> `file_path` — the init_db probe went from 6 to 80 of 106 caller files
> once this pass landed. Two caller classes stay invisible to static
> resolution and need a grep complement on security-critical sweeps:
> module-alias attribute calls (`import database as db; db.init_db()`)
> and calls embedded in string literals (`python -c "from … import …"`).
Every response carries `data.meta.layer="graph"` and `data.meta.backend`
(`sqlite` — the single store since Kùzu was retired 2026-05-18). The
`meta.backend_fallback` flag is reserved for a future graph-native
plug-in; today it is always absent/false, so you never need to treat a
result as a lower-precision fallback.
## Coverage contract — never trust a single call blindly
The biggest soundness trap in graph queries is **silent truncation**:
asking "who calls X?" on a 500-caller hub with `limit=100` (the
default) returns 100 rows and **no signal** that 400 more exist —
unless you read the coverage metadata. Every coverage-sensitive tool
exposes it; the rule below is mandatory before you act on a result.
### Signals to read on every response
| Tool | `data.total_count` | Coverage signal in meta | `data.meta.<budget>` |
|---|---|---|---|
| `cos_graph_references` | ✓ | `result_truncated` (limit hit or token-trim) · `zero_from_kind_filter` (0 rows, wrong edge kinds) | `limit` · `kinds` |
| `cos_graph_impact` | – | `walk_truncated` (BFS cap hit) | `visit_limit` · `depth` |
| `cos_graph_context` | – | `walk_truncated` (BFS cap hit) | `visit_limit` · `depth` |
| `cos_graph_path` | – | `walk_truncated` (hop saturation) | `hop_limit` |
| `cos_graph_export` | – | `result_truncated` + `max_nodes_requested` vs `max_nodes_effective` (ceiling 50k); Hub badge renders the same meta | `max_nodes` · `max_hops` |
**`result_truncated == true` or `walk_truncated == true` ⇒ the answer
is incomplete. Do NOT proceed on it.**
Why two names? `result_truncated` / `walk_truncated` flag *coverage*
truncation (a caller `limit` or BFS cap was hit); `data.meta.truncated`
flags that the envelope's *token-budget* trimmer ran (response too big,
tail rows dropped). They are separate causes, but a token-budget trim
that drops list items now ALSO sets `result_truncated` (TASK-056) — the
trimmer can silently shorten a `contracts` / `references` payload, and
the coverage check must catch that too. Net: **`result_truncated == true`
means this list is incomplete for ANY reason — re-query**; `meta.truncated`
additionally tells you the cause was the token budget.
**Truncation is not the only way to be wrong — a *zero* can be too.** With no
`kinds` argument, `references` picks defaults per node kind; ask about a kind
whose real edges are structural (`rule`, `task`, `route`, …) with the code
defaults and you get a complete query of the wrong edge types: `total_count: 0`,
`result_truncated: false`, and nothing pointing at the mistake. So the rule has a
second half: **`total_count == 0` with `meta.zero_from_kind_filter == true` ⇒ the
node HAS inbound edges and you queried past them.** `meta.edge_types_present`
names them — re-query with `kinds=` set to those. Only a zero *without* that flag
is the authoritative "nothing points here" the dead-code check depends on.
### The mandatory 2-step probe → widen workflow
```python
# 1. probe with defaults (cheap, gives you the lay of the land)
r = cos_graph_references(uid) # default limit=100
total = r["data"]["total_count"]
shown = r["data"]["count"]
# 2. if incomplete, widen with the actual total
if r["data"]["meta"]["result_truncated"]:
r = cos_graph_references(uid, limit=total) # exhaustive
# alternative: narrow the kinds filter first when total is huge
# r = cos_graph_references(uid, kinds=["calls"], limit=total)
```
For `cos_graph_impact` and `cos_graph_context`, the budget is
`visit_limit` (default 500 nodes). When `meta.walk_truncated` is true:
```python
# option A — raise the cap deliberately
r = cos_graph_impact(uid, depth=3, visit_limit=5000)
# option B — step DOWN in depth and walk each frontier separately
# (more expensive but produces tier-quality risk grouping)
r1 = cos_graph_impact(uid, depth=1)
for caller in r1["data"]["tiers"]["will_break"]:
cos_graph_impact(caller["uid"], depth=2)
```
### Per-task-class budget recipes
| Task class | Tool | Budget |
|---|---|---|
| Quick probe ("does X have any callers?") | `cos_graph_references` | `limit=20` |
| Implementation pre-check ("what neighbours?") | `cos_graph_context` | `depth=1` |
| Refactor planning ("what breaks?") | `cos_graph_impact` | `depth=3`, `visit_limit=2000` |
| Rename — must hit every site | `cos_graph_rename_plan` | (exhaustive by design) |
| Security audit — every caller chain | `cos_graph_references` then `cos_graph_impact` per caller | `limit=10_000`, `visit_limit=10_000` |
| Doc cross-reference audit | `cos_graph_references` | `kinds=["references_doc","links_to","cites_heading"]`, `limit=1000` |
**Cost math:** `cos_graph_references` is O(N) with an index; `limit=10_000`
runs in <50 ms on the highest-degree hubs in this repo. There is no
reason to under-budget a coverage-critical sweep. Pay the 50 ms.
### Anti-patterns
- Calling `cos_graph_references(uid)` once and treating the slice as
complete — without reading `total_count` or `meta.truncated`.
- Setting `limit=20` for a rename or security audit because "small is
safer" — small **hides** coverage gaps, doesn't prevent them.
- Calling `cos_graph_impact(uid, depth=4)` on a hub and not noticing
`meta.truncated` — `depth=4` × hub frontier blows past `visit_limit`
before you reach the interesting frontier.
- Asking the graph the same question twice with the same params hoping
a different answer comes back — the result is deterministic; widen
the budget or narrow the filter.
## Enforcement
- `enforce-graph-context.sh` — when editing a file matching one of the
`rag-config.yaml::graph.enforce_context_on` globs, the hook warns (or
blocks in strict mode) unless `cos_graph_context` wrote a *fresh*
`.graph/ctx-<sha>` marker this session (content-hash-bound — a consult
that went stale because the file changed no longer counts).
`enforce-skill.sh` reads the same glob list to require `graph-explorer`
on those files.
- `enforce-rename-plan.sh` — if you attempt a multi-file rename-like
Edit without a prior `cos_graph_rename_plan` in this session, the
hook warns + suggests the command.
Both hooks default to **warn** so agents discover the graph layer
instead of writing blind. Opt-out with `COS_ENFORCE_GRAPH_CONTEXT=off`
(or `=0`); promote to block with `=strict`. Same for
`COS_ENFORCE_RENAME_PLAN`.
## Auto-reindex contract
The PostToolUse hook `auto-reindex-docs.sh` re-indexes **only the file
just written** via `graph_os.tools.reindex_dispatch.dispatch(path)` —
not the whole repo. The dispatcher is incremental: it extracts that
single file's nodes / edges, upserts them into the existing graph
(idempotent on `uid`), and short-circuits via `file_index_state` when
the content hash hasn't changed. Typical cost: 20–100 ms per file,
fire-and-forget background. Use `cos graph-reindex --force` only after
a bulk shell move (`mv` / `cp` / `git checkout`) where the hook never
fired.
## Fail-loud failure modes
- MCP backend down → tools return `fail("unavailable", ...)` with
`retryable=true`. Retry after `cos graph-reindex` / server restart
— do NOT guess.
- Graph empty (fresh repo) → `_query`/`_context` return empty lists.
Run `make docs-index` / `make task-sync` / `cos graph-reindex` first.
- Confidence below 0.3 → the edge is probably a false positive.
`_impact` clusters these under the `context` tier so agents can
ignore noise.
## Web UI
For visual exploration, the unified React SPA exposes the graph at
[http://127.0.0.1:9188/graph](http://127.0.0.1:9188/graph). Start it
with `cos hub start` (FastAPI + uvicorn singleton on port 9188 that
serves every registered project). The page picks a root node, runs
depth-bounded BFS, and renders with Sigma.js + Graphology — useful
when:
- An impact/rename plan returns >10 affected files and the agent (or
user) wants to see clusters before approving.
- Walking a CONTAINS spine (Folder→File→Class→Method) is easier than
re-issuing tool calls.
- Sharing a snapshot with a human collaborator who needs to *see* the
blast radius rather than read JSON envelopes.
For one-off static export (no live server, embeddable HTML), the
legacy `cos graph-viz` command still works — kept intentionally for
sharing/embedding.
## Link-backs
- Knowledge-graph plan: [docs/phase-i-knowledge-graph-plan.md](../../../docs/phase-i-knowledge-graph-plan.md)
- MCP envelope: [docs/engineering/mcp-error-envelope.md](../../../docs/engineering/mcp-error-envelope.md)
- Rule 14 (envelope): [CLAUDE.md](../../../CLAUDE.md)