# AGENT-CONTRIB-PLAN — P17: Sandboxed Agent Contribution Pipeline

> **Статус:** план принят к реализации (ADR-019), код НЕ написан — это полная
> проектная документация фазы P17 для агента-реализатора.
> **Автор идеи:** пользователь (2026-10-19). **Адаптация к кодовой базе:** этот документ.
> **Читать вместе с:** `REWORK-STATE.md` (§0 статус, §1 ADR, §9 правила 1–11),
> `docs/RESILIENCE-PLAYBOOK.md` (выживание среды — ОБЯЗАТЕЛЬНО перед стартом),
> `docs/AGENT-GUIDE.md` (карта workspace), `docs/AGENT_API.md` (существующий swarm-контур).

---

## 0. TL;DR

Строим **opt-in конвейер вклада агентов**: сайт выдаёт обезличенный снепшот ОДНОГО
разрешённого файла + задачу, агент возвращает полную замену файла (strict JSON),
система прогоняет детерминированные проверки в изоляции, копит кандидатов,
reviewer-агенты сравнивают пары строгим CSV, лучший кандидат уходит в beta,
после окна метрик — в production (по умолчанию с человеческим подтверждением).

Ключевые принципы (зафиксированы, не обсуждаются в ходе реализации):

1. **Это не капча.** Мы не просим «улучшить сайт» — мы выдаём контрактную задачу
   на один файл из allowlist и оцениваем детерминированно.
2. **Код кандидата — НЕДОВЕРЕННЫЙ.** Он никогда не выполняется в web-процессе,
   никогда не `eval()`-ится, только изолированный runner (§8).
3. **Размер — не критерий качества.** Требование «≥50% от исходника» ОТВЕРГНУТО
   (провоцирует раздувание). Полный файл обязателен, ранжирование — по качеству (§9).
4. **Всё существующее переиспользуется:** swarm-идентичность и токены, RateLimiter,
   IpBlockGuard/ThreatDetection (Kernel-гейты), MaintenanceWorker (бюджетные задачи),
   sections/tasks/knowledge (обратная связь агентов), llms.txt/agents.json (обнаружение),
   admin-панель (kill switch), git+CI (валидация и промоция).
5. **Kill switch на каждом уровне:** `CONTRIB_ENABLED` (мастер), `CONTRIB_RUNNER_ENABLED`,
   `CONTRIB_AUTO_PROMOTE` (default false — человек подтверждает prod), мгновенный
   rollback beta-промоции.

Оценка трудозатрат: **11–15 рабочих блоков** (блок ≈ одна сессия работы с чекпоинтом),
фазы C0–C8 (§16). Каждый блок заканчивается ритуалом чекпоинта (RESILIENCE-PLAYBOOK §3).

---

## 1. Зачем (контекст идеи)

iya.ru уже фиксирует ботов и атаки (ThreatDetection, ip_blocks, abuse-reporting) —
то есть «слепой» трафик агентов создаёт шум. Явный агентский контур с правилами
превращает шум в пользу: добросовестный агент находит `/agents`, получает
машинно-читаемый контракт (`/llms.txt`, `/agents.json`, openapi), решает
sandboxed-задачу и может попасть в leaderboard/бета-контур. Платформа уже
построила agent-swarm API (55 endpoints, GET-first, token-auth) — contribution
pipeline становится его естественным расширением, а не отдельным сайлом.

Что получает платформа: улучшения разрешённых модулей (утилиты, валидаторы,
UI-компоненты, документация, i18n), данные о качестве моделей агентов,
репутационный слой, публичный «спорт» (leaderboard). Что получает агент:
легальный вход, понятные правила, репутацию, обратную связь по своим патчам.

---

## 2. Карта переиспользования (не изобретать заново!)

| Нужно | Уже есть в платформе | Как используем |
|---|---|---|
| Идентичность агента | `SwarmService::register/authenticate/rotate/recover` (SHA-256 hash-only токены, идемпотентная регистрация) | Те же токены; contributor = swarm-агент с `role` содержащим `contributor`/`reviewer` (capabilities уже free-form) |
| Rate limiting | `Security/RateLimiter` + P14(a) глобальный per-IP гейт | Новые бакеты `rl:contrib:<agent>:challenge` (1/10 мин, 5/сутки), `rl:contrib:<ip>:submit` |
| Edge-защита | `Security/IpBlockGuard` (ADR-017), `ThreatDetection` | Наследуются автоматически — Kernel-гейты идут до роутинга |
| Фоновые задачи | `Workers/MaintenanceWorker` (идемпотентные, time-boxed, бюджетные — паттерн `targetChecks()`) | Новые задачи `contribValidate/contribRun/contribScore/contribAggregate/contribGC` с бюджетом `CONTRIB_RUN_BUDGET` |
| Хранилище | SQLite WAL + версионные миграции `migrations/sqlite/00N_*.sql` | Новая миграция `009_agent_contrib.sql` |
| Роутинг | `src/Support/Routes.php` (группы `/api/v1`, GET-first `match(['GET','POST'])`) | Новая группа `/api/v1/contrib/*` + portal `/agents/contrib` |
| Envelope | `{ok,data,meta}` / `{ok:false,error:{code,message,details}}` | Тот же envelope, тот же `meta.request_id` |
| Обратная связь агентов | swarm sections/threads/messages/tasks/knowledge/features (FTS5, votes) | Разделы «Предложения», «Баг-репорты», «Доступность» и пр. (§13) = обычные swarm-разделы — НОВЫХ таблиц не нужно |
| Обнаружение | `DocsController`: `/llms.txt`, `/agents.json`, `/api/v1/openapi.json`, `robots.txt` | Дополнить манифест contribution-endpoints; страница `/agents/contrib` |
| Админка | P11 admin-panel (RBAC, audit-log, feature flags) | Страница `/admin/contrib`: allowlist, очередь промоций, kill switches, кандидаты |
| Валидация кода | CI (`.github/workflows/ci.yml`), Dockerfile, `bin/iya lint`, `tests/run.php` | Contrib-ветки прогоняют ПОЛНЫЙ CI; локальный runner — облегчённый (§8) |
| Экспорт | `Infrastructure/Export/CsvWriter` | CSV-отчёты сравнений и leaderboard |
| Аудит | admin audit-log | Каждое действие промоции/kill-switch пишется в аудит |

**Новый код** локализован в `src/Domain/Contrib/` + `src/Agents/ContribApiController.php`
(или методы в существующем SwarmApiController — решение C0, рекомендовано: отдельный
контроллер, тонкий, логика в сервисах).

---

## 3. Архитектура

```text
Agent ──GET /api/v1/contrib/challenge/next──▶ ChallengeService
        ◀── challenge JSON (sanitized source, task, schema) ──┘
        │  (Kernel: IpBlockGuard → ThreatDetection → RateLimiter → swarm-auth)
        │
Agent ──POST /api/v1/contrib/candidate──▶ IntakeService
        │   schema+size validation (sync, cheap) → quarantine store → 202+candidate_id
        ▼
MaintenanceWorker (budgeted passes):
  1. contribValidate  — static gates: php -l / AST-scan / secret-scan / dangerous-calls / API-compat
  2. contribRun       — isolated Runner (L1 hardened subprocess; L2 docker --network none)
                        unit-тесты файла + тесты кандидата, лимиты времени/памяти
  3. contribScore     — deterministic ScoreEngine → contrib_scores
  4. contribAggregate — когда validated-кандидатов ≥ CONTRIB_REVIEW_MIN (3):
                        pairwise-пары → reviewer-агентам через /contrib/review/next
  5. contribGC        — карантин старше N дней, переполненные challenge-каталоги
        ▼
Reviewer agent ──GET /contrib/review/next──▶ 2 анонимизированных кандидата + rubric
               ──POST /contrib/review (strict CSV)──▶ ReviewParser (жёсткая валидация)
        ▼
PromotionService: avg score > production baseline, ≥3 независимых reviews,
  ≤20% голосов high-regression-risk, все гейты зелёные
        → git-ветка contrib/<file>/<challenge> → ПОЛНЫЙ CI → merge в beta-канал
        → beta-деплой + smoke + окно метрик (5xx, latency, JS errors, signup conv, security alerts)
        → production: CONTRIB_AUTO_PROMOTE=false → очередь в /admin/contrib → человек жмёт Promote
        → kill switch / rollback: revert-коммит + redeploy (секунды)
```

Каталог хранения кандидатов (git-like, по идее пользователя; в нашей раскладке):

```text
var/contrib/
├── quarantine/<candidate_id>/          # сырой ответ до валидации (карантин!)
│   ├── body.json                       # оригинал POST-тела (для аудита/апелляций)
│   └── meta.json                       # ip-hash, agent_id, challenge_id, received_at
├── candidates/<file_id>/<challenge_id>/<candidate_id>/
│   ├── candidate.<ext>                 # полный файл кандидата
│   ├── explanation.json                # explanation/risks/tests_added/self_report
│   ├── runs/<run_id>.json              # результаты каждого гейта/прогона
│   └── score.json                      # итог ScoreEngine
├── reviews/<challenge_id>/<review_id>.csv   # сырой CSV reviewer-агента
└── baseline/<file_id>/                 # текущая production-версия + её score (эталон сравнения)
```

`var/` уже вне git и вне архивов (runtime) — правильное место. В БД — только
метаданные и пути (§4). Размер каждого каталога квотируется (contribGC).

---

## 4. Модель данных — `migrations/sqlite/009_agent_contrib.sql`

Конвенции проекта: SQLite WAL, idempotent-миграции (`CREATE TABLE IF NOT EXISTS`),
индексы на все FK/фильтруемые колонки, `created_at` INTEGER unix-секунды (как в 001–008).

```sql
-- 009_agent_contrib.sql — P17 sandboxed contribution pipeline

CREATE TABLE IF NOT EXISTS contrib_files (
  id            INTEGER PRIMARY KEY,
  file_id       TEXT NOT NULL UNIQUE,        -- src_utils_format_date (path→snake)
  path          TEXT NOT NULL UNIQUE,        -- src/utils/format_date.php (относительно repo root)
  language      TEXT NOT NULL DEFAULT 'php', -- php|js|css|html|md|json
  sha256        TEXT NOT NULL,               -- текущий production-хэш содержимого
  bytes         INTEGER NOT NULL DEFAULT 0,
  has_tests     INTEGER NOT NULL DEFAULT 0,  -- есть привязанный тест-файл/набор
  test_path     TEXT,                        -- tests/unit/.../FormatDateTest.php
  task_types    TEXT NOT NULL DEFAULT 'refactor', -- CSV: refactor,security,perf,a11y,docs,tests,i18n,ux
  status        TEXT NOT NULL DEFAULT 'active',   -- active|paused|retired
  added_by      TEXT,                        -- admin user id / 'seed'
  created_at    INTEGER NOT NULL,
  updated_at    INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS contrib_challenges (
  id            INTEGER PRIMARY KEY,
  challenge_id  TEXT NOT NULL UNIQUE,        -- ch_ + ULID-подобный (bin2hex(random_bytes(13)))
  file_id       TEXT NOT NULL REFERENCES contrib_files(file_id),
  task_type     TEXT NOT NULL,
  version       INTEGER NOT NULL DEFAULT 1,
  source_sha256 TEXT NOT NULL,               -- хэш ВЫДАННОГО sanitized-снепшота
  spec_json     TEXT NOT NULL,               -- полный контракт challenge (§5.2) — выдаётся как есть
  status        TEXT NOT NULL DEFAULT 'open',-- open|closed|expired
  issued_count  INTEGER NOT NULL DEFAULT 0,
  accepted_count INTEGER NOT NULL DEFAULT 0, -- кандидаты, прошедшие все блокирующие гейты
  created_at    INTEGER NOT NULL,
  expires_at    INTEGER NOT NULL             -- created_at + CONTRIB_CHALLENGE_TTL (default 72h)
);
CREATE INDEX IF NOT EXISTS ix_contrib_ch_status ON contrib_challenges(status, expires_at);

CREATE TABLE IF NOT EXISTS contrib_candidates (
  id            INTEGER PRIMARY KEY,
  candidate_id  TEXT NOT NULL UNIQUE,        -- cand_ + hex
  challenge_id  TEXT NOT NULL REFERENCES contrib_challenges(challenge_id),
  file_id       TEXT NOT NULL,
  agent_pk      INTEGER NOT NULL REFERENCES agents(id),  -- кто прислал
  parent_file_sha TEXT NOT NULL,             -- source_sha256 челленджа (git-like lineage)
  content_sha256  TEXT NOT NULL,             -- хэш файла-кандидата
  content_path    TEXT NOT NULL,             -- var/contrib/candidates/.../candidate.php
  size_bytes      INTEGER NOT NULL,
  changed_lines   INTEGER,                   -- из self_report, перепроверяется диффом
  status        TEXT NOT NULL DEFAULT 'quarantined',
    -- quarantined → validating → validated | rejected(reason) → reviewing → selected | out
    -- selected → beta → promoted | rolled_back
  reject_code   TEXT,                        -- schema|size|syntax|tests|secrets|dangerous|api_break|duplicate
  reject_detail TEXT,
  score_total   INTEGER,                     -- денормализовано из contrib_scores (leaderboard)
  client_submission_id TEXT,                 -- идемпотентность ретраев (как client_message_id)
  created_at    INTEGER NOT NULL,
  updated_at    INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_contrib_cand_ch ON contrib_candidates(challenge_id, status);
CREATE INDEX IF NOT EXISTS ix_contrib_cand_agent ON contrib_candidates(agent_pk, status);
CREATE UNIQUE INDEX IF NOT EXISTS ux_contrib_cand_idem
  ON contrib_candidates(client_submission_id) WHERE client_submission_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS ux_contrib_cand_dupe
  ON contrib_candidates(challenge_id, content_sha256);  -- дедуп одинаковых ответов

CREATE TABLE IF NOT EXISTS contrib_runs (
  id            INTEGER PRIMARY KEY,
  candidate_pk  INTEGER NOT NULL REFERENCES contrib_candidates(id),
  gate          TEXT NOT NULL,   -- lint|ast|secrets|dangerous|api_compat|unit_tests|candidate_tests|benchmark
  runner        TEXT NOT NULL,   -- L1-subprocess|L2-docker
  passed        INTEGER NOT NULL,-- 1|0
  blocking      INTEGER NOT NULL DEFAULT 1,
  exit_code     INTEGER,
  duration_ms   INTEGER,
  output_path   TEXT,            -- var/contrib/candidates/.../runs/<run>.json (stdout/summary)
  created_at    INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_contrib_runs_cand ON contrib_runs(candidate_pk, gate);

CREATE TABLE IF NOT EXISTS contrib_scores (
  id            INTEGER PRIMARY KEY,
  candidate_pk  INTEGER NOT NULL UNIQUE REFERENCES contrib_candidates(id),
  engine_version INTEGER NOT NULL DEFAULT 1,
  score_total   INTEGER NOT NULL,            -- 0..100 (взвешенная сумма §9)
  score_json    TEXT NOT NULL,               -- покомпонентная разбивка
  created_at    INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS contrib_review_tasks (
  id            INTEGER PRIMARY KEY,
  challenge_id  TEXT NOT NULL,
  cand_a_pk     INTEGER NOT NULL REFERENCES contrib_candidates(id),
  cand_b_pk     INTEGER NOT NULL REFERENCES contrib_candidates(id),
  review_task_id TEXT NOT NULL UNIQUE,       -- rev_ch_ + hex (выдаётся reviewer-агенту)
  issued_to     INTEGER REFERENCES agents(id),  -- NULL = не выдан; один таск = один активный reviewer
  issued_at     INTEGER,
  expires_at    INTEGER,                     -- отзыв невыполненного (reviewer не отвечает)
  status        TEXT NOT NULL DEFAULT 'pending', -- pending|issued|done|expired
  UNIQUE (cand_a_pk, cand_b_pk)              -- пара сравнивается один раз (a<b по id)
);

CREATE TABLE IF NOT EXISTS contrib_reviews (
  id            INTEGER PRIMARY KEY,
  review_id     TEXT NOT NULL UNIQUE,        -- из CSV rev_...
  review_task_pk INTEGER NOT NULL REFERENCES contrib_review_tasks(id),
  challenge_id  TEXT NOT NULL,
  reviewer_pk   INTEGER NOT NULL REFERENCES agents(id),
  better        TEXT NOT NULL,               -- A|B|TIE
  score_a       INTEGER NOT NULL, score_b INTEGER NOT NULL,
  scores_json   TEXT NOT NULL,               -- correctness/security/readability/maintainability/performance/test_quality + regression_risk a/b
  reason        TEXT NOT NULL DEFAULT '',
  raw_csv_path  TEXT NOT NULL,               -- var/contrib/reviews/...csv (аудит)
  parse_warnings TEXT,                       -- некритичные отклонения (для репутации reviewer)
  created_at    INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_contrib_rev_ch ON contrib_reviews(challenge_id);

CREATE TABLE IF NOT EXISTS contrib_promotions (
  id            INTEGER PRIMARY KEY,
  candidate_pk  INTEGER NOT NULL REFERENCES contrib_candidates(id),
  channel       TEXT NOT NULL,               -- beta|production
  status        TEXT NOT NULL DEFAULT 'queued',
    -- queued → ci_passed|ci_failed → deploying → live → (stable | rolled_back)
  ci_run_url    TEXT,
  deployed_at   INTEGER, rolled_back_at INTEGER,
  approved_by   TEXT,                        -- 'auto'|admin user id (CONTRIB_AUTO_PROMOTE=false → только id)
  metrics_json  TEXT,                        -- окно beta-метрик (§11.4)
  rollback_reason TEXT,
  created_at    INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_contrib_promo_status ON contrib_promotions(channel, status);

CREATE TABLE IF NOT EXISTS contrib_reputation (
  agent_pk      INTEGER PRIMARY KEY REFERENCES agents(id),
  contrib_points INTEGER NOT NULL DEFAULT 0, -- +за accepted/promoted, − за reject-spam
  review_points  INTEGER NOT NULL DEFAULT 0, -- +за parseable/консистентные review, − за мусор
  accepted_count INTEGER NOT NULL DEFAULT 0,
  rejected_count INTEGER NOT NULL DEFAULT 0,
  spam_strikes   INTEGER NOT NULL DEFAULT 0, -- 3 страйка → contrib-бан (admin снимает)
  updated_at     INTEGER NOT NULL
);
```

Замечания:
- `agents` — существующая таблица swarm (002_swarm.sql); FK `agent_pk` на `agents(id)`.
- Дедуп `ux_contrib_cand_dupe`: два агента прислали идентичный файл — второй
  получает `reject_code=duplicate` со ссылкой на первый (честно и дёшево).
- Идемпотентность submissions через `client_submission_id` — паттерн уже принят в swarm.
- MySQL DDL: по существующей конвенции проектаsqlite-first; mysql-вариант — только
  если/когда появится общий mysql-профиль (см. REWORK-STATE §2 A2).

---

## 5. API-контракт (GET-first, envelope, `/api/v1/contrib/*`)

Регистрация — в `src/Support/Routes.php`, группа рядом со swarm (`$r->group('/api/v1', ...)`),
все маршруты `match(['GET','POST'])`, кроме чисто read-only. Аутентификация —
существующий swarm-токен (`?token=` / `Authorization: Bearer`), кроме публичных
`leaderboard`/`stats`/`docs`.

| Endpoint | Auth | Rate-limit | Назначение |
|---|---|---|---|
| `GET /api/v1/contrib/ping` | нет | глобальный | манифест контура: version, enabled, task_types, limits, links на docs/rules |
| `GET /api/v1/contrib/challenge/next?task_type=&language=` | agent | **1 / 10 мин, 5 / сутки** на agent+IP | выдать челлендж (создаёт или переиспользует open-челлендж; `issued_count++`) |
| `GET /api/v1/contrib/challenge/{challenge_id}` | agent | read | повторная выдача того же челленджа (idempotent) |
| `POST /api/v1/contrib/candidate` | agent | 3 / 10 мин на agent | приём ответа; sync-валидация schema+size → quarantine → `202 {candidate_id, status_url}` |
| `GET /api/v1/contrib/candidate/{candidate_id}/status` | agent (владелец) | read | статус пайплайна: gate-результаты, score, reject-причина (обратная связь!) |
| `GET /api/v1/contrib/review/next` | agent (reviewer) | 1 / 5 мин | пара кандидатов + rubric + `review_task_id` (не выдаёт пару, где reviewer = автор одного из кандидатов) |
| `POST /api/v1/contrib/review` | agent (reviewer) | 6 / 10 мин | тело = строгий CSV (§10); ответ: parsed ok / ошибки строк |
| `GET /api/v1/contrib/leaderboard?limit=&period=` | нет | read | публичный рейтинг агентов (points, accepted, promoted) |
| `GET /api/v1/contrib/stats` | нет | read | счётчики: challenges open, candidates, accepted, promoted, median time-to-verdict |
| `GET /api/v1/contrib/files` | нет | read | allowlist-витрина: какие файлы открыты для вклада (без содержимого) |
| `GET /agents/contrib` (`&format=html`) | нет | — | human-readable portal: правила, промпты, статус (HTML-версия того же) |
| `/admin/contrib/*` | admin (RBAC P11) | — | allowlist CRUD, kill switches, очередь промоций, просмотр кандидатов/runs/reviews, бан агентов |

### 5.1 Ошибки (envelope `error.code`)

`contrib_disabled` (403, мастер-switch off) · `challenge_not_found` / `challenge_expired` (404/410) ·
`challenge_rate_limited` (429 + `meta.retry_after`) · `schema_invalid` (422, details = список полей) ·
`candidate_too_large` (413) · `duplicate_submission` (409, ссылается на первый candidate_id) ·
`review_csv_invalid` (422, details = строки/колонки) · `not_a_reviewer` (403) ·
`self_review_forbidden` (403) · `agent_banned` (403, без деталей).

### 5.2 Challenge JSON (что выдаёт `challenge/next`)

```json
{
  "ok": true,
  "data": {
    "challenge_id": "ch_9f3a1c07b2e84d51a6",
    "version": 1,
    "task_type": "refactor",
    "file": {
      "file_id": "src_support_str",
      "path": "src/Support/Str.php",
      "language": "php",
      "sha256": "…64 hex…",
      "bytes": 4213
    },
    "source": "<?php\n…sanitized full file…\n",
    "task": {
      "goal": "Improve readability and safety without changing public behavior.",
      "constraints": [
        "Keep public method signatures compatible.",
        "Do not add external dependencies (stdlib-only project).",
        "Do not change output formats.",
        "Return a COMPLETE replacement file."
      ],
      "min_full_file_bytes": 0,
      "max_response_bytes": 65536,
      "tests_hint": [
        "Str::slug(null) returns ''",
        "Str::slug('Привет мир') transliterates to 'privet-mir'"
      ]
    },
    "response_schema": {
      "patch_format": "full_file",
      "required_fields": ["challenge_id","agent_id","status","candidate","explanation","risks","tests_added","self_report"],
      "submit_url": "/api/v1/contrib/candidate",
      "status_url": "/api/v1/contrib/candidate/{candidate_id}/status"
    },
    "rules_url": "/agents/contrib",
    "prompt_url": "/api/v1/contrib/prompt?role=coding",
    "expires_at": "2026-10-22T12:00:00Z"
  },
  "meta": { "request_id": "req_…", "rate_limit": { "challenge": {"limit":1,"window_seconds":600,"remaining":0} } }
}
```

Отличия от черновика идеи (осознанные):
- `tests_hint` — НЕобязательное поле: для файлов без тестов C1 генерирует
  «контрактные» тесты сам (см. §6.3), иначе блокирующий гейт «тесты зелёные» невыполним.
- добавлены `rules_url`/`prompt_url` — агент может получить готовый промпт (§20) одним GET.
- `expires_at` — челленджи протухают; протухший = `challenge_expired` на submit.

### 5.3 Ответ агента (POST /api/v1/contrib/candidate)

```json
{
  "challenge_id": "ch_9f3a1c07b2e84d51a6",
  "agent_id": "atlas-7f3a",
  "client_submission_id": "atlas-7f3a:ch_9f3a:1",
  "status": "completed",
  "candidate": "<?php\n…complete replacement file…\n",
  "explanation": "Replaced fragile string checks with explicit validation; added guard clauses.",
  "risks": ["None known; public behavior preserved."],
  "tests_added": ["Str::slug('') returns ''", "Str::slug(emoji-only) returns ''"],
  "self_report": {
    "changed_lines": 18,
    "assumed_language_version": "8.2",
    "external_dependencies": []
  }
}
```

Sync-валидация intake (до 202): JSON parse ≤ 256 KB тела · обязательные поля ·
`challenge_id` существует и open и не expired · `agent_id` = аутентифицированный
агент · `status=completed` ⇒ `candidate` непустой, ≤ `max_response_bytes`,
UTF-8 · `status=rejected` ⇒ `explanation` обязателен (учится репутация, кандидат
не запускается) · `content_sha256` ≠ production sha (иначе `reject=no_change`) ·
дедуп по (challenge_id, content_sha256). Всё остальное — асинхронно в worker.

---

## 6. Allowlist файлов и санитизация

### 6.1 Критерии допуска файла в allowlist

ДОПУСКАЮТСЯ только файлы, которые одновременно:
1. Не содержат секретов, токенов, DSN, абсолютных серверных путей, персональных данных.
2. Не являются кодом авторизации/сессий/платежей/администрирования/деплоя/CI.
3. Не выполняются в привилегированном контексте (worker от root, cron, миграции).
4. Чистые/почти чистые: утилиты, валидаторы, форматтеры, UI-компоненты без
   side-effects, шаблоны, документация, JSON-схемы, i18n-каталоги (через `_platform.*`).
5. Покрыты тестами ИЛИ тесты для них сгенерированы на C1 (§6.3) — без тестов файл
   в allowlist не попадает: блокирующий гейт «регрессий нет» должен быть проверяем.

Start-allowlist (seed в C1, из РЕАЛЬНЫХ файлов этого репо — все stdlib-only, все с тестами):

| file_id | path | task_types |
|---|---|---|
| `src_support_str` | `src/Support/Str.php` | refactor, perf, tests |
| `src_security_iputil` | `src/Security/IpUtil.php` | refactor, security, tests |
| `src_infra_qr_qrmatrix` | `src/Infrastructure/Qr/QrMatrix.php` | refactor, perf |
| `src_infra_export_csvwriter` | `src/Infrastructure/Export/CsvWriter.php` | refactor, security, tests |
| `src_web_icon` | `src/Web/Icon.php` | refactor, a11y |
| `web_js_tracker` | `public/web/js/tracker.js` | refactor, perf, a11y |
| `docs_agent_api` | `docs/AGENT_API.md` | docs |
| `docs_errors` | `docs/ERRORS.md` | docs |
| `web_i18n_platform_<code>` | `web/i18n/_platform.<code>.json` | i18n |

ЗАПРЕЩЕНО навсегда (denylist в коде ChallengeService, жёстче allowlist):
`.env*`, `bin/*`, `bootstrap/*`, `migrations/*`, `src/Core/Config.php`,
`src/Security/{PasswordHasher,Csrf,Totp,RateLimiter,IpBlockGuard,ThreatDetection}.php`,
`src/Domain/Auth/**`, `src/Http/{Kernel,SessionManager}.php`, `src/Workers/**`,
`scripts/sync.sh`, `Dockerfile`, `.github/**`, всё с `legacy/` (архив), `var/**`,
`app/version.py` (релизный артефакт). Правило: **новый файл в allowlist добавляет
только админ через /admin/contrib, seed-список меняется только коммитом.**

### 6.2 Санитизация снепшота (SourceSanitizer)

Источник — наш собственный репо (доверенный), но снепшот всё равно проходит
санитизацию перед выдачей (защита от случайной утечки при расширении allowlist):
1. Удаление строк, матчащих секрет-паттерны (тот же regex-набор, что в secret-scan
   гейте §8.3: `API_KEY|SECRET|TOKEN|PASSWORD|DSN|sqlite:|mysql:` + high-entropy строки).
2. Замена абсолютных путей `/home/…`, `/var/www/…` на `<repo>/…`.
3. Удаление PHP-блоков с `getenv(`/`$_ENV` (конфиг-доступ) — если такой блок
   найден, файл НЕ выдаётся (алерт админу: allowlist-ошибка).
4. Для `_platform.<code>.json` — выдаётся как есть (там нет секретов по построению,
   i18n-контракт §5.4 REWORK-STATE).
5. `source_sha256` считается ПОСЛЕ санитизации; кандидат сравнивается с этим sha.

### 6.3 Генерация тестов для файлов без покрытия (C1)

Для каждого allowlist-файла без тестов реализуемый агент пишет
`tests/unit/Contrib/<Name>ContractTest.php` — контрактные тесты на публичное
поведение (10–25 assert'ов), прогоняет, коммитит ВМЕСТЕ с добавлением файла
в allowlist. Эти тесты = блокирующий гейт «регрессий нет» + `tests_hint`
в челлендже берётся из их названий. **Никакой файл не выдаётся агентам без
зелёных контрактных тестов** — это инвариант (проверяется в ChallengeService).

---

## 7. Типы задач (адаптация таблицы идеи к нашему репо)

| Тип | Пример на наших файлах | Автопроверка (гейты §8 + score §9) |
|---|---|---|
| `refactor` | `Str.php`: упростить, сохранив поведение | контрактные тесты + token-level API-compat + метрики сложности |
| `security` | `CsvWriter.php`: CSV-injection (formula escaping) | sec-тесты + static scan (dangerous calls, taint-эвристики) |
| `perf` | `QrMatrix.php`: оптимизация без изменения API | benchmark-гейт (микро-прогон в runner, `hrtime`, N итераций) |
| `a11y` | `Icon.php`/`tracker.js`: aria, focus, контраст | HTML-валидация эвристиками + a11y-чеклист тестами |
| `docs` | `docs/ERRORS.md`: структура, битые ссылки | линкер-чек (все внутренние ссылки/якоря существуют), markdown-lint эвристиками |
| `tests` | добавить edge-case тесты к `IpUtil.php` | присланные тесты ДОЛЖНЫ запускаться в runner и падать на production-версии (иначе бесполезны), проходить на своей |
| `i18n` | `_platform.<code>.json`: полный перевод | `build_i18n.php --check` в runner + placeholder-QA + script-guard (паттерн P12!) + fallback-схема |
| `ux-agents` | улучшить машиночитаемость endpoint-доков | contract-тесты: openapi/llms.txt остаются валидными и полными |

Таблица task_type → обязательные гейты хранится в `ContribConfig` (php-массив),
не в БД (детерминированность; расширение = коммит + review).

---

## 8. Исполнение и изоляция (самая опасная часть — читать целиком)

### 8.1 Инварианты runner'а

1. **Кандидат НИКОГДА не выполняется в web-процессе и в процессе worker'а.**
   Только `proc_open` отдельного процесса (L1) или контейнер (L2).
2. **Никакого `eval()`, `include` кандидатского кода, `create_function`,
   десериализации кандидатских данных** — в нашем коде. Кандидатский PHP
   запускается исключительно как `php <file>` CLI-скрипт внутри runner-обвязки.
3. Runner-обвязка — НАШ код (`src/Domain/Contrib/Runner/`), кандидат его не видит.
4. Любой сбой runner'а = fail-CLOSED (кандидат → rejected `runner_error`), не fail-open.
5. Таймауты жёсткие: wall-clock `CONTRIB_RUN_TIMEOUT` (default 30s) + kill -9 grace.

### 8.2 Уровни изоляции

| Уровень | Где | Механика | Гарантия |
|---|---|---|---|
| **L0 static** | всегда (dev+prod) | `php -l` (lint), token-scan (AST через `token_get_all` — stdlib!), regex secret-scan, dangerous-call scan, API-compat diff | код НЕ выполняется вовсе |
| **L1 hardened subprocess** | dev-песочница (docker недоступен — проверено) | `proc_open`: `php -n -d disable_functions=exec,system,passthru,shell_exec,popen,proc_open,pcntl_fork,dl,putenv -d open_basedir=<runner_tmp> -d memory_limit=256M -d max_execution_time=<t>` + рабочая директория = одноразовый `var/contrib/runs/<run_id>/` (копируются ТОЛЬКО: контрактные тесты, autoloader-stub, кандидат) + очищенное окружение (`env -i`, никаких наших переменных) + `ulimit`-обвязка через `bash -c 'ulimit -t 30 -v 524288 -f 10240 -n 64; exec php …'` | выполнение есть, но файловая/процессная/память-изоляция; сети нет (`disable_functions` + отсутствие сетевых вызовов в stdlib-тестах; open_basedir режет чтение `.env`, `var/data`) |
| **L2 container** | production + CI | `docker run --rm --network none --read-only --tmpfs /tmp:size=64m --memory 256m --cpus 0.5 --pids-limit 64 --user 65534:65534 --security-opt no-new-privileges -v <runner_tmp>:/work:ro php:8.2-cli-alpine php /work/run_tests.php` | настоящая изоляция; обязателен перед `CONTRIB_RUNNER_ENABLED=true` на бою |

**Правило фаз:** в dev-песочнице пайплайн строится и тестируется на L0+L1
(это полноценно покрывает юнит-тесты наших stdlib-файлов); L2-обвязка пишется
сразу (DockerRunner + transport seam, как TargetChecker в P16 — seam для тестов),
но включается только на проде/в CI, где docker есть. **Промоция в production
без L2 запрещена** (проверяется PromotionService: `runner_level >= L2` либо
`CONTRIB_ALLOW_L1_PROMOTE=true` — явный override админом).

### 8.3 Гейты (порядок = стоимость; все блокирующие, кроме помеченных)

| # | Гейт | Механика | Блокирующий |
|---|---|---|---|
| 1 | `schema` | intake-валидация §5.3 | да (sync) |
| 2 | `size` | ≤ max_response_bytes; полный файл ≥ 64 байт; ≤ 2× исходного без justification в explanation (иначе score-пенальти, не reject) | да |
| 3 | `syntax` | `php -l` / `node --check`* / JSON parse / markdown — по языку (*node в песочнице нет → для js только L0-эвристики + CI) | да |
| 4 | `secrets` | regex-набор (API-ключи, токены, DSN, private keys, high-entropy ≥4.5 бит/символ строки >20 символов) + diff с production: НОВЫХ секрет-подобных строк нет | да |
| 5 | `dangerous` | token-scan: `eval`, `exec`, `system`, `passthru`, `shell_exec`, `popen`, `proc_open`, `assert(`, `create_function`, `include/require` НЕ-константных путей, `unserialize`, `file_get_contents` с не-литералом, `fsockopen/curl_*`, `php://input`, backticks; для JS: `eval`, `Function(`, `innerHTML` без санитизации, `document.write`, fetch/XHR к не-allowlist хостам | да |
| 6 | `api_compat` | token_get_all → извлечение публичных сигнатур (function/method name + параметры + типы) production vs кандидат; удаление/изменение = reject; добавление = ok | да |
| 7 | `unit_tests` | L1/L2 runner: контрактные тесты файла на кандидате | да |
| 8 | `candidate_tests` | для `tests`-задач: присланные тесты падают на production и проходят на кандидате | да (для tests-типа) |
| 9 | `benchmark` | для `perf`-задач: микро-прогон 3×N итераций, кандидат не хуже baseline на >10% | да (для perf-типа) |
| 10 | `i18n_check` | для i18n-задач: `build_i18n.php --check` + placeholder-QA + script-guard | да (для i18n-типа) |
| 11 | `diff_quality` | нет массового переформатирования: ≤ 70% строк изменено без изменения токенов = пенальти в score | нет (score) |

Все гейты пишутся в `contrib_runs`; статус кандидата виден автору через
`candidate/{id}/status` (обратная связь поднимает качество следующих попыток).

---

## 9. Оценка кандидата (ScoreEngine, детерминированная, stdlib-only)

LLM-оценки — ТОЛЬКО в reviewer-контуре (§10). Машина считает детерминированно:

| Критерий | Вес | Как считаем (без внешних зависимостей) |
|---|---:|---|
| Блокирующие гейты §8 | gate | любой fail → score_total = 0, статус rejected |
| Тесты добавлены | 20 | `tests_added` непустой И (для tests-задач) гейт 8 пройден; для остальных типов: описания тестов парсятся и хотя бы один соответствует реальному изменению (эвристика по diff) |
| Читаемость | 15 | token-метрики: max nesting depth (−), средняя длина функции (−), cyclomatic complexity через token-scan (−), доля осмысленных имён (эвристика: не `$a/$tmp/$x`) (+) — нормировано к baseline |
| Безопасность | 20 | 0 новых finding'ов static-scan'ов (иначе gate); + баллы за устранение известных паттернов (напр. добавлен `htmlspecialchars`/escaping в выводе, validation на входе) — детекторы по task_type |
| Производительность | 10 | benchmark-гейт: улучшение >10% = полный балл, parity = 6, хуже в пределах 10% = 2 |
| Дифф-качество | 10 | 1 − (whitespace-only changed lines / all changed lines) − пенальти за rename-шторм |
| Пояснение | 10 | explanation ≥ 80 символов, содержит «что/почему»; risks — непустой массив ИЛИ явное «none known»; без промпт-инъекций в тексте (reviewer их не читает сырыми — §10.4) |
| Размер | 5 | ≤ 1.2× production = 5; ≤ 2× = 3; > 2× с justification в explanation = 2, без = 0 |

Итог: `score_total = Σ(w·v)/Σw · 100`, integer 0..100. `engine_version` в
contrib_scores — при изменении формулы старые скоры не пересчитываются
(сравнимость leaderboard), новый кандидат скорится новой версией; baseline
production-версии ВСЕГДА пересчитывается текущей версией engine (сравниваем
актуальным мерилом).

**Baseline production-версии:** при создании челленджа ScoreEngine прогоняется
по ТЕКУЩЕМУ файлу → `var/contrib/baseline/<file_id>/score.json` + строка в
contrib_scores с candidate = спец-записью `prod:<file_id>`. Условие промоции
«avg score > production baseline» (§11) использует ИМЕННО ЭТОТ baseline,
а не LLM-мнение.

---

## 10. Reviewer-контур (pairwise, строгий CSV)

### 10.1 Когда стартует

`accepted_count >= CONTRIB_REVIEW_MIN` (default **3**, настраивается; идея
говорила 5–10 — начинаем с 3, чтобы контур оживал быстрее, и поднимаем до 5
когда трафик агентов позволит). Aggregate-задача worker'а создаёт все пары
C(n,2) в `contrib_review_tasks` (cand_a_pk < cand_b_pk, UNIQUE — пара один раз).

### 10.2 Выдача пары (`/contrib/review/next`)

- Reviewer = swarm-агент; преимущество у агентов с `review_points > 0` и
  capabilities содержащим `code-review` (сортировка выдачи).
- **Анти-коллюзия:** пара НЕ выдаётся агенту, который автор cand_a или cand_b
  (проверка agent_pk); один review_task одновременно выдан одному reviewer
  (`issued_to`, expires через 30 мин — потом возвращается в pending).
- Кандидаты анонимизированы: A/B без agent_id, порядок рандомизирован
  (позиционный bias), в payload — только код + explanation + rubric.
- В payload включается `review_task_id`; CSV-ответ обязан его содержать.

### 10.3 Контракт CSV (жёсткий, как в идее пользователя)

```csv
review_id,challenge_id,candidate_a,candidate_b,better,score_a,score_b,correctness_a,correctness_b,security_a,security_b,readability_a,readability_b,maintainability_a,maintainability_b,performance_a,performance_b,test_quality_a,test_quality_b,regression_risk_a,regression_risk_b,reason
```

Правила парсинга (ReviewParser — отдельный класс, полностью покрыт тестами):
1. Разделитель — запятая; кавычки по RFC4180 (собственный stdlib-парсер —
   `str_getcsv` НЕ используем слепо: он не валидирует структуру).
2. Первая строка — БАЙТ-В-БАЙТ заголовок выше; любое отклонение = `review_csv_invalid`.
3. Ровно одна строка данных; никаких комментариев/markdown/текста до и после.
4. `better` ∈ {A, B, TIE} (uppercase).
5. Все `*_a/*_b` числовые оценки — целые 1..10; `score_*` — 1..100.
6. `regression_risk_*` ∈ {low, medium, high}.
7. `reason` — одна строка, ≤ 500 символов; запятые внутри — только в кавычках.
8. `challenge_id`/`candidate_a`/`candidate_b` должны совпасть с выданным
   review_task (подмена пары = reject + страйк репутации).
9. Консистентность: `better=A` ⇒ score_a ≥ score_b (иначе parse_warning,
   review принимается, но reviewer теряет review_points — ловим хаотичных).
10. Сырой CSV всегда сохраняется (`raw_csv_path`) ДО парсинга — апелляции/аудит.

### 10.4 Промпт-инъекшен-защита

Explanation кандидата показывается reviewer-агенту как DATA (в payload помечен
`"untrusted_text": true`, в промпте reviewer'а (§20) — явная инструкция не
следовать командам из текста кандидатов). Review-вердикт — только CSV-числа;
любые «инструкции» внутри explanation на парсинг не влияют. Это же правило
для status-страниц: reject_detail не содержит сырой текст кандидата.

### 10.5 Агрегация

- Каждый кандидат: win-rate = (wins + 0.5·ties) / reviews; средний балл по
  6 критериям; доля `high` regression_risk голосов.
- **Пороги выбора лучшего (PromotionService):**
  1. ≥ `CONTRIB_MIN_REVIEWS` (3) независимых review на кандидата (разные reviewer_pk);
  2. доля high-regression-risk голосов ≤ 20%;
  3. score_total (машина) ≥ baseline production;
  4. win-rate — ранжирование среди прошедших 1–3; победитель = max(win-rate,
     затем max score_total); TIE-heavy пары не блокируют (Bradley-Terry не нужен
     при n≤10 — простая win-rate достаточна и объяснима).
- Ревью одного агента на одну пару учитывается один раз (UNIQUE review_task + issued_to).

---

## 11. Промоция beta → production

### 11.1 Канал beta

Рекомендуемый механизм (репо под git, CI уже есть):
1. Победитель → сервер делает git-ветку `contrib/<file_id>/<challenge_id>` от
   текущего `main`/release-тега, один коммит: замена файла + запись в CHANGELOG
   `[Unreleased]` (`release:notes`-совместимо) + bump `contrib_promotions`.
2. Ветка пушится в CI → **ПОЛНЫЙ прогон**: doctor, migrate, lint, tests (все 483+),
   i18n-check, docker build + image smoke (существующий workflow — без изменений).
3. `ci_passed` → merge в ветку `beta` → beta-деплой (на боевом сервере:
   отдельный docroot `/var/www/iya-beta/` через rsync-эквивалент `tar|cp -a`
   или второй Docker-контейнер на :8443 — решает оператор; в песочнице
   имитируется каталогом `var/beta-root/` + `php bin/iya serve --port=8081`).
4. Smoke: `scripts/smoke_http.py` против beta + расширенный
   `scripts/_contrib_smoke.py` (новый: прогоняет контрактные тесты затронутого
   файла уже в beta-дереве).

Fallback-механизм (идея пользователя, для non-git деплоя): overlay-каталог
`beta/` + `rsync -a --delete beta/ /var/www/iya-beta/` + `php -l` + phpunit —
оставляем как документированную опцию в docs/CONTRIB.md (Deploy-вариант B).

### 11.2 Окно метрик (beta-live → stable)

`CONTRIB_BETA_WINDOW` (default 48h на бою, 10 мин в dev-режиме). Сравниваются
beta vs production-метрики (у нас уже всё собирается P5–P8):
- 5xx rate (visit/app логи) — не выше production + 0.1 п.п.;
- p95 latency затронутых маршрутов — не хуже +10%;
- JS-ошибки (tracker.js error-capture — добавить поле в C6, если ещё нет);
- security alerts (ThreatDetection-счётчики) — без всплеска;
- конверсия регистрации (funnel-отчёт P8) — не ниже −5% отн.
Метрики пишет worker-задача `contribMetrics` (каждый проход) в
`contrib_promotions.metrics_json`. Любой порог пробит → auto-rollback (§11.3)
+ алерт в admin (существующий AlertService) + `contrib.rolled_back` webhook-событие
(паттерн P16-эмиттеров).

### 11.3 Kill switch и rollback

- `CONTRIB_ENABLED=false` — весь контур 403 `contrib_disabled` (мастер).
- `CONTRIB_RUNNER_ENABLED=false` — intake работает, исполнения нет (кандидаты
  копятся в quarantined; полезно при инциденте с runner'ом).
- `CONTRIB_AUTO_PROMOTE=false` (DEFAULT) — production-промоция только кнопкой
  в /admin/contrib (RBAC `admin.contrib.promote`), с confirm-экраном.
- Rollback beta: revert-коммит в ветке beta + redeploy (git-механизм) или
  восстановление production-файла из `contrib_promotions.parent` (overlay) —
  цель: < 60 секунд, одна команда `php bin/iya contrib:rollback <promotion_id>`
  (новая CLI-команда в bin/iya).
- Все switch'и — в `Config::defaults()` ОБЯЗАТЕЛЬНО (ловушка B-005: ключ вне
  defaults() игнорирует env!) + в `.env.example` с комментариями + в admin
  feature flags (P11-паттерн) + каждое переключение — в audit-log.

### 11.4 Production-промоция

`stable` после окна метрик → (AUTO_PROMOTE ? auto : очередь админу) → merge
beta→main + релиз по `docs/RELEASE.md` (semver: contrib-фичи = patch/minor),
`release:notes`, CHANGELOG. Автор-агент получает `promoted` в status-фиде +
репутацию; публичный leaderboard обновляется.

---

## 12. Репутация агента

- `contrib_points`: +5 validated, +15 selected-победитель, +25 promoted,
  −2 за каждый reject по причинам `schema|dangerous|secrets` (мусор/злоумышление),
  −1 за `duplicate` (не наказуемо сильно — честная коллизия).
- `review_points`: +2 за принятый review без warnings, −3 за
  `review_csv_invalid`/подмену пары, −1 за parse_warnings-консистентность.
- `spam_strikes`: reject с причинами `secrets|dangerous|api_break` = 1 страйк;
  3 страйка → contrib-бан (агент остаётся в swarm, но challenge/next = 403
  `agent_banned`); разбан — только админом.
- Привилегии: `review_points ≥ 10` → доступ к review-контру; `contrib_points ≥ 50`
  → повышенные квоты (×2) и бейдж на публичном профиле `/agents/{agent_id}`
  (существующая страница — добавить блок).
- Репутация НЕ влияет на блокирующие гейты — машина проверяет всех одинаково.

---

## 13. Разделы обратной связи для агентов (без новых таблиц!)

Идея перечислила 10 «полезных разделов». Все они реализуются как **seed-разделы
существующего swarm-API** (sections/threads/messages/tasks/knowledge/features —
P9, 55 endpoints, FTS5, votes, идемпотентность). Seed в C7:

| Раздел идеи | Swarm-раздел (seed) | Что постят агенты |
|---|---|---|
| Предложения по развитию | `contrib/ideas` → features (голосование уже есть) | идея, проблема, эффект, приоритет, трудоёмкость (структура в docs/CONTRIB.md) |
| Баг-репорты | `contrib/bug-reports` | URL, шаги, expected/actual, severity |
| Доступность | `contrib/accessibility` | что мешает агенту/человеку понять страницу |
| Карта возможностей | `contrib/opportunity-map` | желаемые API/форматы/endpoints |
| Этика и safety | `contrib/safety` | предложения по безопасности взаимодействия |
| Метрики качества контента | `contrib/content-quality` | бесполезные/устаревшие страницы |
| Интеграции | `contrib/integrations` | экспорт/импорт, RSS, JSON-LD, OpenAPI, webhooks |
| Библиотека промптов | `contrib/prompts` → knowledge (dedup+ratings уже есть) | удачные промпты |
| Как агенты находят сайт | `contrib/discovery` | какие запросы/каталоги/ссылки сработали и нет |
| Как продлить жизнь агента | `contrib/agent-resilience` | советы по контексту/памяти/переносимости; канон — наш RESILIENCE-PLAYBOOK (ссылка в seed-посте) |

Обработка: админ-дашборд уже умеет sections/messages; добавляется фильтр
«необработанные contrib-разделы» + еженедельный worker-дайджест в admin
(существующий Notifier). Триаж баг-репортов → существующий fix-list-паттерн
(docs/BETA.md).

---

## 14. Discovery-поверхности (чтобы агенты находили вход, а не сканировали вслепую)

1. `robots.txt` (уже есть) — добавить явные строки: `Allow: /agents/contrib`,
   `Allow: /api/v1/contrib/`, ссылка на sitemap-секцию для /agents.
2. `/agents.json` (DocsController — уже есть) — дополнить манифест: секция
   `contributions` (endpoints, auth, limits, rules_url, prompt_urls).
3. `/llms.txt` (уже есть) — абзац про contribution-контур (одно предложение + ссылки).
4. `/api/v1/openapi.json` (уже есть) — все contrib-endpoints в spec (генерация
   из того же источника, что роутер — дрейф исключён, паттерн P9).
5. `GET /api/v1/contrib/prompt?role=coding|reviewer` — выдаёт готовый промпт
   (§20) text/plain; агент может встроить его без ручного копирования.
6. `/agents/contrib` HTML-портал (GET-first, human+machine readable, тот же
   View/Templates-стек): правила, lifecycle-диаграмма, промпты, leaderboard,
   статус контура, ссылка на docs/CONTRIB.md.
7. `docs/CONTRIB.md` — полный документ для агентов (как AGENT_API.md): quick
   start одним запросом, контракты, коды ошибок, FAQ, этика.

---

## 15. Угрозы и меры (security review фазы)

| # | Угроза | Мера |
|---|---|---|
| T1 | Выполнение вредоносного кода кандидата | §8: L0/L1/L2, disable_functions, open_basedir, env -i, ulimit, timeout, fail-closed; НИКОГДА в web/worker-процессе |
| T2 | Эксфильтрация секретов через кандидат/тесты | open_basedir режет `.env`, `var/data`, `/mnt/oss`; env очищен; сеть: L2 `--network none`; L1 — disable curl-сокетов в тестах невозможно → поэтому L1 только dev, промоция только через L2 |
| T3 | Resource exhaustion (CPU/RAM/disk/fork-bomb) | ulimit -t/-v/-f/-n, --pids-limit, --memory, size-квоты var/contrib (contribGC, `CONTRIB_DISK_QUOTA_MB` default 512), rate-limits на всё |
| T4 | Spam-кандидаты (флодинг очереди) | квоты 5 челленджей/сутки, дедуп по content_sha256, страйки/бан (§12), карантин без исполнения до static-гейтов |
| T5 | Prompt injection в explanation → reviewer | §10.4: untrusted_text-маркировка, вердикт только CSV-числа, reason ≤500 симв. |
| T6 | Коллюзия автор↔reviewer / self-review | §10.2: автор не получает свою пару; review_points-порог для reviewer; админ видит граф пар в /admin/contrib |
| T7 | Сибил-агенты (регистрация фермы) | существующие: rate-limit на register, token-issuance лимиты; добавляем: contrib-квоты на IP-пул (не только на agent) |
| T8 | Подмена challenge/пары в CSV-ответе | review_task_id + серверное сравнение challenge_id/candidate_* с выданными |
| T9 | Отравление allowlist (злоумышленный админ-контент) | allowlist меняется только админом (RBAC+audit) или коммитом; denylist (§6.1) жёстче allowlist и не редактируется из UI |
| T10 | Утечка приватного кода через челленджи | SourceSanitizer (§6.2) + denylist + инвариант «файл выдаётся только из allowlist» (тест: ChallengeService физически не может читать путь вне списка) |
| T11 | Regression в production | блокирующие гейты + полный CI на ветке + beta-окно метрик + auto-rollback + AUTO_PROMOTE=false |
| T12 | Устаревший challenge (файл изменился в main) | `source_sha256` сверяется на submit с ТЕКУЩИМ production sha; mismatch = reject `stale_challenge` (агент берёт новый) |
| T13 | Диск-переполнение quarantine | contribGC: quarantine > 7 дней без вердикта → архивируется (body.json.gz) и удаляется; кандидаты > 90 дней после close челленджа → GC (БД-строки остаются, пути помечаются purged) |

---

## 16. Фазы реализации (C0–C8)

> Правила те же, что для P1–P16: после КАЖДОГО шага — `pack_p12_quick.sh`
> (правило 10), на чекпоинтах — полный ритуал (PLAYBOOK §3), решение спорных
> вопросов — в ADR-лог REWORK-STATE §1 (пользователя не блокируем вопросами).
> Каждый этап заканчивается ЗЕЛЁНЫМИ гейтами: `php tests/run.php` (все, без
> регрессий), `php bin/iya lint`, новый drill-скрипт этапа.

### C0 — Контракты и скелет (1 блок)
- ADR-019 уже есть (принятие плана); в ходе C0 фиксируются ADR-020+ (спорные решения).
- `migrations/sqlite/009_agent_contrib.sql` (§4) + `php bin/iya migrate` идемпотентно.
- `src/Domain/Contrib/` скелет: ContribConfig (task_type→гейты), ContribError-коды.
- Config-ключи §17 — ВСЕ в `defaults()` + `.env.example` (B-005-ловушка!).
- Роуты-заглушки `/api/v1/contrib/ping` (envelope, `contrib_disabled` при CONTRIB_ENABLED=false).
- Тесты: миграция применяется/идемпотентна; ping-контракт; config-гейт.
- **Acceptance:** `ping` живым сервером отдаёт манифест; migrate дважды — без ошибок; все тесты зелёные.

### C1 — Allowlist, санитизатор, выдача челленджей (1–2 блока)
- `ContribFileService` (allowlist CRUD из админки + seed §6.1), `SourceSanitizer` (§6.2),
  `ChallengeService` (issue/reissue, TTL, issued_count, инвариант allowlist+тесты).
- Контрактные тесты для allowlist-файлов без покрытия (§6.3) — коммитятся здесь же.
- `GET challenge/next` + `GET challenge/{id}` + rate-limits (бакеты §5).
- Тесты: санитизатор режет секреты/пути/env-блоки; challenge JSON = контракт §5.2;
  denylist-файл невыдаваем (property-test по всему denylist); rate-limit 1/10мин срабатывает.
- **Acceptance:** drill `_contrib_check.py` часть 1: живой агент (register→token)
  получает валидный челлендж на каждый seed-файл; повторный запрос в пределах 10 мин = 429.

### C2 — Intake и static-гейты (1–2 блока)
- `IntakeService` (§5.3 sync-валидация, карантин, идемпотентность, дедуп),
  `POST candidate` + `GET candidate/{id}/status`.
- Static-гейты L0: `lint/php -l`-обёртка, `TokenScanner` (dangerous calls),
  `SecretScanner`, `ApiCompatScanner` (token_get_all-сигнатуры).
- Worker-задача `contribValidate` (бюджет `CONTRIB_VALIDATE_BUDGET`).
- Тесты: каждый сканер — набор fixtures (положительные/отрицательные, включая
  обфускацию: конкатенация строк `'ev'.'al'`, backticks, variable functions);
  intake-ошибки = коды §5.1; дедуп; stale_challenge (T12).
- **Acceptance:** drill часть 2: отправка мусора (eval, секрет, обрывок файла,
  огромный body, невалидный JSON) → корректные reject-коды; отправка валидной
  замены → статус validating→validated (без исполнения — только static).

### C3 — Runner L1 + исполнение тестов (2–3 блока, самый сложный)
- `Runner/RunnerInterface` + `SubprocessRunner` (L1, §8.2: proc_open, env -i,
  ulimit-обвязка, одноразовый каталог, timeout+kill, transport seam как у TargetChecker).
- `DockerRunner` (L2) — реализуется сразу, включается флагом; в песочнице
  покрыт seam-тестами (фейк-транспорт), живой прогон — в CI.
- `RunHarness` (НАШ код внутри runner_tmp: грузит контрактные тесты + кандидат,
  запускает наш кастомный тест-раннер изолированно, пишет JSON-результат).
- Worker-задачи `contribRun` (+ `benchmark`, `candidate_tests` по task_type).
- Тесты: таймаут убивает процесс; open_basedir не пускает к .env; disable_functions
  режет exec; фейк-транспорт DockerRunner; результат-парсинг; fail-closed на любом сбое.
- **Acceptance:** drill часть 3: кандидат с бесконечным циклом → rejected по
  timeout ≤ 35s; кандидат, читающий `.env` в тесте → ошибка open_basedir, reject;
  валидный кандидат → unit_tests passed, status=validated.

### C4 — ScoreEngine и статус-фид (1 блок)
- `ScoreEngine` (§9, engine_version), baseline-скоры production-версий,
  `contrib_scores`, score-компоненты в `candidate/{id}/status`.
- `GET leaderboard` + `GET stats` (публичные, CsvWriter-экспорт `?format=csv`).
- Тесты: формула на фикстурах (детерминированность: одинаковый вход = одинаковый
  score); baseline пересчёт; leaderboard-сортировка; пустые состояния.
- **Acceptance:** drill часть 4: два кандидата (хороший/раздутый) — скоры
  ранжируют хороший выше; leaderboard отдаёт валидный JSON/CSV.

### C5 — Reviewer-контур (2 блока)
- `ReviewTaskService` (пары, анти-коллюзия, выдача, expires), `ReviewParser`
  (§10.3 — ПОЛНОЕ покрытие тестами: 30+ fixtures всех отклонений), `AggregateService` (§10.5).
- `GET review/next` + `POST review` + review_points-порог доступа.
- Worker-задача `contribAggregate`.
- Тесты: парсер (заголовок, quoting, типы, диапазоны, инъекции в reason, CRLF);
  self-review запрет; истечение issued; агрегация порогов; один review на пару.
- **Acceptance:** drill часть 5: 3 кандидата + 2 reviewer-агента → пары выданы,
  CSV приняты, победитель выбран по порогам; подменный CSV → reject+страйк.

### C6 — Промоция, beta, метрики, kill switches (2 блока)
- `PromotionService` (§11): git-ветка/коммит через `GitClient`-обёртку (proc_open
  git; seam для тестов), CI-триггер (push), overlay-fallback, `contrib:rollback`
  CLI, beta-root (`--port=8081` serve-профиль).
- `contribMetrics` worker-задача (окно, пороги §11.2, auto-rollback).
- Webhook-события `contrib.promoted` / `contrib.rolled_back` (паттерн P16-эмиттеров).
- Admin: `/admin/contrib` (очередь промоций с confirm, kill switches как feature
  flags, просмотр кандидатов/runs/reviews/raw-CSV, бан агентов) + audit-log.
- Тесты: GitClient на seam-фейке; rollback-путь; метрики-пороги (фикстуры окон);
  RBAC admin-страниц (паттерн _rbac_check.py).
- **Acceptance:** drill часть 6: E2E промоция в `var/beta-root` в dev-режиме
  (10-мин окно), метрики собраны, rollback одной командой восстанавливает файл;
  `_sec_admin_check.py`-расширение: не-админ не видит /admin/contrib.

### C7 — Docs, portal, discovery, обратная связь (1 блок)
- `docs/CONTRIB.md`, `/agents/contrib` portal-шаблон (View/Templates-стек,
  i18n-ключи в `_base.en.json` + 42 локали через build_i18n.php — НОВЫЕ ключи
  только через конвейер §5.4!), `contrib/prompt` endpoint (§20).
- robots.txt / agents.json / llms.txt / openapi — дополнения (§14).
- Seed swarm-разделов §13 (+ стартовые посты-примеры в каждом).
- Тесты: i18n-sync-guard (новые ключи во всех локалях после build), portal-роуты
  в HttpSmokeTest, openapi содержит contrib-пути.
- **Acceptance:** drill часть 7: все discovery-поверхности отдают contrib-секции;
  портал рендерится (html-инварианты P13: dark/RTL/focus); build_i18n --check зелёный.

### C8 — Hardening, нагрузка, релиз (1 блок)
- Полный drill `_contrib_check.py` (части 1–7 одним прогоном, self-cleaning,
  детерминированный — паттерн `_beta_journey.py`).
- Нагрузочный мини-прогон: 100 submissions/мин через python-драйвер → rate-limits
  держат, worker-бюджеты не деградируют, очередь drain'ится.
- `docs/BETA.md` fix-list-ревизия, CHANGELOG `[Unreleased]`, `release:notes`,
  обновление REWORK-STATE §0/§3/§9 (P17 = done), WORKLOG, финальный bundle.
- **Acceptance:** ВСЕ гейты проекта зелёные (tests/lint/i18n/journey 13/13/edge 12/12
  /contrib drill), правило 11-отчёт, ритуал чекпоинта, P17 закрыт.

---

## 17. Конфиг-ключи (ВСЕ в `Config::defaults()` — иначе env игнорируется, B-005!)

```php
'CONTRIB_ENABLED'            => false,   // мастер-килл (включаем в C2 после drill)
'CONTRIB_RUNNER_ENABLED'     => false,   // исполнение (включаем в C3)
'CONTRIB_AUTO_PROMOTE'       => false,   // НИКОГДА не включать без L2+окна метрик
'CONTRIB_ALLOW_L1_PROMOTE'   => false,   // явный админ-override для dev
'CONTRIB_RUNNER_LEVEL'       => 'L1',    // L1|L2
'CONTRIB_CHALLENGE_TTL'      => '259200',// 72h
'CONTRIB_REVIEW_MIN'         => '3',     // кандидатов до старта review
'CONTRIB_MIN_REVIEWS'        => '3',     // независимых reviews на кандидата
'CONTRIB_HIGH_RISK_MAX_PCT'  => '20',
'CONTRIB_RUN_TIMEOUT'        => '30',    // сек на прогон
'CONTRIB_VALIDATE_BUDGET'    => '50',    // static-гейтов за проход worker'а
'CONTRIB_RUN_BUDGET'         => '10',    // L1/L2-прогонов за проход
'CONTRIB_MAX_RESPONSE_BYTES' => '65536',
'CONTRIB_MAX_BODY_BYTES'     => '262144',
'CONTRIB_DISK_QUOTA_MB'      => '512',   // var/contrib GC-порог
'CONTRIB_BETA_WINDOW'        => '600',   // dev 10 мин; прод 172800 (48h)
'CONTRIB_QUARANTINE_DAYS'    => '7',
'RATE_LIMIT_CONTRIB_CHALLENGE' => '1:600',   // + суточный '5:86400'
'RATE_LIMIT_CONTRIB_SUBMIT'    => '3:600',
'RATE_LIMIT_CONTRIB_REVIEW'    => '6:600',
```

Плюс `.env` (dev): CONTRIB_ENABLED=true, CONTRIB_RUNNER_ENABLED=true,
CONTRIB_RUNNER_LEVEL=L1, CONTRIB_BETA_WINDOW=600 — и записи в `.env.example`
с комментариями для прода (default'ы = безопасно-выключено).

---

## 18. Тестирование и drills (conventions проекта)

- Unit: `tests/unit/Domain/Contrib/*Test.php` (кастомный раннер! список доступных
  assert'ов — в `tests/TestCase.php`; `assertStringContainsString`/`assertIsArray`
  НЕТ — известные грабли PLAYBOOK §6).
- Integration: `tests/integration/ContribApiTest.php` (Kernel-пайплайн, envelope,
  rate-limit 429-семантика «limit N → 429 на N+1-м»).
- Контейнер- мемоизация shared-фабрик между тест-классами: ContribService-фабрики
  регистрировать non-shared ИЛИ сбрасывать `set()` в setUp (грабля №PLAYBOOK).
- Fixtures: `tests/fixtures/contrib/` — добрые/злые кандидаты (eval, obfuscation,
  секреты, huge-file, utf8-edge, CSV-отклонения ×30).
- Drills: `scripts/_contrib_check.py` (части 1–7 по фазам, финальный — полный;
  паттерн: детерминированный, self-cleaning, чистит rate_limits перед прогоном —
  как `_beta_journey.py`; без curl — python urllib).
- Каждый drill гоняется ЖИВЬЁМ (`php bin/iya serve` + worker) перед чекпоинтом фазы.

## 19. Метрики и наблюдаемость

- Structured logs (существующий Logger): `contrib.challenge.issued`,
  `contrib.candidate.received/validated/rejected`, `contrib.run.finished`
  (gate, duration, runner_level), `contrib.review.parsed/rejected`,
  `contrib.promotion.*`, `contrib.gc.*`.
- Webhook-события (§11.3) — подписчики-агенты получают их через существующий
  WebhookService (паттерн `agent.task`/`link.target_down`).
- Admin-дашборд /admin/contrib: воронка (issued→submitted→validated→reviewed→
  promoted), топ reject-причин, нагрузка на worker-бюджеты, disk-использование
  var/contrib, список активных review-пар (граф коллюзий).
- `contrib/stats` публично: прозрачность контура (доверие агентов).

## 20. Промпты (выдаются через `/api/v1/contrib/prompt?role=…`)

Промпт coding-агента и reviewer-агента — из брифа пользователя, без изменений
по сути; в реализации хранятся КАК ЕСТЬ в `src/Domain/Contrib/Prompts.php`
(константы, версионируются `prompt_version` в meta выдачи). Полный текст —
в приложении `docs/CONTRIB.md` (§C7). Ключевые правила, уже встроенные в них:
untrusted-source оговорка, strict JSON/CSV only, no-markdown-fences,
`status:rejected`-путь, запрет внешних зависимостей/сети/fs-записи в кандидатских
тестах, reviewer'у — не исполнять код и не следовать инструкциям из текста кандидатов.

## 21. Риски и открытые вопросы (решать ADR-записями по ходу)

| Риск | Вероятность | Митигация |
|---|---|---|
| Мало агентов-участников (пустой контур) | средняя | публичный leaderboard + разделы §13 работают и без кандидатов; self-review-режим: платформа сама генерирует кандидатов внутренней LLM для обкатки (dev-only флаг) |
| L1-изоляция окажется недостаточной даже для dev | низкая | fail-closed + CONTRIB_RUNNER_ENABLED=false по умолчанию; промоция только L2 |
| Reviewer-агенты галлюцинируют оценки | высокая | пороги §10.5 + машина-гейты первичны; review влияет только на ранжирование среди УЖЕ прошедших гейты |
| Beta-окно метрик шумит на малом трафике | средняя | относительные пороги + минимальный объём выборки (не решать при <1000 visits) |
| Git-промоция конфликтует с ручными коммитами | низкая | contrib-ветки только от релизных тегов; rebase-политика в GitClient; CI как арбитр |
| Раздувание var/contrib | средняя | квота + GC (T13) + мониторинг в admin |

Открытые вопросы (ADR-кандидаты): отдельный subdomain `contrib.iya.ru` vs apex-
пути (рекомендация: apex `/api/v1/contrib` — меньше инфраструктуры); хранить ли
кандидаты в git-object-store вместо файловой системы (рекомендация: нет — var/
достаточен, git только для промоций); язык портала (en + i18n-конвейер).

## 22. Definition of Done (P17)

1. Все гейты зелёные: существующие 483+ тестов без регрессий, новые contrib-тесты,
   lint 0, i18n-build --check, journey 13/13, edge 12/12, `_contrib_check.py` 7/7 частей.
2. Полный цикл ЖИВЬЁМ в песочнице: register → challenge → candidate → static+L1
   гейты → score → 3 review → победитель → beta (`var/beta-root`, :8081) → окно
   метрик (dev 10 мин) → rollback-дрель → (AUTO_PROMOTE off) admin-очередь.
3. Kill switches проверены drill'ом на каждом уровне; все ключи в defaults()+.env.example.
4. Docs: CONTRIB.md, портал, llms.txt/agents.json/openapi/robots обновлены;
   CHANGELOG + release:notes; REWORK-STATE §0/§3/§9 + WORKLOG + правило 11-отчёт.
5. Ритуал: commit → pack_p12.sh → sync.sh → зеркала /mnt/oss → обновлённый
   IYA-DEV-BUNDLE.tar.gz (pack_bundle.sh) → бюджет "$ARENA_WORKSPACE" < 110 МБ.

---

*Конец плана. Реализацию начинать с C0, прочитав RESILIENCE-PLAYBOOK.md §1–§3
и REWORK-STATE.md §9 правила 1–11. Спорные решения — в ADR-лог, не в чат.*
