Skip to content

adapters

code_context_agent.tools.graph.adapters

Input adapters for converting tool outputs to graph elements.

This module provides functions to ingest outputs from: - LSP tools (symbols, references, definitions) - AST-grep tools (pattern matches, rule pack results) - ripgrep tools (text matches) - Test file mappings

ingest_lsp_symbols

ingest_lsp_symbols(symbols_result, file_path)

Convert lsp_document_symbols output to CodeNodes and containment edges.

Parameters:

Name Type Description Default
symbols_result dict[str, Any]

JSON result from lsp_document_symbols tool

required
file_path str

Path to the source file

required

Returns:

Type Description
tuple[list[CodeNode], list[CodeEdge]]

Tuple of (nodes, edges) where edges represent containment relationships

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_lsp_symbols(
    symbols_result: dict[str, Any],
    file_path: str,
) -> tuple[list[CodeNode], list[CodeEdge]]:
    """Convert lsp_document_symbols output to CodeNodes and containment edges.

    Args:
        symbols_result: JSON result from lsp_document_symbols tool
        file_path: Path to the source file

    Returns:
        Tuple of (nodes, edges) where edges represent containment relationships
    """
    nodes: list[CodeNode] = []
    edges: list[CodeEdge] = []

    if symbols_result.get("status") != "success":
        return nodes, edges

    def process_symbol(
        symbol: dict[str, Any],
        parent_id: str | None = None,
    ) -> None:
        """Recursively process a symbol and its children."""
        name = symbol.get("name", "")
        kind = symbol.get("kind", 13)  # Default to Variable
        range_data = symbol.get("range", {})

        line_start = range_data.get("start", {}).get("line", 0)
        line_end = range_data.get("end", {}).get("line", line_start)

        node_id = _make_symbol_id(file_path, name, line_start)
        node_type = lsp_kind_to_node_type(kind)

        node = CodeNode(
            id=node_id,
            name=name,
            node_type=node_type,
            file_path=file_path,
            line_start=line_start,
            line_end=line_end,
            metadata={"lsp_kind": kind},
        )
        nodes.append(node)

        # Create containment edge from parent
        if parent_id is not None:
            edges.append(
                CodeEdge(
                    source=parent_id,
                    target=node_id,
                    edge_type=EdgeType.CONTAINS,
                ),
            )

        # Process children recursively
        for child in symbol.get("children", []):
            process_symbol(child, parent_id=node_id)

    # Process top-level symbols
    for symbol in symbols_result.get("symbols", []):
        process_symbol(symbol)

    return nodes, edges

ingest_lsp_references

ingest_lsp_references(references_result, source_node_id)

Convert lsp_references output to reference edges.

Creates edges from each reference location back to the source symbol. The source is the referrer, target is the referenced symbol.

Parameters:

Name Type Description Default
references_result dict[str, Any]

JSON result from lsp_references tool

required
source_node_id str

ID of the symbol being referenced

required

Returns:

Type Description
list[CodeEdge]

List of CodeEdge objects representing references

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_lsp_references(
    references_result: dict[str, Any],
    source_node_id: str,
) -> list[CodeEdge]:
    """Convert lsp_references output to reference edges.

    Creates edges from each reference location back to the source symbol.
    The source is the referrer, target is the referenced symbol.

    Args:
        references_result: JSON result from lsp_references tool
        source_node_id: ID of the symbol being referenced

    Returns:
        List of CodeEdge objects representing references
    """
    edges: list[CodeEdge] = []

    if references_result.get("status") != "success":
        return edges

    for ref in references_result.get("references", []):
        uri = ref.get("uri", "")
        range_data = ref.get("range", {})
        line = range_data.get("start", {}).get("line", 0)

        file_path = _uri_to_path(uri)
        referrer_id = _make_location_id(file_path, line)

        edges.append(
            CodeEdge(
                source=referrer_id,
                target=source_node_id,
                edge_type=EdgeType.REFERENCES,
                metadata={
                    "file": file_path,
                    "line": line,
                },
            ),
        )

    return edges

ingest_lsp_definition

ingest_lsp_definition(
    definition_result, from_file, from_line
)

Convert lsp_definition output to import/call edges.

Creates edges from the usage location to the definition location.

Parameters:

Name Type Description Default
definition_result dict[str, Any]

JSON result from lsp_definition tool

required
from_file str

File where the symbol is used

required
from_line int

Line where the symbol is used

required

Returns:

Type Description
list[CodeEdge]

List of CodeEdge objects representing the definition relationship

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_lsp_definition(
    definition_result: dict[str, Any],
    from_file: str,
    from_line: int,
) -> list[CodeEdge]:
    """Convert lsp_definition output to import/call edges.

    Creates edges from the usage location to the definition location.

    Args:
        definition_result: JSON result from lsp_definition tool
        from_file: File where the symbol is used
        from_line: Line where the symbol is used

    Returns:
        List of CodeEdge objects representing the definition relationship
    """
    edges: list[CodeEdge] = []

    if definition_result.get("status") != "success":
        return edges

    source_id = _make_location_id(from_file, from_line)

    for defn in definition_result.get("definitions", []):
        uri = defn.get("uri", "")
        range_data = defn.get("range", {})
        line = range_data.get("start", {}).get("line", 0)

        target_file = _uri_to_path(uri)
        target_id = _make_location_id(target_file, line)

        # Determine edge type: IMPORTS if different file, CALLS if same file
        edge_type = EdgeType.IMPORTS if target_file != from_file else EdgeType.CALLS

        edges.append(
            CodeEdge(
                source=source_id,
                target=target_id,
                edge_type=edge_type,
                metadata={
                    "from_file": from_file,
                    "to_file": target_file,
                    "to_line": line,
                },
            ),
        )

    return edges

ingest_astgrep_matches

ingest_astgrep_matches(matches_result)

Convert astgrep_scan output to CodeNodes.

Each match becomes a PATTERN_MATCH node with the pattern in metadata.

Parameters:

Name Type Description Default
matches_result dict[str, Any]

JSON result from astgrep_scan tool

required

Returns:

Type Description
list[CodeNode]

List of CodeNode objects representing pattern matches

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_astgrep_matches(
    matches_result: dict[str, Any],
) -> list[CodeNode]:
    """Convert astgrep_scan output to CodeNodes.

    Each match becomes a PATTERN_MATCH node with the pattern in metadata.

    Args:
        matches_result: JSON result from astgrep_scan tool

    Returns:
        List of CodeNode objects representing pattern matches
    """
    nodes: list[CodeNode] = []

    if matches_result.get("status") not in ("success", "no_matches"):
        return nodes

    pattern = matches_result.get("pattern", "")

    for match in matches_result.get("matches", []):
        file_path = match.get("file", "")
        range_data = match.get("range", {})
        line_start = range_data.get("start", {}).get("line", 0)
        line_end = range_data.get("end", {}).get("line", line_start)
        text = match.get("text", "")

        node_id = _make_location_id(file_path, line_start)

        nodes.append(
            CodeNode(
                id=node_id,
                name=text[:50] if len(text) > 50 else text,  # Truncate long matches
                node_type=NodeType.PATTERN_MATCH,
                file_path=file_path,
                line_start=line_start,
                line_end=line_end,
                metadata={
                    "pattern": pattern,
                    "full_text": text,
                },
            ),
        )

    return nodes

ingest_astgrep_rule_pack

ingest_astgrep_rule_pack(rule_pack_result)

Convert astgrep_scan_rule_pack output to CodeNodes.

Each match becomes a PATTERN_MATCH node with rule_id and category in metadata.

Parameters:

Name Type Description Default
rule_pack_result dict[str, Any]

JSON result from astgrep_scan_rule_pack tool

required

Returns:

Type Description
list[CodeNode]

List of CodeNode objects representing categorized matches

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_astgrep_rule_pack(
    rule_pack_result: dict[str, Any],
) -> list[CodeNode]:
    """Convert astgrep_scan_rule_pack output to CodeNodes.

    Each match becomes a PATTERN_MATCH node with rule_id and category in metadata.

    Args:
        rule_pack_result: JSON result from astgrep_scan_rule_pack tool

    Returns:
        List of CodeNode objects representing categorized matches
    """
    nodes: list[CodeNode] = []

    if rule_pack_result.get("status") not in ("success", "no_matches"):
        return nodes

    matches_by_rule = rule_pack_result.get("matches_by_rule", {})

    for rule_id, matches in matches_by_rule.items():
        for match in matches:
            file_path = match.get("file", "")
            range_data = match.get("range", {})
            line_start = range_data.get("start", {}).get("line", 0)
            line_end = range_data.get("end", {}).get("line", line_start)
            text = match.get("text", "")
            message = match.get("message", "")

            node_id = f"{file_path}:L{line_start}:{rule_id}"

            # Extract category from rule_id (e.g., "ts-db-write" -> "db")
            category = _extract_category_from_rule_id(rule_id)

            nodes.append(
                CodeNode(
                    id=node_id,
                    name=text[:50] if len(text) > 50 else text,
                    node_type=NodeType.PATTERN_MATCH,
                    file_path=file_path,
                    line_start=line_start,
                    line_end=line_end,
                    metadata={
                        "rule_id": rule_id,
                        "category": category,
                        "message": message,
                        "full_text": text,
                    },
                ),
            )

    return nodes

ingest_rg_matches

ingest_rg_matches(rg_result)

Convert rg_search output to preliminary CodeNodes.

Creates nodes for text matches that can be refined by LSP later.

Parameters:

Name Type Description Default
rg_result dict[str, Any]

JSON result from rg_search tool

required

Returns:

Type Description
list[CodeNode]

List of CodeNode objects representing text matches

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_rg_matches(
    rg_result: dict[str, Any],
) -> list[CodeNode]:
    """Convert rg_search output to preliminary CodeNodes.

    Creates nodes for text matches that can be refined by LSP later.

    Args:
        rg_result: JSON result from rg_search tool

    Returns:
        List of CodeNode objects representing text matches
    """
    nodes: list[CodeNode] = []

    if rg_result.get("status") != "success":
        return nodes

    pattern = rg_result.get("pattern", "")

    for match in rg_result.get("matches", []):
        file_path = match.get("path", "")
        line = match.get("line_number", 1)
        text = match.get("lines", "").strip()

        node_id = _make_location_id(file_path, line)

        nodes.append(
            CodeNode(
                id=node_id,
                name=text[:50] if len(text) > 50 else text,
                node_type=NodeType.PATTERN_MATCH,
                file_path=file_path,
                line_start=line,
                line_end=line,
                metadata={
                    "pattern": pattern,
                    "full_text": text,
                    "source": "rg",
                },
            ),
        )

    return nodes

ingest_inheritance

ingest_inheritance(hover_content, class_node_id, file_path)

Extract inheritance relationships from LSP hover info.

Parses type signatures to find extends/implements relationships.

Parameters:

Name Type Description Default
hover_content str

The hover content string (may be markdown)

required
class_node_id str

ID of the class node

required
file_path str

Path to the file containing the class

required

Returns:

Type Description
list[CodeEdge]

List of CodeEdge objects representing inheritance

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_inheritance(
    hover_content: str,
    class_node_id: str,
    file_path: str,
) -> list[CodeEdge]:
    """Extract inheritance relationships from LSP hover info.

    Parses type signatures to find extends/implements relationships.

    Args:
        hover_content: The hover content string (may be markdown)
        class_node_id: ID of the class node
        file_path: Path to the file containing the class

    Returns:
        List of CodeEdge objects representing inheritance
    """
    edges: list[CodeEdge] = []

    # TypeScript patterns
    # class Foo extends Bar implements IBaz, IQux
    ts_extends = re.search(r"class\s+\w+\s+extends\s+(\w+)", hover_content)
    ts_implements = re.findall(r"implements\s+([\w,\s]+)", hover_content)

    if ts_extends:
        base_class = ts_extends.group(1)
        edges.append(
            CodeEdge(
                source=class_node_id,
                target=f"{file_path}:{base_class}",
                edge_type=EdgeType.INHERITS,
                metadata={"language": "typescript"},
            ),
        )

    for impl_match in ts_implements:
        interfaces = [i.strip() for i in impl_match.split(",")]
        for iface in interfaces:
            if iface:
                edges.append(
                    CodeEdge(
                        source=class_node_id,
                        target=f"{file_path}:{iface}",
                        edge_type=EdgeType.IMPLEMENTS,
                        metadata={"language": "typescript"},
                    ),
                )

    # Python patterns
    # class Foo(Bar, Baz):
    py_bases = re.search(r"class\s+\w+\s*\(([^)]+)\)", hover_content)
    if py_bases:
        bases = [b.strip() for b in py_bases.group(1).split(",")]
        for base in bases:
            # Skip common non-inheritance bases
            if base and base not in ("object", "ABC", "Protocol"):
                edges.append(
                    CodeEdge(
                        source=class_node_id,
                        target=f"{file_path}:{base}",
                        edge_type=EdgeType.INHERITS,
                        metadata={"language": "python"},
                    ),
                )

    return edges

ingest_test_mapping

ingest_test_mapping(test_files, production_files)

Create test coverage edges based on file naming conventions.

Maps test files to production files using common naming patterns: - test_foo.py -> foo.py - foo_test.py -> foo.py - foo.test.ts -> foo.ts - tests/foo.js -> foo.js

Parameters:

Name Type Description Default
test_files list[str]

List of test file paths

required
production_files list[str]

List of production file paths

required

Returns:

Type Description
list[CodeEdge]

List of CodeEdge objects representing test-to-production relationships

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_test_mapping(
    test_files: list[str],
    production_files: list[str],
) -> list[CodeEdge]:
    """Create test coverage edges based on file naming conventions.

    Maps test files to production files using common naming patterns:
    - test_foo.py -> foo.py
    - foo_test.py -> foo.py
    - foo.test.ts -> foo.ts
    - __tests__/foo.js -> foo.js

    Args:
        test_files: List of test file paths
        production_files: List of production file paths

    Returns:
        List of CodeEdge objects representing test-to-production relationships
    """
    edges: list[CodeEdge] = []

    # Build a lookup map for production files by name
    prod_by_name: dict[str, str] = {}
    for prod_file in production_files:
        name = Path(prod_file).stem
        prod_by_name[name.lower()] = prod_file

    for test_file in test_files:
        prod_file = _match_test_to_prod(test_file, prod_by_name)
        if prod_file:
            edges.append(
                CodeEdge(
                    source=test_file,
                    target=prod_file,
                    edge_type=EdgeType.TESTS,
                    metadata={"convention": "name_match"},
                ),
            )

    return edges

ingest_git_cochanges

ingest_git_cochanges(cochanges_result, min_percentage=20.0)

Convert git_files_changed_together output to co-change edges.

Creates bidirectional edges between files that frequently change together, with weight based on co-change frequency.

Parameters:

Name Type Description Default
cochanges_result dict[str, Any]

JSON result from git_files_changed_together tool

required
min_percentage float

Minimum co-change percentage to create an edge (default 20%)

20.0

Returns:

Type Description
list[CodeEdge]

List of CodeEdge objects representing co-change relationships

Example

result = json.loads(git_files_changed_together("/repo", "src/auth.py")) edges = ingest_git_cochanges(result, min_percentage=15.0)

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_git_cochanges(
    cochanges_result: dict[str, Any],
    min_percentage: float = 20.0,
) -> list[CodeEdge]:
    """Convert git_files_changed_together output to co-change edges.

    Creates bidirectional edges between files that frequently change together,
    with weight based on co-change frequency.

    Args:
        cochanges_result: JSON result from git_files_changed_together tool
        min_percentage: Minimum co-change percentage to create an edge (default 20%)

    Returns:
        List of CodeEdge objects representing co-change relationships

    Example:
        >>> result = json.loads(git_files_changed_together("/repo", "src/auth.py"))
        >>> edges = ingest_git_cochanges(result, min_percentage=15.0)
    """
    edges: list[CodeEdge] = []

    if cochanges_result.get("status") != "success":
        return edges

    source_file = cochanges_result.get("file_path", "")
    if not source_file:
        return edges

    for cochange in cochanges_result.get("cochanged_files", []):
        percentage = cochange.get("percentage", 0)
        if percentage < min_percentage:
            continue

        target_file = cochange.get("path", "")
        if not target_file:
            continue

        # Weight is normalized co-change frequency (0-1)
        weight = percentage / 100.0

        edges.append(
            CodeEdge(
                source=source_file,
                target=target_file,
                edge_type=EdgeType.COCHANGES,
                weight=weight,
                metadata={
                    "count": cochange.get("count", 0),
                    "percentage": percentage,
                    "source": "git_cochanges",
                },
            ),
        )

    return edges

ingest_git_hotspots

ingest_git_hotspots(hotspots_result)

Convert git_hotspots output to file nodes with churn metadata.

Creates FILE nodes for each hotspot with commit frequency in metadata. This helps identify files that may need attention.

Parameters:

Name Type Description Default
hotspots_result dict[str, Any]

JSON result from git_hotspots tool

required

Returns:

Type Description
list[CodeNode]

List of CodeNode objects representing hotspot files

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_git_hotspots(
    hotspots_result: dict[str, Any],
) -> list[CodeNode]:
    """Convert git_hotspots output to file nodes with churn metadata.

    Creates FILE nodes for each hotspot with commit frequency in metadata.
    This helps identify files that may need attention.

    Args:
        hotspots_result: JSON result from git_hotspots tool

    Returns:
        List of CodeNode objects representing hotspot files
    """
    nodes: list[CodeNode] = []

    if hotspots_result.get("status") != "success":
        return nodes

    for hotspot in hotspots_result.get("hotspots", []):
        file_path = hotspot.get("path", "")
        if not file_path:
            continue

        nodes.append(
            CodeNode(
                id=file_path,
                name=Path(file_path).name,
                node_type=NodeType.FILE,
                file_path=file_path,
                line_start=0,
                line_end=0,
                metadata={
                    "commits": hotspot.get("commits", 0),
                    "churn_percentage": hotspot.get("percentage", 0),
                    "source": "git_hotspots",
                },
            ),
        )

    return nodes

ingest_git_contributors

ingest_git_contributors(
    contributors_result, _file_path=None
)

Extract contributor metadata from git_contributors or git_blame_summary.

Returns metadata that can be attached to file or repo nodes.

Parameters:

Name Type Description Default
contributors_result dict[str, Any]

JSON result from git_contributors or git_blame_summary

required
_file_path str | None

Reserved for future use (file context for blame results)

None

Returns:

Type Description
dict[str, Any]

Dictionary of contributor metadata suitable for node metadata

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_git_contributors(
    contributors_result: dict[str, Any],
    _file_path: str | None = None,
) -> dict[str, Any]:
    """Extract contributor metadata from git_contributors or git_blame_summary.

    Returns metadata that can be attached to file or repo nodes.

    Args:
        contributors_result: JSON result from git_contributors or git_blame_summary
        _file_path: Reserved for future use (file context for blame results)

    Returns:
        Dictionary of contributor metadata suitable for node metadata
    """
    if contributors_result.get("status") != "success":
        return {}

    # Handle both contributors and blame_summary formats
    authors = contributors_result.get("contributors") or contributors_result.get("authors", [])

    if not authors:
        return {}

    # Get primary contributor
    primary = authors[0] if authors else {}

    return {
        "primary_author": primary.get("email", ""),
        "author_count": len(authors),
        "authors": [a.get("email", "") for a in authors[:5]],  # Top 5
        "source": "git_contributors" if "contributors" in contributors_result else "git_blame",
    }

ingest_clone_results

ingest_clone_results(clone_result)

Convert clone detection results into SIMILAR_TO edges.

Creates edges between files that share duplicate code blocks.

Parameters:

Name Type Description Default
clone_result dict[str, Any]

JSON result from detect_clones tool

required

Returns:

Type Description
list[CodeEdge]

List of CodeEdge objects with SIMILAR_TO type

Source code in src/code_context_agent/tools/graph/adapters.py
def ingest_clone_results(
    clone_result: dict[str, Any],
) -> list[CodeEdge]:
    """Convert clone detection results into SIMILAR_TO edges.

    Creates edges between files that share duplicate code blocks.

    Args:
        clone_result: JSON result from detect_clones tool

    Returns:
        List of CodeEdge objects with SIMILAR_TO type
    """
    if clone_result.get("status") != "success":
        return []

    edges: list[CodeEdge] = []
    clones = clone_result.get("clones", [])

    for clone in clones:
        first_file = clone.get("first_file", "")
        second_file = clone.get("second_file", "")

        if not first_file or not second_file:
            continue

        edges.append(
            CodeEdge(
                source=first_file,
                target=second_file,
                edge_type=EdgeType.SIMILAR_TO,
                metadata={
                    "first_start": clone.get("first_start", 0),
                    "first_end": clone.get("first_end", 0),
                    "second_start": clone.get("second_start", 0),
                    "second_end": clone.get("second_end", 0),
                    "duplicated_lines": clone.get("lines", 0),
                    "tokens": clone.get("tokens", 0),
                    "fragment": clone.get("fragment", ""),
                },
            ),
        )

    return edges