# drift_analyzer_tool

**Drift Analyzer — 6-й gate у release_check**  
Знаходить розбіжності між "джерелами правди" (docs/inventory/config) та фактичним станом repo.

---

## Огляд

`drift_analyzer_tool` — детерміністичний (без LLM), read-only аналізатор drift у 4 категоріях.

| Категорія | Джерело правди | Факт | Приклад drift |
|-----------|---------------|------|---------------|
| **services** | `inventory_services.csv` / `01_SERVICE_CATALOG.md` | `docker-compose*.yml` | DEPLOYED сервіс відсутній у compose |
| **openapi** | `docs/contracts/*.openapi.yaml` | FastAPI route decorators у коді | Endpoint у spec але нема в коді |
| **nats** | `inventory_nats_topics.csv` | `nc.publish/subscribe` у коді | Subject у коді не задокументований |
| **tools** | `config/tools_rollout.yml` + `rbac_tools_matrix.yml` | Handlers у `tool_manager.py` | Tool у rollout але нема handler |

---

## Використання

### Через агента (OpenCode / Telegram)

```
"Запусти drift аналіз"
"Перевір drift для категорій tools та openapi"
"Drift check перед релізом"
```

### Через execute_tool

```json
{
  "action": "analyze",
  "categories": ["services", "openapi", "nats", "tools"],
  "timeout_sec": 25
}
```

### Через release_check (Gate 6, optional)

```json
{
  "action": "start_task",
  "params": {
    "task_id": "release_check",
    "inputs": {
      "service_name": "router",
      "run_drift": true,
      "drift_categories": ["openapi", "tools"],
      "drift_timeout_sec": 20
    }
  }
}
```

---

## Параметри

| Параметр | Тип | Обов'язковий | Опис |
|----------|-----|:---:|------|
| `action` | `"analyze"` | ✅ | Єдина дія |
| `categories` | array | — | Підмножина `["services","openapi","nats","tools"]` (default: всі) |
| `timeout_sec` | number | — | Таймаут в секундах (default: 25, max: 30) |

---

## Формат відповіді

```json
{
  "pass": false,
  "summary": "❌ Drift analysis FAILED. 2 error(s), 1 warning(s).",
  "stats": {
    "errors": 2,
    "warnings": 1,
    "infos": 0,
    "skipped": [],
    "items_checked": {
      "services": 42,
      "openapi": 18,
      "tools": 65
    },
    "elapsed_ms": 1234.5,
    "by_category": { "...": "..." }
  },
  "findings": [
    {
      "category": "tools",
      "severity": "error",
      "id": "DRIFT-TOOLS-001",
      "title": "Tool 'fake_tool_x' in tools_rollout.yml but no handler in tool_manager.py",
      "evidence": {
        "path": "config/tools_rollout.yml",
        "details": "'fake_tool_x' referenced in rollout groups but missing from KNOWN_TOOL_HANDLERS"
      },
      "recommended_fix": "Add handler for 'fake_tool_x' in tool_manager.py execute_tool dispatch, or remove from rollout."
    }
  ]
}
```

### Pass/Fail правило

| Умова | `pass` |
|-------|--------|
| Будь-який `severity: error` | `false` |
| Тільки `warning` / `info` | `true` |
| Категорія відсутня (skipped) | не впливає |

---

## Категорії деталі

### 1. services — Service Catalog vs docker-compose

**Джерела:**
- A: `docs/architecture_inventory/inventory_services.csv` → поле `type` (DEPLOYED/DEFINED/...)
- B: всі `docker-compose*.yml` у repo root + `infra/compose/docker-compose.yml`

**Findings:**

| ID | Severity | Умова |
|----|----------|-------|
| `DRIFT-SVC-001` | error | Сервіс `DEPLOYED` у catalog, але відсутній в compose |
| `DRIFT-SVC-002` | warning | Сервіс є в compose, але не в catalog |

**Normalization:** `my-svc` ↔ `my_svc` (dash/underscore equivalence).

---

### 2. openapi — API Spec vs Code Routes

**Джерела:**
- A: `docs/contracts/*.openapi.yaml` та будь-які `openapi*.yaml/yml/json` у repo
- B: Python файли — `@app.get(...)`, `@router.post(...)`, `.add_api_route(...)`

**Findings:**

| ID | Severity | Умова |
|----|----------|-------|
| `DRIFT-OAS-001` | error | Path у OpenAPI spec але не знайдено в коді |
| `DRIFT-OAS-002` | error | Path `/v1/*` є в коді але не описаний у spec |
| `DRIFT-OAS-003` | warning | Method mismatch для тієї самої path |

**Normalization:** trailing slash, lowercase path comparison.  
**Скоп коду:** тільки `/v1/` routes перевіряються для OAS-002.

---

### 3. nats — Subject Inventory vs Code Usage

**Джерела:**
- A: `docs/architecture_inventory/inventory_nats_topics.csv` (поле `subject`)
- B: regex пошук `nc.publish(...)`, `nc.subscribe(...)`, `subject=...` у `.py` файлах

**Findings:**

| ID | Severity | Умова |
|----|----------|-------|
| `DRIFT-NATS-001` | warning | Subject використовується в коді але відсутній у inventory |
| `DRIFT-NATS-002` | info | Subject у inventory але не знайдено в коді (можливо legacy) |

**Wildcard matching:** `agent.run.{agent_id}` → `agent.run.*` → `agent.run.>`.  
**Skipped:** якщо `inventory_nats_topics.csv` відсутній — категорія `skipped`, gate не падає.

---

### 4. tools — Rollout/Matrix vs Handlers

**Джерела:**
- A: `config/tools_rollout.yml` (всі tool-назви у groups, з @group expand)
- B: `config/rbac_tools_matrix.yml` (секція `tools:`)
- C: `KNOWN_TOOL_HANDLERS` у `drift_analyzer.py` (compile-time список)
- D: `agent_tools_config.effective_tools` для ролей `agent_default` і `agent_cto`

**Findings:**

| ID | Severity | Умова |
|----|----------|-------|
| `DRIFT-TOOLS-001` | error | Tool у rollout але нема handler |
| `DRIFT-TOOLS-002` | warning | Handler є але tool відсутній у RBAC matrix |
| `DRIFT-TOOLS-003` | warning | Tool у matrix але ніколи не потрапляє в effective_tools |

**Maintenance:** при додаванні нового tool handler — оновіть `KNOWN_TOOL_HANDLERS` у `drift_analyzer.py`.

---

## Безпека

- **Read-only:** не записує нічого у repo
- **Path traversal:** сканує тільки всередині `REPO_ROOT`
- **Excluded dirs:** `node_modules`, `.git`, `venv*`, `__pycache__`, `dist`, `build`, `rollback_backups`
- **File size limit:** max 256KB per file
- **File count limit:** max 300 files per category scan
- **Secret redaction:** evidence маскується `_redact_evidence()` перед поверненням
- **Governance:** проходить через `ToolGovernance.pre_call/post_call` (RBAC, limits, audit)

---

## RBAC Entitlements

| Entitlement | Хто | Що дозволяє |
|-------------|-----|-------------|
| `tools.drift.read` | `agent_cto`, `agent_oncall` | Запускати drift analyze |
| `tools.drift.gate` | `agent_cto` | Запускати drift у release gate |

---

## Limits (`config/tool_limits.yml`)

| Параметр | Значення |
|----------|----------|
| `timeout_ms` | 30 000 (30s) |
| `max_chars_in` | 5 000 |
| `max_bytes_out` | 524 288 (512KB) |
| `rate_limit_rpm` | 5 |
| `concurrency` | 1 |

---

## Оновлення `KNOWN_TOOL_HANDLERS`

Коли додається новий tool handler у `tool_manager.py`:

1. Додай tool name до `KNOWN_TOOL_HANDLERS` у `drift_analyzer.py`
2. Додай tool до `config/tools_rollout.yml` (потрібна роль)
3. Додай tool до `config/rbac_tools_matrix.yml` (actions + entitlements)
4. Запусти `pytest tests/test_drift_analyzer.py::TestToolsDrift` щоб перевірити

```python
# drift_analyzer.py
KNOWN_TOOL_HANDLERS: FrozenSet[str] = frozenset({
    ...,
    "my_new_tool",    # add here
})
```

---

## Файли

| Файл | Призначення |
|------|-------------|
| `services/router/drift_analyzer.py` | Вся логіка аналізу (4 категорії) |
| `services/router/tool_manager.py` | Handler `_drift_analyzer_tool` + TOOL_DEFINITIONS |
| `services/router/release_check_runner.py` | Gate 6 `_run_drift()` |
| `config/tools_rollout.yml` | `cto_tools` включає `drift_analyzer_tool` |
| `config/rbac_tools_matrix.yml` | `drift_analyzer_tool` actions + `tools.drift.*` entitlements |
| `config/tool_limits.yml` | `drift_analyzer_tool` limits |
| `tests/test_drift_analyzer.py` | 29 тестів + fixtures |
