"""Tests for pipeline_validator.py — structural + semantic invariant checks.

Tests the validator against synthetic pipeline runs with known
good/bad data to verify all 7 checks fire correctly.

Key properties tested:
    - Stage order enforcement across all 5 profiles
    - Artifact existence (required for all stages except reflect)
    - Artifact schema (required vs recommended fields)
    - Decision logging (required for non-optional stages)
    - Budget recording (token_cost > 0)
    - Profile respect (stage must be in selected profile)
    - DDD cross-document consistency (non-goals vs approach, failed patterns)
    - Summary command validates all stages
    - Edge cases: missing run, missing stage record, corrupt data
"""

import json
import os
import sys
from pathlib import Path

import pytest

# Add backend to path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from scripts.pipeline_validator import (
    DECISION_OPTIONAL_STAGES,
    NO_ARTIFACT_STAGES,
    STAGE_ROUTING,
    STAGE_SCHEMAS,
    _check_artifact_exists,
    _check_budget_recorded,
    _check_decision_logged,
    _check_output_routing,
    _check_profile_respected,
    _check_skip_justification,
    _check_stage_order,
    _parse_non_goals,
    _parse_failed_patterns,
    _compute_doc_checksum,
    check_artifact_freshness,
    check_ddd_consistency,
    check_ddd_staleness,
    validate,
)
from scripts.artifact_cli import (
    _extract_run_metrics,
    _record_validation_event,
    _load_artifact_for_metrics,
)
from core.pipeline_profiles import get_profile_stages, PIPELINE_PROFILES


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def workspace(tmp_path, monkeypatch):
    """Create a temporary workspace with a project and pipeline run."""
    ws = tmp_path / "SwarmWS"
    project_dir = ws / "Projects" / "TestProject" / ".artifacts"
    runs_dir = project_dir / "runs" / "run_test1"
    runs_dir.mkdir(parents=True)
    monkeypatch.setenv("SWARM_WORKSPACE", str(ws))
    return ws


def _make_run(runs_dir: Path, run_id: str = "run_test1", profile: str = "full",
              stages: list | None = None, status: str = "running") -> dict:
    """Create a run.json file and return the run dict."""
    run = {
        "id": run_id,
        "project": "TestProject",
        "requirement": "Test requirement",
        "profile": profile,
        "status": status,
        "stages": stages or [],
        "taste_decisions": [],
        "budget": {"session_total": 800000, "consumed": 0, "remaining": 800000,
                   "stage_estimates": {}, "calibration_source": "defaults"},
        "created_at": "2026-03-24T00:00:00Z",
        "updated_at": "2026-03-24T00:00:00Z",
        "completed_at": None,
    }
    run_file = runs_dir / "run.json"
    run_file.parent.mkdir(parents=True, exist_ok=True)
    run_file.write_text(json.dumps(run))
    return run


def _make_artifact(artifacts_dir: Path, run_id: str, artifact_id: str,
                   artifact_type: str, data: dict) -> None:
    """Create an artifact file and register in manifest."""
    # Write artifact data file in runs/<run_id>/
    run_dir = artifacts_dir / "runs" / run_id
    run_dir.mkdir(parents=True, exist_ok=True)
    filename = f"{artifact_type}-20260324.json"
    (run_dir / filename).write_text(json.dumps(data))

    # Update manifest
    manifest_file = artifacts_dir / "manifest.json"
    if manifest_file.exists():
        manifest = json.loads(manifest_file.read_text())
    else:
        manifest = {"artifacts": [], "pipeline_state": "evaluate"}

    manifest["artifacts"].append({
        "id": artifact_id,
        "type": artifact_type,
        "file": f"runs/{run_id}/{filename}",
        "producer": "test",
        "summary": "test artifact",
        "created_at": "2026-03-24T00:00:00Z",
        "run_id": run_id,
    })
    manifest_file.write_text(json.dumps(manifest))


_SENTINEL = object()

def _stage_record(stage: str, status: str = "completed",
                  artifact_id: str | None = "art_test",
                  token_cost: int = 5000,
                  decisions: list | object = _SENTINEL) -> dict:
    """Build a stage record dict."""
    if decisions is _SENTINEL:
        decisions = [
            {"description": "test decision", "classification": "mechanical",
             "reasoning": "test"}
        ]
    return {
        "stage": stage,
        "status": status,
        "artifact_id": artifact_id,
        "started_at": "2026-03-24T00:00:00Z",
        "completed_at": "2026-03-24T00:01:00Z",
        "token_cost": token_cost,
        "retry_count": 0,
        "notes": f"{stage} completed",
        "decisions": decisions,
    }


# ---------------------------------------------------------------------------
# Check 1: Stage Order
# ---------------------------------------------------------------------------

class TestStageOrder:
    def test_first_stage_always_valid(self):
        """First stage in any profile is always valid."""
        for profile_name, stages in PIPELINE_PROFILES.items():
            result = _check_stage_order(
                stages[0], profile_name,
                [_stage_record(stages[0])]
            )
            assert result is True, f"First stage '{stages[0]}' in '{profile_name}' should be valid"

    def test_second_stage_requires_first(self):
        """Second stage requires first to be completed."""
        stages_list = [
            _stage_record("evaluate", status="completed"),
            _stage_record("build", status="running"),
        ]
        # In trivial profile: evaluate -> build
        assert _check_stage_order("build", "trivial", stages_list) is True

    def test_skipped_stage_before_fails(self):
        """Skipping a required prior stage fails order check."""
        # In full profile: evaluate -> think -> plan -> build
        # Missing think and plan
        stages_list = [
            _stage_record("evaluate", status="completed"),
            _stage_record("build", status="running"),
        ]
        assert _check_stage_order("build", "full", stages_list) is False

    def test_skipped_status_counts_as_done(self):
        """Stages with status 'skipped' count as completed for order."""
        stages_list = [
            _stage_record("evaluate", status="completed"),
            _stage_record("think", status="skipped"),
            _stage_record("plan", status="skipped"),
            _stage_record("build", status="running"),
        ]
        assert _check_stage_order("build", "full", stages_list) is True

    def test_stage_not_in_profile(self):
        """Stage not in profile fails order check."""
        stages_list = [_stage_record("think")]
        # think is NOT in trivial profile
        assert _check_stage_order("think", "trivial", stages_list) is False


# ---------------------------------------------------------------------------
# Check 2: Artifact Exists
# ---------------------------------------------------------------------------

class TestArtifactExists:
    def test_with_artifact_id(self):
        assert _check_artifact_exists("build", {"artifact_id": "art_123"}) is True

    def test_missing_artifact_id(self):
        assert _check_artifact_exists("build", {"artifact_id": None}) is False

    def test_empty_artifact_id(self):
        assert _check_artifact_exists("build", {"artifact_id": ""}) is False

    def test_reflect_exempt(self):
        """reflect never needs an artifact."""
        assert _check_artifact_exists("reflect", {"artifact_id": None}) is True

    def test_all_non_reflect_require_artifact(self):
        """Every stage except reflect requires an artifact."""
        for stage in ["evaluate", "think", "plan", "build", "review", "test", "deliver"]:
            assert _check_artifact_exists(stage, {"artifact_id": None}) is False


# ---------------------------------------------------------------------------
# Check 3: Artifact Schema (tested via validate integration)
# ---------------------------------------------------------------------------

class TestArtifactSchema:
    def test_evaluate_required_fields(self, workspace):
        """Evaluate artifact must have recommendation and scope."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "trivial"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is True
        assert len(result["errors"]) == 0

    def test_evaluate_missing_required(self, workspace):
        """Missing required field produces BLOCK error."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        # Missing 'scope'
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is False
        assert any("scope" in e for e in result["errors"])

    def test_missing_recommended_is_warning(self, workspace):
        """Missing recommended field produces warning, not error."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        # Has required fields but missing recommended 'acceptance_criteria'
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "trivial"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is True  # Still valid
        assert any("acceptance_criteria" in w for w in result["warnings"])

    def test_all_stages_have_schemas(self):
        """Every non-reflect stage has a schema defined."""
        for stage in ["evaluate", "think", "plan", "build", "review", "test", "deliver"]:
            assert stage in STAGE_SCHEMAS, f"Missing schema for {stage}"


# ---------------------------------------------------------------------------
# Check 4: Decision Logged
# ---------------------------------------------------------------------------

class TestDecisionLogged:
    def test_with_decisions(self):
        record = _stage_record("build", decisions=[
            {"description": "x", "classification": "mechanical", "reasoning": "y"}
        ])
        assert _check_decision_logged("build", record) is True

    def test_no_decisions(self):
        record = _stage_record("build", decisions=[])
        assert _check_decision_logged("build", record) is False

    def test_reflect_optional(self):
        """Reflect doesn't require decisions."""
        record = _stage_record("reflect", decisions=[])
        assert _check_decision_logged("reflect", record) is True

    def test_deliver_optional(self):
        """Deliver doesn't require decisions."""
        record = _stage_record("deliver", decisions=[])
        assert _check_decision_logged("deliver", record) is True

    def test_optional_stages_match_constant(self):
        """DECISION_OPTIONAL_STAGES contains exactly reflect and deliver."""
        assert DECISION_OPTIONAL_STAGES == {"reflect", "deliver"}


# ---------------------------------------------------------------------------
# Check 5: Budget Recorded
# ---------------------------------------------------------------------------

class TestBudgetRecorded:
    def test_positive_cost(self):
        assert _check_budget_recorded({"token_cost": 5000}) is True

    def test_zero_cost(self):
        assert _check_budget_recorded({"token_cost": 0}) is False

    def test_missing_cost(self):
        assert _check_budget_recorded({}) is False

    def test_negative_cost(self):
        """Negative cost is technically > 0 check — this is False."""
        assert _check_budget_recorded({"token_cost": -1}) is False


# ---------------------------------------------------------------------------
# Check 6: Profile Respected
# ---------------------------------------------------------------------------

class TestProfileRespected:
    def test_stage_in_profile(self):
        assert _check_profile_respected("evaluate", "full") is True
        assert _check_profile_respected("build", "trivial") is True

    def test_stage_not_in_profile(self):
        assert _check_profile_respected("think", "trivial") is False
        assert _check_profile_respected("build", "research") is False
        assert _check_profile_respected("test", "docs") is False

    def test_all_profiles_include_evaluate(self):
        """Evaluate is in every profile."""
        for profile in PIPELINE_PROFILES:
            assert _check_profile_respected("evaluate", profile) is True

    def test_all_profiles_include_reflect(self):
        """Reflect is in every profile (except goal — handles it inside goal_cycle)."""
        for profile in PIPELINE_PROFILES:
            if profile == "goal":
                continue  # goal_cycle manages REFLECT internally (two-tier model)
            assert _check_profile_respected("reflect", profile) is True


# ---------------------------------------------------------------------------
# Integration: validate() full pipeline
# ---------------------------------------------------------------------------

class TestValidateIntegration:
    def test_missing_run(self, workspace):
        """Non-existent run returns error."""
        result = validate("TestProject", "run_nonexistent", "evaluate")
        assert result["valid"] is False
        assert "not found" in result["errors"][0]

    def test_missing_stage_record(self, workspace):
        """Stage not in run's stages list returns error."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_run(runs_dir, stages=[_stage_record("evaluate")])

        result = validate("TestProject", "run_test1", "build")
        assert result["valid"] is False
        assert "No stage record" in result["errors"][0]

    def test_perfect_stage(self, workspace):
        """A well-formed stage passes all 7 checks."""
        # Create DDD docs so check 7 doesn't warn about missing docs
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# Test\n\n## Non-Goals\n\n- **Not X** -- skip.\n")
        (project_dir / "TECH.md").write_text("# Test\n\n## Architecture\n\nDesktop app.\n")
        (project_dir / "IMPROVEMENT.md").write_text(
            "# Test\n\n## What Worked\n\n- OK\n\n## What Failed\n\n- **Retry logic was too aggressive** -- caused cascading failures\n"
        )
        (project_dir / "PROJECT.md").write_text("# Test\n")

        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard",
                        "acceptance_criteria": ["a"], "scores": {"s": 5}})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is True
        assert result["checks_passed"] >= 8  # 8 base + Check 13 routing
        assert result["checks_total"] >= 8
        assert len(result["errors"]) == 0
        assert len(result["warnings"]) == 0

    def test_reflect_passes_with_nothing(self, workspace):
        """Reflect stage passes with no artifact, no decisions, any token_cost."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        full_stages = [_stage_record(s) for s in get_profile_stages("full")[:-1]]
        full_stages.append(_stage_record("reflect", artifact_id=None, decisions=[]))
        _make_run(runs_dir, stages=full_stages)

        result = validate("TestProject", "run_test1", "reflect")
        assert result["valid"] is True
        assert result["checks_passed"] >= 8  # 8 base + Check 13 routing

    def test_warnings_dont_block(self, workspace):
        """Warnings don't make valid=false, and checks_passed stays at 7."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval", token_cost=0, decisions=[]),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is True  # No BLOCK errors
        assert result["checks_passed"] >= 8  # Warnings don't reduce count
        assert len(result["warnings"]) >= 2  # Missing decisions + zero budget

    def test_multiple_errors_accumulate(self, workspace):
        """Multiple violations all appear in errors list."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        # think stage in trivial profile (violation) + no artifact (violation)
        _make_run(runs_dir, profile="trivial", stages=[
            _stage_record("think", artifact_id=None),
        ])

        result = validate("TestProject", "run_test1", "think")
        assert result["valid"] is False
        assert len(result["errors"]) >= 2  # Profile + artifact


# ---------------------------------------------------------------------------
# Summary command integration
# ---------------------------------------------------------------------------

class TestSummary:
    def test_summary_validates_all_stages(self, workspace):
        """Summary validates all completed/running stages."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_artifact(artifacts_dir, "run_test1", "art_build", "changeset",
                       {"files_changed": ["a.py"], "tdd": {"green_pass": True}})

        _make_run(runs_dir, profile="trivial", stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
            _stage_record("build", artifact_id="art_build"),
        ])

        # Import and call the summary logic directly
        from scripts.pipeline_validator import _load_run
        run = _load_run("TestProject", "run_test1")
        assert run is not None

        results = []
        for stage_rec in run["stages"]:
            if stage_rec["status"] in ("completed", "running"):
                r = validate("TestProject", "run_test1", stage_rec["stage"])
                results.append(r)

        assert len(results) == 2
        assert all(r["valid"] for r in results)


# ---------------------------------------------------------------------------
# Check 7: DDD Cross-Document Consistency
# ---------------------------------------------------------------------------

class TestParseNonGoals:
    def test_extracts_bold_keywords(self):
        text = """## Non-Goals

- **Not a cloud SaaS** -- Desktop-first, local-first.
- **Not a general chatbot** -- Opinionated.
"""
        goals = _parse_non_goals(text)
        assert "not a cloud saas" in goals
        assert "not a general chatbot" in goals

    def test_empty_section(self):
        text = """## Non-Goals

## Next Section
"""
        assert _parse_non_goals(text) == []

    def test_no_section(self):
        text = "# Product\n\nSome content without non-goals."
        assert _parse_non_goals(text) == []

    def test_non_bold_bullets(self):
        text = """## Non-Goals

- We don't do cloud hosting
- No mobile app planned
"""
        goals = _parse_non_goals(text)
        assert len(goals) == 2
        assert "we don't do cloud hosting" in goals


class TestParseFailedPatterns:
    def test_extracts_bold_patterns(self):
        text = """## What Failed

- **Big-bang refactor of 5,000+ line module** -- caused 15+ bugs
- **Memory pipeline trusting its own output** -- stale snapshots
"""
        patterns = _parse_failed_patterns(text)
        assert len(patterns) == 2
        assert "big-bang refactor of 5,000+ line module" in patterns

    def test_empty_section(self):
        text = """## What Failed

## Known Issues
"""
        assert _parse_failed_patterns(text) == []

    def test_no_section(self):
        text = "# Improvement\n\nNo failures here."
        assert _parse_failed_patterns(text) == []


class TestDocChecksum:
    def test_deterministic(self):
        text = "Hello World"
        assert _compute_doc_checksum(text) == _compute_doc_checksum(text)

    def test_whitespace_insensitive(self):
        assert _compute_doc_checksum("Hello  World") == _compute_doc_checksum("Hello World")
        assert _compute_doc_checksum("Hello\n\nWorld") == _compute_doc_checksum("Hello World")

    def test_different_content(self):
        assert _compute_doc_checksum("Hello") != _compute_doc_checksum("World")


class TestDDDConsistency:
    def test_no_project_dir(self, workspace):
        """Missing project returns warning, not error."""
        result = check_ddd_consistency("NonExistentProject")
        assert len(result["warnings"]) >= 1
        assert "No DDD documents" in result["warnings"][0]

    def test_complete_project_no_conflicts(self, workspace):
        """Well-formed DDD docs with no conflicts produce no warnings."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("""# Test -- Product Context

## Non-Goals

- **Not a mobile app** -- Desktop only.
""")
        (project_dir / "TECH.md").write_text("""# Test -- Technical Context

## Architecture

Desktop app with Tauri shell and Python backend.
""")
        (project_dir / "IMPROVEMENT.md").write_text("""# Test -- Lessons

## What Worked

- Good stuff

## What Failed

- **Retry logic was too aggressive** -- caused cascading failures
""")
        (project_dir / "PROJECT.md").write_text("# Test -- Project Context\n")

        result = check_ddd_consistency("TestProject")
        assert len(result["warnings"]) == 0
        assert len(result["checksums"]) == 4
        assert len(result["non_goals"]) == 1
        assert len(result["failed_patterns"]) == 1

    def test_non_goal_conflict_detected(self, workspace):
        """Non-goal keyword appearing in TECH.md architecture triggers warning."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("""# Test

## Non-Goals

- **Not a cloud SaaS** -- Desktop only.
""")
        (project_dir / "TECH.md").write_text("""# Test

## Architecture

Cloud-native SaaS with microservices deployed on AWS.
""")

        result = check_ddd_consistency("TestProject")
        conflict_warnings = [w for w in result["warnings"] if "DDD conflict" in w]
        assert len(conflict_warnings) >= 1
        assert any("cloud" in w.lower() or "saas" in w.lower() for w in conflict_warnings)

    def test_non_goal_vs_context_text(self, workspace):
        """Non-goal keyword in pipeline context triggers warning."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("""# Test

## Non-Goals

- **Not a cloud SaaS** -- Desktop only.
""")
        (project_dir / "TECH.md").write_text("# Test\n\n## Architecture\n\nDesktop app.\n")

        result = check_ddd_consistency("TestProject", context_text="Deploy to cloud infrastructure")
        conflict_warnings = [w for w in result["warnings"] if "pipeline context" in w]
        assert len(conflict_warnings) >= 1

    def test_missing_docs_warned(self, workspace):
        """Missing DDD docs produce informational warning."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# Test\n\n## Non-Goals\n\n- **Nothing** -- nope\n")

        result = check_ddd_consistency("TestProject")
        incomplete = [w for w in result["warnings"] if "DDD incomplete" in w]
        assert len(incomplete) == 1
        assert "TECH.md" in incomplete[0]

    def test_empty_improvement_warned(self, workspace):
        """Empty What Failed section produces note."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# Test\n")
        (project_dir / "TECH.md").write_text("# Test\n")
        (project_dir / "IMPROVEMENT.md").write_text("""# Test

## What Worked

- Good stuff

## What Failed

## Known Issues
""")
        (project_dir / "PROJECT.md").write_text("# Test\n")

        result = check_ddd_consistency("TestProject")
        assert any("no 'What Failed' entries" in w for w in result["warnings"])

    def test_checksums_computed(self, workspace):
        """All present docs get checksums."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# A\n")
        (project_dir / "TECH.md").write_text("# B\n")

        result = check_ddd_consistency("TestProject")
        assert "PRODUCT.md" in result["checksums"]
        assert "TECH.md" in result["checksums"]
        assert len(result["checksums"]["PRODUCT.md"]) == 12  # md5[:12]


class TestDDDInValidate:
    """Check 7 runs within validate() at evaluate stage."""

    def test_ddd_check_runs_at_evaluate(self, workspace):
        """DDD check runs at evaluate and adds warnings (not errors)."""
        # Setup project with a conflict
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("""# Test

## Non-Goals

- **Not a cloud SaaS** -- Desktop only.
""")
        (project_dir / "TECH.md").write_text("""# Test

## Architecture

Cloud SaaS deployment with Kubernetes.
""")

        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        # DDD warnings should be present but not block
        assert result["valid"] is True
        ddd_warnings = [w for w in result["warnings"] if "DDD" in w]
        assert len(ddd_warnings) >= 1

    def test_ddd_check_skipped_on_other_stages(self, workspace):
        """DDD check auto-passes on non-evaluate stages (no extra warnings)."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_build", "changeset",
                       {"files_changed": ["a.py"]})
        _make_run(runs_dir, profile="trivial", stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
            _stage_record("build", artifact_id="art_build"),
        ])

        result = validate("TestProject", "run_test1", "build")
        ddd_warnings = [w for w in result["warnings"] if "DDD" in w]
        assert len(ddd_warnings) == 0

    def test_checks_total_is_8(self, workspace):
        """Verify checks_total is 8 (7 original + smoke/integration gate)."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["checks_total"] >= 8  # 8 base + Check 13 routing
        assert result["checks_passed"] >= 8


# ---------------------------------------------------------------------------
# DDD Staleness Detection
# ---------------------------------------------------------------------------

def _create_ddd_docs(project_dir: Path, product: str = "# Test\n",
                     tech: str = "# Test\n", improvement: str = "# Test\n",
                     project_ctx: str = "# Test\n") -> None:
    """Helper to create DDD docs in a project directory."""
    project_dir.mkdir(parents=True, exist_ok=True)
    (project_dir / "PRODUCT.md").write_text(product)
    (project_dir / "TECH.md").write_text(tech)
    (project_dir / "IMPROVEMENT.md").write_text(improvement)
    (project_dir / "PROJECT.md").write_text(project_ctx)


def _make_completed_run(workspace: Path, run_id: str, ddd_checksums: dict | None = None,
                        status: str = "completed") -> None:
    """Create a completed run.json with optional ddd_checksums."""
    runs_dir = workspace / "Projects" / "TestProject" / ".artifacts" / "runs" / run_id
    runs_dir.mkdir(parents=True, exist_ok=True)
    run = {
        "id": run_id,
        "project": "TestProject",
        "requirement": "Test",
        "profile": "full",
        "status": status,
        "stages": [_stage_record("evaluate")],
        "taste_decisions": [],
        "budget": {"session_total": 800000, "consumed": 0, "remaining": 800000,
                   "stage_estimates": {}, "calibration_source": "defaults"},
        "created_at": "2026-03-24T00:00:00Z",
        "updated_at": "2026-03-24T00:00:00Z",
        "completed_at": "2026-03-24T01:00:00Z",
    }
    if ddd_checksums is not None:
        run["ddd_checksums"] = ddd_checksums
    (runs_dir / "run.json").write_text(json.dumps(run))


class TestDDDStaleness:
    def test_no_runs(self, workspace):
        """Empty project has no stale runs."""
        _create_ddd_docs(workspace / "Projects" / "TestProject")
        result = check_ddd_staleness("TestProject")
        assert result["stale_runs"] == []
        assert result["fresh_runs"] == []
        assert len(result["current_checksums"]) == 4

    def test_fresh_run(self, workspace):
        """Run with matching checksums is fresh."""
        project_dir = workspace / "Projects" / "TestProject"
        _create_ddd_docs(project_dir)

        # Get current checksums and store them in a run
        current = check_ddd_consistency("TestProject")
        _make_completed_run(workspace, "run_fresh", ddd_checksums=current["checksums"])

        result = check_ddd_staleness("TestProject")
        assert len(result["fresh_runs"]) == 1
        assert "run_fresh" in result["fresh_runs"]
        assert result["stale_runs"] == []

    def test_stale_run_detected(self, workspace):
        """Run with old checksums is detected as stale after doc change."""
        project_dir = workspace / "Projects" / "TestProject"
        _create_ddd_docs(project_dir)

        # Store old checksums
        old_checksums = check_ddd_consistency("TestProject")["checksums"]
        _make_completed_run(workspace, "run_old", ddd_checksums=old_checksums)

        # Now change PRODUCT.md
        (project_dir / "PRODUCT.md").write_text("# Test v2 -- updated priorities\n")

        result = check_ddd_staleness("TestProject")
        assert len(result["stale_runs"]) == 1
        assert result["stale_runs"][0]["run_id"] == "run_old"
        assert "PRODUCT.md" in result["stale_runs"][0]["stale_docs"]

    def test_multiple_stale_docs(self, workspace):
        """Multiple changed docs all reported."""
        project_dir = workspace / "Projects" / "TestProject"
        _create_ddd_docs(project_dir)
        old_checksums = check_ddd_consistency("TestProject")["checksums"]
        _make_completed_run(workspace, "run_multi", ddd_checksums=old_checksums)

        # Change two docs
        (project_dir / "PRODUCT.md").write_text("# Changed product\n")
        (project_dir / "TECH.md").write_text("# Changed tech\n")

        result = check_ddd_staleness("TestProject")
        stale = result["stale_runs"][0]
        assert "PRODUCT.md" in stale["stale_docs"]
        assert "TECH.md" in stale["stale_docs"]

    def test_untracked_run(self, workspace):
        """Run without ddd_checksums is reported as untracked."""
        _create_ddd_docs(workspace / "Projects" / "TestProject")
        _make_completed_run(workspace, "run_no_checksums", ddd_checksums=None)

        result = check_ddd_staleness("TestProject")
        assert "run_no_checksums" in result["untracked_runs"]
        assert result["stale_runs"] == []
        assert result["fresh_runs"] == []

    def test_running_runs_ignored(self, workspace):
        """Active (non-completed) runs are not checked for staleness."""
        _create_ddd_docs(workspace / "Projects" / "TestProject")
        old_checksums = check_ddd_consistency("TestProject")["checksums"]
        _make_completed_run(workspace, "run_active", ddd_checksums=old_checksums, status="running")

        # Change docs
        (workspace / "Projects" / "TestProject" / "PRODUCT.md").write_text("# Changed\n")

        result = check_ddd_staleness("TestProject")
        # Running run should NOT appear in stale_runs
        assert result["stale_runs"] == []
        assert result["fresh_runs"] == []

    def test_new_doc_added_makes_run_stale(self, workspace):
        """If a DDD doc is added after a run, that run is stale."""
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        # Start with only 2 docs
        (project_dir / "PRODUCT.md").write_text("# Test\n")
        (project_dir / "TECH.md").write_text("# Test\n")

        old_checksums = check_ddd_consistency("TestProject")["checksums"]
        _make_completed_run(workspace, "run_partial", ddd_checksums=old_checksums)

        # Now add IMPROVEMENT.md
        (project_dir / "IMPROVEMENT.md").write_text("# New lessons\n")

        result = check_ddd_staleness("TestProject")
        assert len(result["stale_runs"]) == 1
        assert "IMPROVEMENT.md" in result["stale_runs"][0]["stale_docs"]


class TestStalenessInValidate:
    """Staleness warnings appear in validate() at evaluate stage."""

    def test_staleness_warning_in_validate(self, workspace):
        """When prior run is stale, validate() adds a staleness warning."""
        project_dir = workspace / "Projects" / "TestProject"
        _create_ddd_docs(project_dir,
                         improvement="# T\n\n## What Worked\n\n- OK\n\n## What Failed\n\n- **Old pattern was bad** -- fix\n")

        # Create an old completed run with old checksums
        old_checksums = check_ddd_consistency("TestProject")["checksums"]
        _make_completed_run(workspace, "run_old", ddd_checksums=old_checksums)

        # Change PRODUCT.md
        (project_dir / "PRODUCT.md").write_text("# Test v2 -- new priorities\n")

        # Now create a new run being validated
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is True  # Staleness is WARN, not BLOCK
        staleness_warnings = [w for w in result["warnings"] if "staleness" in w.lower()]
        assert len(staleness_warnings) >= 1
        assert "PRODUCT.md" in staleness_warnings[0]

    def test_no_staleness_when_fresh(self, workspace):
        """No staleness warning when prior run has matching checksums."""
        project_dir = workspace / "Projects" / "TestProject"
        _create_ddd_docs(project_dir,
                         improvement="# T\n\n## What Worked\n\n- OK\n\n## What Failed\n\n- **Old pattern was bad** -- fix\n")

        # Create a prior run with current checksums (no changes)
        current_checksums = check_ddd_consistency("TestProject")["checksums"]
        _make_completed_run(workspace, "run_current", ddd_checksums=current_checksums)

        # Create new run
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        staleness_warnings = [w for w in result["warnings"] if "staleness" in w.lower()]
        assert len(staleness_warnings) == 0


# ---------------------------------------------------------------------------
# Enforcement Redesign Tests (L0-L3)
# ---------------------------------------------------------------------------


class TestL0ArtifactAuthenticity:
    """Layer 0: artifact_id must resolve to a real artifact in the manifest."""

    def test_fabricated_artifact_id_blocks(self, workspace):
        """Artifact ID not in manifest → BLOCK error."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        # Create run with a fabricated artifact_id (no matching manifest entry)
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_FABRICATED"),
        ])
        # Empty manifest (no artifacts)
        (artifacts_dir / "manifest.json").write_text(json.dumps({"artifacts": [], "pipeline_state": "evaluate"}))

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is False
        assert any("not found in manifest" in e or "missing or corrupt" in e for e in result["errors"]), \
            f"Expected artifact resolution error, got: {result['errors']}"

    def test_real_artifact_id_passes(self, workspace):
        """Artifact ID that exists in manifest → passes L0."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
        ])

        result = validate("TestProject", "run_test1", "evaluate")
        assert result["valid"] is True


class TestL1SchemaEnforcement:
    """Layer 1: V1.10.0 required fields must be present in artifacts."""

    def test_review_missing_runtime_patterns_blocks(self, workspace):
        """REVIEW artifact without runtime_patterns → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        # Review artifact with old schema (no runtime_patterns)
        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review",
                       {"approved": True, "integration_trace": {"checked": 1}})
        _make_run(runs_dir, profile="full", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("think", artifact_id="art_t"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_rev"),
        ])
        # Create minimal artifacts for prior stages
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "standard"}),
            ("art_t", "think", {"key_findings": ["x"]}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "review")
        schema_errors = [e for e in result["errors"] if "runtime_patterns" in e]
        assert len(schema_errors) >= 1, f"Expected runtime_patterns error, got: {result['errors']}"

    def test_deliver_missing_adversarial_review_blocks(self, workspace):
        """DELIVER artifact without adversarial_review on bugfix profile → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery",
                       {"title": "Done", "status": "delivered", "confidence_score": {"score": 8, "breakdown": [], "penalties": []}, "completion_audit": {"all_green": True, "gaps": 0}})
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1}, "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]}, "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        adv_errors = [e for e in result["errors"] if "adversarial_review" in e]
        assert len(adv_errors) >= 1, f"Expected adversarial_review error, got: {result['errors']}"

    def test_build_missing_tdd_blocks(self, workspace):
        """BUILD artifact without tdd → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_b", "build",
                       {"files_changed": ["a.py"]})  # no tdd field
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "build")
        tdd_errors = [e for e in result["errors"] if "tdd" in e.lower()]
        assert len(tdd_errors) >= 1, f"Expected tdd error, got: {result['errors']}"


class TestL2DepthValidation:
    """Layer 2: field values must indicate real work, not hollow data."""

    def test_confidence_score_bare_number_blocks(self, workspace):
        """confidence_score as a bare integer → BLOCK (must be from script)."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery",
                       {"title": "X", "status": "delivered", "confidence_score": 9,
                        "completion_audit": {"all_green": True, "gaps": 0},
                        "adversarial_review": {"profile_tier": "pe_only", "findings": []}})
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1}, "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]}, "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        cs_errors = [e for e in result["errors"] if "confidence_score" in e.lower() and ("bare number" in e.lower() or "must run" in e.lower())]
        assert len(cs_errors) >= 1, f"Expected bare-number confidence error, got: {result['errors']}"

    def test_adversarial_review_skipped_on_bugfix_blocks(self, workspace):
        """adversarial_review.profile_tier='skipped' on bugfix → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery",
                       {"title": "X", "status": "delivered",
                        "confidence_score": {"score": 8, "breakdown": [], "penalties": []},
                        "completion_audit": {"all_green": True, "gaps": 0},
                        "adversarial_review": {"profile_tier": "skipped"}})
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1}, "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]}, "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        adv_errors = [e for e in result["errors"] if "adversarial_review" in e and "skipped" in e]
        assert len(adv_errors) >= 1, f"Expected skipped-on-bugfix error, got: {result['errors']}"


class TestL3ConfidenceGate:
    """Layer 3: confidence < 7 blocks delivery unless human_override."""

    def test_low_confidence_blocks_delivery(self, workspace):
        """confidence_score.score=4 without human_override → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery",
                       {"title": "X", "status": "delivered",
                        "confidence_score": {"score": 4, "max_possible": 12, "flag_for_review": True, "breakdown": [], "penalties": [{"rule": "test", "points": -3, "detail": "x"}]},
                        "completion_audit": {"all_green": True, "gaps": 0},
                        "adversarial_review": {"profile_tier": "pe_only", "findings": []}})
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1}, "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]}, "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        gate_errors = [e for e in result["errors"] if "confidence" in e.lower() and ("< 7" in e or "score=" in e)]
        assert len(gate_errors) >= 1, f"Expected confidence gate error, got: {result['errors']}"

    def test_human_override_downgrades_to_warning(self, workspace):
        """confidence_score.score=4 WITH human_override → WARNING not BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery",
                       {"title": "X", "status": "delivered",
                        "confidence_score": {"score": 4, "max_possible": 12, "flag_for_review": True, "breakdown": [], "penalties": [{"rule": "test", "points": -3, "detail": "x"}]},
                        "completion_audit": {"all_green": True, "gaps": 0},
                        "adversarial_review": {"profile_tier": "pe_only", "findings": []}})
        run = _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        # Add human_override flag
        run["human_override"] = True
        (runs_dir / "run.json").write_text(json.dumps(run))
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1}, "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]}, "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        # Should NOT have confidence BLOCK error
        gate_errors = [e for e in result["errors"] if "confidence" in e.lower() and "< 7" in e]
        assert len(gate_errors) == 0, f"human_override should prevent BLOCK, got: {result['errors']}"
        # Should have WARNING instead
        gate_warns = [w for w in result["warnings"] if "confidence" in w.lower() and "override" in w.lower()]
        assert len(gate_warns) >= 1, f"Expected override warning, got: {result['warnings']}"


# ---------------------------------------------------------------------------
# Pipeline Metrics Tests
# ---------------------------------------------------------------------------

class TestSemanticDepthChecks:
    """Test _check_semantic_depth() — WARN-level heuristic checks for content quality."""

    # --- AC1: Completion audit evidence quality ---

    def test_completion_audit_weak_evidence_warns(self, workspace):
        """>=70% of checklist entries lack file/test refs → WARN."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 8, "breakdown": [], "penalties": []},
            "completion_audit": {
                "all_green": True, "gaps": 0,
                "checklist": [
                    {"criterion": "AC1", "evidence": "implemented", "status": "pass"},
                    {"criterion": "AC2", "evidence": "verified", "status": "pass"},
                    {"criterion": "AC3", "evidence": "done", "status": "pass"},
                ],
            },
            "adversarial_review": {"profile_tier": "pe_only", "findings": []},
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1},
                                 "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
                                 "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        evidence_warns = [w for w in result["warnings"] if "evidence" in w.lower() and "completion" in w.lower()]
        assert len(evidence_warns) >= 1, f"Expected evidence quality warning, got warnings: {result['warnings']}"

    def test_completion_audit_strong_evidence_no_warn(self, workspace):
        """>=70% entries cite file paths or test names → no WARN."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 8, "breakdown": [], "penalties": []},
            "completion_audit": {
                "all_green": True, "gaps": 0,
                "checklist": [
                    {"criterion": "AC1", "evidence": "test_session_unit.py::test_spawn passes", "status": "pass"},
                    {"criterion": "AC2", "evidence": "backend/core/memory_index.py line 42 verified", "status": "pass"},
                    {"criterion": "AC3", "evidence": "CHANGELOG.md updated with version entry", "status": "pass"},
                ],
            },
            "adversarial_review": {"profile_tier": "pe_only", "findings": []},
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1},
                                 "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
                                 "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        evidence_warns = [w for w in result["warnings"] if "evidence" in w.lower() and "completion" in w.lower()]
        assert len(evidence_warns) == 0, f"Strong evidence should not warn, got: {result['warnings']}"

    # --- AC2: RP patterns evidence quality ---

    def test_rp_patterns_bare_pass_warns(self, workspace):
        """Non-N/A patterns with just 'PASS' (no evidence) → WARN."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review", {
            "approved": True,
            "integration_trace": {"checked": 1},
            "runtime_patterns": {
                "checked": 4,
                "patterns": [
                    {"id": "RP1", "result": "PASS"},
                    {"id": "RP7", "result": "PASS"},
                    {"id": "RP8", "result": "N/A"},
                    {"id": "RP17", "result": "PASS"},
                ],
            },
            "findings_count": 0,
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_rev"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "review")
        rp_warns = [w for w in result["warnings"] if "runtime_patterns" in w.lower() and "evidence" in w.lower()]
        assert len(rp_warns) >= 1, f"Expected RP evidence warning, got warnings: {result['warnings']}"

    def test_rp_patterns_with_evidence_no_warn(self, workspace):
        """Non-N/A patterns with substantive evidence → no WARN."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review", {
            "approved": True,
            "integration_trace": {"checked": 1},
            "runtime_patterns": {
                "checked": 3,
                "patterns": [
                    {"id": "RP1", "result": "PASS", "evidence": "No subprocess calls in changeset"},
                    {"id": "RP7", "result": "PASS", "evidence": "Error constants match in validator.py line 85"},
                    {"id": "RP8", "result": "N/A"},
                ],
            },
            "findings_count": 0,
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_rev"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "review")
        rp_warns = [w for w in result["warnings"] if "runtime_patterns" in w.lower() and "evidence" in w.lower()]
        assert len(rp_warns) == 0, f"Evidenced patterns should not warn, got: {result['warnings']}"

    # --- AC3: Confidence penalty consistency ---

    def test_no_penalties_with_findings_warns(self, workspace):
        """Review had findings but confidence penalties is empty → WARN."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        # Review artifact with 3 findings
        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review", {
            "approved": True,
            "integration_trace": {"checked": 1},
            "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
            "findings_count": 3,
        })
        # Deliver artifact with EMPTY penalties despite findings
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 9, "breakdown": [{"rule": "x", "points": 3}], "penalties": []},
            "completion_audit": {"all_green": True, "gaps": 0},
            "adversarial_review": {"profile_tier": "pe_only", "findings": []},
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_rev"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        penalty_warns = [w for w in result["warnings"] if "penalt" in w.lower() and "finding" in w.lower()]
        assert len(penalty_warns) >= 1, f"Expected penalty consistency warning, got warnings: {result['warnings']}"

    def test_penalties_present_with_findings_no_warn(self, workspace):
        """Review had findings AND penalties are non-empty → no WARN."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review", {
            "approved": True,
            "integration_trace": {"checked": 1},
            "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
            "findings_count": 2,
        })
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 7, "breakdown": [], "penalties": [
                {"rule": "review_findings", "points": -2, "detail": "2 findings fixed"}
            ]},
            "completion_audit": {"all_green": True, "gaps": 0},
            "adversarial_review": {"profile_tier": "pe_only", "findings": []},
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_rev"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        penalty_warns = [w for w in result["warnings"] if "penalt" in w.lower() and "finding" in w.lower()]
        assert len(penalty_warns) == 0, f"Penalties present should not warn, got: {result['warnings']}"


class TestValueDeliveryRules:
    """Test Rules 15-18: value over completion, evidence over assertion,
    no premature completion, specific adversarial findings."""

    def test_vague_adversarial_findings_blocked(self, workspace):
        """Rule 18: >=50% vague findings → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 8, "breakdown": [], "penalties": []},
            "completion_audit": {"all_green": True, "gaps": 0},
            "adversarial_review": {
                "profile_tier": "full",
                "findings": [
                    {"severity": "HIGH", "finding": "looks good", "resolved": True},
                    {"severity": "MED", "finding": "could improve", "resolved": True},
                ],
            },
        })
        _make_run(runs_dir, profile="full", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("think", artifact_id="art_th"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "test"}),
            ("art_th", "research", {"key_findings": "x"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1},
                                 "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
                                 "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        vague_errors = [e for e in result["errors"] if "vague" in e.lower()]
        assert len(vague_errors) >= 1, f"Expected vague findings block, got: {result['errors']}"

    def test_specific_adversarial_findings_pass(self, workspace):
        """Rule 18: specific findings (with file refs) pass depth check."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 8, "breakdown": [], "penalties": []},
            "completion_audit": {"all_green": True, "gaps": 0},
            "adversarial_review": {
                "profile_tier": "pe_only",
                "findings": [
                    {"severity": "HIGH", "resolved": True,
                     "finding": "backend/core/session_unit.py line 42: race condition in _spawn() when concurrent tabs call simultaneously"},
                    {"severity": "MED", "resolved": True,
                     "finding": "pipeline_validator.py _check_depth() doesn't handle non-dict types for runtime_patterns"},
                ],
            },
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1},
                                 "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
                                 "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        vague_errors = [e for e in result["errors"] if "vague" in e.lower()]
        assert len(vague_errors) == 0, f"Specific findings should pass, got: {result['errors']}"

    def test_unfixable_gaps_without_justification_blocked(self, workspace):
        """Rule 17: unfixable_gaps > 0 without justification → BLOCK."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 7, "breakdown": [], "penalties": []},
            "completion_audit": {
                "all_green": False,
                "gaps": 1,
                "unfixable_gaps": 1,
                # No unfixable_justification — should be blocked
            },
            "adversarial_review": {"profile_tier": "pe_only", "findings": []},
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1},
                                 "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
                                 "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        justification_errors = [e for e in result["errors"] if "unfixable_justification" in e]
        assert len(justification_errors) >= 1, f"Expected justification requirement, got: {result['errors']}"

    def test_unfixable_gaps_with_justification_passes(self, workspace):
        """Rule 17: unfixable_gaps with justification → allowed (just -1 confidence)."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "X", "status": "done",
            "confidence_score": {"score": 7, "breakdown": [], "penalties": []},
            "completion_audit": {
                "all_green": False,
                "gaps": 1,
                "unfixable_gaps": 1,
                "unfixable_justification": "Windows-only feature, macOS CI can't test",
            },
            "adversarial_review": {"profile_tier": "pe_only", "findings": []},
        })
        _make_run(runs_dir, profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_b"),
            _stage_record("review", artifact_id="art_r"),
            _stage_record("test", artifact_id="art_t"),
            _stage_record("deliver", artifact_id="art_del"),
        ])
        for aid, atype, data in [
            ("art_e", "evaluation", {"recommendation": "GO", "scope": "bugfix"}),
            ("art_p", "plan", {"acceptance_criteria": ["x"]}),
            ("art_b", "build", {"files_changed": ["a.py"], "tdd": {"green_pass": True}}),
            ("art_r", "review", {"approved": True, "integration_trace": {"checked": 1},
                                 "runtime_patterns": {"checked": 1, "patterns": [{"id": "RP1", "result": "N/A"}]},
                                 "findings_count": 0}),
            ("art_t", "test", {"passed": 10}),
        ]:
            _make_artifact(artifacts_dir, "run_test1", aid, atype, data)

        result = validate("TestProject", "run_test1", "deliver")
        justification_errors = [e for e in result["errors"] if "unfixable_justification" in e]
        assert len(justification_errors) == 0, f"Justified unfixable should pass, got: {result['errors']}"


class TestExtractRunMetrics:
    """Test _extract_run_metrics extracts data correctly from run.json + artifacts."""

    def test_basic_metrics_extraction(self, workspace):
        """Full metrics extraction from a completed bugfix run with all artifacts."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        # Create artifacts first
        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review", {
            "approved": True,
            "integration_trace": {"checked": 5},
            "runtime_patterns": {"checked": 8, "patterns": [{"id": "RP1", "result": "PASS"}]},
            "findings_count": 3,
            "findings": [{"desc": "a"}, {"desc": "b"}, {"desc": "c"}],
        })
        _make_artifact(artifacts_dir, "run_test1", "art_del", "delivery", {
            "title": "Test", "status": "done",
            "confidence_score": {"score": 8, "breakdown": [], "penalties": []},
            "completion_audit": {"all_green": True, "gaps": 0},
            "adversarial_review": {
                "profile_tier": "pe_only",
                "findings": [
                    {"severity": "HIGH", "resolved": True, "desc": "a"},
                    {"severity": "MEDIUM", "resolved": False, "desc": "b"},
                ],
            },
        })
        _make_artifact(artifacts_dir, "run_test1", "art_test", "test_report", {
            "passed": 50, "failed": 2,
        })
        _make_artifact(artifacts_dir, "run_test1", "art_build", "changeset", {
            "files_changed": ["a.py", "b.py", "c.py"],
            "tdd": {"green_pass": True, "tests_generated": 12},
        })

        run = _make_run(runs_dir, status="completed", profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_e", decisions=[
                {"description": "GO", "classification": "mechanical", "reasoning": ""},
            ]),
            _stage_record("plan", artifact_id="art_p"),
            _stage_record("build", artifact_id="art_build", token_cost=30000),
            _stage_record("review", artifact_id="art_rev", token_cost=8000),
            _stage_record("test", artifact_id="art_test", token_cost=6000),
            _stage_record("deliver", artifact_id="art_del", token_cost=10000),
            _stage_record("reflect", token_cost=3000),
        ])
        run["completed_at"] = "2026-03-24T01:30:00Z"
        (runs_dir / "run.json").write_text(json.dumps(run))

        metrics = _extract_run_metrics("TestProject", "run_test1", run)

        # Structure
        assert metrics["run_id"] == "run_test1"
        assert metrics["profile"] == "bugfix"
        assert metrics["stages_completed"] == 7

        # Token metrics
        assert metrics["total_tokens"] > 0
        assert "build" in metrics["stage_tokens"]

        # Decision metrics (7 stages × 1 default mechanical each)
        assert metrics["decisions"]["mechanical"] == 7
        assert metrics["decisions"]["total"] == 7

        # Catch metrics — core value
        assert metrics["catches"]["review_findings"] == 3
        assert metrics["catches"]["review_rp_checked"] == 8
        assert metrics["catches"]["adversarial_findings"] == 2
        assert metrics["catches"]["adversarial_high"] == 1
        assert metrics["catches"]["adversarial_resolved"] == 1
        assert metrics["catches"]["test_regressions"] == 2

        # Quality
        assert metrics["quality"]["confidence_score"] == 8
        assert metrics["quality"]["completion_all_green"] is True
        assert metrics["quality"]["test_passed"] == 50
        assert metrics["quality"]["test_failed"] == 2

        # Build
        assert metrics["build"]["files_changed"] == 3
        assert metrics["build"]["tests_generated"] == 12

        # Duration
        assert metrics["duration_minutes"] == 90.0

    def test_metrics_with_missing_artifacts(self, workspace):
        """Metrics extraction when artifacts are missing — should not crash."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        run = _make_run(runs_dir, status="completed", profile="bugfix", stages=[
            _stage_record("evaluate", artifact_id="art_nonexistent"),
            _stage_record("plan", artifact_id="art_also_missing"),
        ])

        metrics = _extract_run_metrics("TestProject", "run_test1", run)

        # Should still produce valid metrics with zeros
        assert metrics["catches"]["review_findings"] == 0
        assert metrics["catches"]["adversarial_findings"] == 0
        assert metrics["quality"]["confidence_score"] == 0

    def test_metrics_with_old_format_artifacts(self, workspace):
        """Old artifacts might have findings as int, not list."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        _make_artifact(artifacts_dir, "run_test1", "art_rev", "review", {
            "approved": True,
            "integration_trace": {"checked": 2},
            "findings_count": 5,
            "findings": 5,  # Old format: int instead of list
        })

        run = _make_run(runs_dir, status="completed", profile="bugfix", stages=[
            _stage_record("review", artifact_id="art_rev"),
        ])

        metrics = _extract_run_metrics("TestProject", "run_test1", run)
        # Should handle int gracefully
        assert metrics["catches"]["review_findings"] == 5


class TestRecordValidationEvent:
    """Test validation event recording in run.json."""

    def test_records_block_event(self, workspace):
        """Validation block is appended to run.json.validation_events[]."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_run(runs_dir, status="running")

        _record_validation_event(
            "TestProject", "run_test1", "build",
            passed=False,
            errors=["Schema violation: missing tdd"],
            warnings=["Budget low"],
        )

        run = json.loads((runs_dir / "run.json").read_text())
        events = run.get("validation_events", [])
        assert len(events) == 1
        assert events[0]["stage"] == "build"
        assert events[0]["passed"] is False
        assert events[0]["error_count"] == 1
        assert "timestamp" in events[0]

    def test_multiple_events_append(self, workspace):
        """Multiple validation events accumulate."""
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        _make_run(runs_dir, status="running")

        for stage in ["build", "review", "build"]:
            _record_validation_event(
                "TestProject", "run_test1", stage,
                passed=False, errors=["err"], warnings=[],
            )

        run = json.loads((runs_dir / "run.json").read_text())
        assert len(run["validation_events"]) == 3

    def test_missing_run_does_not_crash(self, workspace):
        """Recording to a non-existent run silently succeeds (best-effort)."""
        _record_validation_event(
            "TestProject", "run_nonexistent", "build",
            passed=False, errors=["err"], warnings=[],
        )
        # No exception = pass


class TestMetricsCLI:
    """Test run-metrics and run-analytics CLI commands via subprocess."""

    def test_run_metrics_cli(self, workspace):
        """run-metrics generates METRICS.json and outputs JSON."""
        import subprocess

        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"

        _make_artifact(artifacts_dir, "run_test1", "art_t", "test_report",
                       {"passed": 20, "failed": 0})
        _make_run(runs_dir, status="completed", stages=[
            _stage_record("evaluate", artifact_id="art_e"),
            _stage_record("test", artifact_id="art_t", token_cost=5000),
        ])

        result = subprocess.run(
            [sys.executable, "-m", "scripts.artifact_cli",
             "run-metrics", "--project", "TestProject", "--run-id", "run_test1"],
            capture_output=True, text=True,
            cwd=str(Path(__file__).parent.parent),
            env={**os.environ, "SWARM_WORKSPACE": str(workspace)},
        )

        assert result.returncode == 0, f"stderr: {result.stderr}"
        metrics = json.loads(result.stdout)
        assert metrics["run_id"] == "run_test1"
        assert metrics["quality"]["test_passed"] == 20

        # METRICS.json should be written
        metrics_file = runs_dir / "METRICS.json"
        assert metrics_file.exists()

    def test_run_analytics_cli(self, workspace):
        """run-analytics aggregates across multiple completed runs."""
        import subprocess

        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"

        # Create 3 completed runs
        for i in range(3):
            rid = f"run_test{i}"
            runs_dir = artifacts_dir / "runs" / rid
            _make_run(runs_dir, run_id=rid, status="completed", profile="bugfix", stages=[
                _stage_record("evaluate", token_cost=5000 + i * 1000),
                _stage_record("build", token_cost=20000 + i * 5000),
            ])

        result = subprocess.run(
            [sys.executable, "-m", "scripts.artifact_cli",
             "run-analytics", "--project", "TestProject", "--limit", "10"],
            capture_output=True, text=True,
            cwd=str(Path(__file__).parent.parent),
            env={**os.environ, "SWARM_WORKSPACE": str(workspace)},
        )

        assert result.returncode == 0, f"stderr: {result.stderr}"
        analytics = json.loads(result.stdout)
        assert analytics["runs_analyzed"] == 3
        assert analytics["tokens"]["avg_per_run"] > 0
        assert "catches" in analytics
        assert "quality" in analytics
        assert "decisions" in analytics


# ---------------------------------------------------------------------------
# Check 12: Anti-Rationalization Gate
# ---------------------------------------------------------------------------

class TestAntiRationalization:
    """Tests for _check_skip_justification (Check 12)."""

    def test_non_skipped_stage_passes(self):
        """Non-skipped stages are not checked."""
        record = _stage_record("build", status="completed")
        errors = _check_skip_justification(record)
        assert errors == []

    def test_skipped_with_skip_reason_passes(self):
        """Legacy path: skip_reason alone is sufficient."""
        record = _stage_record("think", status="skipped")
        record["skip_reason"] = "Design pre-approved by user"
        errors = _check_skip_justification(record)
        assert errors == []

    def test_skipped_without_any_reason_blocks(self):
        """Skipped without skip_reason or skip_justification → BLOCK."""
        record = {"stage": "think", "status": "skipped", "token_cost": 0}
        errors = _check_skip_justification(record)
        assert len(errors) == 1
        assert "BLOCK" in errors[0]

    def test_full_justification_null_counter_passes(self):
        """Full justification with null counter_argument_check → PASS (AC2)."""
        record = {
            "stage": "think", "status": "skipped", "token_cost": 0,
            "skip_justification": {
                "step_skipped": "think",
                "reason": "Only one approach exists",
                "evidence_skip_safe": "Scope is trivial, proven pattern",
                "counter_argument_check": None,
            },
        }
        errors = _check_skip_justification(record)
        assert errors == []

    def test_full_justification_empty_counter_passes(self):
        """Full justification with empty string counter → PASS."""
        record = {
            "stage": "think", "status": "skipped", "token_cost": 0,
            "skip_justification": {
                "step_skipped": "think",
                "reason": "Only one approach exists",
                "evidence_skip_safe": "Scope is trivial",
                "counter_argument_check": "",
            },
        }
        errors = _check_skip_justification(record)
        assert errors == []

    def test_counter_argument_present_blocks(self):
        """Non-empty counter_argument_check → BLOCK (AC1)."""
        record = {
            "stage": "deliver", "status": "skipped", "token_cost": 0,
            "skip_justification": {
                "step_skipped": "adversarial_review",
                "reason": "Tests pass, code is straightforward",
                "evidence_skip_safe": "All 12 tests green",
                "counter_argument_check": (
                    "C011: 57 tests green but feature 100% non-functional. "
                    "Tests verify what was written, not what was missed."
                ),
            },
        }
        errors = _check_skip_justification(record)
        assert len(errors) == 1
        assert "Anti-rationalization triggered" in errors[0]
        assert "BLOCK" in errors[0]

    def test_missing_required_fields_blocks(self):
        """Missing step_skipped/reason/evidence → BLOCK."""
        record = {
            "stage": "think", "status": "skipped", "token_cost": 0,
            "skip_justification": {
                "step_skipped": "",
                "reason": "",
                "evidence_skip_safe": "",
                "counter_argument_check": None,
            },
        }
        errors = _check_skip_justification(record)
        # Should have 3 errors (one per empty required field)
        assert len(errors) == 3
        assert all("BLOCK" in e for e in errors)

    def test_non_string_counter_argument_blocks(self):
        """F1: Non-string counter_argument_check (list/dict/bool) → BLOCK."""
        for bypass_value in [["C011: tests pass"], {"text": "counter"}, True, 42]:
            record = {
                "stage": "deliver", "status": "skipped", "token_cost": 0,
                "skip_justification": {
                    "step_skipped": "adversarial_review",
                    "reason": "Tests pass",
                    "evidence_skip_safe": "All green",
                    "counter_argument_check": bypass_value,
                },
            }
            errors = _check_skip_justification(record)
            assert any("must be a string" in e for e in errors), (
                f"Non-string type {type(bypass_value).__name__} should BLOCK"
            )

    def test_short_skip_reason_blocks(self):
        """F7: skip_reason shorter than 15 chars → BLOCK."""
        record = {"stage": "think", "status": "skipped", "token_cost": 0,
                  "skip_reason": "ok"}
        errors = _check_skip_justification(record)
        assert len(errors) == 1
        assert "too short" in errors[0]

    def test_adequate_skip_reason_passes(self):
        """skip_reason >= 15 chars passes legacy path."""
        record = {"stage": "think", "status": "skipped", "token_cost": 0,
                  "skip_reason": "Design pre-approved by user in session"}
        errors = _check_skip_justification(record)
        assert errors == []


# ---------------------------------------------------------------------------
# Check 13: Output Routing
# ---------------------------------------------------------------------------

class TestOutputRouting:
    """Tests for _check_output_routing and STAGE_ROUTING (Check 13)."""

    def test_routing_defined_for_all_stages(self):
        """AC3: STAGE_ROUTING has entries for all 8 pipeline stages."""
        expected = {"evaluate", "think", "plan", "build", "review", "test", "deliver", "reflect"}
        assert set(STAGE_ROUTING.keys()) == expected

    def test_evaluate_no_consumes(self):
        """Evaluate has no upstream — always passes routing."""
        run = {"stages": [_stage_record("evaluate")]}
        errors, warnings = _check_output_routing("evaluate", _stage_record("evaluate"), run, "TestProject")
        assert errors == []

    def test_review_missing_consumed_artifacts_field_warns(self):
        """Review without consumed_artifacts field → WARN (backward compat)."""
        build_rec = _stage_record("build")
        review_rec = _stage_record("review")
        # review_rec has no consumed_artifacts field at all
        run = {"stages": [build_rec, review_rec]}
        errors, warnings = _check_output_routing("review", review_rec, run, "TestProject")
        assert errors == []  # No BLOCK — field is missing (legacy)
        assert any("changeset" in w for w in warnings)

    def test_review_empty_consumed_artifacts_blocks(self):
        """AC4: Review with consumed_artifacts present but empty → BLOCK."""
        build_rec = _stage_record("build")
        review_rec = _stage_record("review")
        review_rec["consumed_artifacts"] = []  # Field present but empty
        run = {"stages": [build_rec, review_rec]}
        errors, warnings = _check_output_routing("review", review_rec, run, "TestProject")
        assert len(errors) >= 1
        assert any("changeset" in e and "BLOCK" in e for e in errors)

    def test_review_with_consumed_artifacts_passes(self):
        """AC5: Review with consumed_artifacts referencing changeset → PASS."""
        build_rec = _stage_record("build")
        review_rec = _stage_record("review")
        review_rec["consumed_artifacts"] = [
            {"type": "changeset", "id": "art_build1"}
        ]
        run = {"stages": [build_rec, review_rec]}
        errors, warnings = _check_output_routing("review", review_rec, run, "TestProject")
        assert errors == []

    def test_skipped_stage_skips_routing(self):
        """Skipped stages bypass routing check."""
        record = _stage_record("think", status="skipped")
        run = {"stages": [_stage_record("evaluate"), record]}
        errors, warnings = _check_output_routing("think", record, run, "TestProject")
        assert errors == []
        assert warnings == []

    def test_upstream_not_produced_warns(self):
        """When upstream was skipped and didn't produce, WARN not BLOCK."""
        # plan consumes research, but think was skipped
        think_rec = _stage_record("think", status="skipped")
        plan_rec = _stage_record("plan")
        plan_rec["consumed_artifacts"] = [{"type": "evaluation", "id": "art_eval1"}]
        run = {"stages": [_stage_record("evaluate"), think_rec, plan_rec]}
        errors, warnings = _check_output_routing("plan", plan_rec, run, "TestProject")
        # research not produced by think (skipped) → WARN
        assert any("research" in w for w in warnings)
        # evaluation consumed → no error for that
        assert not any("evaluation" in e for e in errors)


# ---------------------------------------------------------------------------
# Artifact Freshness
# ---------------------------------------------------------------------------

class TestArtifactFreshness:
    """Tests for check_artifact_freshness (P4)."""

    def test_fresh_artifact_within_ttl(self):
        """Recent artifact with no dependency drift → fresh (AC6 negative)."""
        from datetime import datetime, timezone
        now = datetime.now(timezone.utc).isoformat()
        meta = {"created_at": now, "freshness": {"ttl_advisory_hours": 168}}
        result = check_artifact_freshness(meta, "TestProject")
        assert result["fresh"] is True
        assert result["stale_reason"] is None

    def test_stale_artifact_exceeds_ttl(self):
        """Artifact older than TTL → stale (AC6)."""
        meta = {
            "created_at": "2026-01-01T00:00:00+00:00",
            "freshness": {"ttl_advisory_hours": 24},
        }
        result = check_artifact_freshness(meta, "TestProject")
        assert result["fresh"] is False
        assert "exceeds advisory TTL" in result["stale_reason"]
        assert result["age_hours"] > 24

    def test_stale_ddd_checksum_drift(self, workspace):
        """DDD doc changed since artifact creation → stale (AC6)."""
        # Create a DDD doc
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# Old content")

        from datetime import datetime, timezone
        now = datetime.now(timezone.utc).isoformat()
        meta = {
            "created_at": now,
            "freshness": {
                "depends_on": {
                    "ddd_checksums": {"PRODUCT.md": "stale_hash_that_wont_match"}
                },
                "ttl_advisory_hours": 9999,
            },
        }
        result = check_artifact_freshness(meta, "TestProject")
        assert result["fresh"] is False
        assert "PRODUCT.md changed" in result["stale_reason"]

    def test_no_freshness_metadata_defaults_fresh(self):
        """Artifact without freshness metadata → assumed fresh."""
        from datetime import datetime, timezone
        now = datetime.now(timezone.utc).isoformat()
        meta = {"created_at": now}
        result = check_artifact_freshness(meta, "TestProject")
        assert result["fresh"] is True

    def test_no_created_at_defaults_fresh(self):
        """Missing created_at → cannot determine age → assume fresh."""
        meta = {}
        result = check_artifact_freshness(meta, "TestProject")
        assert result["fresh"] is True


# ---------------------------------------------------------------------------
# Integration: Freshness within validate() (D1 gap from PE review)
# ---------------------------------------------------------------------------

class TestFreshnessIntegration:
    """Test freshness sub-check fires correctly through the validate() path."""

    def test_stale_artifact_warning_in_validate(self, workspace):
        """Stale consumed artifact produces STALE warning through full validate()."""
        from datetime import datetime, timezone
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        now = datetime.now(timezone.utc).isoformat()

        # Create a DDD doc that will cause staleness
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# Current content v2")

        # Create ALL artifacts first
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_artifact(artifacts_dir, "run_test1", "art_change", "changeset",
                       {"files_changed": ["file.py"], "tdd": {"green_pass": True, "smoke_tests": 1}})
        _make_artifact(artifacts_dir, "run_test1", "art_review", "review",
                       {"approved": True, "findings_count": 0,
                        "integration_trace": {"checked": 1, "clean": True},
                        "runtime_patterns": {"checked": 1, "violations": 0,
                                             "patterns": [{"pattern": "test", "status": "pass", "detail": "checked manually"}]}})

        # THEN update manifest: all entries fresh, art_eval has stale DDD hash
        manifest_file = artifacts_dir / "manifest.json"
        manifest = json.loads(manifest_file.read_text())
        for entry in manifest["artifacts"]:
            entry["created_at"] = now  # All recent (avoids TTL false-stale)
            if entry["id"] == "art_eval":
                entry["freshness"] = {
                    "depends_on": {
                        "ddd_checksums": {"PRODUCT.md": "stale_hash_mismatch"}
                    },
                    "ttl_advisory_hours": 9999,
                }
        manifest_file.write_text(json.dumps(manifest))

        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
            _stage_record("build", artifact_id="art_change"),
            {
                "stage": "review",
                "status": "completed",
                "artifact_id": "art_review",
                "started_at": "2026-03-24T00:00:00Z",
                "completed_at": "2026-03-24T00:01:00Z",
                "token_cost": 5000,
                "retry_count": 0,
                "decisions": [{"description": "test", "classification": "mechanical", "reasoning": "test"}],
                "consumed_artifacts": [
                    {"type": "changeset", "id": "art_change"},
                    {"type": "evaluation", "id": "art_eval"},  # This one is stale
                ],
            },
        ])

        result = validate("TestProject", "run_test1", "review")
        # Should produce a STALE warning for art_eval (DDD checksum drift)
        stale_warnings = [w for w in result["warnings"] if "STALE" in w]
        assert len(stale_warnings) >= 1, (
            f"Expected STALE warning for art_eval, got warnings: {result['warnings']}"
        )
        assert "art_eval" in stale_warnings[0]
        assert "PRODUCT.md" in stale_warnings[0]

    def test_fresh_artifact_no_warning_in_validate(self, workspace):
        """Fresh consumed artifact produces no STALE warning through validate()."""
        from datetime import datetime, timezone
        from scripts.pipeline_validator import _compute_doc_checksum
        artifacts_dir = workspace / "Projects" / "TestProject" / ".artifacts"
        runs_dir = artifacts_dir / "runs" / "run_test1"
        now = datetime.now(timezone.utc).isoformat()

        # Create DDD doc
        project_dir = workspace / "Projects" / "TestProject"
        project_dir.mkdir(parents=True, exist_ok=True)
        (project_dir / "PRODUCT.md").write_text("# Content")
        current_hash = _compute_doc_checksum("# Content")

        # Create ALL artifacts first
        _make_artifact(artifacts_dir, "run_test1", "art_eval", "evaluation",
                       {"recommendation": "GO", "scope": "standard"})
        _make_artifact(artifacts_dir, "run_test1", "art_change", "changeset",
                       {"files_changed": ["file.py"], "tdd": {"green_pass": True, "smoke_tests": 1}})
        _make_artifact(artifacts_dir, "run_test1", "art_review", "review",
                       {"approved": True, "findings_count": 0,
                        "integration_trace": {"checked": 1, "clean": True},
                        "runtime_patterns": {"checked": 1, "violations": 0,
                                             "patterns": [{"pattern": "test", "status": "pass", "detail": "checked it"}]}})

        # THEN update manifest: ALL entries fresh timestamps + matching hash
        manifest_file = artifacts_dir / "manifest.json"
        manifest = json.loads(manifest_file.read_text())
        for entry in manifest["artifacts"]:
            entry["created_at"] = now  # All recent
            if entry["id"] == "art_eval":
                entry["freshness"] = {
                    "depends_on": {"ddd_checksums": {"PRODUCT.md": current_hash}},
                    "ttl_advisory_hours": 9999,
                }
        manifest_file.write_text(json.dumps(manifest))

        _make_run(runs_dir, stages=[
            _stage_record("evaluate", artifact_id="art_eval"),
            _stage_record("build", artifact_id="art_change"),
            {
                "stage": "review",
                "status": "completed",
                "artifact_id": "art_review",
                "started_at": "2026-03-24T00:00:00Z",
                "completed_at": "2026-03-24T00:01:00Z",
                "token_cost": 5000,
                "retry_count": 0,
                "decisions": [{"description": "d", "classification": "mechanical", "reasoning": "r"}],
                "consumed_artifacts": [
                    {"type": "changeset", "id": "art_change"},
                    {"type": "evaluation", "id": "art_eval"},
                ],
            },
        ])

        result = validate("TestProject", "run_test1", "review")
        stale_warnings = [w for w in result["warnings"] if "STALE" in w]
        assert stale_warnings == [], f"Expected no STALE warnings, got: {stale_warnings}"
