Author the graph_os knowledge-graph internals — extractors, backends, indexers, queries. Use when adding a new node/edge type, writing a new extractor (Python AST, doc-headings, etc.), evolving the SQLite backend, or modifying the reindex dispatcher. Enforces idempotency on uid, append-only file_index_state, backend-agnostic tool layer, confidence-scoring discipline. Pairs with graph-explorer (which queries the graph this skill builds), python-meta-server, and mcp-tool-authoring (for `cos_gra...
Scanned 9/4/2026
Install to Claude Code
npx -y skills add kouroshez/coding-os --skill graph-os-authoring --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Graph Os Authoring?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kouroshez-graph-os-authoring)More formats (shields.io, HTML) on the badges page.
---
name: graph-os-authoring
tier: stack
domain: [architecture]
description: Author the graph_os knowledge-graph internals — extractors, backends, indexers, queries. Use when adding a new node/edge type, writing a new extractor (Python AST, doc-headings, etc.), evolving the SQLite backend, or modifying the reindex dispatcher. Enforces idempotency on uid, append-only file_index_state, backend-agnostic tool layer, confidence-scoring discipline. Pairs with graph-explorer (which queries the graph this skill builds), python-meta-server, and mcp-tool-authoring (for `cos_graph_*` tools).
last_reviewed: "2026-05-11"
---
# graph-os-authoring
Purpose: The graph is the third retrieval layer. Authoring it correctly — idempotent extraction, backend-neutral query layer, honest confidence scores — is what makes `cos_graph_*` tools faster + more accurate than grep. Getting any one of those wrong corrupts the graph silently, and the agent starts trusting hallucinations.
Read when: editing files matching:
- `src/core/graph_os/backends/*.py` — SQLite (single backend today; Kuzu retired 2026-05-18 after benchmark showed SQLite p99 < 30 ms on 5-hop @ 1M nodes).
- `src/core/graph_os/extractors/*.py` — Python AST / doc / config extractors.
- `src/core/graph_os/tools/*.py` — the `cos_graph_*` MCP tools (also see [mcp-tool-authoring](../mcp-tool-authoring/SKILL.md)).
- `src/core/graph_os/tools/reindex_dispatch.py` — incremental reindex orchestration.
- `src/core/graph_os/types.py` — node + edge type definitions (the contract).
Skip when: only querying the graph (use [graph-explorer](../../../core/skills/graph-explorer/SKILL.md) for that).
## The Five Hard Contracts
### 1. UID scheme (immutable across rebuilds)
Every node has a uid that survives reindex. Scheme:
```
code:file:<path>
code:function:<path>::<name>
code:class:<path>::<name>
code:module:<dotted-name>
doc:file:<path>
doc:heading:<path>#<slug>:<level>
folder:<path>
config:<path>::<key>
```
Path is repo-relative, POSIX, no leading slash. **Never** include line numbers in uids — line numbers shift; uids must be stable.
When adding a new node type, extend the registry in [src/core/graph_os/types.py](../../../core/graph_os/types.py) and document the uid grammar there. The grammar IS the contract — agents reference uids in saved memories.
### 2. Idempotent extraction (keyed on uid)
Every extractor MUST be re-runnable. The contract: extracting the same file twice produces the same nodes + edges. Implementation:
```python
def extract(file_path: Path) -> ExtractionResult:
nodes = []
edges = []
for symbol in _walk_ast(file_path):
nodes.append(GraphNode(
uid=_make_uid(symbol), # deterministic from file + symbol name
kind="function",
label=symbol.name,
file_path=str(file_path),
start_line=symbol.start,
end_line=symbol.end,
confidence=1.0, # AST gives certainty
))
for callee in symbol.callees:
edges.append(GraphEdge(
source_uid=nodes[-1].uid,
target_uid=_resolve_callee_uid(callee, file_path),
kind="calls",
confidence=_calc_confidence(callee), # see §3
))
return ExtractionResult(nodes=nodes, edges=edges)
```
**Hard rules:**
- **Upsert on uid**, never blind-insert.
- **Removed symbols are HARD-deleted** — if function `foo` disappears from a file, `delete_nodes_for_file` removes its node before the file is re-extracted. The graph mirrors HEAD-of-tree, so a symbol that left the tree is *gone*, not historical; git is the forensic record of what used to exist. There is no `deleted_at` column and no tombstone compaction — they would accumulate millions of dead rows for a property (historical uid resolution) nothing in the system needs today. The graph keeps **no deletion ledger** for node prune — it runs on every per-file reindex, so a per-prune ledger row would be pure churn. Routine node prune is logged at `logger.debug`; git is the record. If historical uid resolution is ever required, add tombstoning as a deliberate feature then — don't assume it exists.
- **Edge confidence is part of the key idempotency** — re-extracting the same edge updates the confidence, doesn't append a duplicate.
### 3. Honest confidence scores
Every edge has a confidence ∈ [0, 1]. Calibrated guide:
| Confidence | Meaning | Example |
|---|---|---|
| 1.0 | AST-certain | `import X`, `class Y(Z):` (direct inheritance), `def foo(): bar()` (direct call site) |
| 0.7-0.9 | Strong heuristic | Attribute access through `self.something.method()` (dynamic dispatch but typed) |
| 0.4-0.6 | Weak heuristic | String reference (`"my_module.func"`), dict-keyed dispatch |
| 0.1-0.3 | Possible | Same-named symbol in different module (could be aliasing or coincidence) |
| < 0.1 | Drop | Don't emit; noise |
Tools (`cos_graph_impact`, `cos_graph_references`) cluster low-confidence edges into a separate "context" tier so agents can ignore them. Inflating confidence = agents trusting noise = hallucinations.
### 4. Backend-agnostic tool layer
The tool layer (`src/core/graph_os/tools/*.py`) MUST NOT leak SQL or Cypher into MCP tool responses. The tool calls into a backend abstraction:
```python
# src/core/graph_os/tools/_query.py
from core.graph_os.backends import get_backend
async def query_references(uid: str) -> list[GraphEdge]:
backend = get_backend() # returns SqliteBackend today; abstraction kept so a future store can plug in.
return await backend.find_references(uid)
```
Backends implement a common Protocol:
```python
class GraphBackend(Protocol):
async def find_references(self, uid: str) -> list[GraphEdge]: ...
async def find_context(self, uid: str, depth: int) -> list[GraphEdge]: ...
async def upsert_nodes(self, nodes: list[GraphNode]) -> None: ...
async def upsert_edges(self, edges: list[GraphEdge]) -> None: ...
# ...
```
Adding a future backend later = new backend file only, zero changes to the tool layer.
### 5. Reindex is incremental + short-circuited
Full reindex of a large repo is minutes. Production cost must be milliseconds per save. Implementation:
```python
# src/core/graph_os/tools/reindex_dispatch.py
async def dispatch(file_path: str) -> ReindexResult:
new_hash = _hash_file_contents(file_path)
old_hash = await _file_index_state.get(file_path)
if new_hash == old_hash:
return ReindexResult(skipped=True, reason="content unchanged")
extraction = _extract(file_path)
await _backend.upsert_nodes(extraction.nodes)
await _backend.upsert_edges(extraction.edges)
await _file_index_state.set(file_path, new_hash)
return ReindexResult(skipped=False, nodes=len(extraction.nodes), edges=len(extraction.edges))
```
`file_index_state` is the bookkeeping table mapping path → content-hash → last-indexed-at. Without it, every PostToolUse hook re-extracts unchanged files.
## Extractor — the Decision Tree
When adding a new extractor:
1. **What kind of artifact?** Code (Python / Go / TS / etc.) → AST extractor. Docs (Markdown / RST) → header extractor. Config (YAML / TOML / JSON) → key extractor.
2. **What nodes?** One per top-level symbol — functions, classes, modules. Sub-symbols (nested classes, methods) get their own node only if they're called from elsewhere.
3. **What edges?** `calls`, `imports`, `inherits`, `references` (doc → code), `contains` (folder → file → class → method), `tested_by` (test → subject).
4. **Where does ambiguity go?** Low-confidence edges. **Never** invent edges to fill in gaps.
### AST extractor anatomy (Python example)
```python
# src/core/graph_os/extractors/python_ast.py
import ast
from pathlib import Path
from core.graph_os.types import GraphNode, GraphEdge, ExtractionResult
class PythonAstExtractor:
"""Extracts functions, classes, imports, calls from .py files."""
def extract(self, path: Path) -> ExtractionResult:
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
nodes: list[GraphNode] = []
edges: list[GraphEdge] = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
nodes.append(self._function_node(path, node))
edges.extend(self._call_edges(path, node))
elif isinstance(node, ast.ClassDef):
nodes.append(self._class_node(path, node))
edges.extend(self._inherit_edges(path, node))
elif isinstance(node, (ast.Import, ast.ImportFrom)):
edges.extend(self._import_edges(path, node))
return ExtractionResult(nodes=nodes, edges=edges)
# ... helpers below
```
Test the extractor with a representative file + assert exact uid + count of nodes/edges:
```python
def test_extracts_function_with_two_calls(tmp_path):
src = tmp_path / "sample.py"
src.write_text("""
def outer():
inner()
other.method()
def inner():
pass
""")
result = PythonAstExtractor().extract(src)
assert len(result.nodes) == 2 # outer + inner
assert any(e.kind == "calls" and e.target_uid.endswith("::inner") for e in result.edges)
```
## Backend — the Decision Tree
`graph_os` ships a single backend:
- **SQLite** — embedded relational, FTS5 + JSON1, p99 < 30 ms on 5-hop traversal at 1M nodes with PRAGMA tuning + ANALYZE (benchmark 2026-05-18).
The Kuzu backend was retired the same day after the benchmark showed SQLite was well inside budget for every realistic consumer scale. The abstraction (`GraphBackend` Protocol) is kept so a future graph-native store can plug in if a real workload exceeds SQLite's headroom.
When adding a future backend:
1. Implement the `GraphBackend` Protocol.
2. Register it in `src/core/graph_os/backends/__init__.py`.
3. Add parity tests against SQLite — same queries → same results (within confidence-tier tolerance).
4. Document the backend's strengths / weaknesses in [docs/engineering/graph_os-queries.md](../../../docs/engineering/graph_os-queries.md).
**Hard rule:** the backend choice is a deployment concern, never visible to the agent or tool layer. `cos_graph_references` returns identical envelope shape regardless of which backend answered.
## Reindex Dispatch Patterns
| Trigger | Behavior |
|---|---|
| PostToolUse hook (single file written) | `dispatch(path)` — extract that file only |
| Doc indexer (markdown changed) | Same — incremental |
| Bulk migration / `git checkout` | `cos graph-reindex --force` — re-extract everything because content hashes are now misleading |
The PostToolUse path is fire-and-forget: it can't block the agent. Failures log + alert but the user's edit completes regardless.
## Tool-Side (when authoring `cos_graph_*`)
`cos_graph_*` tools are MCP tools — also see [mcp-tool-authoring](../mcp-tool-authoring/SKILL.md). Graph-specific contracts on top of the universal MCP contract:
- **Every response carries `data.meta.layer="graph"`** so agents know which retrieval layer answered.
- **`data.meta.backend`** tells which backend answered (`sqlite` today).
- **`data.meta.backend_fallback=true`** when the primary backend was unavailable and an alternate answered (no alternate today — flag reserved for a future graph-native plug-in).
- **Empty result is valid** — fresh repo, unindexed file. Don't return `fail`; return `ok({results: []})`.
## Anti-patterns (reject in review)
- **Non-idempotent extractor** — running twice produces duplicate edges.
- **Confidence inflation** — labeling a weak heuristic 0.9 to make `cos_graph_impact` return it in the strong tier. Pollutes agent trust.
- **SQL / Cypher leaking into tool responses** — breaks backend abstraction.
- **Hardcoded backend in tool layer** — must go through `get_backend()`.
- **Reindex without content-hash check** — kills perf.
- **Edge to a uid that doesn't exist** — dangling reference. Validate target uid exists or queue resolution.
- **Mutable uid** — including line number, timestamp, hash in the uid breaks cross-rebuild stability.
- **Deleting nodes without committing or while holding a stale edge** — leaves dangling edges; `delete_nodes_for_file` deletes + commits under the write lock, and `cos_graph_doctor` sweeps any survivors.
- **Synchronous extraction in PostToolUse hook** — blocks the agent. Always fire-and-forget.
## Verification (after authoring)
```bash
# Extractor parity tests
uv run --extra graph_os pytest src/core/graph_os/tests/extractors/ -q
# Backend parity tests
uv run --extra graph_os pytest src/core/graph_os/tests/test_backends/ -q
# Tool layer tests
uv run --extra graph_os pytest src/core/graph_os/tests/test_tools.py -q
# Live graph doctor (after reindex)
cos graph-reindex
cos_graph_doctor # check for dangling edges, orphan nodes, hash mismatch
```
Pre-merge: `make verify` + `cos doctor`.
## Tooling
Scaffold an extractor stub (idempotent uid, content-hash short-circuit, typed Node/Edge):
`python3 scripts/new_extractor.py --lang python`
## See also
- [assets/graph-os-checklist.md](assets/graph-os-checklist.md) — the extractor + backend gate.
- [graph-explorer](../../../core/skills/graph-explorer/SKILL.md) — the query-side counterpart of this skill.
- [docs/engineering/graph_os-queries.md](../../../docs/engineering/graph_os-queries.md) — query routing + freshness contract.
- [docs/engineering/graph-hallucination-cures.md](../../../docs/engineering/graph-hallucination-cures.md) — the 16 `cos_graph_*` tools and what each cures.
- [mcp-tool-authoring](../mcp-tool-authoring/SKILL.md) — when authoring a `cos_graph_*` tool.
- [python-meta-server](../python-meta-server/SKILL.md) — Python conventions for this codebase.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!