# Story 3.8: Composable Redaction Presets

**Status:** Ready for Code Review
**Priority:** High
**Depends on:** None (builds on existing redactor infrastructure)

---

## Context / Background

Fapilog's redaction system currently has two levels:

1. **Environment presets** (`dev`, `production`, `fastapi`, etc.) - full configuration bundles
2. **Per-redactor configuration** - flat lists of fields/patterns

**The gap:** There's no middle layer for *domain-specific* redaction patterns. Users who need GDPR compliance must:
- Manually research what fields to redact
- Copy-paste field lists from documentation
- Risk missing categories or making mistakes

**Business problem:** Organizations cannot afford GDPR data leaking into logs. A €20M fine for logging an unredacted email is not acceptable. They need confidence that "GDPR PII" is handled, not a checklist of 30 field names to maintain.

**Developer problem:** Developers want one-liner protection, not a research project. They want to write `.with_gdpr_protection()` and move on.

**Current state:**
- `src/fapilog/core/presets.py` - environment presets (not domain presets)
- `src/fapilog/builder.py:176-201` - `with_redaction(fields=..., patterns=...)` accepts flat lists
- No concept of named, composable pattern groups

---

## Design Decision: Field-Name Matching Only

After researching GDPR PII categories, two approaches were considered:

| Approach | Performance | Coverage | False Positives |
|----------|-------------|----------|-----------------|
| **Field-name matching** | O(n) fields | Catches `user.email`, `data.phone` | Low |
| **Content pattern scanning** | O(m×k) per string | Catches `"email me at john@x.com"` anywhere | Higher |

**Decision:** This story implements **field-name matching only**.

**Rationale:**
1. **Performance** - Content scanning every string value is expensive for a logging hot path
2. **Predictability** - Field names are developer-controlled; content is user-controlled
3. **False positives** - Date patterns match DOB and order dates; IP patterns match version numbers
4. **Composability** - Field lists are additive and easy to reason about

**Future consideration:** Content scanning can be added later as opt-in (`scan_content=True`) with performance warnings.

---

## Scope (In / Out)

### In Scope

- New `RedactionPreset` dataclass with inheritance support (`extends` field)
- Preset metadata: `regulation`, `region`, `tags` for organization and filtering
- Hierarchical preset structure:
  - Base presets: `CONTACT_INFO`, `PERSONAL_IDENTIFIERS`, `ONLINE_IDENTIFIERS`, etc.
  - Regional presets: `US_GOVERNMENT_IDS`, `UK_GOVERNMENT_IDS`, `EU_GOVERNMENT_IDS`
  - Regulation presets: `GDPR_PII`, `GDPR_PII_UK`, `CCPA_PII`, `HIPAA_PHI`, `PCI_DSS`, `CREDENTIALS`
- Builder API: `.with_redaction_preset("GDPR_PII")`
- Composable: multiple presets can be combined at runtime
- Inheritance: presets can extend other presets (resolved at config time)
- Additive: custom fields extend presets, don't replace
- Introspection: users can inspect preset fields, inheritance chain, and metadata
- Filtering: users can query presets by regulation, region, or tags

### Out of Scope

- Content pattern scanning (performance concern, future story)
- User-defined custom presets at runtime (future story — registry is extensible)
- Runtime preset loading from files (future story)
- Preset versioning/changelog (future story — for tracking regulation updates)

---

## Acceptance Criteria

### AC1: Preset Registry with Built-in Presets

**Description:** A registry of named redaction presets exists with base, regional, and regulation presets.

**Validation:**
```python
from fapilog.redaction import list_redaction_presets, get_redaction_preset

# List available presets
presets = list_redaction_presets()

# Base presets
assert "CONTACT_INFO" in presets
assert "PERSONAL_IDENTIFIERS" in presets
assert "ONLINE_IDENTIFIERS" in presets

# Regional presets
assert "US_GOVERNMENT_IDS" in presets
assert "UK_GOVERNMENT_IDS" in presets
assert "EU_GOVERNMENT_IDS" in presets

# Regulation presets
assert "GDPR_PII" in presets
assert "GDPR_PII_UK" in presets
assert "CCPA_PII" in presets
assert "HIPAA_PHI" in presets
assert "PCI_DSS" in presets
assert "CREDENTIALS" in presets

# Get preset definition
gdpr = get_redaction_preset("GDPR_PII")
assert gdpr.description
assert gdpr.regulation == "GDPR"
assert gdpr.region == "EU"
```

### AC2: Builder API for Single Preset

**Description:** The builder accepts a single redaction preset by name.

**Validation:**
```python
from fapilog import LoggerBuilder

logger = (
    LoggerBuilder()
    .with_redaction_preset("GDPR_PII")
    .build()
)

# Logging with PII field - should be redacted
logger.info("user signup", email="john@example.com", name="John Doe")
# Output: {"data": {"email": "***", "name": "***"}, ...}
```

### AC3: Multiple Presets Are Composable

**Description:** Multiple presets can be applied and their fields are merged.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_redaction_preset("GDPR_PII")
    .with_redaction_preset("PCI_DSS")
    .build()
)

# Both PII and payment fields redacted
logger.info("purchase", email="a@b.com", card_number="4111111111111111")
# Output: {"data": {"email": "***", "card_number": "***"}, ...}
```

### AC4: Custom Fields Extend Presets

**Description:** Custom fields add to preset fields, not replace them.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_redaction_preset("GDPR_PII")
    .with_redaction(fields=["patient_id", "mrn"])  # Domain-specific
    .build()
)

# All fields redacted
logger.info("patient", email="a@b.com", patient_id="12345")
# Output: {"data": {"email": "***", "patient_id": "***"}, ...}
```

### AC5: Preset Introspection

**Description:** Users can inspect what fields a preset covers for audit/documentation.

**Validation:**
```python
from fapilog.redaction import get_redaction_preset

gdpr = get_redaction_preset("GDPR_PII")
print(gdpr.name)         # "GDPR_PII"
print(gdpr.description)  # "GDPR Article 4 personal data identifiers"
print(gdpr.fields)       # ["email", "phone", "name", ...]
print(gdpr.patterns)     # ["(?i).*email.*", ...] - field name patterns
```

### AC6: Environment Presets Can Reference Redaction Presets

**Description:** Environment presets (production, fastapi) can use redaction presets internally.

**Validation:**
```python
# production preset internally uses CREDENTIALS preset
# This is an implementation detail, not user-facing
# Verified by checking preset configuration
```

### AC7: Preset Inheritance Resolution

**Description:** Presets can extend other presets, and resolution includes all inherited fields.

**Validation:**
```python
from fapilog.redaction import get_redaction_preset, resolve_preset_fields

# GDPR_PII_UK extends GDPR_PII and UK_GOVERNMENT_IDS
uk_gdpr = get_redaction_preset("GDPR_PII_UK")
assert "GDPR_PII" in uk_gdpr.extends
assert "UK_GOVERNMENT_IDS" in uk_gdpr.extends

# Resolution includes fields from all ancestors
fields, patterns = resolve_preset_fields("GDPR_PII_UK")

# From CONTACT_INFO (via GDPR_PII)
assert "email" in fields
assert "phone" in fields

# From UK_GOVERNMENT_IDS
assert "national_insurance" in fields or "ni_number" in fields
assert "nhs_number" in fields
```

### AC8: Preset Filtering by Metadata

**Description:** Users can query presets by regulation, region, or tags.

**Validation:**
```python
from fapilog.redaction import get_presets_by_regulation, get_presets_by_region, get_presets_by_tag

# Filter by regulation
gdpr_presets = get_presets_by_regulation("GDPR")
assert "GDPR_PII" in gdpr_presets

# Filter by region
us_presets = get_presets_by_region("US")
assert "CCPA_PII" in us_presets
assert "HIPAA_PHI" in us_presets

uk_presets = get_presets_by_region("UK")
assert "GDPR_PII_UK" in uk_presets

# Filter by tag
pii_presets = get_presets_by_tag("pii")
assert "GDPR_PII" in pii_presets
assert "CCPA_PII" in pii_presets

healthcare_presets = get_presets_by_tag("healthcare")
assert "HIPAA_PHI" in healthcare_presets
```

### AC9: GDPR_PII Preset Covers Required Categories (via inheritance)

**Description:** The resolved GDPR_PII preset includes fields for all patternable GDPR personal data categories.

**Validation:**
```python
from fapilog.redaction import resolve_preset_fields

fields, patterns = resolve_preset_fields("GDPR_PII")

# Direct identifiers (from CONTACT_INFO)
assert "email" in fields
assert "phone" in fields

# Personal identifiers (from PERSONAL_IDENTIFIERS)
assert "name" in fields or "first_name" in fields
assert "dob" in fields or "date_of_birth" in fields

# Address (from CONTACT_INFO)
assert "address" in fields

# Government IDs (from EU_GOVERNMENT_IDS)
assert "passport" in fields
assert "national_id" in fields

# Online identifiers (from ONLINE_IDENTIFIERS)
assert "ip" in fields or "ip_address" in fields

# Financial (from FINANCIAL_IDENTIFIERS)
assert "iban" in fields
assert "account_number" in fields or "bank_account" in fields
```

### AC10: Regional Variants Extend Base Correctly

**Description:** Regional variants (GDPR_PII_UK, CCPA_PII) properly extend and specialize.

**Validation:**
```python
from fapilog.redaction import resolve_preset_fields

# UK variant has NI numbers, NHS numbers
uk_fields, _ = resolve_preset_fields("GDPR_PII_UK")
assert "nhs_number" in uk_fields
assert "national_insurance" in uk_fields or "ni_number" in uk_fields

# US CCPA variant has SSN
ccpa_fields, _ = resolve_preset_fields("CCPA_PII")
assert "ssn" in ccpa_fields or "social_security" in ccpa_fields

# HIPAA has medical record numbers
hipaa_fields, _ = resolve_preset_fields("HIPAA_PHI")
assert "mrn" in hipaa_fields or "medical_record_number" in hipaa_fields
assert "patient_id" in hipaa_fields
```

---

## Implementation Notes

### File Changes

```
src/fapilog/redaction/__init__.py (NEW - public API exports)
src/fapilog/redaction/presets.py (NEW - RedactionPreset dataclass + all preset definitions)
src/fapilog/redaction/registry.py (NEW - registry functions with caching)
src/fapilog/builder.py (MODIFIED - add with_redaction_preset)
src/fapilog/core/presets.py (MODIFIED - use CREDENTIALS preset internally)
tests/unit/test_redaction_presets.py (NEW - preset definitions and inheritance)
tests/unit/test_redaction_registry.py (NEW - registry and filtering)
tests/integration/test_redaction_preset_integration.py (NEW - end-to-end)
tests/performance/test_preset_overhead.py (NEW - performance validation)
docs/user-guide/redaction-presets.md (NEW - user guide)
docs/reference/redaction-presets.md (NEW - complete field list for audit)
```

### Preset Definitions

```python
# src/fapilog/redaction/presets.py

from dataclasses import dataclass, field

@dataclass(frozen=True)
class RedactionPreset:
    """A named collection of field patterns for redaction.

    Presets can extend other presets via the `extends` field,
    allowing hierarchical composition (e.g., GDPR_UK extends GDPR_PII).
    """
    name: str
    description: str
    fields: tuple[str, ...] = ()           # Exact field names
    patterns: tuple[str, ...] = ()         # Regex patterns for field names
    extends: tuple[str, ...] = ()          # Parent preset names to inherit from
    regulation: str | None = None          # e.g., "GDPR", "CCPA", "HIPAA"
    region: str | None = None              # e.g., "EU", "UK", "US", "US-CA"
    tags: tuple[str, ...] = ()             # Arbitrary tags for filtering

    def resolve(self, registry: dict[str, "RedactionPreset"]) -> tuple[set[str], set[str]]:
        """Resolve all fields and patterns including inherited ones."""
        all_fields: set[str] = set(self.fields)
        all_patterns: set[str] = set(self.patterns)

        for parent_name in self.extends:
            parent = registry.get(parent_name)
            if parent:
                parent_fields, parent_patterns = parent.resolve(registry)
                all_fields.update(parent_fields)
                all_patterns.update(parent_patterns)

        return all_fields, all_patterns


# =============================================================================
# Base Presets (building blocks)
# =============================================================================

CONTACT_INFO = RedactionPreset(
    name="CONTACT_INFO",
    description="Contact information fields (email, phone, address)",
    tags=("pii", "contact"),
    fields=(
        "email", "e_mail", "email_address",
        "phone", "phone_number", "telephone", "mobile", "cell", "fax",
        "address", "street", "street_address", "postal_address",
        "postcode", "postal_code", "zipcode", "zip_code", "zip",
        "city", "town", "state", "province", "country",
    ),
    patterns=(
        r"(?i).*email.*",
        r"(?i).*phone.*",
        r"(?i).*mobile.*",
        r"(?i).*address.*",
        r"(?i).*postcode.*",
        r"(?i).*zipcode.*",
    ),
)

PERSONAL_IDENTIFIERS = RedactionPreset(
    name="PERSONAL_IDENTIFIERS",
    description="Personal identity fields (name, DOB)",
    tags=("pii", "identity"),
    fields=(
        "name", "first_name", "last_name", "full_name", "surname", "given_name",
        "middle_name", "maiden_name", "nickname",
        "dob", "date_of_birth", "birth_date", "birthday", "age",
        "gender", "sex",
    ),
    patterns=(
        r"(?i).*\bname\b.*",  # Word boundary to avoid "filename"
        r"(?i).*\bdob\b.*",
        r"(?i).*birth.*",
    ),
)

ONLINE_IDENTIFIERS = RedactionPreset(
    name="ONLINE_IDENTIFIERS",
    description="Online/digital identifiers (IP, device ID, cookies)",
    tags=("pii", "digital"),
    fields=(
        "ip", "ip_address", "ipv4", "ipv6", "client_ip", "remote_ip",
        "device_id", "device_identifier", "udid", "idfa", "gaid",
        "mac_address", "hardware_id",
        "user_agent", "browser_fingerprint",
        "cookie_id", "tracking_id", "visitor_id",
    ),
    patterns=(
        r"(?i).*\bip\b.*",
        r"(?i).*device.?id.*",
        r"(?i).*mac.?addr.*",
    ),
)

FINANCIAL_IDENTIFIERS = RedactionPreset(
    name="FINANCIAL_IDENTIFIERS",
    description="Financial account identifiers (IBAN, account numbers)",
    tags=("pii", "financial"),
    fields=(
        "iban", "bic", "swift",
        "bank_account", "account_number", "account_no",
        "sort_code", "routing_number", "bsb",
    ),
    patterns=(
        r"(?i).*\biban\b.*",
        r"(?i).*account.?(num|no).*",
        r"(?i).*routing.*",
    ),
)

# =============================================================================
# Government ID Presets (regional)
# =============================================================================

US_GOVERNMENT_IDS = RedactionPreset(
    name="US_GOVERNMENT_IDS",
    description="US government-issued identifiers",
    regulation="US",
    region="US",
    tags=("government-id", "us"),
    fields=(
        "ssn", "social_security", "social_security_number",
        "itin", "ein",  # Tax IDs
        "passport", "passport_number",
        "drivers_license", "driver_license", "dl_number",
    ),
    patterns=(
        r"(?i).*\bssn\b.*",
        r"(?i).*social.?security.*",
    ),
)

UK_GOVERNMENT_IDS = RedactionPreset(
    name="UK_GOVERNMENT_IDS",
    description="UK government-issued identifiers",
    region="UK",
    tags=("government-id", "uk"),
    fields=(
        "national_insurance", "ni_number", "nino",
        "nhs_number",
        "passport", "passport_number",
        "driving_licence", "licence_number",
    ),
    patterns=(
        r"(?i).*national.?insurance.*",
        r"(?i).*\bni.?(num|no)\b.*",
        r"(?i).*\bnino\b.*",
        r"(?i).*\bnhs\b.*",
    ),
)

EU_GOVERNMENT_IDS = RedactionPreset(
    name="EU_GOVERNMENT_IDS",
    description="EU government-issued identifiers (generic)",
    region="EU",
    tags=("government-id", "eu"),
    fields=(
        "national_id", "id_number", "identity_number", "id_card",
        "passport", "passport_number",
        "tax_id", "tin", "vat_number",
        "drivers_license", "licence_number",
    ),
    patterns=(
        r"(?i).*national.?id.*",
        r"(?i).*passport.*",
        r"(?i).*\btin\b.*",
        r"(?i).*tax.?id.*",
        r"(?i).*licen[cs]e.*",
    ),
)

# =============================================================================
# Regulation Presets (compose from building blocks)
# =============================================================================

GDPR_PII = RedactionPreset(
    name="GDPR_PII",
    description="GDPR Article 4 personal data identifiers",
    regulation="GDPR",
    region="EU",
    tags=("gdpr", "pii", "eu"),
    extends=("CONTACT_INFO", "PERSONAL_IDENTIFIERS", "ONLINE_IDENTIFIERS",
             "FINANCIAL_IDENTIFIERS", "EU_GOVERNMENT_IDS"),
    # Additional GDPR-specific fields not in base presets
    fields=("biometric_data", "genetic_data", "health_data"),
)

GDPR_PII_UK = RedactionPreset(
    name="GDPR_PII_UK",
    description="UK GDPR personal data identifiers (post-Brexit UK variant)",
    regulation="UK-GDPR",
    region="UK",
    tags=("gdpr", "pii", "uk"),
    extends=("GDPR_PII", "UK_GOVERNMENT_IDS"),
)

CCPA_PII = RedactionPreset(
    name="CCPA_PII",
    description="California Consumer Privacy Act personal information",
    regulation="CCPA",
    region="US-CA",
    tags=("ccpa", "pii", "us"),
    extends=("CONTACT_INFO", "PERSONAL_IDENTIFIERS", "ONLINE_IDENTIFIERS",
             "FINANCIAL_IDENTIFIERS", "US_GOVERNMENT_IDS"),
    # CCPA-specific: includes inferences and household data
    fields=("household_id", "inferred_preferences", "purchase_history"),
)

HIPAA_PHI = RedactionPreset(
    name="HIPAA_PHI",
    description="HIPAA Protected Health Information identifiers",
    regulation="HIPAA",
    region="US",
    tags=("hipaa", "phi", "healthcare", "us"),
    extends=("CONTACT_INFO", "PERSONAL_IDENTIFIERS", "US_GOVERNMENT_IDS"),
    fields=(
        # HIPAA 18 identifiers
        "mrn", "medical_record_number", "patient_id",
        "health_plan_id", "beneficiary_id",
        "account_number",
        "certificate_number", "license_number",
        "vehicle_id", "vin",
        "device_serial", "device_identifier",
        "url", "web_url",  # When related to patient
        "biometric_id", "fingerprint", "voiceprint",
        "photo", "image",  # Full-face photos
    ),
    patterns=(
        r"(?i).*\bmrn\b.*",
        r"(?i).*medical.?record.*",
        r"(?i).*patient.?id.*",
        r"(?i).*health.?plan.*",
    ),
)

PCI_DSS = RedactionPreset(
    name="PCI_DSS",
    description="PCI-DSS cardholder data elements",
    regulation="PCI-DSS",
    tags=("pci", "payment", "financial"),
    fields=(
        "card_number", "credit_card", "cc_number", "pan",
        "cvv", "cvc", "cvv2", "cid", "security_code", "card_security",
        "expiry", "expiry_date", "expiration", "exp_date", "exp_month", "exp_year",
        "cardholder", "cardholder_name", "card_holder",
        "card_pin", "pin",
        "track_data", "track1", "track2",  # Magnetic stripe data
    ),
    patterns=(
        r"(?i).*card.?(num|no).*",
        r"(?i).*credit.?card.*",
        r"(?i).*\bcvv\b.*",
        r"(?i).*\bcvc\b.*",
        r"(?i).*expir.*",
        r"(?i).*cardholder.*",
        r"(?i).*\bpan\b.*",
    ),
)

CREDENTIALS = RedactionPreset(
    name="CREDENTIALS",
    description="Authentication and authorization secrets",
    tags=("security", "auth", "secrets"),
    fields=(
        "password", "passwd", "pwd", "pass",
        "secret", "api_secret", "client_secret", "shared_secret",
        "token", "access_token", "refresh_token", "auth_token", "bearer_token", "jwt",
        "api_key", "apikey", "api_token",
        "private_key", "secret_key", "signing_key", "encryption_key",
        "authorization", "auth_header",
        "session_id", "session_token", "session_key",
        "cookie", "session_cookie", "auth_cookie",
        "otp", "totp", "mfa_code", "verification_code",
    ),
    patterns=(
        r"(?i).*password.*",
        r"(?i).*passwd.*",
        r"(?i).*\bsecret\b.*",
        r"(?i).*\btoken\b.*",
        r"(?i).*api.?key.*",
        r"(?i).*private.?key.*",
        r"(?i).*auth.*",
        r"(?i).*\botp\b.*",
    ),
)

# =============================================================================
# Registry
# =============================================================================

BUILTIN_PRESETS = {
    # Base building blocks
    "CONTACT_INFO": CONTACT_INFO,
    "PERSONAL_IDENTIFIERS": PERSONAL_IDENTIFIERS,
    "ONLINE_IDENTIFIERS": ONLINE_IDENTIFIERS,
    "FINANCIAL_IDENTIFIERS": FINANCIAL_IDENTIFIERS,
    # Regional government IDs
    "US_GOVERNMENT_IDS": US_GOVERNMENT_IDS,
    "UK_GOVERNMENT_IDS": UK_GOVERNMENT_IDS,
    "EU_GOVERNMENT_IDS": EU_GOVERNMENT_IDS,
    # Regulation presets
    "GDPR_PII": GDPR_PII,
    "GDPR_PII_UK": GDPR_PII_UK,
    "CCPA_PII": CCPA_PII,
    "HIPAA_PHI": HIPAA_PHI,
    "PCI_DSS": PCI_DSS,
    "CREDENTIALS": CREDENTIALS,
}
```

### Builder Integration

```python
# In builder.py

def with_redaction_preset(self, preset_name: str) -> LoggerBuilder:
    """Apply a named redaction preset.

    Presets are composable - calling multiple times merges fields.
    Inheritance is resolved at build time (not runtime) for performance.
    Custom fields from with_redaction() extend preset fields.

    Args:
        preset_name: Preset name (e.g., "GDPR_PII", "GDPR_PII_UK", "HIPAA_PHI")

    Example:
        >>> builder.with_redaction_preset("GDPR_PII")
        >>> builder.with_redaction_preset("GDPR_PII_UK")  # Extends GDPR_PII + UK_GOVERNMENT_IDS
        >>> builder.with_redaction_preset("GDPR_PII").with_redaction_preset("PCI_DSS")  # Combine
    """
    from .redaction import resolve_preset_fields

    # Resolve inheritance at config time (cached for performance)
    fields, patterns = resolve_preset_fields(preset_name)

    # Merge fields and patterns into existing config
    redactors = self._config.setdefault("core", {}).setdefault("redactors", [])
    redactor_config = self._config.setdefault("redactor_config", {})

    # Enable field_mask if we have fields
    if fields:
        if "field_mask" not in redactors:
            redactors.append("field_mask")
        existing_fields = redactor_config.setdefault("field_mask", {}).setdefault("fields_to_mask", [])
        # Add with data. prefix for envelope structure
        for field in fields:
            prefixed = f"data.{field}"
            if prefixed not in existing_fields:
                existing_fields.append(prefixed)

    # Enable regex_mask if we have patterns
    if patterns:
        if "regex_mask" not in redactors:
            redactors.append("regex_mask")
        existing_patterns = redactor_config.setdefault("regex_mask", {}).setdefault("patterns", [])
        for pattern in patterns:
            if pattern not in existing_patterns:
                existing_patterns.append(pattern)

    return self
```

### Registry API

```python
# In src/fapilog/redaction/registry.py

from functools import lru_cache
from .presets import BUILTIN_PRESETS, RedactionPreset

def get_redaction_preset(name: str) -> RedactionPreset:
    """Get a preset by name."""
    if name not in BUILTIN_PRESETS:
        valid = ", ".join(sorted(BUILTIN_PRESETS.keys()))
        raise ValueError(f"Unknown redaction preset '{name}'. Available: {valid}")
    return BUILTIN_PRESETS[name]

def list_redaction_presets() -> list[str]:
    """List all available preset names."""
    return sorted(BUILTIN_PRESETS.keys())

@lru_cache(maxsize=32)
def resolve_preset_fields(name: str) -> tuple[frozenset[str], frozenset[str]]:
    """Resolve all fields and patterns including inherited ones.

    Cached for performance - inheritance resolution happens once per preset.
    """
    preset = get_redaction_preset(name)
    fields, patterns = preset.resolve(BUILTIN_PRESETS)
    return frozenset(fields), frozenset(patterns)

def get_presets_by_regulation(regulation: str) -> list[str]:
    """Get preset names matching a regulation (e.g., 'GDPR', 'HIPAA')."""
    return [
        name for name, preset in BUILTIN_PRESETS.items()
        if preset.regulation == regulation
    ]

def get_presets_by_region(region: str) -> list[str]:
    """Get preset names matching a region (e.g., 'US', 'UK', 'EU')."""
    return [
        name for name, preset in BUILTIN_PRESETS.items()
        if preset.region == region
    ]

def get_presets_by_tag(tag: str) -> list[str]:
    """Get preset names containing a tag (e.g., 'pii', 'healthcare')."""
    return [
        name for name, preset in BUILTIN_PRESETS.items()
        if tag in preset.tags
    ]
```

### Performance Consideration

The current field masking is O(n) where n is the number of fields in the log event. Adding more fields to the preset does not significantly impact performance because:

1. Field lookup uses dict key matching, not iteration
2. Regex patterns are compiled once at config time
3. Pattern matching is against field *paths*, not field *values*

Benchmark validation should confirm <5% overhead vs. no redaction for typical payloads.

---

## Tasks

### Phase 1: Core Infrastructure

- [ ] Create `src/fapilog/redaction/` package
- [ ] Implement `RedactionPreset` dataclass with `extends`, `regulation`, `region`, `tags`
- [ ] Implement `resolve()` method for inheritance resolution
- [ ] Add cycle detection for inheritance (prevent circular extends)
- [ ] Cache resolved presets for performance
- [ ] Implement preset registry:
  - `get_redaction_preset(name)` — get single preset
  - `list_redaction_presets()` — list all names
  - `resolve_preset_fields(name)` — get resolved fields/patterns
  - `get_presets_by_regulation(regulation)` — filter by regulation
  - `get_presets_by_region(region)` — filter by region
  - `get_presets_by_tag(tag)` — filter by tag
- [ ] Add unit tests for registry and inheritance

### Phase 2: Preset Definitions

- [ ] Define base presets: `CONTACT_INFO`, `PERSONAL_IDENTIFIERS`, `ONLINE_IDENTIFIERS`, `FINANCIAL_IDENTIFIERS`
- [ ] Define regional presets: `US_GOVERNMENT_IDS`, `UK_GOVERNMENT_IDS`, `EU_GOVERNMENT_IDS`
- [ ] Define regulation presets: `GDPR_PII`, `GDPR_PII_UK`, `CCPA_PII`, `HIPAA_PHI`
- [ ] Define security presets: `PCI_DSS`, `CREDENTIALS`
- [ ] Add tests verifying each preset covers required fields

### Phase 3: Builder Integration

- [ ] Add `with_redaction_preset()` to `LoggerBuilder`
- [ ] Resolve inheritance at builder time (not runtime)
- [ ] Ensure presets are composable (multiple calls merge)
- [ ] Ensure custom fields extend presets
- [ ] Add integration tests for builder usage

### Phase 4: Environment Preset Integration

- [ ] Refactor `production` preset to use `CREDENTIALS` internally
- [ ] Update `fastapi` and `serverless` presets similarly
- [ ] Verify existing tests still pass

### Phase 5: Documentation

- [ ] Create `docs/user-guide/redaction-presets.md`
- [ ] Document preset hierarchy diagram
- [ ] Document each regulation preset and what it covers
- [ ] Add preset field lists to documentation for audit purposes
- [ ] Update `docs/core-concepts/redaction.md` with preset info
- [ ] Update CHANGELOG

---

## Tests

### Unit Tests

- `tests/unit/test_redaction_presets.py`
  - `test_list_presets_returns_all_builtin_names`
  - `test_get_preset_returns_preset_object`
  - `test_get_preset_unknown_raises_valueerror`
  - `test_preset_is_immutable`
  - `test_preset_metadata_fields`
  - **Inheritance tests:**
  - `test_resolve_preset_includes_own_fields`
  - `test_resolve_preset_includes_parent_fields`
  - `test_resolve_preset_includes_grandparent_fields`
  - `test_circular_inheritance_raises_error`
  - **Filtering tests:**
  - `test_get_presets_by_regulation`
  - `test_get_presets_by_region`
  - `test_get_presets_by_tag`
  - `test_get_presets_by_multiple_tags`
  - **Coverage tests:**
  - `test_gdpr_preset_resolves_contact_fields`
  - `test_gdpr_preset_resolves_identity_fields`
  - `test_gdpr_uk_includes_nhs_number`
  - `test_ccpa_includes_ssn`
  - `test_hipaa_includes_mrn`
  - `test_pci_includes_card_fields`
  - `test_credentials_includes_secrets`

### Integration Tests

- `tests/integration/test_redaction_preset_integration.py`
  - `test_single_preset_redacts_fields`
  - `test_inherited_preset_redacts_parent_fields`
  - `test_multiple_presets_composable`
  - `test_custom_fields_extend_preset`
  - `test_preset_with_environment_preset`
  - `test_gdpr_preset_redacts_email`
  - `test_gdpr_preset_redacts_phone`
  - `test_gdpr_uk_redacts_ni_number`
  - `test_hipaa_redacts_mrn`
  - `test_pci_preset_redacts_card_number`

### Performance Tests

- `tests/performance/test_preset_overhead.py`
  - `test_preset_resolution_cached`
  - `test_single_preset_overhead_under_5_percent`
  - `test_inherited_preset_overhead_under_5_percent`

---

## Definition of Done

### Code Complete

- [ ] All acceptance criteria implemented
- [ ] Code follows project patterns
- [ ] No new linting errors

### Quality Assurance

- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] `ruff check` passes
- [ ] `mypy` passes
- [ ] No regression in existing tests
- [ ] Performance benchmark confirms <5% overhead

### Documentation

- [ ] User guide for redaction presets
- [ ] Preset field lists documented (for compliance audit)
- [ ] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Preset field lists are incomplete for compliance
   - **Mitigation:** Document that presets are "defense in depth", not complete compliance solutions; encourage users to audit and extend

2. **Risk:** Field name collisions (e.g., "name" matches "filename")
   - **Mitigation:** Use word boundaries in patterns; document known limitations; allow users to exclude fields

3. **Risk:** Performance regression from large field lists
   - **Mitigation:** Benchmark before/after; field matching is O(1) per field via dict lookup

### Rollback Plan

If issues occur:
1. Presets are additive - disabling is just removing the `with_redaction_preset()` call
2. Registry is isolated - can be removed without affecting existing redactor functionality

---

## Related Stories

- **Related:** Story 3.7 - Secure Defaults URL Credential Redaction (similar pattern)
- **Related:** Story 4.50 - Regex ReDoS Protection (patterns must be safe)
- **Enables:** Future story for content pattern scanning (opt-in, performance-gated)
- **Enables:** Future story for user-defined custom presets (register_redaction_preset API)
- **Enables:** Future story for preset file loading (YAML/JSON preset definitions)
- **Enables:** Future story for additional regulations (LGPD, PIPEDA, PDPA, etc.)
- **Enables:** Future story for preset versioning (track regulation updates)

---

## Business Value

**For developers:**
- One-liner protection: `.with_redaction_preset("GDPR_PII")` or `.with_redaction_preset("HIPAA_PHI")`
- No need to research regulation requirements
- Regional variants: `GDPR_PII_UK` for UK-specific fields (NI numbers, NHS numbers)
- Composable: combine presets and add domain-specific fields

**For businesses:**
- Multi-regulation support: GDPR, CCPA, HIPAA, PCI-DSS in one library
- Defense-in-depth against PII/PHI leakage
- Auditable: `resolve_preset_fields()` documents exactly what's redacted
- Regional compliance: UK GDPR, California CCPA, US HIPAA variants
- Demonstrates due diligence for compliance audits
- Clear promise: "Known PII field names are automatically redacted"

**The honest positioning:**
> Fapilog provides regulation-aware redaction presets covering GDPR, CCPA, HIPAA, and PCI-DSS. Presets are hierarchical (GDPR_PII_UK extends GDPR_PII), composable (combine multiple regulations), and extensible (add your domain-specific fields). We catch predictable PII patterns by field name — you add your application's custom sensitive data.

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-26 | Initial draft | Claude |
| 2026-01-26 | Added preset inheritance, regional variants, multi-regulation support | Claude |
