Architecture

May 9, 2026 · View on GitHub

graphify is a Claude Code skill backed by a Python library. The skill orchestrates the library; the library can be used standalone.

Pipeline

detect()  →  extract()  →  build_graph()  →  cluster()  →  analyze()  →  report()  →  export()

Each stage is a single function in its own module. They communicate through plain Python dicts and NetworkX graphs - no shared state, no side effects outside graphify-out/.

Module responsibilities

ModuleFunctionInput → Output
detect.pycollect_files(root)directory → [Path] filtered list
extract.pyextract(path)file path → {nodes, edges} dict
build.pybuild_graph(extractions)list of extraction dicts → nx.Graph
cluster.pycluster(G)graph → graph with community attr on each node
analyze.pyanalyze(G)graph → analysis dict (god nodes, surprises, questions)
report.pyrender_report(G, analysis)graph + analysis → GRAPH_REPORT.md string
export.pyexport(G, out_dir, ...)graph → Obsidian vault, graph.json, graph.html, graph.svg
callflow_html.pywrite_callflow_html(...)graphify-out files → Mermaid architecture/call-flow HTML
ingest.pyingest(url, ...)URL → file saved to corpus dir
cache.pycheck_semantic_cache / save_semantic_cachefiles → (cached, uncached) split
security.pyvalidation helpersURL / path / label → validated or raises
validate.pyvalidate_extraction(data)extraction dict → raises on schema errors
serve.pystart_server(graph_path)graph file path → MCP stdio server
watch.pywatch(root, flag_path)directory → writes flag file on change
benchmark.pyrun_benchmark(graph_path)graph file → corpus vs subgraph token comparison

Extraction output schema

Every extractor returns:

{
  "nodes": [
    {"id": "unique_string", "label": "human name", "source_file": "path", "source_location": "L42"}
  ],
  "edges": [
    {"source": "id_a", "target": "id_b", "relation": "calls|imports|uses|...", "confidence": "EXTRACTED|INFERRED|AMBIGUOUS"}
  ]
}

validate.py enforces this schema before build_graph() consumes it.

Confidence labels

LabelMeaning
EXTRACTEDRelationship is explicitly stated in the source (e.g., an import statement, a direct call)
INFERREDRelationship is a reasonable deduction (e.g., call-graph second pass, co-occurrence in context)
AMBIGUOUSRelationship is uncertain; flagged for human review in GRAPH_REPORT.md

Adding a new language extractor

  1. Add a extract_<lang>(path: Path) -> dict function in extract.py following the existing pattern (tree-sitter parse → walk nodes → collect nodes and edges → call-graph second pass for INFERRED calls edges).
  2. Register the file suffix in extract() dispatch and collect_files().
  3. Add the suffix to CODE_EXTENSIONS in detect.py and _WATCHED_EXTENSIONS in watch.py.
  4. Add the tree-sitter package to pyproject.toml dependencies.
  5. Add a fixture file to tests/fixtures/ and tests to tests/test_languages.py.

Security

All external input passes through graphify/security.py before use:

  • URLs → validate_url() (http/https only) + _NoFileRedirectHandler (blocks file:// redirects)
  • Fetched content → safe_fetch() / safe_fetch_text() (size cap, timeout)
  • Graph file paths → validate_graph_path() (must resolve inside graphify-out/)
  • Node labels → sanitize_label() (strips control chars, caps 256 chars, HTML-escapes)

See SECURITY.md for the full threat model.

Testing

One test file per module under tests/. Run with:

pytest tests/ -q

All tests are pure unit tests - no network calls, no file system side effects outside tmp_path.