# Strict Hook/Filter Registration Extension

The Strict Registration Extension enforces fail-fast validation when registering hooks and filters, preventing silent failures from typos or undefined hook/filter names.

## Overview

**Default Behavior:** All hooks/filters MUST be defined before registration. Attempts to register to undefined hooks/filters raise `FrameworkError` immediately.

**Key Features:**
- **Fail-fast validation**: Catches typos at startup, not runtime
- **Fuzzy matching suggestions**: "Did you mean X?" error messages
- **Plugin identification**: Shows which plugin caused the error
- **Optional escape hatch**: `@optional()` decorator for truly optional registrations

## Why Strict-by-Default?

**Before (Cement default):**
```
[DEBUG] filter name 'INFERENCE_PREPROCESS_PRE_FILTR' is not defined! ignoring...
# Application continues
# Filter never executes
# Silent failure = hard to debug
```

**After (Strict mode):**
```
[ERROR] Filter 'INFERENCE_PREPROCESS_PRE_FILTR' is not defined.
Did you mean: INFERENCE_PREPROCESS_PRE_FILTER?

FrameworkError: Filter 'INFERENCE_PREPROCESS_PRE_FILTR' is not defined.
# Application stops immediately
# Error is obvious
```

## Configuration

The extension is automatically loaded in `vipr/main.py`:

```python
extensions = [
    'vipr.ext.strict_registration',  # Strict registration
    'vipr.ext.dynamic_hooks_filters',       # Dynamic YAML loading
]
```

**Important:** `strict_registration` MUST load BEFORE `dynamic_hooks_filters` to enforce validation.

## Usage

### Default: Strict Validation

All registrations are validated:

```python
app.hook.register('TYPO_HOOK', my_func)
# ❌ FrameworkError: Hook 'TYPO_HOOK' is not defined.
```

```yaml
vipr:
  inference:
    filters:
      INFERENCE_PREPROCESS_PRE_FILTR:  # Typo!
        - class: MyPlugin
          method: my_method
# ❌ Error at startup with suggestions
```

### Optional Registration: Two Methods

When a hook/filter might legitimately not exist (e.g., optional features):

#### Method 1: `@optional()` Decorator (Code)

```python
from vipr.ext.strict_registration import optional
from vipr.plugins.discovery.decorators import discover_filter

class ExperimentalPlugin:
    @optional()
    @discover_filter('EXPERIMENTAL_FEATURE_FILTER')
    def experimental_feature(self, data, **kwargs):
        """Only runs if the filter namespace exists."""
        return data
```

```yaml
# No special config needed
EXPERIMENTAL_FEATURE_FILTER:
  - class: MyPlugin
    method: experimental_feature
# ⚠️ Warning if filter doesn't exist, but continues
```

#### Method 2: `optional: true` Flag (YAML)

```python
from vipr.plugins.discovery.decorators import discover_filter

class ExperimentalPlugin:
    @discover_filter('EXPERIMENTAL_FEATURE_FILTER')
    def experimental_feature(self, data, **kwargs):
        return data
```

```yaml
EXPERIMENTAL_FEATURE_FILTER:
  - class: MyPlugin
    method: experimental_feature
    optional: true  # ← Set by dynamic_hooks_filters, evaluated by strict_registration
# ⚠️ Warning if filter doesn't exist, but continues
```

### Choosing Between Methods

| Aspect | `@optional()` Decorator | `optional: true` in YAML |
|--------|------------------------|--------------------------|
| **Where** | Python code | YAML config |
| **Flexibility** | Static | Dynamic per deployment |
| **Best for** | Architectural decisions | Configuration variations |

**Use `@optional()` when:**
- Feature is always optional (architectural decision)
- Want code as single source of truth

**Use `optional: true` when:**
- Optionality varies per deployment/environment
- Need configuration flexibility

## Error Messages

### Typo Detection

```
FrameworkError: Filter 'INFERENCE_PREPROCESS_PRE_FILTER2' is not defined.
Attempted registration from plugin 'reflectorch_extension'.

Did you mean one of these?
  - INFERENCE_PREPROCESS_PRE_FILTER
  - INFERENCE_PREPROCESS_POST_FILTER
  - INFERENCE_POSTPROCESS_PRE_FILTER
```

### Plugin Identification

Error messages identify the source plugin:
```
Attempted registration from plugin 'my_plugin'.
```

## Integration with Dynamic Hooks/Filters

The `optional: true` YAML flag is processed by the Dynamic Hooks/Filters extension and checked by Strict Registration:

```python
# In dynamic_hooks_filters extension:
if cfg.get("optional", False):
    callback.__optional__ = True  # Set attribute

# In strict_registration extension:
if hasattr(func, '__optional__') and func.__optional__:
    # Only warning, no error
    return False
```

See [Dynamic Hooks/Filters Extension](dynamic_hooks_filters.md) for YAML configuration details.

## Use Cases

### ✅ Good Use Cases for `@optional()`

**Plugin compatibility across versions:**
```python
from vipr.ext.strict_registration import optional
from vipr.plugins.discovery.decorators import discover_hook

class StreamingCompatPlugin:
    @optional()
    @discover_hook('VIPR_2_0_STREAMING_HOOK')
    def use_streaming_feature(self, app, **kwargs):
        """Works with VIPR 1.x (ignored) and 2.x (executed)."""
        pass
```

**Experimental features:**
```yaml
EXPERIMENTAL_VISUALIZATION_HOOK:
  - class: vipr.plugins.viz.Visualizer
    method: plot_results
    optional: true  # Not all installations have viz plugin
```

**Conditional workflows:**
```python
from vipr.ext.strict_registration import optional
from vipr.plugins.discovery.decorators import discover_filter

class AdvancedPostprocessPlugin:
    @optional()
    @discover_filter('ADVANCED_POSTPROCESS_FILTER')
    def advanced_postprocessing(self, data, **kwargs):
        """Only if advanced plugin installed."""
        return data
```

### ❌ Bad Use Cases

**Hiding typos:**
```yaml
# WRONG! Fix the typo instead
INFERENCE_PREPROCESS_PRE_FILTR:  # Should be INFERENCE_PREPROCESS_PRE_FILTER
  - class: my_plugin.preprocessing.PreprocessPlugin
    method: preprocess
    optional: true  # ← Don't hide bugs!
```

**Core features:**
```python
# WRONG! Core features should be required
from vipr.ext.strict_registration import optional
from vipr.plugins.discovery.decorators import discover_filter

class NormalizerPlugin:
    @optional()
    @discover_filter('INFERENCE_PREPROCESS_PRE_FILTER')
    def normalize(self, data, **kwargs):
        return data
```

## Troubleshooting

### "Hook/Filter 'X' is not defined"

**Cause:** Typo in hook/filter name or hook not defined in framework.

**Solution:**
1. Check error message for suggestions
2. Verify namespace is defined with `app.hook.define('HOOK_NAME')` or `app.filter.define('FILTER_NAME')`
3. If truly optional, add `optional: true` or `@optional()`

### Optional callback not executing

**Cause:** Hook/filter doesn't exist and callback is marked optional.

**Solution:** This is expected behavior. Check logs for warning message.

### Attribute propagation issues

**Cause:** Custom wrappers not propagating `__optional__` attribute.

**Solution:** Ensure wrappers use `_propagate_attributes()` from dynamic_hooks_filters.

## Best Practices

1. **Default to strict**: Only use `@optional()` when truly needed
2. **Fix typos**: Don't hide bugs with `optional: true`
3. **Document optionality**: Comment why a callback is optional
4. **Choose one method**: Don't mix `@optional()` and `optional: true` for same callback
5. **Test both paths**: Test with and without optional hooks/filters loaded
6. **Use class methods for discovery decorators**: `@discover_hook`/`@discover_filter` reject standalone functions

## Technical Details

### Implementation

The extension monkey-patches Cement's registration methods:

```python
def enhanced_hook_register(name, func, weight=0):
    if name not in app.hook.__hooks__:
        if hasattr(func, '__optional__') and func.__optional__:
            # Log warning, return False
            return False
        # Raise FrameworkError with suggestions
        raise FrameworkError(error_msg)
    return original_hook_register(name, func, weight)
```

### Attribute Propagation

The `__optional__` attribute is propagated through wrapper functions in `dynamic_hooks_filters`:

```python
def _propagate_attributes(source_cb, target_cb):
    for attr in ("__name__", "__module__", "__optional__"):
        if hasattr(source_cb, attr):
            setattr(target_cb, attr, getattr(source_cb, attr))
```

### Load Order

Critical: Extension must load BEFORE `dynamic_hooks_filters`:

```python
extensions = [
    'vipr.ext.strict_registration',  # 1. Patches registration
    'vipr.ext.dynamic_hooks_filters',       # 2. Uses patched methods
]
```

## Migration Guide

**Breaking Change:** Strict validation now fails immediately on undefined hooks/filters or typos.

**Quick Fix:**
1. Run your app - errors show typos with "Did you mean..." suggestions
2. Fix the typo in config/code, or
3. Mark as optional with `optional: true` (YAML) or `@optional()` (code)

**Example Error:**
```
FrameworkError: Filter 'INFERENCE_PREDICTION_PRE_FILTR' is not defined.
Did you mean: INFERENCE_PREDICTION_PRE_FILTER?
```

**Solution:** Fix typo in YAML or mark genuinely optional features with `optional: true`.

## See Also

- [Dynamic Hooks/Filters Extension](dynamic_hooks_filters.md) - YAML configuration
- [Inference Plugin](../plugins/built-in/inference.md) - Hook/filter workflow
- `vipr-core/vipr/ext/strict_registration.py` - Implementation
