---
phase: 21-contradiction-detection
plan: 02
type: execute
wave: 2
depends_on: ["21-01"]
files_modified:
  - src/ztlctl/services/contradiction.py
  - src/ztlctl/services/check.py
  - src/ztlctl/actions/_check.py
  - src/ztlctl/mcp/resources.py
  - src/ztlctl/controllers/contradiction.py
  - tests/test_contradiction_integration.py
  - tests/test_contradiction_resource.py
autonomous: true
requirements: [CNTR-03, CNTR-04, CNTR-05, CNTR-06]

must_haves:
  truths:
    - "Running check with CAT_SEMANTIC reports contradiction candidates as CheckIssues"
    - "confirm_contradiction records a bidirectional contradicts edge in the graph"
    - "MCP resource ztlctl://review/contradictions returns JSON array of scored candidate pairs"
    - "check_contradictions is registered in ActionRegistry under analysis category"
    - "Contradiction check gracefully skips if no vector index exists"
  artifacts:
    - path: "src/ztlctl/services/check.py"
      provides: "CAT_SEMANTIC category and _check_semantic method"
      contains: "CAT_SEMANTIC"
    - path: "src/ztlctl/actions/_check.py"
      provides: "check_contradictions and confirm_contradiction ActionDefinitions"
      contains: "check_contradictions"
    - path: "src/ztlctl/mcp/resources.py"
      provides: "ztlctl://review/contradictions resource"
      contains: "review/contradictions"
    - path: "src/ztlctl/services/contradiction.py"
      provides: "confirm_contradiction method with graph edge recording"
      contains: "def confirm_contradiction"
    - path: "tests/test_contradiction_integration.py"
      provides: "Integration tests for check + action registration"
    - path: "tests/test_contradiction_resource.py"
      provides: "MCP resource impl tests"
  key_links:
    - from: "src/ztlctl/services/check.py"
      to: "src/ztlctl/services/contradiction.py"
      via: "ContradictionService.find_candidates called in _check_semantic"
      pattern: "ContradictionService.*find_candidates"
    - from: "src/ztlctl/services/contradiction.py"
      to: "VaultTransaction.insert_edge"
      via: "confirm_contradiction records bidirectional edges"
      pattern: "insert_edge.*contradicts"
    - from: "src/ztlctl/mcp/resources.py"
      to: "src/ztlctl/services/contradiction.py"
      via: "contradictions_review_impl calls find_candidates"
      pattern: "ContradictionService.*find_candidates"
    - from: "src/ztlctl/actions/_check.py"
      to: "src/ztlctl/controllers/contradiction.py"
      via: "ActionDefinition handlers delegate to ContradictionController"
      pattern: "ContradictionController.*check_contradictions"
---

<objective>
Wire ContradictionService into CheckService (CAT_SEMANTIC), record confirmed contradictions as graph edges, expose candidates via MCP resource, and register actions in ActionRegistry.

Purpose: Complete the contradiction detection feature by integrating the analytical engine (Plan 01) into all platform surfaces (check, graph, MCP, registry).
Output: Full contradiction detection feature wired into check, graph, MCP, and CLI/MCP action surfaces.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/21-contradiction-detection/21-CONTEXT.md
@.planning/phases/21-contradiction-detection/21-01-SUMMARY.md

@src/ztlctl/services/check.py
@src/ztlctl/services/contradiction.py
@src/ztlctl/controllers/contradiction.py
@src/ztlctl/actions/_check.py
@src/ztlctl/actions/__init__.py
@src/ztlctl/mcp/resources.py
@src/ztlctl/infrastructure/vault.py

<interfaces>
<!-- From Plan 01 outputs -->

From src/ztlctl/services/contradiction.py (created in Plan 01):
```python
class ContradictionService(BaseService):
    def find_candidates(self, *, similarity_threshold: float = 0.85, max_pairs: int = 20) -> ServiceResult: ...
    # Returns data={"candidates": [{"note_a", "note_b", "title_a", "title_b", "score", "signals"}], "count": N}
```

From src/ztlctl/controllers/contradiction.py (created in Plan 01):
```python
class ContradictionController(BaseController):
    def check_contradictions(self, *, similarity_threshold: float = 0.85, max_pairs: int = 20) -> ServiceResult: ...
    def confirm_contradiction(self, *, note_a: str, note_b: str) -> ServiceResult: ...
```

From src/ztlctl/services/check.py:
```python
CAT_DB_FILE = "db_file_consistency"
CAT_SCHEMA = "schema_integrity"
CAT_GRAPH = "graph_health"
CAT_STRUCTURAL = "structural_validation"
CAT_GARDEN = "garden_health"
CAT_SCHEMA_VERSION = "schema_version"

class CheckService(BaseService):
    def check(self, *, min_severity: str = "warning") -> ServiceResult: ...
    # Calls _check_* methods for each category, filters by severity
```

From src/ztlctl/infrastructure/vault.py (VaultTransaction):
```python
class VaultTransaction:
    def insert_edge(self, source_id, target_id, edge_type, source_layer, today, *, weight=1.0, check_duplicate=False, check_target_exists=False) -> bool: ...
```

From src/ztlctl/mcp/resources.py:
```python
_RESOURCE_CATALOG: tuple[dict[str, str], ...]  # Add entry here
def register_resources(server, vault) -> None:  # Register @server.resource here
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: CheckService CAT_SEMANTIC integration, confirm_contradiction with graph edges, and MCP resource</name>
  <files>
    src/ztlctl/services/check.py,
    src/ztlctl/services/contradiction.py,
    src/ztlctl/mcp/resources.py,
    tests/test_contradiction_integration.py,
    tests/test_contradiction_resource.py
  </files>
  <read_first>
    - src/ztlctl/services/check.py (full file — understand check() flow, category constants, _check_* pattern)
    - src/ztlctl/services/contradiction.py (from Plan 01)
    - src/ztlctl/mcp/resources.py (full file — _RESOURCE_CATALOG, sessions_recent_impl pattern, register_resources)
    - src/ztlctl/infrastructure/vault.py lines 170-220 (VaultTransaction.insert_edge signature)
    - src/ztlctl/services/_helpers.py (today_iso for edge created date)
  </read_first>
  <behavior>
    - Test 1: check() with min_severity="info" includes CAT_SEMANTIC issues when contradictions found
    - Test 2: _check_semantic returns empty list when VectorService unavailable (graceful skip)
    - Test 3: _check_semantic returns CheckIssue entries with severity="info" for each candidate pair
    - Test 4: confirm_contradiction records two edges (A->B and B->A) with edge_type="contradicts"
    - Test 5: confirm_contradiction returns error when note_a or note_b not found in vault
    - Test 6: confirm_contradiction skips duplicate edges (check_duplicate=True)
    - Test 7: contradictions_review_impl returns JSON-serializable dict with candidates array
    - Test 8: contradictions_review_impl returns empty candidates when vector unavailable
  </behavior>
  <action>
    **1. Add CAT_SEMANTIC to CheckService** (per CNTR-03):
    - Add constant: `CAT_SEMANTIC = "semantic_analysis"` alongside existing CAT_* constants
    - Add `_check_semantic(self, conn: Connection) -> list[dict[str, Any]]` method:
      - Use lazy import: `from ztlctl.services.contradiction import ContradictionService`
      - Call `ContradictionService(self._vault).find_candidates()`
      - If result.ok and candidates exist, create CheckIssue dicts:
        ```python
        {"severity": SEVERITY_INFO, "category": CAT_SEMANTIC,
         "message": f"Possible contradiction: '{title_a}' vs '{title_b}' (score: {score:.2f})",
         "note_a": note_a, "note_b": note_b, "score": score, "signals": signals}
        ```
      - Return empty list if vector unavailable or no candidates (graceful skip, no error)
    - Wire into `check()` method: add `with trace_span("semantic_analysis"): issues.extend(self._check_semantic(conn))` after the garden_health span

    **2. Implement confirm_contradiction on ContradictionService** (per CNTR-04):
    - Add method `confirm_contradiction(self, *, note_a: str, note_b: str) -> ServiceResult`:
      - Validate both note IDs exist in nodes table
      - Use vault.transaction() to insert two edges:
        - `txn.insert_edge(note_a, note_b, "contradicts", "user", today_iso(), check_duplicate=True, check_target_exists=True)`
        - `txn.insert_edge(note_b, note_a, "contradicts", "user", today_iso(), check_duplicate=True, check_target_exists=True)`
      - Return ServiceResult(ok=True, op="confirm_contradiction", data={"note_a": note_a, "note_b": note_b, "edges_created": count})
      - Dispatch post_action event via `_dispatch_post_action_event`
    - Update the stub in ContradictionController to call the real method (remove NOT_IMPLEMENTED error)

    **3. Add MCP resource ztlctl://review/contradictions** (per CNTR-05):
    - Add entry to `_RESOURCE_CATALOG`:
      ```python
      {"uri": "ztlctl://review/contradictions", "description": "Contradiction candidate pairs scored by heuristic analysis."}
      ```
    - Add `contradictions_review_impl(vault: Any) -> dict[str, Any]` function:
      - `from ztlctl.services.contradiction import ContradictionService`
      - Call `ContradictionService(vault).find_candidates()`
      - Return `{"candidates": result.data.get("candidates", []), "count": result.data.get("count", 0)}` if ok
      - Return `{"candidates": [], "count": 0}` if not ok
    - Register in `register_resources`:
      ```python
      @server.resource("ztlctl://review/contradictions")  # type: ignore[untyped-decorator]
      def contradictions_review_resource() -> str:
          import json
          return json.dumps(contradictions_review_impl(vault), indent=2)
      ```

    Create tests in `tests/test_contradiction_integration.py` for CheckService CAT_SEMANTIC and confirm_contradiction.
    Create tests in `tests/test_contradiction_resource.py` for contradictions_review_impl.
  </action>
  <verify>
    <automated>uv run pytest tests/test_contradiction_integration.py tests/test_contradiction_resource.py -x -v</automated>
  </verify>
  <acceptance_criteria>
    - CAT_SEMANTIC category exists and check() includes semantic analysis
    - Contradiction check gracefully skips when vector unavailable
    - confirm_contradiction creates bidirectional "contradicts" edges
    - MCP resource returns candidate pairs sorted by score
    - All tests pass
  </acceptance_criteria>
  <done>CheckService semantic category wired, graph edges recorded, MCP resource functional</done>
</task>

<task type="auto">
  <name>Task 2: ActionRegistry registration for check_contradictions and confirm_contradiction</name>
  <files>
    src/ztlctl/actions/_check.py,
    tests/test_contradiction_actions.py
  </files>
  <read_first>
    - src/ztlctl/actions/_check.py (existing check action registrations — follow exact pattern)
    - src/ztlctl/actions/_session.py lines 275-356 (recall action registration pattern with lazy controller import)
    - src/ztlctl/actions/definitions.py (ActionDefinition, ActionParam signatures)
    - src/ztlctl/controllers/contradiction.py (ContradictionController method signatures)
  </read_first>
  <action>
    Add two ActionDefinitions to `_register_check_actions()` in `src/ztlctl/actions/_check.py` (per CNTR-06):

    1. **`check_contradictions`** in category `"analysis"`:
       ```python
       from ztlctl.controllers.contradiction import ContradictionController

       registry.register(
           ActionDefinition(
               name="check_contradictions",
               description="Find candidate note pairs that may contradict each other.",
               category="analysis",
               params=(
                   ActionParam(
                       "similarity_threshold",
                       float,
                       required=False,
                       default=0.85,
                       description="Minimum cosine similarity to consider a pair (0.0-1.0).",
                   ),
                   ActionParam(
                       "max_pairs",
                       int,
                       required=False,
                       default=20,
                       description="Maximum number of candidate pairs to return.",
                   ),
               ),
               handler=lambda vault, **kw: ContradictionController(vault).check_contradictions(**kw),
               side_effect="read",
               mcp_when_to_use="Scanning the vault for notes that may contradict each other based on semantic similarity and heuristic signals.",
               mcp_avoid_when="The vault has no decision notes or no vector index.",
               cli_group="check",
               cli_name="contradictions",
           )
       )
       ```

    2. **`confirm_contradiction`** in category `"analysis"`:
       ```python
       registry.register(
           ActionDefinition(
               name="confirm_contradiction",
               description="Record a confirmed contradiction between two notes as graph edges.",
               category="analysis",
               params=(
                   ActionParam(
                       "note_a",
                       str,
                       required=True,
                       description="ID of the first note in the contradiction pair.",
                       cli_is_argument=True,
                       mcp_example="ZTL-0042",
                   ),
                   ActionParam(
                       "note_b",
                       str,
                       required=True,
                       description="ID of the second note in the contradiction pair.",
                       cli_is_argument=True,
                       mcp_example="ZTL-0099",
                   ),
               ),
               handler=lambda vault, **kw: ContradictionController(vault).confirm_contradiction(**kw),
               side_effect="write",
               mcp_when_to_use="Confirming a contradiction surfaced by check_contradictions after human or agent review.",
               mcp_avoid_when="The contradiction has not been reviewed yet.",
               mcp_common_errors=("NOT_FOUND",),
               cli_group="check",
               cli_name="confirm-contradiction",
           )
       )
       ```

    Create `tests/test_contradiction_actions.py`:
    - Test that `check_contradictions` and `confirm_contradiction` are registered in ActionRegistry
    - Test that their category is "analysis"
    - Test that handler callables are wired to ContradictionController
    - Verify params match expected names and types
  </action>
  <verify>
    <automated>uv run pytest tests/test_contradiction_actions.py -x -v && uv run ruff check src/ztlctl/actions/_check.py && uv run mypy src/ztlctl/actions/_check.py</automated>
  </verify>
  <acceptance_criteria>
    - check_contradictions registered in ActionRegistry under "analysis" category
    - confirm_contradiction registered in ActionRegistry under "analysis" category
    - Both actions have correct params, side_effects, and MCP metadata
    - CLI names are "contradictions" and "confirm-contradiction" under "check" group
    - All tests pass, mypy clean, ruff clean
  </acceptance_criteria>
  <done>Both contradiction actions registered in ActionRegistry, auto-generating CLI commands and MCP tools</done>
</task>

</tasks>

<verification>
uv run pytest tests/test_contradiction_integration.py tests/test_contradiction_resource.py tests/test_contradiction_actions.py -x -v
uv run ruff check src/ztlctl/services/check.py src/ztlctl/services/contradiction.py src/ztlctl/actions/_check.py src/ztlctl/mcp/resources.py
uv run mypy src/ztlctl/services/check.py src/ztlctl/services/contradiction.py src/ztlctl/actions/_check.py src/ztlctl/mcp/resources.py
uv run pytest --tb=short  # Full test suite regression check
</verification>

<success_criteria>
- CAT_SEMANTIC in CheckService reports contradiction candidates at info severity (CNTR-03)
- confirm_contradiction records bidirectional "contradicts" edges in graph (CNTR-04)
- ztlctl://review/contradictions MCP resource returns scored candidates (CNTR-05)
- check_contradictions registered in ActionRegistry under analysis category (CNTR-06)
- Full test suite passes, mypy strict clean, ruff clean
</success_criteria>

<output>
After completion, create `.planning/phases/21-contradiction-detection/21-02-SUMMARY.md`
</output>
