# Sprint: Quality Suite Hardening

## Overview

**Goal**: Transform the quality test suite from threshold-based enforcement to baseline-based enforcement, fix detection holes, and add self-tests to prevent regression.

**Priority**: High - Quality gates are the primary defense against technical debt creep

**Estimated Effort**: Medium (2-3 days)

## Problem Statement

The current quality suite has several weaknesses that make it "gameable" or prone to silent failures:

1. **Threshold-Based Enforcement**: Using `max_allowed = N` caps that drift over time
2. **Silent Parse Failures**: `except SyntaxError: continue` hides unparseable files
3. **Detection Holes**: Some rules are compiled but not applied (skipif), or have logic bugs (hardcoded paths)
4. **Allowlist Maintenance Sink**: Magic values allowlists will grow unbounded
5. **Inconsistent File Discovery**: Multiple `find_python_files()` implementations with different excludes
6. **No Self-Tests**: Quality detectors can silently degrade

## Proposed Architecture

### 1. Baseline-Based Enforcement System

Replace all `assert len(violations) <= N` with baseline comparison:

```
tests/quality/
├── __init__.py
├── _baseline.py              # NEW: Baseline loading and comparison
├── baselines.json            # NEW: Frozen violation snapshots
├── test_baseline_self.py     # NEW: Self-tests for baseline system
├── test_code_smells.py       # MODIFIED: Use baseline enforcement
├── test_stale_code.py        # MODIFIED: Use baseline enforcement
├── test_test_smells.py       # MODIFIED: Use baseline enforcement
├── test_magic_values.py      # MODIFIED: Use baseline enforcement
├── test_duplicate_code.py    # MODIFIED: Use baseline enforcement
├── test_unnecessary_wrappers.py  # MODIFIED: Use baseline enforcement
├── test_decentralized_helpers.py # MODIFIED: Use baseline enforcement
└── _helpers.py               # NEW: Centralized file discovery
```

### 2. Stable Violation IDs

Violation IDs must be stable across refactors (avoid line-number-only IDs):

| Rule Category | ID Format |
|---------------|-----------|
| Function-level | `rule|relative_path|function_name` |
| Class-level | `rule|relative_path|class_name` |
| Line-level | `rule|relative_path|content_hash` |
| Wrapper | `thin_wrapper|relative_path|function_name|wrapped_call` |

### 3. Baseline JSON Structure

```json
{
  "schema_version": 1,
  "generated_at": "2025-12-31T00:00:00Z",
  "rules": {
    "high_complexity": [
      "src/noteflow/infrastructure/summarization/_parsing.py|parse_llm_response",
      "src/noteflow/grpc/_mixins/streaming/_mixin.py|StreamTranscription"
    ],
    "thin_wrapper": [
      "src/noteflow/config/settings.py|get_settings|_load_settings"
    ],
    "stale_todo": [
      "src/noteflow/grpc/service.py|hash:abc123"
    ]
  }
}
```

## Implementation Plan

### Phase 1: Foundation (Day 1)

#### Task 1.1: Create Baseline Infrastructure

**File**: `tests/quality/_baseline.py`

```python
"""Baseline-based quality enforcement infrastructure.

This module provides the foundation for "no new debt" quality gates.
Instead of allowing N violations, we compare against a frozen baseline
of existing violations. Any new violation fails immediately.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

BASELINE_PATH = Path(__file__).parent / "baselines.json"
SCHEMA_VERSION = 1


@dataclass(frozen=True)
class Violation:
    """Represents a quality rule violation with stable identity."""

    rule: str
    relative_path: str
    identifier: str  # function/class name or content hash
    detail: str = ""  # optional detail (wrapped call, metric value, etc.)

    @property
    def stable_id(self) -> str:
        """Generate stable ID for baseline comparison."""
        parts = [self.rule, self.relative_path, self.identifier]
        if self.detail:
            parts.append(self.detail)
        return "|".join(parts)

    def __str__(self) -> str:
        """Human-readable representation."""
        return f"{self.relative_path}:{self.identifier} [{self.rule}]"


@dataclass
class BaselineResult:
    """Result of baseline comparison."""

    new_violations: list[Violation]
    fixed_violations: list[str]  # IDs that were in baseline but not found
    current_count: int
    baseline_count: int

    @property
    def passed(self) -> bool:
        """True if no new violations introduced."""
        return len(self.new_violations) == 0


def load_baseline() -> dict[str, set[str]]:
    """Load baseline violations from JSON file."""
    if not BASELINE_PATH.exists():
        return {}

    data = json.loads(BASELINE_PATH.read_text(encoding="utf-8"))

    # Version check
    if data.get("schema_version", 0) != SCHEMA_VERSION:
        raise ValueError(
            f"Baseline schema version mismatch: "
            f"expected {SCHEMA_VERSION}, got {data.get('schema_version')}"
        )

    return {rule: set(ids) for rule, ids in data.get("rules", {}).items()}


def save_baseline(violations_by_rule: dict[str, list[Violation]]) -> None:
    """Save current violations as new baseline.

    This should only be called manually when intentionally updating the baseline.
    """
    data = {
        "schema_version": SCHEMA_VERSION,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "rules": {
            rule: sorted(v.stable_id for v in violations)
            for rule, violations in violations_by_rule.items()
        }
    }

    BASELINE_PATH.write_text(
        json.dumps(data, indent=2, sort_keys=True) + "\n",
        encoding="utf-8"
    )


def assert_no_new_violations(
    rule: str,
    current_violations: list[Violation],
    *,
    max_new_allowed: int = 0,
) -> BaselineResult:
    """Assert no new violations beyond the frozen baseline.

    Args:
        rule: The rule name (e.g., "high_complexity", "thin_wrapper")
        current_violations: List of violations found in current scan
        max_new_allowed: Allow up to N new violations (default 0)

    Returns:
        BaselineResult with comparison details

    Raises:
        AssertionError: If new violations exceed max_new_allowed
    """
    baseline = load_baseline()
    allowed_ids = baseline.get(rule, set())

    current_ids = {v.stable_id for v in current_violations}

    new_ids = current_ids - allowed_ids
    fixed_ids = allowed_ids - current_ids

    new_violations = [v for v in current_violations if v.stable_id in new_ids]

    result = BaselineResult(
        new_violations=sorted(new_violations, key=lambda v: v.stable_id),
        fixed_violations=sorted(fixed_ids),
        current_count=len(current_violations),
        baseline_count=len(allowed_ids),
    )

    if len(new_violations) > max_new_allowed:
        message_parts = [
            f"[{rule}] {len(new_violations)} NEW violations introduced "
            f"(baseline: {len(allowed_ids)}, current: {len(current_violations)}):",
        ]
        for v in new_violations[:20]:
            message_parts.append(f"  + {v}")

        if fixed_ids:
            message_parts.append(f"\nFixed {len(fixed_ids)} violations (can update baseline):")
            for fid in list(fixed_ids)[:5]:
                message_parts.append(f"  - {fid}")

        raise AssertionError("\n".join(message_parts))

    return result


def content_hash(content: str, length: int = 8) -> str:
    """Generate short hash of content for stable line-level IDs."""
    return hashlib.sha256(content.encode()).hexdigest()[:length]
```

#### Task 1.2: Create Centralized File Discovery

**File**: `tests/quality/_helpers.py`

```python
"""Centralized helpers for quality tests.

All quality tests should use these helpers to ensure consistent
file discovery and avoid gaps in coverage.
"""

from __future__ import annotations

import ast
from pathlib import Path

# Root paths
PROJECT_ROOT = Path(__file__).parent.parent.parent
SRC_ROOT = PROJECT_ROOT / "src" / "noteflow"
TESTS_ROOT = PROJECT_ROOT / "tests"

# Excluded patterns (generated code)
GENERATED_PATTERNS = {"*_pb2.py", "*_pb2_grpc.py", "*_pb2.pyi"}

# Excluded directories
EXCLUDED_DIRS = {".venv", "__pycache__", "node_modules", ".git"}


def find_source_files(
    root: Path = SRC_ROOT,
    *,
    include_tests: bool = False,
    include_conftest: bool = False,
    include_migrations: bool = False,
    include_quality: bool = False,
) -> list[Path]:
    """Find Python source files with consistent exclusions.

    Args:
        root: Root directory to search
        include_tests: Include test files (test_*.py)
        include_conftest: Include conftest.py files
        include_migrations: Include Alembic migration files
        include_quality: Include tests/quality/ files

    Returns:
        List of Path objects for matching files
    """
    files: list[Path] = []

    for py_file in root.rglob("*.py"):
        # Skip excluded directories
        if any(d in py_file.parts for d in EXCLUDED_DIRS):
            continue

        # Skip generated files
        if any(py_file.match(p) for p in GENERATED_PATTERNS):
            continue

        # Skip conftest unless included
        if not include_conftest and py_file.name == "conftest.py":
            continue

        # Skip migrations unless included
        if not include_migrations and "migrations" in py_file.parts:
            continue

        # Skip tests unless included
        if not include_tests and "tests" in py_file.parts:
            continue

        # Skip quality tests unless included (prevents recursion)
        if not include_quality and "quality" in py_file.parts:
            continue

        files.append(py_file)

    return sorted(files)


def find_test_files(
    root: Path = TESTS_ROOT,
    *,
    include_quality: bool = False,
) -> list[Path]:
    """Find test files with consistent exclusions.

    Args:
        root: Root directory to search
        include_quality: Include tests/quality/ files

    Returns:
        List of test file paths
    """
    files: list[Path] = []

    for py_file in root.rglob("test_*.py"):
        # Skip excluded directories
        if any(d in py_file.parts for d in EXCLUDED_DIRS):
            continue

        # Skip quality tests unless included
        if not include_quality and "quality" in py_file.parts:
            continue

        files.append(py_file)

    return sorted(files)


def parse_file_safe(file_path: Path) -> tuple[ast.AST | None, str | None]:
    """Parse a Python file, returning AST or error message.

    Unlike bare `ast.parse`, this never silently fails.

    Returns:
        (ast, None) on success
        (None, error_message) on failure
    """
    try:
        source = file_path.read_text(encoding="utf-8")
        tree = ast.parse(source)
        return tree, None
    except SyntaxError as e:
        return None, f"{file_path}: SyntaxError at line {e.lineno}: {e.msg}"
    except Exception as e:
        return None, f"{file_path}: {type(e).__name__}: {e}"


def relative_path(file_path: Path, root: Path = SRC_ROOT) -> str:
    """Get path relative to project root for stable IDs."""
    try:
        return str(file_path.relative_to(PROJECT_ROOT))
    except ValueError:
        return str(file_path)
```

#### Task 1.3: Create Self-Tests for Quality Infrastructure

**File**: `tests/quality/test_baseline_self.py`

```python
"""Self-tests for quality infrastructure.

These tests ensure the quality detectors themselves work correctly.
This prevents the quality suite from silently degrading.
"""

from __future__ import annotations

import ast
import tempfile
from pathlib import Path

import pytest

from tests.quality._baseline import (
    Violation,
    assert_no_new_violations,
    content_hash,
    load_baseline,
)
from tests.quality._helpers import parse_file_safe


class TestParseFileSafe:
    """Tests for safe file parsing."""

    def test_valid_python_parses(self, tmp_path: Path) -> None:
        """Valid Python code should parse successfully."""
        file = tmp_path / "valid.py"
        file.write_text("def foo(): pass\n")

        tree, error = parse_file_safe(file)

        assert tree is not None
        assert error is None

    def test_syntax_error_returns_message(self, tmp_path: Path) -> None:
        """Syntax errors should return descriptive message, not raise."""
        file = tmp_path / "invalid.py"
        file.write_text("def foo(\n")  # Incomplete

        tree, error = parse_file_safe(file)

        assert tree is None
        assert error is not None
        assert "SyntaxError" in error


class TestViolation:
    """Tests for Violation dataclass."""

    def test_stable_id_format(self) -> None:
        """Stable ID should include all components."""
        v = Violation(
            rule="thin_wrapper",
            relative_path="src/foo.py",
            identifier="my_func",
            detail="wrapped_call",
        )

        assert v.stable_id == "thin_wrapper|src/foo.py|my_func|wrapped_call"

    def test_stable_id_without_detail(self) -> None:
        """Stable ID should work without detail."""
        v = Violation(
            rule="high_complexity",
            relative_path="src/bar.py",
            identifier="complex_func",
        )

        assert v.stable_id == "high_complexity|src/bar.py|complex_func"


class TestContentHash:
    """Tests for content hashing."""

    def test_same_content_same_hash(self) -> None:
        """Same content should produce same hash."""
        content = "# TODO: fix this"

        assert content_hash(content) == content_hash(content)

    def test_different_content_different_hash(self) -> None:
        """Different content should produce different hash."""
        assert content_hash("foo") != content_hash("bar")


# =============================================================================
# Detector Self-Tests
# =============================================================================


class TestSkipifDetection:
    """Self-tests for skipif detection (prevents the hole we found)."""

    def test_detects_skip_without_reason(self) -> None:
        """Should detect @pytest.mark.skip without reason."""
        code = '''
@pytest.mark.skip
def test_something():
    pass
'''
        # This is what the detector should catch
        import re
        skip_pattern = re.compile(r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE)

        matches = skip_pattern.findall(code)
        assert len(matches) == 1

    def test_detects_skip_with_empty_parens(self) -> None:
        """Should detect @pytest.mark.skip() with empty parens."""
        code = "@pytest.mark.skip()\ndef test_foo(): pass"
        import re
        skip_pattern = re.compile(r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE)

        assert skip_pattern.search(code) is not None

    def test_detects_skipif_without_reason(self) -> None:
        """Should detect @pytest.mark.skipif without reason keyword."""
        code = '@pytest.mark.skipif(sys.platform == "win32")\ndef test_foo(): pass'
        import re
        # This pattern should match skipif without reason=
        skipif_pattern = re.compile(
            r"@pytest\.mark\.skipif\s*\([^)]*\)(?!\s*#.*reason)",
            re.MULTILINE
        )

        # The current code compiles but doesn't use this pattern - this is the bug!
        # The test validates what SHOULD happen
        match = skipif_pattern.search(code)
        # We expect to find it (without reason=)
        assert match is not None
        # But if reason= is present, we shouldn't match
        code_with_reason = '@pytest.mark.skipif(sys.platform == "win32", reason="Windows")'
        assert "reason=" in code_with_reason


class TestHardcodedPathDetection:
    """Self-tests for hardcoded path detection (fixes the split bug)."""

    def test_detects_home_path(self) -> None:
        """Should detect /home/user paths."""
        import re
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        line = 'PATH = "/home/user/data"'
        assert re.search(pattern, line) is not None

    def test_ignores_path_in_comment(self) -> None:
        """Should ignore paths that appear after # comment."""
        import re
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        line = '# Example: PATH = "/home/user/data"'
        match = re.search(pattern, line)

        if match:
            # The bug: line.split(pattern) doesn't work because pattern is regex
            # This is the CORRECT check:
            comment_pos = line.find("#")
            if comment_pos != -1 and comment_pos < match.start():
                # Path is in comment, should be ignored
                assert True
            else:
                # Path is NOT in comment, should be flagged
                assert False, "Should have been ignored"

    def test_detects_path_with_inline_comment_after(self) -> None:
        """Path before inline comment should still be detected."""
        import re
        pattern = r'["\']\/(?:home|usr|var|etc|opt|tmp)\/\w+'

        line = 'PATH = "/home/user/thing"  # legit comment'
        match = re.search(pattern, line)

        assert match is not None
        # Comment is AFTER the match, so this should be flagged
        comment_pos = line.find("#")
        assert comment_pos > match.start(), "Comment should be after the path"


class TestThinWrapperDetection:
    """Self-tests for thin wrapper detection."""

    def test_detects_simple_passthrough(self) -> None:
        """Should detect simple return-only wrappers."""
        code = '''
def wrapper():
    return wrapped()
'''
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.FunctionDef)

        # The body has one statement (Return with Call)
        assert len(func.body) == 1
        stmt = func.body[0]
        assert isinstance(stmt, ast.Return)
        assert isinstance(stmt.value, ast.Call)

    def test_detects_await_passthrough(self) -> None:
        """Should detect async return await wrappers."""
        code = '''
async def wrapper():
    return await wrapped()
'''
        tree = ast.parse(code)
        func = tree.body[0]
        assert isinstance(func, ast.AsyncFunctionDef)

        stmt = func.body[0]
        assert isinstance(stmt, ast.Return)
        # The value is Await wrapping Call
        assert isinstance(stmt.value, ast.Await)
        assert isinstance(stmt.value.value, ast.Call)

    def test_ignores_wrapper_with_logic(self) -> None:
        """Should ignore wrappers that add logic."""
        code = '''
def wrapper(x):
    if x:
        return wrapped()
    return None
'''
        tree = ast.parse(code)
        func = tree.body[0]

        # Multiple statements = not a thin wrapper
        assert len(func.body) > 1
```

### Phase 2: Fix Detection Holes (Day 1-2)

#### Task 2.1: Fix skipif Detection Bug

**File**: `tests/quality/test_test_smells.py`

The current code compiles the skipif pattern but never uses it:

```python
# CURRENT (broken):
skip_pattern = re.compile(r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE)
re.compile(  # <-- compiled but NOT assigned!
    r"@pytest\.mark\.skipif\s*\([^)]*\)\s*$", re.MULTILINE
)

# FIXED:
skip_pattern = re.compile(r"@pytest\.mark\.skip\s*(?:\(\s*\))?$", re.MULTILINE)
skipif_pattern = re.compile(
    r"@pytest\.mark\.skipif\s*\([^)]*\)(?!\s*,\s*reason=)",
    re.MULTILINE
)
```

Then use both patterns in the detection loop.

#### Task 2.2: Fix Hardcoded Path Detection Bug

**File**: `tests/quality/test_magic_values.py`

The current code has a logic bug with `line.split(pattern)`:

```python
# CURRENT (broken):
if re.search(pattern, line):
    if "test" not in line.lower() and "#" not in line.split(pattern)[0]:
        # line.split(pattern) splits on LITERAL string, not regex!
        violations.append(...)

# FIXED:
match = re.search(pattern, line)
if match:
    # Check if # appears BEFORE the match
    comment_pos = line.find("#")
    if comment_pos != -1 and comment_pos < match.start():
        continue  # Path is in comment, skip
    if "test" not in line.lower():
        violations.append(...)
```

#### Task 2.3: Fix Silent SyntaxError Handling

Replace all `except SyntaxError: continue` with error collection:

```python
# CURRENT (silent failure):
for py_file in find_python_files(src_root):
    source = py_file.read_text(encoding="utf-8")
    try:
        tree = ast.parse(source)
    except SyntaxError:
        continue  # <-- Silent skip!

# FIXED (fail loudly):
from tests.quality._helpers import parse_file_safe

parse_errors: list[str] = []

for py_file in find_python_files(src_root):
    tree, error = parse_file_safe(py_file)
    if error:
        parse_errors.append(error)
        continue
    # ... process tree ...

# At the end of the test:
assert not parse_errors, (
    f"Quality scan hit {len(parse_errors)} parse error(s):\n"
    + "\n".join(parse_errors)
)
```

### Phase 3: Migrate Tests to Baseline (Day 2)

#### Task 3.1: Migrate High-Impact Tests First

Priority order (highest gaming risk first):

1. `test_no_stale_todos` - Easy to add TODOs
2. `test_no_trivial_wrapper_functions` - High cap (42)
3. `test_no_high_complexity_functions` - Complexity creep
4. `test_no_long_parameter_lists` - High cap (35)
5. `test_no_repeated_code_patterns` - Very high cap (177)

Example migration for `test_no_stale_todos`:

```python
# BEFORE:
def test_no_stale_todos() -> None:
    # ... detection logic ...
    max_allowed = 10
    assert len(stale_comments) <= max_allowed, ...

# AFTER:
from tests.quality._baseline import Violation, assert_no_new_violations, content_hash
from tests.quality._helpers import find_source_files, parse_file_safe, relative_path

def test_no_stale_todos() -> None:
    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files():
        lines = py_file.read_text(encoding="utf-8").splitlines()
        rel_path = relative_path(py_file)

        for i, line in enumerate(lines, start=1):
            match = stale_pattern.search(line)
            if match:
                tag = match.group(1).upper()
                message = match.group(2).strip()
                violations.append(
                    Violation(
                        rule="stale_todo",
                        relative_path=rel_path,
                        identifier=content_hash(f"{i}:{line.strip()}"),
                        detail=tag,
                    )
                )

    assert not parse_errors, "\n".join(parse_errors)
    assert_no_new_violations("stale_todo", violations)
```

#### Task 3.2: Generate Initial Baseline

After migrating all tests, generate the baseline:

```bash
# Run with special env var to generate baseline
QUALITY_GENERATE_BASELINE=1 pytest tests/quality/ -v

# Or use a management script
python -m tests.quality._baseline --generate
```

### Phase 4: Advanced Improvements (Day 3)

#### Task 4.1: Replace Magic Value Allowlists with "Must Be Named" Rule

Instead of maintaining `ALLOWED_NUMBERS` and `ALLOWED_STRINGS`, use:

```python
def test_no_repeated_literals() -> None:
    """Detect literals that repeat and should be constants.

    Rule: Any literal that appears more than once in a module or
    across multiple modules should be promoted to a named constant.

    Universal exceptions (0, 1, -1, "utf-8", HTTP verbs) are allowed.
    """
    UNIVERSAL_EXCEPTIONS = {
        0, 1, 2, -1,  # Universal integers
        0.0, 1.0,     # Universal floats
        "", " ", "\n", "\t",  # Universal strings
        "utf-8", "utf-8",
        "GET", "POST", "PUT", "DELETE", "PATCH",
    }

    literal_occurrences: dict[object, list[Violation]] = defaultdict(list)

    for py_file in find_source_files():
        # ... collect literals ...
        for node in ast.walk(tree):
            if isinstance(node, ast.Constant):
                value = node.value
                if value in UNIVERSAL_EXCEPTIONS:
                    continue
                if isinstance(value, str) and len(value) < 3:
                    continue  # Short strings OK
                # ... add to occurrences ...

    # Flag any literal appearing 3+ times
    violations = []
    for value, occurrences in literal_occurrences.items():
        if len(occurrences) >= 3:
            violations.extend(occurrences)

    assert_no_new_violations("repeated_literal", violations)
```

#### Task 4.2: Add CODEOWNERS Protection

**File**: `.github/CODEOWNERS`

```
# Quality suite requires maintainer approval
tests/quality/ @your-team/maintainers
tests/quality/baselines.json @your-team/maintainers
```

### Phase 5: Documentation and Rollout

#### Task 5.1: Update QUALITY_STANDARDS.md

Add section explaining baseline enforcement:

```markdown
## Baseline-Based Quality Gates

Quality tests use baseline enforcement instead of fixed caps:

- **No new debt**: Adding any new violation fails immediately
- **Baseline file**: `tests/quality/baselines.json` freezes existing violations
- **Reducing debt**: Fix violations, then update baseline to remove entries
- **Protected file**: Baseline changes require maintainer approval

### Updating the Baseline

When you've fixed violations and want to update the baseline:

```bash
# Regenerate baseline with current violations
python -m pytest tests/quality/ --generate-baseline

# Review and commit
git diff tests/quality/baselines.json
git add tests/quality/baselines.json
git commit -m "chore: reduce quality baseline (N violations fixed)"
```

### Adding Exceptions (Rare)

If a new violation is intentional:

1. Document why in a comment near the code
2. Add the stable ID to `baselines.json`
3. Get maintainer approval for the change
```

## Migration Strategy

### Step 1: Add Infrastructure (No Behavior Change)

1. Add `_baseline.py` and `_helpers.py`
2. Add `test_baseline_self.py`
3. Verify self-tests pass

### Step 2: Fix Detection Bugs

1. Fix skipif detection
2. Fix hardcoded path comment handling
3. Fix silent SyntaxError handling
4. Verify with self-tests

### Step 3: Parallel Run Period

1. Add baseline checks alongside existing caps
2. Both must pass (cap AND baseline)
3. Monitor for issues

### Step 4: Remove Caps

1. Remove `max_allowed` assertions
2. Baseline becomes sole enforcement
3. Generate and commit initial baseline

### Step 5: Reduce Baseline Over Time

1. Sprint goals include "reduce N violations"
2. Update baseline when fixes land
3. Celebrate progress!

## Success Metrics

| Metric | Current | Target |
|--------|---------|--------|
| Parse error handling | Silent skip | Fail loudly |
| Enforcement mechanism | Threshold caps | Baseline comparison |
| Detection holes | 2+ known | 0 known |
| Self-test coverage | 0% | 10+ detector tests |
| Baseline violations | N/A | Tracked and decreasing |

## Risks and Mitigations

| Risk | Mitigation |
|------|------------|
| Baseline file conflicts | Small file, clear ownership |
| Too strict initially | Start with current counts frozen |
| Self-tests incomplete | Add tests as holes are found |
| Agent edits baseline | CODEOWNERS + branch protection |

## References

- [Test Smells](https://testsmells.org/)
- [xUnit Test Patterns](http://xunitpatterns.com/)
- [Quality Debt](https://martinfowler.com/bliki/TechnicalDebt.html)
