Graph-OS Query Guide
P: Decision guide for picking the right
cos_graph_*tool per query and slotting it into the three-layer retrieval contract. R: Routing an agent flow that touches the graph (rename, impact analysis, dependency walk). S: Internals of a single tool — see graph-use-cases.md. N: graph-hallucination-cures.md, graph-use-cases.md
Nav: Section Index | Docs Index
When to use each
cos_graph_*tool, and which three-layer retrieval slot it occupies. Read this before routing any agent flow that touches the knowledge graph subsystem.
Three-layer retrieval recap
| Layer | Question | Tools |
|---|---|---|
| 1. Memory | "Have I solved this before?" | cos_search, cos_timeline, cos_details, cos_learn_suggest |
| 2. Docs | "What does the spec say?" | cos_doc_search |
| 3. Graph | "What is connected to what?" | cos_graph_* |
Tool cheat sheet
| Question | Tool |
|---|---|
| What depends on X? | cos_graph_impact(uid, direction="downstream") |
| Who calls X? | cos_graph_references(uid) |
| Where is X used + surrounding context? | cos_graph_context(uid_or_name, depth=1) |
| Trace execution from entry point | cos_graph_trace(entry_uid) |
| Find symbol by label | cos_graph_query(q) |
| Something semantically similar? | cos_graph_similar(uid) |
| Shortest path between X and Y? | cos_graph_path(X, Y) |
| API surface (HTTP, MCP, gRPC, events) | cos_graph_contracts() |
| Plan before rename | cos_graph_rename_plan(uid, new_name) |
| Pre-commit: what changes broke? | cos_graph_detect_changes(files=[...]) |
| Visualise | `cos_graph_export(format="mermaid" |
Routing decision
- Exact identifier (function / task id / file) → Grep or
cos_graph_query. - Conceptual question →
cos_doc_search. - "Have I seen this?" →
cos_search. - "What is connected?" →
cos_graph_*.
Each envelope carries data.meta.layer so consumers can audit which
layer answered.
cos_graph_similar — persisted-embedding fast path
cos_graph_similar prefers persisted graph_node vectors when available:
reindex_all embeds the meaningful kinds (function · method · class ·
route · mcp_tool · doc_heading) into embeddings(source_table='graph_nodes'),
and the tool ranks the full pool with a single query encode
(meta.scorer="persisted-embeddings", ~25 ms vs ~1800 ms for the legacy
per-candidate path). When no persisted vectors exist (or the embedding
model is unavailable) it transparently falls back to the on-the-fly
difflib baseline (meta.scorer="bge-m3+difflib-blend" /
"difflib-baseline"). Raw cosine and the legacy blended score live on
different scales, so the persisted path caps its floor at a
model-calibrated value (persisted_similarity_floor: MiniLM 0.25,
BGE-M3 0.60 — measured) so a legacy confidence_min default can't suppress
the fast path; meta.floor reports the effective value. Run
cos brain --reindex (or python -m embeddings --reindex) to populate the
vectors after a bulk graph change, or make migrate-embeddings to cut the
whole corpus over to BGE-M3 (re-embed + flip the .coding-os/.embedding-model
active marker; the dual-model bridge keeps search correct mid-migration).
ANN index + cos_graph_search (free-text hybrid)
For scale, the persisted path uses an ANN index (graph_os/vec_index.py) with a
three-tier fallback chain (each degrades cleanly to the next, same knn()
contract):
- usearch HNSW — true sublinear O(log N) kNN. The scale answer: measured
query latency stays ~flat from 100k→1M vectors while a flat scan grows ~10×.
The index is a derived cache persisted next to the DB (
.graph-hnsw.usearch), rebuilt from theembeddingstable. - sqlite-vec
vec0— SIMD-accelerated exact (flat) scan. Honest finding (measured): vec0 in 0.1.x is not HNSW — it's ~5× faster than the numpy scan by constant factor but still O(N). Vectors are unit-normalised, so its L2 distance maps to cosine bycos = 1 − d²/2. - brute force — the caller's streaming numpy scan (
knnreturns None).
cos_graph_search(query) answers "where is the code that does X?" by free text,
blending semantic cosine (0.7) + FTS5 lexical presence (0.2) + in-degree
centrality (0.1); cos_graph_similar(uid) stays node-to-node. Accuracy is
strong on BGE-M3: a doc-only query retrieves the source symbol at recall@1 ≈99%,
recall@5 100%, MRR ≈0.99 (measured, 80-node sample).
Wave numbering note (for auditors). The 2026-06 embedding epic shipped as TASK-279 (wave 1: persisted node embeddings), TASK-280 (wave 3: sqlite-vec ANN + hybrid search), TASK-281 (wave 4: durable embedding outbox), TASK-282 (wave 5: usearch HNSW). There is no wave-2 task — that slot (BGE-M3 dual-model bridge + cutover) landed directly via commit
99236e19without its own TASK id. A missing "wave 2" in the board is a numbering artifact, not lost work.
What gets indexed (walk coverage)
The file walk (src/core/graph_os/ingest/base.py::walk_local) decides
which files reach the extractors:
- Include —
DEFAULT_INCLUDEextensions only. First-class hand-written extractors:.py .ts .tsx .js .jsx .mjs .cjs .go .php .sh .yaml .yml .json .toml .md. Polyglot baseline via the table-drivencode_genericextractor:.rs .rb .java .c .h .cc .cpp .cxx .hpp .hh .cs .scala .kt .kts .lua— functions/classes +contains, extracted when that language's tree-sitter grammar is installed. Thegraph_osextra ships grammars for rust, ruby, java, c, c++, c#, scala, kotlin and lua; any other extension with a_LANG_SPECrow activates once its grammar is installed. SQL is intentionally excluded (its DDL symbols don't fit the function/class model). Rust and Ruby also get calls/imports/inherits edges (per-language hooks in code_generic); the other generic languages stay node+contains until a hook is added for them. - Exclude — the union of two layers: the static
DEFAULT_EXCLUDEdenylist (node_modules,.venv,dist,build, …) and the repo's.gitignore(root + nested +.git/info/exclude), parsed viapathspec. The walk therefore drops exactly whatgit statusignores. Ifpathspecis unavailable the.gitignorelayer is skipped and the denylist remains the backstop (fail-open). - Skipped — symlinks (target indexed on its own pass) and files
over
COS_GRAPH_MAX_FILE_BYTES(default 2 MB).
Coverage is not guaranteed 100 %: a file can index without raising
yet still have an extractor hit a parse error on part of it, dropping
some symbols. That count is surfaced — see files_with_parse_errors in
cos_graph_doctor and parse_errors= in the cos graph-reindex
summary. truncated == true on a query is a different signal (budget
cut, re-query); parse errors are coverage gaps (some symbols never
extracted).
Reindex reconciliation (self-healing)
A full, uncapped cos graph-reindex (walk target == repo root, not
--path-scoped, len(files) < max_files) is authoritative: when it
finishes it reconciles the graph to disk reality so residue from bulk
directory moves/deletes (git mv, rm -rf) can't accumulate. Two prunes
run under that guard:
- File reconcile — nodes whose
file_index_statepath the walk did not visit (deleted or now-gitignored files) are removed. This covers only rowsfile_index_statetracks, which are file rows. - Residue sweep — the folder-spine nodes and zero-edge phantoms that
file_index_statenever tracked (a folder is not a file row; phantoms carry a NULL / off-treefile_path) are removed via thecos_graph_doctorsafe-repair. It runs after the global cross-file link so live external stubs already hold their edges and are never swept; the sweep deletes only paths absent on disk and zero-edge orphans, so an on-disksrc/-prefixed node withcontainsedges survives.
Per-file auto-reindex prunes a single deleted file's nodes on the
PostToolUse hook (reindex_dispatch._prune_graph_for_deleted_file), but a
bulk mv/rm of a directory fires no per-file hook — the old-path
folder-spine and phantom nodes are left for the authoritative full walk
(or a manual cos graph-doctor --fix / cos graph-reindex --prune-stale)
to clear. A --path sub-walk or a max_files-capped walk skips both
prunes: it is not authoritative, so every un-walked file would falsely
look stale.
Common failure modes
fail("unavailable", ...)withretryable=true— backend missing. Retry aftercos graph-reindex/ server restart.meta.dim_mismatch_skipped>0(embedding-aware tools) — BGE-M3 migration still in progress; fallback search may be degraded.meta.backend_fallback— reserved for a future graph-native store; currently always absent/false. SQLite is the sole backend (Kùzu retired 2026-05-18, ADR-0002), so this is a no-op signal today.
Formula linkage
Every formula in docs/code-os-core-docs/thinkingos-formulas/formulas-en.md
that mentions a graph has a specific cos_graph_* call behind it:
| Formula | Tool |
|---|---|
| F1 Research | cos_graph_context(entry_point) |
| F2 Dependency Map | cos_graph_impact(uid) |
| F3 API Design | cos_graph_references(handler) |
| F4 Docs | cos_graph_contracts() |
| F5 Pre-Implementation | cos_graph_context(file) |
| F6 Regression Tests | cos_graph_detect_changes(files=...) |
| F7 Fault Isolation | cos_graph_trace(entry_uid) |
| F8 Auth Audit | cos_graph_references("verify_auth") + cos_graph_contracts() |
| F9 Release Gate | cos_graph_contracts() + cos_graph_detect_changes("HEAD~1..HEAD") |
| F10 Tracing | cos_graph_trace |
| F11 Refactor Plan | cos_graph_impact + cos_graph_similar |
Skills
graph-explorer— the canonical entry skill (src/core/skills/graph-explorer/SKILL.md).codebase-explorer— pairs with graph-explorer; codebase-explorer is better for conceptual reading, graph-explorer is better for symbol-precise lookups.
Commands
cos graph-reindex— rebuild the graph from scratch. Shows a live per-file progress bar on an interactive terminal (auto-hidden when stdout is piped/CI);--workers Nparallelises and--forcebypasses the content-hash cache. The final line reportsprocessed/skipped/errors/duration. A full uncapped walk is authoritative and self-heals — see Reindex reconciliation above;--prune-staleadditionally runs the doctor safe-repair before the walk.cos graph-query "<phrase>"— convenience CLI wrapper overcos_graph_query.cos graph-viz [--root <uid>]— produce the HTML viewer (plan §15 / I.10).
Coverage, budgets, and benchmarks (README deep-dive)
Moved from the README front page (kept there in summary form). Live measurements on this repo unless noted.
Most-depended nodes (cos_graph_centrality / cos_graph_ranking)
| Node | Kind | Inbound deps | Why it's load-bearing |
|---|---|---|---|
GraphNode |
class | 118 | data contract every extractor + backend constructs |
init_db |
function | 108 | DB bootstrap — also the #1 betweenness chokepoint |
GraphEdge |
class | 106 | the edge half of the node/edge contract |
ok / safe_tool |
function | 89 / 84 | MCP envelope wrappers around every cos_* tool |
cos-env.sh |
file | 79 | every hook sources it (top file-level hub) |
cos graph-centrality --metric betweenness re-ranks by bridge
importance: init_db and _backend top that list. The repo is
acyclic (cos graph-cycles → 0 import cycles).
Per-kind coverage
All 23 indexed node kinds probed end-to-end (one cos_graph_context
call per kind on the highest-degree sample): 23/23 ok=true,
non-empty neighbours, zero errors, latency 0–23 ms.
| Kind | Latency | Typical context tokens |
|---|---|---|
function, method, class |
1–5 ms | 5K–13K |
file, folder, module |
3–16 ms | 9K–71K |
route, mcp_tool, hook, task |
<1 ms | 250–760 |
doc_file, doc_heading, rule, skill |
1 ms | 1K–85K |
interface, import_, variable, event, tool, contract |
<1 ms | 250–700 |
Tip: for high-degree hubs (folders, large modules, unresolved:str)
start with cos_graph_references(limit=20) or
cos_graph_impact(depth=2), not cos_graph_context(depth=2).
View modes (Hub Graph tab)
Four deliberate views at http://127.0.0.1:9188/graph, each backed by
a different edge-bucket recipe in cos_graph_export:
| Mode | What you see | Use for |
|---|---|---|
auto (default) |
Balanced blend across 8 edge buckets | One-glance "how is this system wired?" |
containment |
Folder → file → class → method spine only | Navigating the structural skeleton |
dependencies |
Semantic edges only (no contains) | Auditing call graphs / API surface |
processes |
Louvain communities + member edges | Discovering implicit subsystems |
Budgets, truncation, and "did I see everything?"
| Tool | Knob | Default | Coverage signal |
|---|---|---|---|
cos_graph_references |
limit (edges returned) |
100 | data.total_count · data.meta.result_truncated · data.meta.limit |
cos_graph_impact |
depth + visit_limit |
3 / 500 | data.meta.walk_truncated · data.meta.visit_limit |
cos_graph_context |
depth + visit_limit |
1 / 500 | data.meta.walk_truncated · data.meta.visit_limit |
cos_graph_export |
max_nodes + max_hops |
500 / 3 | UI "truncated · raise depth budget" badge |
cos_graph_path |
max_hops |
5 | data.meta.walk_truncated · data.meta.hop_limit |
Two distinct names: data.meta.truncated = envelope token-budget
trimming; result_truncated / walk_truncated = tool-level coverage
truncation (limit cut a result set / BFS hit its node cap).
Recommended workflow: probe with defaults → check coverage
(total_count > count? result_truncated? walk_truncated?) →
if incomplete, widen limit, narrow kinds, or split the question.
Exhaustive sweeps: explicit limit=10_000 is fine (<50 ms even on the
highest-degree hubs). limit=20 is a probe default; limit=100 is the
correctness default.
Health, freshness, hallucination guards
cos_graph_doctorreports orphans, dangling/duplicate edges, self-loops, and stale-path nodes;fix=truesweeps remediable issues (it caught 3,727 ghost nodes after the pre-src/reorganization).- Idempotent re-indexing fires on every Write/Edit (PostToolUse hook,
touched file only); bulk changes via
cos graph-reindex. - Confidence-tiered edges:
cos_graph_impactgroups results by tier (will_break/should_review/context).