# Plan 6 — Subject-level regions Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Заменить 8-битный `projects.region_mask` на 89-кодовый `projects.regions INT[]` с outbound-передачей поставщику crm.bp-gr.ru.

**Architecture:** Approach 2 — dual-write transition. Add `regions INT[]` колонка с GIN-индексом; backend пишет в обе колонки (regions источник истины + legacy `region_mask=255 + region_mode='include'`); outbound `SyncSupplierProjectsJob` копирует regions напрямую без bitmask конверсии; UI заменяется на v-autocomplete с 89 субъектами и подзаголовком ФО. Полное удаление legacy — отдельный Plan 6.5.

**Tech Stack:** PostgreSQL 16 INT[] + GIN, Laravel 13 (Eloquent casts, FormRequest validation), Vue 3 + Vuetify 3 (v-autocomplete), Pest 4 + Vitest 4.

**Spec:** [docs/superpowers/specs/2026-05-14-plan-6-regions-subject-level-design.md](../specs/2026-05-14-plan-6-regions-subject-level-design.md)

---

## File Structure

**Modify (12 файлов):**

| Файл | Ответственность |
|---|---|
| [db/schema.sql](../../../db/schema.sql) | Add `regions INT[]` column в CREATE TABLE projects + GIN index + COMMENT |
| [db/CHANGELOG_schema.md](../../../db/CHANGELOG_schema.md) | v8.20 entry |
| [app/resources/js/constants/regions.ts](../../../app/resources/js/constants/regions.ts) | Replace 31 → 89 + `federalDistrict` field + `FEDERAL_DISTRICT_NAMES` export |
| [app/app/Models/Project.php](../../../app/app/Models/Project.php) | `$fillable` += `regions`; `$casts` += `'regions' => 'array'` |
| [app/app/Http/Requests/StoreProjectRequest.php](../../../app/app/Http/Requests/StoreProjectRequest.php) | Drop `region_mask`/`region_mode` rules; add `regions` + `regions.*` rules |
| [app/app/Http/Requests/UpdateProjectRequest.php](../../../app/app/Http/Requests/UpdateProjectRequest.php) | Same with `sometimes` |
| [app/app/Services/Project/ProjectService.php](../../../app/app/Services/Project/ProjectService.php) | `create()` dual-write logic |
| [app/app/Jobs/Supplier/SyncSupplierProjectsJob.php](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php) | `adaptProjectsForAllocator()` direct copy |
| [app/resources/js/stores/projectsStore.ts](../../../app/resources/js/stores/projectsStore.ts) | `Project.regions?: number[]` |
| [app/resources/js/views/projects/NewProjectDialog.vue](../../../app/resources/js/views/projects/NewProjectDialog.vue) | Replace `<v-alert>` placeholder с v-autocomplete; form schema |
| [app/resources/js/components/projects/ProjectDetailsDrawer.vue](../../../app/resources/js/components/projects/ProjectDetailsDrawer.vue) | Replace existing region UI на 89-codes model |
| [app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php](../../../app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php) | +3 tests |
| [app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php](../../../app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php) | +2 tests |
| [app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php](../../../app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php) | +2 tests |
| [app/tests/Frontend/NewProjectDialog.spec.ts](../../../app/tests/Frontend/NewProjectDialog.spec.ts) | +2 tests |
| [app/tests/Frontend/ProjectDetailsDrawer.spec.ts](../../../app/tests/Frontend/ProjectDetailsDrawer.spec.ts) | Refactor 3 existing region tests |

**Создать:** ничего (все правки existing).

**Schema rollout:** В этом проекте canonical state хранится в `db/schema.sql`; миграция `database/migrations/2026_05_07_000001_load_initial_schema.php` raw-загружает его через `DB::unprepared(file_get_contents(...))` (CLAUDE.md §6). Поэтому `regions INT[]` добавляется **inline в CREATE TABLE projects block**, не отдельным ALTER. Применение через `php artisan migrate:fresh`. После — `php artisan partitions:create-months` для re-create partition'ов.

---

## Task 0: Orientation & baseline

**Files:** read-only

- [ ] **Step 1: Re-read spec и baseline тестовые метрики**

Read: [docs/superpowers/specs/2026-05-14-plan-6-regions-subject-level-design.md](../specs/2026-05-14-plan-6-regions-subject-level-design.md) — освежить контекст.

- [ ] **Step 2: Verify clean working tree before starting**

Run:

```bash
cd app && git status
```

Expected: только `app/dev-indices.json` modified (live feedback feature, не Plan 6 scope). Если что-то ещё — обсудить.

- [ ] **Step 3: Snapshot regression baseline**

Run:

```bash
cd app && ./vendor/bin/pest --parallel --no-coverage 2>&1 | tail -3
```

Expected: `passed":742 passed":739` matching memory baseline 742/739/3sk/0 (или текущий).

Run:

```bash
cd app && npx vitest run 2>&1 | tail -5
```

Expected: 92 files / 758 passed / 3 skipped / 0 failed (или текущий).

Run:

```bash
cd app && npx vue-tsc --noEmit; echo "EXIT=$?"
```

Expected: `EXIT=0`.

Record metrics для сравнения в Task 6 финальной регрессии.

---

## Task 1: Schema delta v8.19 → v8.20

**Files:**

- Modify: `db/schema.sql` (inline в CREATE TABLE projects, около line 811-844)
- Modify: `db/CHANGELOG_schema.md` (top — новая v8.20 запись)

- [ ] **Step 1: Add `regions` column inline в CREATE TABLE projects**

В [db/schema.sql](../../../db/schema.sql) найти block `CREATE TABLE projects (...)` (около line 781). После строки `region_mode VARCHAR(10) NOT NULL DEFAULT 'include' CHECK (region_mode IN ('include','exclude')),` добавить:

```sql
    -- v8.20 (Plan 6): Subject-level regions array. 89 codes из resources/js/constants/regions.ts.
    -- Пустой массив = «вся РФ» (паритет с legacy region_mask=255 + region_mode='include').
    -- region_mask/region_mode остаются для legacy reader'ов (PhonePrefixService, LeadRouter),
    -- DEPRECATED — удаляются в Plan 6.5 после переключения читателей.
    regions                         INT[] NOT NULL DEFAULT '{}'::INT[],
```

Добавить inline-комментарий к существующей строке `region_mask INT NOT NULL DEFAULT 255,`:

```sql
    region_mask                     INT NOT NULL DEFAULT 255,  -- DEPRECATED Plan 6.5: см. regions INT[]
```

- [ ] **Step 2: Add GIN index**

В [db/schema.sql](../../../db/schema.sql) найти block `CREATE INDEX ... ON projects` (около строк после CREATE TABLE projects, рядом с другими project indexes). Добавить:

```sql
-- v8.20 (Plan 6): GIN-индекс для outbound regions queries.
CREATE INDEX idx_projects_regions ON projects USING GIN (regions);
```

- [ ] **Step 3: Add COMMENT ON COLUMN**

В [db/schema.sql](../../../db/schema.sql) (рядом с другими COMMENT блоками для projects, либо в конце CREATE TABLE projects секции) добавить:

```sql
COMMENT ON COLUMN projects.regions IS
    'Subject-level region filter (1..89 коды субъектов РФ). Пустой массив = вся РФ. Plan 6 (v8.20).';
```

- [ ] **Step 4: Update schema-header version в начале файла**

В [db/schema.sql:4](../../../db/schema.sql#L4) найти строку с version (типа `-- Версия: v8.19 от ...`) и обновить:

```sql
-- Версия: v8.20 от 2026-05-14 (Plan 6 — Subject-level regions array)
```

- [ ] **Step 5: Add CHANGELOG entry**

В [db/CHANGELOG_schema.md](../../../db/CHANGELOG_schema.md) **сверху** (после header, перед предыдущей записью) добавить:

```markdown
## v8.20 от 2026-05-14 (Plan 6 — Subject-level regions)

**Изменения:**
- `projects` +1 колонка: `regions INT[] NOT NULL DEFAULT '{}'`
- `projects` +1 GIN-индекс: `idx_projects_regions`
- `projects` +1 COMMENT ON COLUMN на `regions`

**Не изменено (deprecated, удаление в Plan 6.5):**
- `projects.region_mask` (помечен inline-комментарием DEPRECATED)
- `projects.region_mode`
- CHECK `chk_projects_region_mask_range`

**Семантика:**
- `regions=[]` → «вся РФ» (паритет с legacy `region_mask=255 + region_mode='include'`)
- `regions=[77,82]` → проект принимает лиды только из Москвы (77) и Чукотки (82)

**Schema baseline после v8.20:** 63 базовых таблиц / 12 партиций / **118 индексов** (+1) / 39 RLS / 5 функций / 13 триггеров.

**Связано:** [docs/superpowers/specs/2026-05-14-plan-6-regions-subject-level-design.md](../docs/superpowers/specs/2026-05-14-plan-6-regions-subject-level-design.md)
```

- [ ] **Step 6: Apply schema via migrate:fresh**

Run:

```bash
cd app && php artisan migrate:fresh
```

Expected: `INFO  Migration table created successfully.` + `2026_05_07_000001_load_initial_schema ... DONE`. Time ~1-2s.

Если падает с ERROR на ALTER/CHECK — читать ошибку, fixнуть schema.sql, retry.

- [ ] **Step 7: Verify new column existence + index**

Run:

```bash
cd app && php artisan tinker --execute="
echo 'regions_column_exists: ' . (\Schema::hasColumn('projects', 'regions') ? 'YES' : 'NO') . PHP_EOL;
echo 'gin_index_exists: ' . (\DB::selectOne(\"SELECT 1 AS x FROM pg_indexes WHERE indexname = 'idx_projects_regions'\") ? 'YES' : 'NO') . PHP_EOL;
"
```

Expected:

```
regions_column_exists: YES
gin_index_exists: YES
```

- [ ] **Step 8: Re-create partitions**

Run:

```bash
cd app && php artisan partitions:create-months
```

Expected: командa завершается без ошибок, выводит число созданных partition'ов.

- [ ] **Step 9: Regression smoke на RLS + Tenant tests**

Run:

```bash
cd app && ./vendor/bin/pest tests/Feature/RlsSmokeTest.php tests/Feature/TenantModelsTest.php --no-coverage 2>&1 | tail -5
```

Expected: все pass, 0 failed.

- [ ] **Step 10: Commit**

```bash
cd .. && git add app/database/migrations db/schema.sql db/CHANGELOG_schema.md
git commit -m "$(cat <<'EOF'
db(schema): v8.20 — add projects.regions INT[] for subject-level filtering

Adds INT[] column + GIN index to support 89-code regions (Plan 6).
region_mask/region_mode kept for backward-compat (DEPRECATED, removal in Plan 6.5).
Empty array semantically equivalent to legacy region_mask=255 (all of Russia).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 2: REGIONS constant 31 → 89 + FEDERAL_DISTRICT_NAMES

**Files:**

- Modify: `app/resources/js/constants/regions.ts`

- [ ] **Step 1: Replace file content with 89-region table + districts map**

Перезаписать [app/resources/js/constants/regions.ts](../../../app/resources/js/constants/regions.ts) полностью:

```ts
export interface Region {
    code: number;            // 1..89, sequential по конституционному порядку (Art. 65)
    name: string;            // официальное название субъекта
    federalDistrict: number; // 1..8 (см. FEDERAL_DISTRICT_NAMES)
}

// Конституционный порядок (ст. 65 Конституции РФ, ред. 2022):
// 24 республики (1..24) → 9 краёв (25..33) → 48 областей (34..81) →
// 3 города фед.знач. (82..84) → 1 АО Еврейская (85) → 4 АО (86..89).
// Sentinel code:0 = "Вся РФ" (UI hint, в БД хранится как regions=[]).
export const REGIONS: Region[] = [
    { code: 0,  name: 'Вся РФ',                                federalDistrict: 0 },
    // 24 республики
    { code: 1,  name: 'Республика Адыгея',                     federalDistrict: 3 },
    { code: 2,  name: 'Республика Алтай',                      federalDistrict: 7 },
    { code: 3,  name: 'Республика Башкортостан',               federalDistrict: 5 },
    { code: 4,  name: 'Республика Бурятия',                    federalDistrict: 8 },
    { code: 5,  name: 'Республика Дагестан',                   federalDistrict: 4 },
    { code: 6,  name: 'Донецкая Народная Республика',          federalDistrict: 3 },
    { code: 7,  name: 'Республика Ингушетия',                  federalDistrict: 4 },
    { code: 8,  name: 'Кабардино-Балкарская Республика',       federalDistrict: 4 },
    { code: 9,  name: 'Республика Калмыкия',                   federalDistrict: 3 },
    { code: 10, name: 'Карачаево-Черкесская Республика',       federalDistrict: 4 },
    { code: 11, name: 'Республика Карелия',                    federalDistrict: 2 },
    { code: 12, name: 'Республика Коми',                       federalDistrict: 2 },
    { code: 13, name: 'Республика Крым',                       federalDistrict: 3 },
    { code: 14, name: 'Луганская Народная Республика',         federalDistrict: 3 },
    { code: 15, name: 'Республика Марий Эл',                   federalDistrict: 5 },
    { code: 16, name: 'Республика Мордовия',                   federalDistrict: 5 },
    { code: 17, name: 'Республика Саха (Якутия)',              federalDistrict: 8 },
    { code: 18, name: 'Республика Северная Осетия — Алания',   federalDistrict: 4 },
    { code: 19, name: 'Республика Татарстан',                  federalDistrict: 5 },
    { code: 20, name: 'Республика Тыва',                       federalDistrict: 7 },
    { code: 21, name: 'Удмуртская Республика',                 federalDistrict: 5 },
    { code: 22, name: 'Республика Хакасия',                    federalDistrict: 7 },
    { code: 23, name: 'Чеченская Республика',                  federalDistrict: 4 },
    { code: 24, name: 'Чувашская Республика',                  federalDistrict: 5 },
    // 9 краёв
    { code: 25, name: 'Алтайский край',                        federalDistrict: 7 },
    { code: 26, name: 'Забайкальский край',                    federalDistrict: 8 },
    { code: 27, name: 'Камчатский край',                       federalDistrict: 8 },
    { code: 28, name: 'Краснодарский край',                    federalDistrict: 3 },
    { code: 29, name: 'Красноярский край',                     federalDistrict: 7 },
    { code: 30, name: 'Пермский край',                         federalDistrict: 5 },
    { code: 31, name: 'Приморский край',                       federalDistrict: 8 },
    { code: 32, name: 'Ставропольский край',                   federalDistrict: 4 },
    { code: 33, name: 'Хабаровский край',                      federalDistrict: 8 },
    // 48 областей
    { code: 34, name: 'Амурская область',                      federalDistrict: 8 },
    { code: 35, name: 'Архангельская область',                 federalDistrict: 2 },
    { code: 36, name: 'Астраханская область',                  federalDistrict: 3 },
    { code: 37, name: 'Белгородская область',                  federalDistrict: 1 },
    { code: 38, name: 'Брянская область',                      federalDistrict: 1 },
    { code: 39, name: 'Владимирская область',                  federalDistrict: 1 },
    { code: 40, name: 'Волгоградская область',                 federalDistrict: 3 },
    { code: 41, name: 'Вологодская область',                   federalDistrict: 2 },
    { code: 42, name: 'Воронежская область',                   federalDistrict: 1 },
    { code: 43, name: 'Запорожская область',                   federalDistrict: 3 },
    { code: 44, name: 'Ивановская область',                    federalDistrict: 1 },
    { code: 45, name: 'Иркутская область',                     federalDistrict: 7 },
    { code: 46, name: 'Калининградская область',               federalDistrict: 2 },
    { code: 47, name: 'Калужская область',                     federalDistrict: 1 },
    { code: 48, name: 'Кемеровская область',                   federalDistrict: 7 },
    { code: 49, name: 'Кировская область',                     federalDistrict: 5 },
    { code: 50, name: 'Костромская область',                   federalDistrict: 1 },
    { code: 51, name: 'Курганская область',                    federalDistrict: 6 },
    { code: 52, name: 'Курская область',                       federalDistrict: 1 },
    { code: 53, name: 'Ленинградская область',                 federalDistrict: 2 },
    { code: 54, name: 'Липецкая область',                      federalDistrict: 1 },
    { code: 55, name: 'Магаданская область',                   federalDistrict: 8 },
    { code: 56, name: 'Московская область',                    federalDistrict: 1 },
    { code: 57, name: 'Мурманская область',                    federalDistrict: 2 },
    { code: 58, name: 'Нижегородская область',                 federalDistrict: 5 },
    { code: 59, name: 'Новгородская область',                  federalDistrict: 2 },
    { code: 60, name: 'Новосибирская область',                 federalDistrict: 7 },
    { code: 61, name: 'Омская область',                        federalDistrict: 7 },
    { code: 62, name: 'Оренбургская область',                  federalDistrict: 5 },
    { code: 63, name: 'Орловская область',                     federalDistrict: 1 },
    { code: 64, name: 'Пензенская область',                    federalDistrict: 5 },
    { code: 65, name: 'Псковская область',                     federalDistrict: 2 },
    { code: 66, name: 'Ростовская область',                    federalDistrict: 3 },
    { code: 67, name: 'Рязанская область',                     federalDistrict: 1 },
    { code: 68, name: 'Самарская область',                     federalDistrict: 5 },
    { code: 69, name: 'Саратовская область',                   federalDistrict: 5 },
    { code: 70, name: 'Сахалинская область',                   federalDistrict: 8 },
    { code: 71, name: 'Свердловская область',                  federalDistrict: 6 },
    { code: 72, name: 'Смоленская область',                    federalDistrict: 1 },
    { code: 73, name: 'Тамбовская область',                    federalDistrict: 1 },
    { code: 74, name: 'Тверская область',                      federalDistrict: 1 },
    { code: 75, name: 'Томская область',                       federalDistrict: 7 },
    { code: 76, name: 'Тульская область',                      federalDistrict: 1 },
    { code: 77, name: 'Тюменская область',                     federalDistrict: 6 },
    { code: 78, name: 'Ульяновская область',                   federalDistrict: 5 },
    { code: 79, name: 'Херсонская область',                    federalDistrict: 3 },
    { code: 80, name: 'Челябинская область',                   federalDistrict: 6 },
    { code: 81, name: 'Ярославская область',                   federalDistrict: 1 },
    // 3 города федерального значения
    { code: 82, name: 'Москва',                                federalDistrict: 1 },
    { code: 83, name: 'Санкт-Петербург',                       federalDistrict: 2 },
    { code: 84, name: 'Севастополь',                           federalDistrict: 3 },
    // 1 автономная область
    { code: 85, name: 'Еврейская автономная область',          federalDistrict: 8 },
    // 4 автономных округа
    { code: 86, name: 'Ненецкий автономный округ',             federalDistrict: 2 },
    { code: 87, name: 'Ханты-Мансийский автономный округ — Югра', federalDistrict: 6 },
    { code: 88, name: 'Чукотский автономный округ',            federalDistrict: 8 },
    { code: 89, name: 'Ямало-Ненецкий автономный округ',       federalDistrict: 6 },
];

export const FEDERAL_DISTRICT_NAMES: Record<number, string> = {
    1: 'Центральный',
    2: 'Северо-Западный',
    3: 'Южный',
    4: 'Северо-Кавказский',
    5: 'Приволжский',
    6: 'Уральский',
    7: 'Сибирский',
    8: 'Дальневосточный',
};
```

- [ ] **Step 2: Run vue-tsc to verify types**

Run:

```bash
cd app && npx vue-tsc --noEmit; echo "EXIT=$?"
```

Expected: `EXIT=0` (no breakage on existing consumers — REGIONS interface backward-compat since we only added optional field).

Note: existing PDD code в [ProjectDetailsDrawer.vue](../../../app/resources/js/components/projects/ProjectDetailsDrawer.vue) использует `REGIONS` — может сломаться если `Region` interface несовместим. Если `EXIT≠0` — это ожидаемо (Task 5 закроет): пометить, но не блокировать.

Если ошибка про `federalDistrict` required field на старых consumers — не блокирующая, переходим к коммиту (этот task — только const data).

- [ ] **Step 3: Commit**

```bash
cd .. && git add app/resources/js/constants/regions.ts
git commit -m "$(cat <<'EOF'
chore(regions): expand REGIONS constant 31 → 89 + add federal district mapping

89 субъектов РФ по конституционному порядку (ст. 65, ред. 2022).
Adds federalDistrict field for UI group-by + FEDERAL_DISTRICT_NAMES map.
Sentinel code:0 "Вся РФ" сохранён для UI hint; в БД = regions=[].
Plan 6 (см. docs/superpowers/specs/2026-05-14-plan-6-regions-subject-level-design.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 3: Backend regions support (Store/Update/Service/Model + 5 Pest tests)

**Files:**

- Modify: `app/app/Models/Project.php`
- Modify: `app/app/Http/Requests/StoreProjectRequest.php`
- Modify: `app/app/Http/Requests/UpdateProjectRequest.php`
- Modify: `app/app/Services/Project/ProjectService.php`
- Modify: `app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php` (+3 tests)
- Modify: `app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php` (+2 tests)

### Step block A — Failing tests (red)

- [ ] **Step A1: Read existing test files to determine pattern**

Read full: [app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php](../../../app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php) и [app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php](../../../app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php). Запомнить:

- Конструкцию `actingAs($user)` / `Sanctum::actingAs`
- Структуру test data (factory? plain array? tenant_id resolved как?)
- Стиль assertions (Pest expect vs PHPUnit assert)
- Существующий тест "POST /api/projects creates project with valid payload" — взять как baseline.

- [ ] **Step A2: Add 3 ProjectsStore red-tests**

В конец [app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php](../../../app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php) перед закрывающей скобкой / в new test block добавить (адаптируя под существующий pattern, заменяя `$tenant->id`, `$user`, и т.п. на токены из existing tests):

```php
it('creates project with subject-level regions array', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $user = \App\Models\User::factory()->create(['tenant_id' => $tenant->id]);

    $response = $this->actingAs($user)->postJson('/api/projects', [
        'name' => 'Regions Test Project',
        'signal_type' => 'site',
        'signal_identifier' => 'regions-test.example',
        'daily_limit_target' => 50,
        'delivery_days_mask' => 127,
        'regions' => [82, 83],  // Москва + СПб
    ]);

    $response->assertStatus(201);
    $created = \App\Models\Project::where('name', 'Regions Test Project')->firstOrFail();
    expect($created->regions)->toBe([82, 83]);
});

it('dual-writes region_mask=255 + region_mode=include for backward-compat', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $user = \App\Models\User::factory()->create(['tenant_id' => $tenant->id]);

    $response = $this->actingAs($user)->postJson('/api/projects', [
        'name' => 'Dual Write Test',
        'signal_type' => 'site',
        'signal_identifier' => 'dualwrite.example',
        'daily_limit_target' => 50,
        'delivery_days_mask' => 127,
        'regions' => [77],
    ]);

    $response->assertStatus(201);
    $created = \App\Models\Project::where('name', 'Dual Write Test')->firstOrFail();
    expect($created->region_mask)->toBe(255);
    expect($created->region_mode)->toBe('include');
});

it('rejects regions code out of 1..89 range with 422', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $user = \App\Models\User::factory()->create(['tenant_id' => $tenant->id]);

    $response = $this->actingAs($user)->postJson('/api/projects', [
        'name' => 'Invalid Code Test',
        'signal_type' => 'site',
        'signal_identifier' => 'invalid.example',
        'daily_limit_target' => 50,
        'delivery_days_mask' => 127,
        'regions' => [90, 100],
    ]);

    $response->assertStatus(422);
    $response->assertJsonValidationErrors(['regions.0', 'regions.1']);
});
```

- [ ] **Step A3: Add 2 ProjectsUpdate red-tests**

В [app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php](../../../app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php) добавить (адаптируя под существующий pattern):

```php
it('updates regions array via PATCH', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $user = \App\Models\User::factory()->create(['tenant_id' => $tenant->id]);
    $project = \App\Models\Project::factory()->create(['tenant_id' => $tenant->id, 'regions' => []]);

    $response = $this->actingAs($user)->patchJson("/api/projects/{$project->id}", [
        'regions' => [82],
    ]);

    $response->assertStatus(200);
    expect($project->fresh()->regions)->toBe([82]);
});

it('preserves regions when PATCH omits the field (sometimes rule)', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $user = \App\Models\User::factory()->create(['tenant_id' => $tenant->id]);
    $project = \App\Models\Project::factory()->create([
        'tenant_id' => $tenant->id,
        'regions' => [82, 83],
    ]);

    $response = $this->actingAs($user)->patchJson("/api/projects/{$project->id}", [
        'name' => 'Renamed Project',
    ]);

    $response->assertStatus(200);
    expect($project->fresh()->regions)->toBe([82, 83]);
});
```

- [ ] **Step A4: Run red tests — confirm failures**

Run:

```bash
cd app && ./vendor/bin/pest --filter "subject-level regions array|dual-writes region_mask|rejects regions code|updates regions array via PATCH|preserves regions when PATCH" --no-coverage 2>&1 | tail -15
```

Expected: 5 tests, **all failed** (red). Reasons:

- `regions` field not in fillable/casts → Eloquent strip
- StoreProjectRequest/UpdateProjectRequest still validate region_mask not regions
- ProjectService не устанавливает regions

Если какой-то test уже passes — root cause investigate, может быть test mis-specified.

### Step block B — Implementation (green)

- [ ] **Step B1: Project model — fillable + casts**

В [app/app/Models/Project.php](../../../app/app/Models/Project.php) найти `protected $fillable = [...];` и добавить `'regions',` в массив. Найти `protected $casts = [...];` и добавить `'regions' => 'array',`.

(Полные значения — Read existing file сначала, чтобы preserve other entries.)

- [ ] **Step B2: StoreProjectRequest — drop region_mask/region_mode, add regions**

В [app/app/Http/Requests/StoreProjectRequest.php](../../../app/app/Http/Requests/StoreProjectRequest.php) метод `rules()` — в `$base` массиве:

**Удалить:**

```php
'region_mask' => ['required', 'integer', 'min:0'],
'region_mode' => ['required', Rule::in(['include', 'exclude'])],
```

**Добавить (на их место):**

```php
'regions' => ['present', 'array'],
'regions.*' => ['integer', 'between:1,89'],
```

- [ ] **Step B3: UpdateProjectRequest — sometimes regions**

Read full [app/app/Http/Requests/UpdateProjectRequest.php](../../../app/app/Http/Requests/UpdateProjectRequest.php) (он может быть короткий и проще read целиком). Изменить аналогично StoreProjectRequest, но с `sometimes`:

**Удалить** region_mask/region_mode rules.

**Добавить:**

```php
'regions' => ['sometimes', 'array'],
'regions.*' => ['integer', 'between:1,89'],
```

- [ ] **Step B4: ProjectService::create — dual-write**

В [app/app/Services/Project/ProjectService.php](../../../app/app/Services/Project/ProjectService.php) (метод `create`, около строк 182-199) **заменить:**

```php
$data['tenant_id'] = $tenant->id;
$data['is_active'] = true;
$project = Project::create($data);

SyncSupplierProjectJob::dispatch($project->id);

return $project->fresh();
```

**на:**

```php
$data['tenant_id'] = $tenant->id;
$data['is_active'] = true;
$data['regions'] = $data['regions'] ?? [];
// Plan 6 dual-write: regions[] источник истины; region_mask/mode — legacy для
// PhonePrefixService / LeadRouter, удаляются в Plan 6.5 после переключения читателей.
$data['region_mask'] = 255;
$data['region_mode'] = 'include';
$project = Project::create($data);

SyncSupplierProjectJob::dispatch($project->id);

return $project->fresh();
```

- [ ] **Step B5: Run red tests — confirm green**

Run:

```bash
cd app && ./vendor/bin/pest --filter "subject-level regions array|dual-writes region_mask|rejects regions code|updates regions array via PATCH|preserves regions when PATCH" --no-coverage 2>&1 | tail -15
```

Expected: 5 tests, **all passed** (green).

Если что-то fails — читать output, исправить, не commit'ить пока не green.

- [ ] **Step B6: Regression — full ProjectsStore + ProjectsUpdate suites**

Run:

```bash
cd app && ./vendor/bin/pest tests/Feature/Plan5/Projects/ --no-coverage 2>&1 | tail -10
```

Expected: existing tests all green + 5 new tests green. 0 failed.

Если existing tests сломались (например, они слали `region_mask` в payload и теперь получают 422 потому что rules больше не принимают unknown field) — fix existing tests: убрать legacy `region_mask`/`region_mode` из их payloads (они не нужны больше — backend dual-writes).

- [ ] **Step B7: Commit**

```bash
cd .. && git add app/app/Models/Project.php app/app/Http/Requests/StoreProjectRequest.php app/app/Http/Requests/UpdateProjectRequest.php app/app/Services/Project/ProjectService.php app/tests/Feature/Plan5/Projects/
git commit -m "$(cat <<'EOF'
feat(projects): backend support for subject-level regions array (Plan 6)

- Project model: +regions in fillable + casts to array
- StoreProjectRequest / UpdateProjectRequest: drop region_mask/mode rules,
  add regions array validation (1..89 each, present/sometimes)
- ProjectService::create: dual-write — regions source-of-truth + legacy
  region_mask=255 + region_mode='include' for PhonePrefixService/LeadRouter
  compatibility (Plan 6.5 cleanup)
- +5 Pest tests covering create/update/dual-write/validation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 4: Outbound adapter (SyncSupplierProjectsJob + 2 Pest tests)

**Files:**

- Modify: `app/app/Jobs/Supplier/SyncSupplierProjectsJob.php`
- Modify: `app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php` (+2 tests)

- [ ] **Step 1: Read existing SyncSupplierProjectsJobTest to learn pattern**

Read full: [app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php](../../../app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php). Существующие тесты проверяют `adaptProjectsForAllocator` или `handle()` end-to-end? Используют ли они `invade()` / reflection для private method? Какой sample project they create?

- [ ] **Step 2: Add 2 red tests**

В [app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php](../../../app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php) добавить (адаптируя под existing pattern; если private adaptProjectsForAllocator — использовать `(fn () => $this->adaptProjectsForAllocator(...))->call($job)` Closure binding, либо тестировать через handle() и инспектировать `supplier_projects.current_regions`):

```php
it('passes regions directly to allocator without bitmask conversion', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $project = \App\Models\Project::factory()->create([
        'tenant_id' => $tenant->id,
        'regions' => [82, 83],  // Москва + СПб
        'region_mask' => 255,    // legacy dual-write
    ]);

    $job = new \App\Jobs\Supplier\SyncSupplierProjectsJob($tenant->id);
    $adapted = (fn () => $this->adaptProjectsForAllocator(
        \App\Models\Project::where('tenant_id', $tenant->id)->get()
    ))->call($job);

    expect($adapted->first()->regions)->toBe([82, 83]);
});

it('passes empty array to allocator when project has regions=[]', function () {
    $tenant = \App\Models\Tenant::factory()->create();
    $project = \App\Models\Project::factory()->create([
        'tenant_id' => $tenant->id,
        'regions' => [],
        'region_mask' => 255,
    ]);

    $job = new \App\Jobs\Supplier\SyncSupplierProjectsJob($tenant->id);
    $adapted = (fn () => $this->adaptProjectsForAllocator(
        \App\Models\Project::where('tenant_id', $tenant->id)->get()
    ))->call($job);

    expect($adapted->first()->regions)->toBe([]);
});
```

Note: signature `new SyncSupplierProjectsJob($tenant->id)` — verify against actual job constructor; adapt if разные args.

- [ ] **Step 3: Run red tests — confirm fails**

Run:

```bash
cd app && ./vendor/bin/pest --filter "passes regions directly|passes empty array to allocator" --no-coverage 2>&1 | tail -10
```

Expected: 2 tests, both failed. Reason: текущий adaptProjectsForAllocator делает bitmask conversion из region_mask=255 → `regions=[]`. С `region_mask=255` второй тест может уже pass (coincidence). Первый тест должен fail (он ждёт `[82, 83]`, получит `[]` because bitmask=255 → empty).

- [ ] **Step 4: Replace adapter logic**

В [app/app/Jobs/Supplier/SyncSupplierProjectsJob.php](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php) метод `adaptProjectsForAllocator()` (около строк 215-231) **заменить:**

```php
// region_mask=255 (все 8 ФО, default) — catch-all семантика → пустой массив
// у supplier ("без региональных ограничений"). Иначе — список выставленных битов.
$regionMask = (int) $p->region_mask;
$obj->regions = $regionMask === 255
    ? []
    : $this->bitmaskToList($regionMask, 8);
```

**на:**

```php
// Plan 6: projects.regions[] напрямую копируется в supplier_projects.current_regions.
// Empty array = "вся РФ" (паритет с supplier API semantics).
// Legacy region_mask/region_mode игнорируются — они dual-write для PhonePrefixService,
// outbound к supplier использует только regions[]. Cleanup в Plan 6.5.
$obj->regions = array_values((array) $p->regions);
```

Также обновить docblock-комментарий метода (около строк 203-214) — заменить:

```php
 *   regions      ← биты region_mask (bit 0=Центральный, …, bit 7=Дальневосточный) → 1..8
```

**на:**

```php
 *   regions      ← projects.regions INT[] (subject codes 1..89) direct copy
```

- [ ] **Step 5: Run tests — confirm green**

Run:

```bash
cd app && ./vendor/bin/pest --filter "passes regions directly|passes empty array to allocator" --no-coverage 2>&1 | tail -10
```

Expected: 2 tests passed.

- [ ] **Step 6: Run full supplier suite — regression**

Run:

```bash
cd app && ./vendor/bin/pest tests/Feature/Supplier/ --no-coverage 2>&1 | tail -10
```

Expected: 0 failed. Existing supplier-related tests могут сломаться если они опирались на `region_mask` → `bitmaskToList` path. Если так — fix их под новую модель (test data: `regions=[1..8]` вместо `region_mask=N`).

- [ ] **Step 7: Commit**

```bash
cd .. && git add app/app/Jobs/Supplier/SyncSupplierProjectsJob.php app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php
git commit -m "$(cat <<'EOF'
feat(supplier): outbound adapter direct-copy regions[] (Plan 6)

SyncSupplierProjectsJob::adaptProjectsForAllocator no longer converts
8-bit region_mask via bitmaskToList. Instead direct-copies projects.regions[]
(89-code subject array) into supplier_projects.current_regions / DTO.

region_mask still dual-written for PhonePrefixService backward-compat (Plan 6.5
cleanup will switch readers and drop dual-write).

+2 Pest tests verifying direct copy + empty-array semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 5: Frontend regions UI (types + NewProjectDialog + ProjectDetailsDrawer + 5 tests)

**Files:**

- Modify: `app/resources/js/stores/projectsStore.ts` (Project type)
- Modify: `app/resources/js/views/projects/NewProjectDialog.vue`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`
- Modify: `app/tests/Frontend/NewProjectDialog.spec.ts` (+2 tests)
- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts` (refactor 3 existing tests)

### Step block A — Type + NewProjectDialog (red→green→commit half)

- [ ] **Step A1: Read existing Project interface**

Read [app/resources/js/stores/projectsStore.ts](../../../app/resources/js/stores/projectsStore.ts) — найти `export interface Project { ... }`. Запомнить fields.

- [ ] **Step A2: Add regions field to Project type**

В [app/resources/js/stores/projectsStore.ts](../../../app/resources/js/stores/projectsStore.ts) найти `Project` interface и добавить:

```ts
regions?: number[]; // Plan 6 — subject codes 1..89; пустой массив = вся РФ
```

(Optional с `?` потому что server response может не включать это поле в legacy responses — defensive, но Project Resource будет включать с Task 5 onwards.)

- [ ] **Step A3: Add 2 red tests в NewProjectDialog.spec**

Read existing [app/tests/Frontend/NewProjectDialog.spec.ts](../../../app/tests/Frontend/NewProjectDialog.spec.ts) (мокает apiClient после Task этой сессии). Добавить:

```ts
it('renders regions autocomplete with 89 selectable subjects (excluding "Вся РФ" sentinel)', async () => {
    const wrapper = factory();
    await flushPromises();
    const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' });
    expect(autocomplete.exists()).toBe(true);
    expect(autocomplete.props('items')).toHaveLength(89);
    expect((autocomplete.props('items') as Array<{ code: number }>).every(r => r.code !== 0)).toBe(true);
});

it('sends regions array in POST payload', async () => {
    const wrapper = factory();
    await flushPromises();

    const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' });
    await autocomplete.vm.$emit('update:model-value', [82, 83]);

    // Fill required site-tab fields
    const inputs = wrapper.findAll('input');
    // Find domain input by label proximity — simple approach: first v-text-field
    await wrapper.find('input').setValue('plan6.example');  // domain (first text-field)
    // Better: target by parent label — but pattern depends on Vuetify rendering;
    // adapt based on what works in existing tests if this is brittle.

    await flushPromises();
    await wrapper.find('[data-testid="submit-btn"]').trigger('click');
    await flushPromises();

    expect(apiClient.post).toHaveBeenCalledWith(
        '/api/projects',
        expect.objectContaining({ regions: [82, 83] }),
    );
});
```

Note: второй test может быть brittle на JSDOM из-за Vuetify form. Если падает не из-за regions, а из-за form fill — упростить (заполнить form ref напрямую или skip с TODO). Главное — first test (count=89) должен работать.

- [ ] **Step A4: Run red tests — confirm fails**

Run:

```bash
cd app && npx vitest run tests/Frontend/NewProjectDialog.spec.ts 2>&1 | tail -10
```

Expected: new 2 tests fail (v-autocomplete не существует — на месте сейчас error banner).

- [ ] **Step A5: Update NewProjectDialog.vue — template**

В [app/resources/js/views/projects/NewProjectDialog.vue](../../../app/resources/js/views/projects/NewProjectDialog.vue) **заменить** блок `<v-alert v-if="generalError">` (interim A placeholder) на:

```vue
<v-autocomplete
    v-model="form.regions"
    :items="selectableRegions"
    item-title="name"
    item-value="code"
    label="Регионы (пусто = вся РФ)"
    multiple chips clearable
    density="comfortable"
    class="ld-input-quiet"
    data-testid="regions-autocomplete"
>
    <template #item="{ props: itemProps, item }">
        <v-list-item v-bind="itemProps">
            <template #subtitle>
                {{ FEDERAL_DISTRICT_NAMES[item.raw.federalDistrict] || '' }}
            </template>
        </v-list-item>
    </template>
</v-autocomplete>

<v-alert
    v-if="generalError"
    type="error"
    variant="tonal"
    density="compact"
    class="mt-3"
    closable
    @click:close="generalError = null"
>
    {{ generalError }}
</v-alert>
```

- [ ] **Step A6: Update NewProjectDialog.vue — script**

В script section [app/resources/js/views/projects/NewProjectDialog.vue](../../../app/resources/js/views/projects/NewProjectDialog.vue):

**Заменить:**

```ts
import { apiClient, ensureCsrfCookie, extractErrorMessage } from '../../api/client';
import type { Project } from '../../stores/projectsStore';
import DevIndexBadge from '../../components/DevIndexBadge.vue';
```

**на:**

```ts
import { apiClient, ensureCsrfCookie, extractErrorMessage } from '../../api/client';
import { REGIONS, FEDERAL_DISTRICT_NAMES } from '../../constants/regions';
import type { Project } from '../../stores/projectsStore';
import DevIndexBadge from '../../components/DevIndexBadge.vue';

const selectableRegions = REGIONS.filter(r => r.code !== 0);
```

В form reactive — **заменить:**

```ts
const form = reactive({
    name: '',
    signal_type: 'site' as 'site' | 'call' | 'sms',
    signal_identifier: '',
    sms_senders: [] as string[],
    sms_keyword: '',
    daily_limit_target: 50,
    region_mask: 255,
    region_mode: 'include' as 'include' | 'exclude',
    delivery_days_mask: 127,
});
```

**на:**

```ts
const form = reactive({
    name: '',
    signal_type: 'site' as 'site' | 'call' | 'sms',
    signal_identifier: '',
    sms_senders: [] as string[],
    sms_keyword: '',
    daily_limit_target: 50,
    regions: [] as number[], // Plan 6: subject codes 1..89; пусто = вся РФ
    delivery_days_mask: 127,
});
```

(`region_mask`/`region_mode` убираются из form; backend dual-writes их сам.)

В блоке `watch(() => props.modelValue, ...)` (`if (open) { ... reset form ... }`) — **заменить:**

```ts
Object.assign(form, {
    name: '',
    signal_type: 'site',
    signal_identifier: '',
    sms_senders: [],
    sms_keyword: '',
    daily_limit_target: 50,
    region_mask: 255,
    region_mode: 'include',
    delivery_days_mask: 127,
});
```

**на:**

```ts
Object.assign(form, {
    name: '',
    signal_type: 'site',
    signal_identifier: '',
    sms_senders: [],
    sms_keyword: '',
    daily_limit_target: 50,
    regions: [],
    delivery_days_mask: 127,
});
```

В edit-mode prefill block (`if (open && props.mode === 'edit' && props.project)`) `Object.assign(form, props.project)` — это автоматически подхватит `regions` если Project type его содержит (Step A2). Не trogue.

- [ ] **Step A7: Run vue-tsc**

Run:

```bash
cd app && npx vue-tsc --noEmit; echo "EXIT=$?"
```

Expected: `EXIT=0`. Если ошибки про missing region_mask/region_mode на edit-prefill — adapt.

- [ ] **Step A8: Run NewProjectDialog spec — confirm green**

Run:

```bash
cd app && npx vitest run tests/Frontend/NewProjectDialog.spec.ts 2>&1 | tail -10
```

Expected: existing 3 tests passed + new 2 tests passed. Если new test #2 (POST payload) brittle — simplified либо skipped TODO. Главное: first test #1 (count=89) — must pass.

### Step block B — ProjectDetailsDrawer (refactor existing tests + impl)

- [ ] **Step B1: Read existing ProjectDetailsDrawer.spec и .vue**

Read full: [app/tests/Frontend/ProjectDetailsDrawer.spec.ts](../../../app/tests/Frontend/ProjectDetailsDrawer.spec.ts) — найти 3 region tests из PDD spec (Section 6). Запомнить структуру.

Read full: [app/components/projects/ProjectDetailsDrawer.vue](../../../app/resources/js/components/projects/ProjectDetailsDrawer.vue) — найти текущую region logic (selectedRegions ref, maskToCodes helper, watch, template v-autocomplete block).

- [ ] **Step B2: Refactor 3 existing region tests**

В [app/tests/Frontend/ProjectDetailsDrawer.spec.ts](../../../app/tests/Frontend/ProjectDetailsDrawer.spec.ts) **заменить** старые region tests на новую модель:

```ts
it('renders region chips for project.regions = [1, 2]', async () => {
    const withRegions: Project = { ...sampleProject, regions: [1, 2] };
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: withRegions } });
    await wrapper.vm.$nextTick();
    const text = wrapper.text();
    expect(text).toContain('Адыгея');         // code 1
    expect(text).toContain('Алтай');          // code 2 (Республика Алтай)
});

it('selecting regions adds to regions array (no bitmask conversion)', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    const autocomplete = wrapper.getComponent({ name: 'VAutocomplete' });
    await autocomplete.vm.$emit('update:model-value', [82, 83]);  // Москва + СПб
    await wrapper.vm.$nextTick();
    (apiClient.patch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        data: { data: sampleProject },
    });
    await wrapper.get('[data-testid="pdd-save"]').trigger('click');
    await wrapper.vm.$nextTick();
    await wrapper.vm.$nextTick();
    expect(apiClient.patch).toHaveBeenCalledWith(
        expect.stringContaining('/api/projects/'),
        expect.objectContaining({ regions: [82, 83] }),
    );
});

it('clearing all regions sets regions=[]', async () => {
    const withRegions: Project = { ...sampleProject, regions: [82, 83] };
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: withRegions } });
    const autocomplete = wrapper.getComponent({ name: 'VAutocomplete' });
    await autocomplete.vm.$emit('update:model-value', []);
    await wrapper.vm.$nextTick();
    (apiClient.patch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
        data: { data: withRegions },
    });
    await wrapper.get('[data-testid="pdd-save"]').trigger('click');
    await wrapper.vm.$nextTick();
    await wrapper.vm.$nextTick();
    expect(apiClient.patch).toHaveBeenCalledWith(
        expect.stringContaining('/api/projects/'),
        expect.objectContaining({ regions: [] }),
    );
});
```

Note: spec mock structure (apiClient vs axios) — adapt to whatever ProjectDetailsDrawer.spec.ts уже использует. Если spec мокает default axios — обновить на apiClient (или продолжать default axios если PDD ещё на default — это часть deferred CSRF cleanup task).

Также `sampleProject` interface — добавить `regions: []` в его default, чтобы не undefined.

- [ ] **Step B3: Run red tests — confirm fails**

Run:

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts 2>&1 | tail -10
```

Expected: 3 refactored tests fail (text contains "Адыгея" — depends on `regions=[1]` not `region_mask=2` decompose; PATCH payload contains `regions: [...]` not `region_mask: ...`).

- [ ] **Step B4: Update ProjectDetailsDrawer.vue — script**

В [app/resources/js/components/projects/ProjectDetailsDrawer.vue](../../../app/resources/js/components/projects/ProjectDetailsDrawer.vue):

**Удалить:**

```ts
const selectedRegions = ref<number[]>([]);
function maskToCodes(mask: number): number[] {
    const codes: number[] = [];
    for (let i = 1; i <= 31; i++) if (mask & (1 << i)) codes.push(i);
    return codes;
}
// inside reseedFromProject:
selectedRegions.value = maskToCodes(p.region_mask ?? 0);
// watch:
watch(selectedRegions, (codes) => { ... mask encode ... });
```

**Добавить (на их место):**

```ts
import { REGIONS, FEDERAL_DISTRICT_NAMES } from '../../constants/regions';

const selectableRegions = REGIONS.filter(r => r.code !== 0);

// form.regions binds directly to v-autocomplete (no encode/decode needed).
// inside reseedFromProject(p):
form.regions = Array.isArray(p.regions) ? [...p.regions] : [];
```

В `form` reactive — убрать `region_mask`/`region_mode`, добавить `regions: [] as number[]`.

В onSave PATCH payload — убедиться что отправляется `regions`, а не `region_mask`/`region_mode`.

- [ ] **Step B5: Update ProjectDetailsDrawer.vue — template**

В template найти существующий `<v-autocomplete v-model="selectedRegions" :items="selectableRegions" ...>` (или подобный). **Заменить:**

```vue
<v-autocomplete
    v-model="form.regions"
    :items="selectableRegions"
    item-title="name"
    item-value="code"
    multiple chips clearable
    density="comfortable"
    hide-details
    data-testid="pdd-regions"
>
    <template #item="{ props: itemProps, item }">
        <v-list-item v-bind="itemProps">
            <template #subtitle>
                {{ FEDERAL_DISTRICT_NAMES[item.raw.federalDistrict] || '' }}
            </template>
        </v-list-item>
    </template>
</v-autocomplete>
```

- [ ] **Step B6: Run tests — confirm green**

Run:

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts 2>&1 | tail -10
```

Expected: refactored 3 tests pass + existing 14 PDD tests still pass.

- [ ] **Step B7: vue-tsc + ESLint + Prettier на изменённых файлах**

Run:

```bash
cd app && npx vue-tsc --noEmit; echo "vue-tsc EXIT=$?"
npx eslint resources/js/views/projects/NewProjectDialog.vue resources/js/components/projects/ProjectDetailsDrawer.vue resources/js/stores/projectsStore.ts resources/js/constants/regions.ts tests/Frontend/NewProjectDialog.spec.ts tests/Frontend/ProjectDetailsDrawer.spec.ts; echo "eslint EXIT=$?"
npx prettier --check resources/js/views/projects/NewProjectDialog.vue resources/js/components/projects/ProjectDetailsDrawer.vue resources/js/stores/projectsStore.ts resources/js/constants/regions.ts tests/Frontend/NewProjectDialog.spec.ts tests/Frontend/ProjectDetailsDrawer.spec.ts; echo "prettier EXIT=$?"
```

Expected: все три EXIT=0.

Если Prettier flags formatting differences — `npx prettier --write <files>` to auto-fix.

- [ ] **Step B8: Commit**

```bash
cd .. && git add app/resources/js/stores/projectsStore.ts app/resources/js/views/projects/NewProjectDialog.vue app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/NewProjectDialog.spec.ts app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "$(cat <<'EOF'
feat(ui): subject-level regions autocomplete in NewProjectDialog + PDD (Plan 6)

- projectsStore: Project.regions?: number[] interface field
- NewProjectDialog: replace interim placeholder with v-autocomplete (89
  subjects + federal district subtitle); form drops region_mask/region_mode
  (backend dual-writes)
- ProjectDetailsDrawer: replace maskToCodes/encode-watch with direct
  form.regions binding; same v-autocomplete component
- Vitest: +2 NewProjectDialog tests (count=89, POST payload includes regions[]);
  refactor 3 existing PDD region tests onto regions[] model

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 6: Final regression sweep + close

**Files:** none modified

- [ ] **Step 1: Full Pest --parallel**

Run:

```bash
cd app && ./vendor/bin/pest --parallel --no-coverage 2>&1 | tail -10
```

Expected: matches baseline +N new tests passed. 0 failed.

Если flake (quirk #72 Redis race / quirk #77 unique-key collision) — retry once. Если consistent fail — investigate via `memory/project_state.md` + pest-parallel-debugger agent.

- [ ] **Step 2: Full Vitest**

Run:

```bash
cd app && npx vitest run 2>&1 | tail -10
```

Expected: 92 files / 758+N passed (N = новые tests из Plan 6) / 3 skipped / 0 failed.

- [ ] **Step 3: vue-tsc + ESLint + Prettier**

Run:

```bash
cd app && npx vue-tsc --noEmit; echo "vue-tsc EXIT=$?"
```

Run ESLint на весь app/resources/js + tests/Frontend:

```bash
cd app && npx eslint resources/js tests/Frontend 2>&1 | tail -5
```

Run Prettier check на same:

```bash
cd app && npx prettier --check resources/js tests/Frontend 2>&1 | tail -5
```

Expected: всё clean.

- [ ] **Step 4: Pre-commit chain (full)**

Run:

```bash
cd .. && lefthook run pre-commit --all-files 2>&1 | tail -30
```

Expected: gitleaks / lychee / larastan / pint / pest — все зелёные. Если pint правит файлы — re-stage и amend (или новый commit "style: pint formatting").

- [ ] **Step 5: Browser smoke (manual, заказчик)**

1. Open `127.0.0.1:8000/projects` → hard reload (Ctrl+F5)
2. Click + СОЗДАТЬ → dialog open
3. Verify «Регионы» field visible с placeholder «Регионы (пусто = вся РФ)»
4. Click in field → dropdown shows 89 entries с subtitle федеральных округов
5. Search «Москва» → filters down to «Москва» (code 82)
6. Select Москва + СПб → 2 chips
7. Fill name «test-regions», domain «test.ru» → СОЗДАТЬ
8. Verify project appears in список без ошибок
9. Open created project's card (PDD) → 2 chips visible
10. Edit chips → save → reopen → актуальное состояние

- [ ] **Step 6: Close summary**

Write summary в conversation:

- Plan 6 completed in N commits (range `<first>..<last>`)
- Test counts: Pest baseline N → N+7 (5 backend + 2 outbound); Vitest baseline N → N+2 (frontend; 3 existing PDD refactored)
- Schema baseline: 117 → 118 indexes
- Memory updates needed: project_state.md (commit range + counts), reference_archive.md (spec/plan version)
- Open Q closed: PDD regions feature (4f60add..f982046) — реализован в правильной модели
- Plan 6.5 stub: created в [docs/Открытые_вопросы_v8_3.md](../../../docs/Открытые_вопросы_v8_3.md) либо отдельной сессией

- [ ] **Step 7: Verify before completion**

Per economy 0% / Pravila §12: invoke `superpowers:verification-before-completion` skill **перед** любыми claims «готово/closed/merged/passed». Run verification commands fresh в финальном message.

---

## Self-Review

**1. Spec coverage:**

| Spec section | Task covering |
|---|---|
| §1 Цель и scope | Все tasks |
| §2 Approach 2 dual-write | Task 1 (schema preserves region_mask) + Task 3 (ProjectService) |
| §3 Schema delta v8.20 | Task 1 |
| §4 REGIONS 31→89 | Task 2 |
| §5 Backend validation | Task 3 |
| §6 UI NewProjectDialog | Task 5 block A |
| §6.2 ProjectDetailsDrawer | Task 5 block B |
| §6.3 Project type | Task 5 step A2 |
| §7 Outbound adapter | Task 4 |
| §8 Data flow | implicit через tasks 3+4 |
| §9 Migration & backfill | Task 1 (DEFAULT '{}') |
| §10 Тесты | Tasks 3 (5 Pest) + 4 (2 Pest) + 5 (2+3=5 Vitest) |
| §11 Error handling | Task 3 (validation) |
| §12 Assumptions | Plan inherits ASSUMPTIONS verbatim |
| §13 Out of scope | Plan не trogue PhonePrefixService/LeadRouter |
| §14 Связанные | Plan ссылается на spec |

Все spec §§ покрыты. ✅

**2. Placeholder scan:**

- Task 4 Step 2: «adapter pattern; if private adaptProjectsForAllocator — use Closure binding» — это **conditional instruction**, не placeholder. Implementer выбирает path based on existing test pattern.
- Task 5 A3 Step: «Better: target by parent label — adapt based on what works in existing tests» — это similar conditional. Acceptable.
- Task 5 B2: «adapt to whatever ProjectDetailsDrawer.spec.ts уже использует» — conditional on existing state.
- Нет `TBD`/`TODO`/«implement later».

**3. Type consistency:**

- `Project.regions?: number[]` — Step A2 — consistent с Task 3 cast 'array' (PHP) / Step B1 frontend
- `selectableRegions = REGIONS.filter(r => r.code !== 0)` — consistent в Task 5 A6 и B4
- `FEDERAL_DISTRICT_NAMES[item.raw.federalDistrict]` — consistent с Task 2 export
- Test naming consistent: `passes regions directly...`, `dual-writes region_mask=255...`
- ProjectService property: `$data['regions']` consistent с request rule key `regions`
- Backend ↔ Frontend payload key: `regions` (array) — consistent everywhere

Все типы и сигнатуры consistent. ✅

---

## Execution Handoff

**Plan complete and saved to `docs/superpowers/plans/2026-05-14-plan-6-regions-subject-level.md`. Two execution options:**

**1. Subagent-Driven (recommended)** — dispatch fresh subagent per task, review между tasks, fast iteration. Особенно подходит для tasks 3 и 5 (большие — backend+5tests и frontend+5tests), где можно distribute.

**2. Inline Execution** — execute tasks в this session через executing-plans skill, batch execution с checkpoints для review.

**Which approach?**
