Browse docs

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

  1. Exact identifier (function / task id / file) → Grep or cos_graph_query.
  2. Conceptual questioncos_doc_search.
  3. "Have I seen this?"cos_search.
  4. "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):

  1. 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 the embeddings table.
  2. 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 by cos = 1 − d²/2.
  3. brute force — the caller's streaming numpy scan (knn returns 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 99236e19 without 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:

  • IncludeDEFAULT_INCLUDE extensions 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-driven code_generic extractor: .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. The graph_os extra ships grammars for rust, ruby, java, c, c++, c#, scala, kotlin and lua; any other extension with a _LANG_SPEC row 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_EXCLUDE denylist (node_modules, .venv, dist, build, …) and the repo's .gitignore (root + nested + .git/info/exclude), parsed via pathspec. The walk therefore drops exactly what git status ignores. If pathspec is unavailable the .gitignore layer 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:

  1. File reconcile — nodes whose file_index_state path the walk did not visit (deleted or now-gitignored files) are removed. This covers only rows file_index_state tracks, which are file rows.
  2. Residue sweep — the folder-spine nodes and zero-edge phantoms that file_index_state never tracked (a folder is not a file row; phantoms carry a NULL / off-tree file_path) are removed via the cos_graph_doctor safe-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-disk src/-prefixed node with contains edges 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", ...) with retryable=true — backend missing. Retry after cos 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 N parallelises and --force bypasses the content-hash cache. The final line reports processed/skipped/errors/duration. A full uncapped walk is authoritative and self-heals — see Reindex reconciliation above; --prune-stale additionally runs the doctor safe-repair before the walk.
  • cos graph-query "<phrase>" — convenience CLI wrapper over cos_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_doctor reports orphans, dangling/duplicate edges, self-loops, and stale-path nodes; fix=true sweeps 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_impact groups results by tier (will_break / should_review / context).