# Story 5.10: Plugin Dependency Declaration

## Status: Cancelled — No Use Case

## Priority: ~~Low~~ N/A

## Estimated Effort: ~~Medium (2-3 days)~~ N/A

## Dependencies: N/A

## Epic: ~~Plugin System Advancement~~ N/A

---

## Cancellation Rationale

**Decision Date:** January 2026

This story has been cancelled because it solves a hypothetical problem that doesn't exist in the current architecture.

### 1. No Existing Inter-Plugin Dependencies

A search of the entire plugin system found **zero plugins that reference other plugins**. All plugins are designed to be independent:

| Plugin Type | Input      | Output            | Cross-Plugin Refs |
| ----------- | ---------- | ----------------- | ----------------- |
| Enricher    | event dict | additional fields | None              |
| Redactor    | event dict | masked event      | None              |
| Filter      | event dict | event or None     | None              |
| Processor   | byte view  | transformed bytes | None              |
| Sink        | event dict | (writes)          | None              |

### 2. Architecture Explicitly Avoids This

The pipeline design is:

```
Event → Enrichers (parallel) → Redactors (sequential) → Filters → Sinks
```

Each stage is independent. There's no mechanism for cross-plugin dependencies because **the design doesn't need it**.

### 3. Examples Are Hypothetical

The story's motivating examples don't exist:

- "An encryption processor depends on a key management enricher" — not implemented
- "A compliance redactor depends on a configuration enricher" — not implemented
- "An aggregating sink depends on a batching processor" — not implemented

### 4. Significant Complexity for Zero Benefit

The proposal would add:

- `DependencyGraph` class with topological sort
- Circular dependency detection
- Conflict detection
- Dependency injection mechanism
- Loader changes

**All for a problem that doesn't currently exist.**

### 5. Simpler Alternatives Exist

If a plugin ever needs shared state:

1. **Use ContextVars** — Already demonstrated by `ContextVarsEnricher`
2. **Shared configuration** — Pass the same config to multiple plugins
3. **Application-level coordination** — Let the app wire dependencies, not the logging library

---

## If This Were Ever Needed

Rather than complex dependency graphs, the recommended pattern would be:

```python
# Use ContextVars for cross-plugin state sharing
from contextvars import ContextVar

# Shared state
encryption_key_var: ContextVar[str] = ContextVar("encryption_key")

# Enricher sets it
class KeyProviderEnricher:
    async def enrich(self, event: dict) -> dict:
        encryption_key_var.set(self._get_key())
        return {}

# Sink reads it
class EncryptedSink:
    async def write(self, entry: dict) -> None:
        key = encryption_key_var.get(None)
        # ... use key ...
```

This is simpler, doesn't require framework changes, and follows Python conventions.

---

## Original Context (Archived)

> Complex plugin ecosystems may have inter-plugin dependencies:
>
> - An encryption processor depends on a key management enricher
> - A compliance redactor depends on a configuration enricher
> - An aggregating sink depends on a batching processor

---

## Original Problem Statement (Archived)

> 1. No way for plugins to declare they depend on other plugins
> 2. Load order is configuration-order only, not dependency-aware
> 3. Missing plugin dependencies discovered at runtime
> 4. No validation of plugin dependency compatibility

---

## Original Acceptance Criteria (Archived)

> **Note:** These criteria are preserved for historical reference. This story has been cancelled.

### ~~AC1: Dependency Declaration~~

- ~~`PLUGIN_METADATA["depends_on"]` - list of required plugin names~~
- ~~`PLUGIN_METADATA["optional_depends_on"]` - list of optional plugins~~
- ~~Dependencies can specify plugin type~~

### ~~AC2: Dependency Resolution~~

- ~~Plugins loaded in dependency order~~
- ~~Missing required dependencies prevent plugin loading~~
- ~~Circular dependencies detected and rejected~~

### ~~AC3: Runtime Access~~

- ~~Plugin can access its dependencies via injected references~~
- ~~Dependency injection happens during plugin instantiation~~

### ~~AC4: Validation~~

- ~~Dependency graph validated at startup~~
- ~~Version constraints supported~~
- ~~Conflict detection for mutually exclusive plugins~~

---

## Original Technical Design (Archived)

> **Note:** This design is preserved for historical reference only.

### 1. Enhanced Plugin Metadata

```python
# Updated PLUGIN_METADATA example

PLUGIN_METADATA = {
    "name": "encrypted_sink",
    "version": "1.0.0",
    "plugin_type": "sink",
    "entry_point": "my_package:EncryptedSink",

    # NEW: Plugin dependencies
    "depends_on": [
        "enricher:key_provider",  # Requires key_provider enricher
    ],
    "optional_depends_on": [
        "processor:compression",  # Optional compression processor
    ],

    # NEW: Conflicts with these plugins
    "conflicts_with": [
        "sink:unencrypted_sink",  # Cannot run together
    ],

    # ... rest of metadata ...
}
```

### 2. Dependency Resolution

```python
# src/fapilog/plugins/dependencies.py
"""Plugin dependency resolution."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass
class PluginNode:
    """Node in the dependency graph."""
    name: str
    plugin_type: str
    depends_on: list[str] = field(default_factory=list)
    optional_depends_on: list[str] = field(default_factory=list)
    conflicts_with: list[str] = field(default_factory=list)
    instance: Any = None
    loaded: bool = False


class DependencyGraph:
    """Directed graph for plugin dependencies."""

    def __init__(self) -> None:
        self._nodes: dict[str, PluginNode] = {}

    def add_node(self, node: PluginNode) -> None:
        key = f"{node.plugin_type}:{node.name}"
        self._nodes[key] = node

    def get_node(self, plugin_type: str, name: str) -> PluginNode | None:
        return self._nodes.get(f"{plugin_type}:{name}")

    def resolve_order(self) -> list[PluginNode]:
        """Topological sort of nodes by dependencies.

        Returns nodes in order that respects dependencies.
        Raises ValueError on circular dependency.
        """
        result: list[PluginNode] = []
        visited: set[str] = set()
        in_progress: set[str] = set()

        def visit(key: str) -> None:
            if key in visited:
                return
            if key in in_progress:
                raise ValueError(f"Circular dependency detected: {key}")

            node = self._nodes.get(key)
            if node is None:
                return

            in_progress.add(key)

            # Visit required dependencies first
            for dep in node.depends_on:
                visit(dep)

            # Visit optional dependencies (if present)
            for dep in node.optional_depends_on:
                if dep in self._nodes:
                    visit(dep)

            in_progress.remove(key)
            visited.add(key)
            result.append(node)

        for key in self._nodes:
            visit(key)

        return result

    def validate(self) -> list[str]:
        """Validate the dependency graph.

        Returns list of validation errors.
        """
        errors: list[str] = []

        for key, node in self._nodes.items():
            # Check required dependencies exist
            for dep in node.depends_on:
                if dep not in self._nodes:
                    errors.append(
                        f"Plugin {key} requires missing dependency: {dep}"
                    )

            # Check conflicts
            for conflict in node.conflicts_with:
                if conflict in self._nodes:
                    errors.append(
                        f"Plugin {key} conflicts with {conflict}"
                    )

        # Check for circular dependencies
        try:
            self.resolve_order()
        except ValueError as e:
            errors.append(str(e))

        return errors


def build_dependency_graph(
    plugins: list[tuple[str, str, dict[str, Any]]],
) -> DependencyGraph:
    """Build dependency graph from plugin metadata.

    Args:
        plugins: List of (plugin_type, name, metadata) tuples.

    Returns:
        DependencyGraph ready for resolution.
    """
    graph = DependencyGraph()

    for plugin_type, name, metadata in plugins:
        node = PluginNode(
            name=name,
            plugin_type=plugin_type,
            depends_on=metadata.get("depends_on", []),
            optional_depends_on=metadata.get("optional_depends_on", []),
            conflicts_with=metadata.get("conflicts_with", []),
        )
        graph.add_node(node)

    return graph
```

### 3. Loader Integration

```python
# Update __init__.py to use dependency resolution

def _build_pipeline(
    settings: _Settings,
) -> tuple[...]:
    """Build pipeline with dependency-aware loading."""

    # Collect all plugin metadata first
    plugin_specs: list[tuple[str, str, dict]] = []

    for name in core_cfg.enrichers:
        metadata = _get_plugin_metadata("fapilog.enrichers", name)
        plugin_specs.append(("enricher", name, metadata))

    for name in core_cfg.redactors:
        metadata = _get_plugin_metadata("fapilog.redactors", name)
        plugin_specs.append(("redactor", name, metadata))

    for name in core_cfg.sinks:
        metadata = _get_plugin_metadata("fapilog.sinks", name)
        plugin_specs.append(("sink", name, metadata))

    # Build and validate dependency graph
    graph = build_dependency_graph(plugin_specs)
    errors = graph.validate()

    if errors:
        for error in errors:
            diagnostics.warn("plugins", "dependency error", error=error)
        # Continue with valid plugins only

    # Load in dependency order
    ordered = graph.resolve_order()
    loaded_plugins: dict[str, Any] = {}

    for node in ordered:
        # Collect dependency instances
        deps = {}
        for dep_key in node.depends_on:
            if dep_key in loaded_plugins:
                deps[dep_key] = loaded_plugins[dep_key]

        # Load plugin with dependency injection
        instance = _loader.load_plugin(
            f"fapilog.{node.plugin_type}s",
            node.name,
            {**configs.get(node.name, {}), "_dependencies": deps},
        )
        loaded_plugins[f"{node.plugin_type}:{node.name}"] = instance

    # ... rest of pipeline building ...
```

### 4. Plugin Access to Dependencies

```python
# Example plugin using dependencies

class EncryptedSink:
    """Sink that encrypts logs using injected key provider."""

    name = "encrypted_sink"

    def __init__(
        self,
        *,
        config: dict = None,
        _dependencies: dict = None,
    ) -> None:
        self._config = config or {}
        self._deps = _dependencies or {}

        # Access required dependency
        self._key_provider = self._deps.get("enricher:key_provider")
        if self._key_provider is None:
            raise ValueError("key_provider enricher is required")

    async def write(self, entry: dict) -> None:
        key = await self._key_provider.get_key()
        encrypted = self._encrypt(entry, key)
        # ... write encrypted ...


PLUGIN_METADATA = {
    "name": "encrypted_sink",
    "depends_on": ["enricher:key_provider"],
    # ...
}
```

---

## Test Plan

### Unit Tests

1. **test_dependency_graph.py**

   - Test topological sort
   - Test circular dependency detection
   - Test missing dependency detection
   - Test conflict detection

2. **test_dependency_loading.py**
   - Test plugins loaded in correct order
   - Test dependencies injected correctly
   - Test missing required dependency fails load

---

## Documentation Updates

1. Update `docs/plugins/authoring.md` with dependency declaration
2. Add `docs/plugins/dependencies.md` guide
3. Update examples with dependency patterns

---

## Related Stories

- ~~Story 5.11: Plugin hot-reload~~ — Not planned

---

## See Also

- `src/fapilog/plugins/enrichers/context_vars.py` — Example of ContextVars for cross-cutting state
- Python `contextvars` documentation — Recommended pattern for shared state
