Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2025-10-28

### Added
- **Processing Modes System**: Introduced modular architecture for different diffgraph generation strategies
- Created `BaseProcessor` abstract class for implementing custom processing modes
- Added processor registry and factory pattern for mode instantiation
- Implemented `@register_processor` decorator for automatic mode registration
- **OpenAI Agents Dependency Graph Mode**: Refactored existing AI analysis into `openai-agents-dependency-graph` mode
- Maintains all existing functionality as the default processing mode
- Analyzes code at component level (classes, functions, methods)
- Generates dependency graphs showing component relationships
- **CLI Enhancements**:
- Added `--mode` / `-m` option to select processing mode
- Added `--list-modes` flag to display available processing modes
- Default mode: `openai-agents-dependency-graph` (backward compatible)
- **Documentation**:
- Added comprehensive developer guide: `docs/ADDING_PROCESSING_MODES.md`
- Updated README.md with processing modes information
- Documented how to create custom processing modes

### Changed
- Refactored `CodeAnalysisAgent` into modular `OpenAIAgentsProcessor`
- Removed direct dependency on `ai_analysis.py` in CLI (now uses processor factory)
- Improved extensibility for adding new analysis approaches (Tree-sitter, data flow, etc.)

### Removed
- `diffgraph/ai_analysis.py` - Functionality moved to `diffgraph/processing_modes/openai_agents_dependency.py`


## [1.0.0] - 2025-08-06

### Changed
Expand Down
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,53 @@ This will:
- `--api-key`: Specify your OpenAI API key (defaults to OPENAI_API_KEY environment variable)
- `--output` or `-o`: Specify the output HTML file path (default: diffgraph.html)
- `--no-open`: Don't automatically open the HTML report in browser
- `--mode` or `-m`: Select processing mode for diffgraph generation (default: openai-agents-dependency-graph)
- `--list-modes`: List all available processing modes
- `--version`: Show version information

Example:
Examples:
```bash
wild --output my-report.html --no-open
# Generate report with default mode
wild diff

# Generate report with custom output path
wild diff --output my-report.html --no-open

# List available processing modes
wild diff --list-modes

# Use a specific processing mode
wild diff --mode openai-agents-dependency-graph
```

## 🔧 Processing Modes

DiffGraph supports multiple processing modes for analyzing code changes. Each mode provides a different perspective on your code:

### Available Modes

#### `openai-agents-dependency-graph` (default)
Uses OpenAI Agents SDK to analyze code and generate component-level dependency graphs. This mode:
- Identifies classes, functions, and methods in your changes
- Analyzes dependencies between components
- Generates a visual dependency graph showing how components relate to each other
- Best for understanding architectural changes and component interactions

### Future Modes

The architecture is designed to support additional processing modes:
- **tree-sitter-dependency-graph**: AST-based analysis using Tree-sitter
- **data-flow-analysis**: Focus on data flow and transformations
- **user-context-analysis**: Analyze changes from a user interaction perspective
- **architecture-analysis**: System-level architectural insights

### Adding Custom Processing Modes

Developers can extend DiffGraph by creating custom processing modes. See the `diffgraph/processing_modes/` directory for examples. Each processor must:
1. Inherit from `BaseProcessor`
2. Implement the `analyze_changes()` method
3. Register itself using the `@register_processor` decorator

## 📊 Example Output

The generated HTML report includes:
Expand Down
2 changes: 1 addition & 1 deletion diffgraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
DiffGraph - A CLI tool for visualizing code changes with AI
"""

__version__ = "0.1.0"
__version__ = "1.1.0"
35 changes: 28 additions & 7 deletions diffgraph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
from click_spinner import spinner
from typing import List, Dict
import os
from diffgraph.ai_analysis import CodeAnalysisAgent
from diffgraph.html_report import generate_html_report, AnalysisResult
from diffgraph.env_loader import load_env_file, debug_environment
from diffgraph.utils import sanitize_diff_args, involves_working_tree
from diffgraph.processing_modes import get_processor, list_available_modes
from diffgraph.consent import ensure_consent

# Load environment variables
load_env_file()
Expand Down Expand Up @@ -136,9 +137,21 @@ def load_file_contents(changed_files: List[Dict[str, str]], diff_args: List[str]
@click.option('--output', '-o', default='diffgraph.html', help='Output HTML file path')
@click.option('--no-open', is_flag=True, help='Do not open the HTML report automatically')
@click.option('--debug-env', is_flag=True, help='Debug environment variable loading')
def main(args, api_key: str, output: str, no_open: bool, debug_env: bool):
@click.option('--mode', '-m', default='local-structural',
help='Processing mode for diffgraph generation (default: local-structural)')
@click.option('--list-modes', is_flag=True, help='List available processing modes and exit')
def main(args, api_key: str, output: str, no_open: bool, debug_env: bool, mode: str, list_modes: bool):
"""wild - Git wrapper CLI with DiffGraph for diff commands."""

# Handle --list-modes flag
if list_modes:
click.echo("Available processing modes:\n")
modes = list_available_modes()
for mode_name, description in modes.items():
click.echo(f" • {mode_name}")
click.echo(f" {description}\n")
return

# Check if this is a diff command
if args and args[0] == 'diff':
# Handle diff command with custom logic
Expand Down Expand Up @@ -167,9 +180,17 @@ def main(args, api_key: str, output: str, no_open: bool, debug_env: bool):
files_with_content = load_file_contents(files, diff_args)

try:
# Initialize the AI analysis agent
click.echo("🤖 Initializing AI analysis...")
agent = CodeAnalysisAgent(api_key=api_key)
# Initialize the processor based on selected mode
click.echo(f"🤖 Initializing {mode} processor...")
try:
processor = get_processor(mode, api_key=api_key)
except ValueError as e:
click.echo(f"❌ Error: {e}", err=True)
click.echo("\nUse --list-modes to see available processing modes.", err=True)
sys.exit(1)

# Consent check — exits cleanly if user declines cloud tier.
ensure_consent(processor.privacy_tier, processor.name)

# Define progress callback
def progress_callback(current_file, total_files, status):
Expand All @@ -178,7 +199,7 @@ def progress_callback(current_file, total_files, status):
return

file_name = os.path.basename(current_file)
current_index = len(agent.graph_manager.processed_files) + 1
current_index = len(processor.graph_manager.processed_files) + 1

if status == "processing":
click.echo(f"🔄 Processing {file_name} ({current_index}/{total_files})...")
Expand All @@ -193,7 +214,7 @@ def progress_callback(current_file, total_files, status):

# Analyze the changes with progress updates
click.echo("🧠 Starting code analysis...")
analysis = agent.analyze_changes(files_with_content, progress_callback)
analysis = processor.analyze_changes(files_with_content, progress_callback)

# Create analysis result
click.echo("📊 Creating analysis result...")
Expand Down
127 changes: 127 additions & 0 deletions diffgraph/consent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Consent management for cloud-tier processors.

When a user runs `wild diff` with a processor that sends data to a cloud API,
they must explicitly consent once. The decision is persisted to:

~/.config/wild/config.json

Local-tier processors (privacy_tier == "local") bypass this check entirely.

Usage
-----
from diffgraph.consent import ensure_consent

# Call before processor.analyze_changes().
# Exits cleanly if the user declines.
ensure_consent(processor.privacy_tier, processor.name)
"""

from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Optional

import click

# ---------------------------------------------------------------------------
# Config storage
# ---------------------------------------------------------------------------

CONFIG_PATH = Path.home() / ".config" / "wild" / "config.json"

_CONSENT_KEY_MAP = {
"cloud_llm": "cloud_llm_consent_given",
"cloud_backend": "cloud_backend_consent_given",
}


def _load_config() -> dict:
if CONFIG_PATH.exists():
try:
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}
return {}


def _save_config(updates: dict) -> None:
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
config = _load_config()
config.update(updates)
CONFIG_PATH.write_text(json.dumps(config, indent=2), encoding="utf-8")


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def has_consent(privacy_tier: str) -> bool:
"""
Return True if the user has already consented (or if no consent is needed).

Local-tier processors never require consent.
"""
if privacy_tier == "local":
return True
consent_key = _CONSENT_KEY_MAP.get(privacy_tier)
if consent_key is None:
# Unknown tier — conservatively require consent.
return False
return bool(_load_config().get(consent_key))


def request_consent(privacy_tier: str, processor_name: str) -> bool:
"""
Prompt the user for one-time consent.

Prints a privacy notice, asks for confirmation, persists the decision, and
returns True if the user consented. Returns False (without raising) if the
user declines.
"""
tier_label = {
"cloud_llm": "a third-party LLM API (e.g. OpenAI)",
"cloud_backend": "the WildestAI cloud backend",
}.get(privacy_tier, "an external cloud service")

click.echo(
f"\n⚠ Privacy notice — '{processor_name}' mode\n"
f"\n"
f" This processor sends your diff to {tier_label}.\n"
f" Your code will leave this machine.\n"
f"\n"
f" You only need to answer this once. Your choice is saved to:\n"
f" {CONFIG_PATH}\n"
)

if not click.confirm(" Continue?", default=False):
click.echo(
"\n Consent declined. No data was sent.\n"
"\n"
" To analyze locally (no data leaves your machine):\n"
"\n"
" wild diff\n"
"\n"
" (Local structural analysis is the default — no flag needed.)\n"
)
return False

consent_key = _CONSENT_KEY_MAP.get(privacy_tier)
if consent_key:
_save_config({consent_key: True})
click.echo(" ✓ Consent recorded. This prompt won't appear again.\n")
return True


def ensure_consent(privacy_tier: str, processor_name: str) -> None:
"""
Ensure the user has consented before running a cloud-tier processor.

Exits cleanly (sys.exit(0)) if the user declines. No-op for local-tier.
"""
if has_consent(privacy_tier):
return
if not request_consent(privacy_tier, processor_name):
sys.exit(0)
81 changes: 81 additions & 0 deletions diffgraph/processing_modes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
Processing modes module for different diffgraph generation strategies.

This module provides a registry of available processing modes and factory
functions to create processor instances.
"""

from typing import Dict, Type, Optional
from .base import BaseProcessor

# Registry of available processing modes
_PROCESSOR_REGISTRY: Dict[str, Type[BaseProcessor]] = {}


def register_processor(mode_name: str):
"""
Decorator to register a processor class.

Args:
mode_name: The name identifier for this processing mode

Example:
@register_processor("openai-agents-dependency-graph")
class OpenAIAgentsProcessor(BaseProcessor):
...
"""
def decorator(cls: Type[BaseProcessor]):
_PROCESSOR_REGISTRY[mode_name] = cls
return cls
return decorator


def get_processor(mode_name: str, **kwargs) -> BaseProcessor:
"""
Factory function to create a processor instance.

Args:
mode_name: The name of the processing mode
**kwargs: Configuration parameters for the processor

Returns:
An instance of the requested processor

Raises:
ValueError: If the mode_name is not registered
"""
if mode_name not in _PROCESSOR_REGISTRY:
available_modes = ", ".join(_PROCESSOR_REGISTRY.keys())
raise ValueError(
f"Unknown processing mode: '{mode_name}'. "
f"Available modes: {available_modes}"
)

processor_class = _PROCESSOR_REGISTRY[mode_name]
return processor_class(**kwargs)


def list_available_modes() -> Dict[str, str]:
"""
Return a dict of {mode_name: description} for all registered processors.

Uses the class-level ``description`` attribute so no instantiation is
required (avoids unsafe ``__new__`` and missing-arg errors).
"""
return {
name: cls.description
for name, cls in _PROCESSOR_REGISTRY.items()
}


# Import processors to trigger registration
from . import openai_agents_dependency # noqa: F401, E402
from . import local_structural # noqa: F401, E402


__all__ = [
"BaseProcessor",
"register_processor",
"get_processor",
"list_available_modes",
]
Loading