# RLS tooling boundary — дизайн (скил `rls-check` ↔ агент `rls-reviewer`)

**Дата:** 2026-05-16
**Статус:** дизайн одобрен заказчиком, готов к `writing-plans`
**Триггер:** заказчик — «rls-reviewer и rls-check оба проверяют соответствие политик RLS … нет чёткой границы когда какой. проверь наличие этого конфликта и предложи решение, но посмотри чтобы это решение не создало новых конфликтов».

---

## 1. Контекст и проблема

Два проектно-локальных инструмента проверяют одно и то же — соответствие политик RLS (Row-Level Security, защита строк по тенанту):

| Инструмент | Файл | Тип | Вызов |
|---|---|---|---|
| `rls-check` | `.claude/skills/rls-check/SKILL.md` | скил | `/rls-check <table_name>` (ручной; `disable-model-invocation: true`) |
| `rls-reviewer` | `.claude/agents/rls-reviewer.md` | субагент | диспатч через Agent tool |

### 1.1. Конфликт подтверждён — 4 измерения

Конфликт реальный, не мнимый. Зафиксирован по 4 измерениям с pin'ами:

1. **Покрытие.** Оба прогоняют один и тот же чеклист RLS. Скил: 7 шагов (`.claude/skills/rls-check/SKILL.md:19-72`), вывод 8 строк, `Pass: 8/8`. Агент: 7 пунктов (`.claude/agents/rls-reviewer.md:41-66`), вывод `Pass: N/7`. Пункты совпадают: tenant_id, ENABLE RLS, SELECT policy, ALL policy, 5-role GRANTs, CHANGELOG, squawk.
2. **Дрейф чеклиста.** Скил включает живой дымовой тест (`pest --filter RlsSmokeTest`, шаг 7 — `.claude/skills/rls-check/SKILL.md:66-72`). У агента такого пункта нет (`.claude/agents/rls-reviewer.md:41-51`). То есть инструменты не идентичны — один из них «шире», и нигде не сказано, что эта асимметрия намеренная.
3. **Нет взаимных ссылок.** Секция «Не использовать когда» скила (`.claude/skills/rls-check/SKILL.md:93-96`) не упоминает агента. Секция «Out of scope» агента (`.claude/agents/rls-reviewer.md:75-79`) не упоминает скил. Читатель ни из одного файла не узнаёт о существовании второго.
4. **Конфликт уже отслеживается.** На карте автоматизации (`docs/automation-graph.html`) он помечен как 🔴 RED («не закрыт правилом») — ребро `CONFLICT('sk_rls','ag_rls', …, 'RED')` на `automation-graph.html:384`, плюс зеркальные записи в `nd()`-узлах `automation-graph.html:685` и `automation-graph.html:766`, плюс метаданные ребра `automation-graph.html:1303`.

### 1.2. Что НЕ является частью проблемы

- Оба инструмента — **проектно-локальные**, вне реестра Tooling и вне PSR_v1. Это явно зафиксировано в спеке-первоисточнике `docs/superpowers/specs/2026-05-13-claude-automation-recommendations-design.md` §8: «`/q-item-add` + `/rls-check` — project-local skills, не входят в Tooling registry»; «Subagents … project-local agents, не trigger'ят PSR_v1 R10 (не plugins)». → правка этих файлов **не задевает нормативку** (Pravila / CLAUDE.md / PSR_v1 / Tooling).

---

## 2. Решение

**Направление** (выбрано заказчиком): оставить оба инструмента, прописать регламент. Не консолидировать в один.

**Подход** (выбран заказчиком): **Подход 1 — асимметрия как граница**. Существующий дрейф чеклиста (скил с дымовым тестом, агент без) превращается из необъяснённого расхождения в намеренную, задокументированную границу.

### 2.1. Правило границы

> **Знаешь имя одной конкретной таблицы → `/rls-check <table>`. Есть diff / ветка / PR с изменениями БД → агент `rls-reviewer`.**

Асимметрия между инструментами намеренная и обоснованная:

| | скил `rls-check` | агент `rls-reviewer` |
|---|---|---|
| Контекст исполнения | основной контекст сессии | отдельный контекст субагента |
| Вход | одна названная таблица | diff / миграция / PR (несколько таблиц) |
| Объём проверки | **8 строк вывода** — 7 статических + живой дымовой тест | **7 строк вывода** — 7 статических |
| Дымовой тест (`pest --filter RlsSmokeTest`) | да, шаг 7 | нет, намеренно |

7 статических строк вывода агента === первые 7 строк вывода скила (общее статическое ядро: tenant_id, ENABLE RLS, SELECT policy, ALL policy, 5-role GRANTs, CHANGELOG, squawk). 8-я строка скила — дымовой тест — у агента отсутствует.

### 2.2. Почему дымовой тест остаётся только в скиле

Это ключ к тому, что решение не создаёт нового конфликта. Запускать Pest в ревью-субагенте — плохая идея по двум причинам:

1. **Скорость.** Полный `pest --filter RlsSmokeTest` — секунды-десятки секунд; ревью-субагент должен возвращать быстрый статический вердикт.
2. **Гонки `--parallel`.** Pest в проекте задевает квирки 72 (Redis `supplier:session` race) и 77 (unique-key collision) — документированы в `.claude/agents/pest-parallel-debugger.md`. Если бы агент `rls-reviewer` сам запускал Pest, появилось бы **новое** ребро-конфликт `rls-reviewer ↔ квирки 72/77`. Оставляя дымовой тест за пределами агента, мы этого конфликта избегаем.

Поэтому граница «скил с живым тестом / агент только статика» — не недоработка, а сознательное проектное решение.

---

## 3. Изменения по файлам

Три файла + одна memory-запись. Точный текст — ниже; финальные формулировки уточняются на этапе `writing-plans` / реализации.

### 3.1. `.claude/skills/rls-check/SKILL.md`

**(a)** Новая секция `## Граница с агентом rls-reviewer` сразу после `## Когда использовать`:

```markdown
## Граница с агентом rls-reviewer

`rls-check` (этот скил) и `rls-reviewer` (агент, `.claude/agents/rls-reviewer.md`)
оба проверяют RLS, но в разных ситуациях. Правило выбора:

- Знаешь имя одной конкретной таблицы, проверка вручную перед коммитом →
  **`/rls-check <table>`** (этот скил).
- Есть diff / ветка / PR с изменениями БД, набор таблиц заранее не известен →
  **агент `rls-reviewer`**.

Скил работает в основном контексте по одной названной таблице и прогоняет
**8 строк вывода** — 7 статических пунктов + живой дымовой тест
(`pest --filter RlsSmokeTest`, шаг 7). Агент работает в отдельном контексте
субагента, разбирает diff/миграцию/PR и прогоняет только **7 статических**
строк — дымовой тест намеренно не запускает.

Первые 7 строк вывода у обоих — общее статическое ядро (tenant_id, ENABLE RLS,
SELECT/ALL политики, GRANT'ы 5 ролей, CHANGELOG, squawk). Это не дубль-ошибка:
ядро одно, обёртки разные. Дымовой тест — только в скиле: запуск Pest в
ревью-субагенте медленный и задевает гонки `--parallel` (квирки 72/77,
см. `.claude/agents/pest-parallel-debugger.md`).
```

**(b)** Третий буллет в `## Не использовать когда`:

```markdown
- Проверяешь не одну названную таблицу, а diff/ветку/PR с изменениями БД —
  это сценарий агента `rls-reviewer`, не скила.
```

**(c)** Уточнение в frontmatter `description` (одно предложение, в конце): для diff/ветки/PR-ревью — агент `rls-reviewer`.

### 3.2. `.claude/agents/rls-reviewer.md`

**(a)** Зеркальная секция `## Граница со скилом /rls-check` сразу после `## Контекст проекта`:

```markdown
## Граница со скилом /rls-check

`rls-reviewer` (этот агент) и скил `/rls-check`
(`.claude/skills/rls-check/SKILL.md`) оба проверяют RLS. Правило выбора:

- Есть diff / ветка / PR с изменениями БД, набор таблиц заранее не известен →
  **этот агент**.
- Знаешь имя одной конкретной таблицы, проверка вручную перед коммитом →
  **скил `/rls-check <table>`**.

Этот агент прогоняет **7 статических пунктов** чеклиста. Живой дымовой тест
(`pest --filter RlsSmokeTest`) намеренно **не входит** в агентский чеклист:
запуск Pest в ревью-субагенте медленный и задевает гонки `--parallel`
(квирки 72/77, см. `.claude/agents/pest-parallel-debugger.md`). Живой дымовой
тест — 8-я строка скила `/rls-check`. 7 пунктов агента === первые 7 строк
вывода скила (общее статическое ядро).
```

**(b)** Четвёртый буллет в `## Out of scope`:

```markdown
- Проверка одной названной таблицы вручную перед коммитом + живой дымовой
  тест — сценарий скила `/rls-check`, не агента.
```

**(c)** Уточнение в frontmatter `description` (одно предложение, в конце): для ручной проверки одной названной таблицы — скил `/rls-check`.

Опционально: строка `limits` узла на `automation-graph.html:762` уже содержит «Не замена скилу rls-check — у них разные сценарии» — после этого спека формулировка становится фактически точной; править не обязательно, можно уточнить ссылкой на новую секцию.

### 3.3. `docs/automation-graph.html` — 4 точки

Конфликт `sk_rls ↔ ag_rls` перекрашивается 🔴 RED → 🟢 GREEN («закрыт правилом»). Удалять ребро **нельзя** — карта моделирует решённые конфликты именно как GREEN-записи в массиве `conflicts` (так сделаны существующие GREEN на строках 388–391); удаление = потеря исторической записи.

| # | Строка | Сейчас | Станет |
|---|---|---|---|
| 1 | `automation-graph.html:384` | `CONFLICT('sk_rls','ag_rls','RLS compliance: оба покрывают, нет регламента','RED')` | label → «RLS: граница задана — скил по таблице, агент по diff/PR (spec 2026-05-16)», type → `'GREEN'` |
| 2 | `automation-graph.html:685` | `nd()` узла `sk_rls`, запись конфликта `type: 'RED'` | `desc` → описание решённой границы, `type: 'GREEN'` |
| 3 | `automation-graph.html:766` | `nd()` узла `ag_rls`, запись конфликта `type: 'RED'` | `desc` → описание решённой границы, `type: 'GREEN'` |
| 4 | `automation-graph.html:1303` | `EDGE_META['sk_rls->ag_rls']`: `when: 'skill и agent оба претендуют на RLS-аудит'`, `rule: 'нет регламента (двойное покрытие)'` | `when` → «граница задана: скил — по одной таблице, агент — по diff/PR», `rule` → ссылка на секции «Граница…» в обоих файлах + этот спек |

**Счётчика конфликтов в HTML нет.** Проверено: легенда `automation-graph.html:134-136` показывает только метки без чисел, `<h1>` на строке 80 — без счётчика. Раскладку 🔴/⚫/🟢 vis.js считает динамически из массива `conflicts`. → в HTML числа править не нужно; после правки фактический расклад становится 🔴2 / ⚫4 / 🟢5 (всего 11 — без изменений).

### 3.4. Memory — `project_automation_map.md`

Описание в memory-файле `project_automation_map.md` содержит строку «11 конфликтов 3-color 🔴3/⚫4/🟢4». После правки карты обновить → «🔴2/⚫4/🟢5» (всего 11 — без изменений). Иначе memory разойдётся с картой — мелкая, но новая несогласованность.

---

## 4. Почему решение не создаёт новых конфликтов

Прямой ответ на ограничение заказчика. Проверены все поверхности, на которых правка могла бы породить новый конфликт:

1. **Нормативка (Pravila / CLAUDE.md / PSR_v1 / Tooling) — 0 правок.** Оба инструмента проектно-локальные, вне реестра Tooling и вне PSR_v1 R10 (спек-первоисточник §8). Регламент границы живёт внутри самих файлов инструментов + карты. Цепочка приоритетов §1 CLAUDE.md не затрагивается.
2. **Карта не устаревает.** Конфликт уже отслеживается как 🔴 RED #1 — если бы мы поправили инструменты, но не карту, карта стала бы stale (= новый конфликт «карта ↔ реальность»). Поэтому правятся все 4 точки карты синхронно + memory-описание.
3. **Дымовой тест остаётся вне агента.** Подход 3 (заставить агента прогонять Pest) был бы соблазнителен ради симметрии, но создал бы **новое** ребро-конфликт `rls-reviewer ↔ квирки 72/77`. Подход 1 этого избегает: см. §2.2.
4. **Ссылки — код-спаны, не markdown-ссылки.** lychee сканирует все `.md`, включая `.claude/**` (квирк 92). Взаимные ссылки оформляются как `` `.claude/agents/rls-reviewer.md` `` (код-спан) — lychee их не проверяет, значит при будущем перемещении файла битой ссылки не появится. markdown-ссылки `[…](…)` создали бы такой риск.
5. **Скил остаётся валидным структурным образцом.** Спек `docs/superpowers/specs/2026-05-16-regression-skill-design.md` использует `rls-check/SKILL.md` как структурный образец. Новая секция «Граница…» добавляется в том же стиле, что секция-редирект «Не использовать когда» соседнего скила `q-item-add` — паттерн уже устоявшийся, образец не ломается.
6. **worktree `plan-6-regions`.** Расхождение этого worktree с `main` — предсуществующий факт (отдельный merge позже), не следствие этой правки.

---

## 5. Вне scope

- Консолидация двух инструментов в один (заказчик выбрал «оставить оба»).
- Изменение самих 7 статических пунктов чеклиста RLS — правится только обёртка/граница, не содержание проверок.
- Любые правки нормативки — см. §4 п.1.
- Автоматический запуск `rls-reviewer` на каждом PR (CI-интеграция) — отдельная тема, не входит.

---

## 6. Верификация (после реализации)

1. `git diff --name-only` — в наборе ровно: `.claude/skills/rls-check/SKILL.md`, `.claude/agents/rls-reviewer.md`, `docs/automation-graph.html`, спек. Файлов нормативки (`docs/Pravila_*`, `CLAUDE.md`, `docs/Plugin_stack_rules_*`, `docs/Tooling_*`) — нет.
2. lychee на `.claude/skills/rls-check/SKILL.md` + `.claude/agents/rls-reviewer.md` + этот спек — 0 битых ссылок.
3. Визуальный smoke `docs/automation-graph.html` — открыть, убедиться: ребро `sk_rls↔ag_rls` зелёное, узлы `rls-check`/`rls-reviewer` показывают конфликт-запись как 🟢, 0 JS-ошибок в консоли, общее число узлов/рёбер не изменилось.
4. Полный pre-commit прогон lefthook перед коммитом (gitleaks + lychee + larastan + pint + pest — по применимым stage'ам).
5. Атомарные коммиты: правка инструментов + карты — один логический change.

---

## 7. Изменённые файлы (итог)

| Файл | Характер правки |
|---|---|
| `.claude/skills/rls-check/SKILL.md` | +секция «Граница…», +буллет, +фраза в `description` |
| `.claude/agents/rls-reviewer.md` | +секция «Граница…», +буллет, +фраза в `description` |
| `docs/automation-graph.html` | 4 точки: ребро 384 + узлы 685/766 + метаданные 1303 — 🔴→🟢 |
| `project_automation_map.md` (memory) | счётчик 🔴3/⚫4/🟢4 → 🔴2/⚫4/🟢5 |
| `docs/superpowers/specs/2026-05-16-rls-tooling-boundary-design.md` | этот спек |

Нормативка — **0 правок**.
