Skip to content

Architecture

Arun Soman edited this page Aug 31, 2026 · 1 revision

Architecture

High-level architecture, written from the codebase at tag v0.1.4 (2026-08-31). Every file reference below was read from the tree, not from memory. If code and wiki disagree, the code wins.

The one-paragraph version

cie turns a codebase into a queryable graph: symbols, files, call edges (receiver-resolved), test edges, tasks/QA records and PRD hierarchy live in ONE store — so questions grep cannot answer ("what breaks, and which tests catch it?") become single tool calls. Three front-ends expose the same 135-tool surface over one envelope; two storage backends serve it; one policy layer enforces what each caller may do.

Layers (bottom to top)

                    ┌───────────────────────────────────────────┐
  front-ends        │ cie-mcp (stdio/HTTP MCP) │ cie CLI │ HTTP  │
                    │        routes.py (tool-mount)             │
                    ├───────────────────────────────────────────┤
  policy            │ cie/tool_policy.py — server-side,         │
                    │ WRITE_TOOLS never registered to callers  │
                    │ that didn't opt in                       │
                    ├───────────────────────────────────────────┤
  service           │ cie/tools/__init__.py — ToolService,     │
                    │ ~135 methods, one envelope for all        │
                    ├───────────────────────────────────────────┤
  engine            │ cie/query.py — QueryEngine over either:   │
                    │  • Neo4jRepository  (team/multi-project)  │
                    │  • EmbeddedRepository (SQLite, zero-     │
                    │    config)  + tasks.db + hierarchy.db     │
                    ├───────────────────────────────────────────┤
  ingestion         │ cie/extract.py (tree-sitter, 9 languages)│
                    │ cie/callgraph.py (receiver-resolved calls)│
                    │ cie/testlink.py (TESTS edges, 4 heuristics)│
                    └───────────────────────────────────────────┘

1. Ingestion — three passes (extract.py, callgraph.py, testlink.py)

  • Pass 1, structural (cie/extract.py::extract_many): tree-sitter per language (_LANG_LOADERS, extract.py:72 — Python, JS/JSX, TS/TSX, Java, Go, Rust, C/C++ (+headers), C#). Emits FILE/FUNC/ METHOD/CLASS nodes with signatures, docstrings, decorators, imports — plus contains/defines edges.
  • Pass 2, call resolution (cie/callgraph.py::resolve_call_edges): call sites resolved through an import map to receiver-resolved edges — a.B().m() links to B.m, not to whatever m grep finds. Confidence-tagged (EXTRACTED/INFERRED); unresolved bases synthesize external:: stub nodes so inheritance queries degrade honestly.
  • Pass 3, tests (cie/testlink.py::resolve_test_edges): TESTS edges from test symbols to what they cover, four heuristics — naming convention, naming+call upgrade, @patch dotted paths, and direct calls (added 2026-08-31 after dogfooding measured 1 TESTS edge in cie's own 308-test suite; on cie itself: 1 → 562). Test-glob files and conftest.py are never targets; still honestly not exhaustive (a test that only reaches its target via a helper gets no edge — the test_map hint says so).

2. Storage — two backends, one selection rule

  • Embedded (zero-config): EmbeddedRepository + task/hierarchy SQLite files under <root>/.cie/ — built by cie.factory.build_tool_service_embedded (factory.py:168). What cie index writes and the demo serves.
  • Neo4j (team/multi-project): Neo4jRepository + shared driver, per---project namespaces inside one graph (node project property + query filters, neo4j_repository.py:451) — built by build_tool_service / build_tool_service_from_config.
  • One selection rule, every front-end (cie.config.resolve_backend): explicit --backendCIE_BACKEND env › the --embedded alias › auto (serve <root>/.cie/graph.db when it exists, else Neo4j). The MCP server states the resolved choice on stderr at startup — never silent; stdout stays byte-clean (it is the JSON-RPC channel). Selection is orthogonal to configuration: NEO4J_* / legacy CIE_NEO4J_* / --neo4j-* configure Neo4j however it was selected.

3. The service — one tool surface, one envelope

ToolService (cie/tools/init.py) is the only thing front-ends talk to: ~135 methods — search, skeleton, callers/callees, impact (affected_by), tests (test_map), traceability, freshness, tasks, coverage, plus write tools (files/tasks/results). Every method returns the same envelope (ok/tool/results/truncated/total/hint/ elapsed_ms), so an agent gets machine-checkable answers and honest empty-hints instead of silence.

4. Policy — the write boundary

cie/tool_policy.py (AgentType :40, permits :110, WRITE_TOOLS :18): policies are enforced server-side — tools a caller may not use are never registered, not "registered then denied". --policy readonly (what cie init registers for clients) shows the 85 read tools; full opts into writes.

5. Front-ends — three doors, one envelope

  • cie-mcp (stdio MCP for Claude Code/Cursor/Codex; also streamable-http/sse for browser clients). Startup resolves backend
    • policy; the same ToolService behind both transports.
  • cie CLI (click): index/load/reindex/watch/export-html + query commands through the same backend-selection rule (_selected_backend, cli.py:101).
  • HTTP tool-mount (cie/routes.py): POST /tools/{tool} per request — Neo4j mode today, per-project service cache (routes.py:127).

6. Optional dependencies — the honest envelope

No provider key or network is ever required: optional deps (mcp, neo4j, embeddings, watchdog) are checked at call time and return unavailable[OPTIONAL_BACKEND_MISSING:...] envelopes, not tracebacks (pinned by tests/test_optional_dependency_envelope.py). The embeddings fallback is env-gated on explicit CIE_EMBED_DSN

  • key — a bare provider key never turns the network on.

How to read the code next

  • Start: cie/cli.py (the commands) → cie/factory.py (the two construction paths) → cie/tools/__init__.py (the surface).
  • The semantic layer (cie/graphrag.py, cie/embed.py) layers lexical+dense+graph search over the same store — see How-to: enable semantic search.
  • In-repo evidence (benchmarks, the demo's uncut casts, production log): docs/demo and docs/benchmarks.md.

From the README (migrated, verified at v0.1.4)

Three front-ends, one envelope

  • MCP (cie.mcp_server / cie-mcp): real Model Context Protocol over stdio (or sse / streamable-http), built with the official mcp SDK. Each tool's JSON Schema comes from SDK introspection of the bound method — one source of truth. Denied-by-policy tools are never registered, not merely refused. Policies: forge/orchestrator (read+write), miner/inspector (read-only).
  • HTTP (cie.routes.py): router mounted into the host FastAPI app (not a separate process). POST /tools/{tool} (kwargs in body), GET /tools, GET /health, GET /schema-version, plus dedicated POST /tasks, GET /tasks/{name}, GET /tasks/pending, POST /hierarchy, POST /telemetry/otlp, etc. Read-only by default, enforced server-side through the same ToolPolicy the MCP path uses: write tools and mutating legacy REST routes (POST /tasks, POST /code/reload, POST /sync/event, POST /telemetry/otlp, …) are 403 (forbidden envelope kind) unless CIE_HTTP_POLICY=orchestrator (or CIE_HTTP_ALLOW_WRITE=1; also miner for read-only-by-name). Mutating requests carrying a cross-origin Origin are rejected even when writes are allowed (the CSRF-to-localhost vector), unless the origin is listed in CIE_HTTP_ALLOWED_ORIGINS. GET /tools discovery is filtered to match — a read-only caller can't even see a tool it can't call.
  • CLI (cie.cli, 47 commands, cie index included): human Rich tables by default; every command honors --json (group-level, before the subcommand) emitting the same SPEC §0 envelope as the HTTP surface, so an agent can drive cie entirely over JSON. Commands mirror the tools above (search, node, neighbors, community, communities, god, stats, search-symbol, view-file, callers, callees, skeleton, failing-context, affected-by, blame, run, reindex, watch, tasks:*, hierarchy:*, coverage:*, validate:*, schema-version, schema:dump, …).

Clone this wiki locally