Compare commits

...

37 Commits

Author SHA1 Message Date
Дмитрий 05706ef429 @
docs(a3): cspell-words.txt +ребейз-family

ребейз/ребейзнута/ребейзом — слова из CLAUDE.md §6/§9 и spec §7
(описание ребейза feat/a3 на origin/main). Единственные реально новые
термины A3-нормативки по cspell-прогону добавленных строк.

Task 10 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 16:06:34 +03:00
Дмитрий 35b48c1b0c @
docs(a3): CLAUDE.md v2.9 — register #47 openapi-mcp-server (A3 integration-tooling)

§3 title 46→47; §3.3 +строка #47 openapi-mcp-server; §1 row 2b 46→47;
§3.3 footer 46→47 + integration-tooling 9-я off-phase подкатегория
(17 off-phase); §0 cross-refs Pravila v1.23 / PSR_v1 v3.9 / Tooling v2.9;
§6 +абзац A3; §9 +запись. Шапка v2.8→v2.9.

Через /claude-md-management:claude-md-improver (§5 п.10).
Task 9 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 16:00:52 +03:00
Дмитрий 046c8b6efa @
docs(a3): Pravila v1.23 — §13.2 +Off-phase integration-tooling

§13.2 +абзац «Off-phase integration-tooling»: #47 openapi-mcp-server
(Tooling §4.22) + api-docs agent (узел карты A3 без Tooling-номера).
Не UI → вне R6/R14. Регулируются PSR_v1 R10.1 Блок 3. v1.22→v1.23.

Task 8 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:54:52 +03:00
Дмитрий fc5f58a992 @
docs(a3): PSR_v1 v3.9 — R10.1 Блок 3 +openapi-mcp (integration-tooling)

R10.1 Блок 3 (MCP-серверы) +1 строка openapi-mcp-server — категория
integration-tooling, off-phase, раздел A3. Не UI → вне R6/R14.
Tooling §4.22 #47. Версия v3.8→v3.9.

Task 7 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:50:02 +03:00
Дмитрий b51d5fb31d @
docs(a3): Tooling Прил. Н v2.9 — register #47 openapi-mcp-server (§4.22)

§4.22 — openapi-mcp-server (@ivotoby/openapi-mcp-server v1.14.0, MIT),
9-я off-phase подкатегория integration-tooling. §0 счётчик 46→47
(17 off-phase, 67 total). Парный узел карты — api-docs agent (без
Tooling-номера). Статус: verified.

Task 6 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:47:07 +03:00
Дмитрий 10b19df1c4 @
feat(map): A3 nodes — api-docs agent + openapi MCP

2 новых узла раздела A3 «Программирование — интеграции»: ag_apidocs
(api-docs agent, claude-flow) + mcp_openapi (openapi MCP, #47). NODES /
NODE_SECTION / NODE_DETAILS nd() / NODE_TIMELINE / EDGES (3 ребра).
pos()-углы 4/175 + 5/5 после Grep-проверки коллизий. Счётчик 116→118.

Task 4 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:39:30 +03:00
Дмитрий df4532d2fd @
feat(map): NODE_SECTION_SECONDARY layer — cross-ref nodes into A3

Аддитивный слой NODE_SECTION_SECONDARY (NODE_SECTION 1:1 не трогается):
кросс-реф mcp_boost/context7/ag_pest/mcp_semgrep/mcp_sentry в раздел A3.
SECTION_NODES build + Паспорт «Раздел» (формат «A1 (+A3)») обновлены.

Task 3 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:34:41 +03:00
Дмитрий d85b9391cc @
docs(a3): re-baseline spec+plan onto origin/main 1313d89

feat/a3 ребейзнута на актуальный origin/main (был форк от D3-эры).
C9/deptrac/A4 уже влиты → openapi-mcp #41→#47, Tooling §4.16→§4.22,
integration-tooling 7-я→9-я off-phase подкатегория. Версии:
Tooling v2.8→v2.9, PSR_v1 v3.8→v3.9, Pravila v1.22→v1.23, CLAUDE.md
v2.8→v2.9. Карта 116→118 узлов. Stale line-anchors → Grep-by-symbol.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:32:03 +03:00
Дмитрий 2018959fdc @
feat(a3): register openapi-mcp-server in .mcp.json

openapi MCP server (@ivotoby/openapi-mcp-server v1.14.0, MIT, stdio) —
отдаёт docs/api/openapi.yaml как MCP-ресурс/тулы. Smoke verified
(npx --help, native-Windows OK). Конфиг — env-vars API_BASE_URL +
OPENAPI_SPEC_PATH (README stdio-форма).

Task 2 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:20:43 +03:00
Дмитрий ff3979d527 @
docs(a3): OpenAPI skeleton for /api/deals — A3 smoke artifact

Стартовый OpenAPI 3.1 скелет для группы /api/deals* (8 эндпоинтов)
как smoke-доказательство api-docs-тулинга. Redocly lint — valid (exit 0,
2 warning о неполноте, ожидаемо для скелета). Не полная спека API.

Task 1 плана A3 integration-tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:19:19 +03:00
Дмитрий 756a8838d6 @
docs(a3): A3 integration-tooling implementation plan

10 задач: api-docs smoke → openapi-mcp install → карта (NODE_SECTION_SECONDARY
слой + 2 узла) → нормативка (Tooling/PSR_v1/Pravila/CLAUDE.md) → регрессия+память.

Точные якоря карты: NODE_SECTION (110 узлов), SECTION_NODES build (1973-1977),
ld-section (2082-2083), форматы ag_pest/mcp_boost. Риск кросс-веточной
нумерации с A11/C9 — Task 10 Step 2.

cspell-words.txt +redocly/ivotoby. Через superpowers:writing-plans.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:19:19 +03:00
Дмитрий a319e4f98a @
docs(a3): A3 integration-tooling design spec

Дизайн интеграции раздела A3 «Программирование — интеграции (API, вебхуки)»
карты automation-graph.html — параллельно A6/D3.

- 2 новых узла: api-docs agent (claude-flow, 0-install) + openapi-mcp-server
  (npm/stdio MCP, Tooling #41 §4.16)
- 5 кросс-реф узлов через новый аддитивный слой NODE_SECTION_SECONDARY
  (context7/Boost/Pest/Semgrep/Sentry — NODE_SECTION 1:1 не ломается)
- Нормативка: Tooling v2.5, PSR_v1 v3.5, Pravila v1.19, CLAUDE.md v2.5
- Риск кросс-веточной нумерации с A11/C9 зафиксирован

cspell-words.txt +4 валидных термина (аудировал/JVM/хендлеров/ivo).
Через superpowers:brainstorming (2 развилки сняты с заказчиком).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-17 15:19:19 +03:00
Дмитрий 1313d89525 docs(a4): add A4 design-tooling integration plan
The 8-task plan executed for the A4 epic, with the post-flight Plan Correction block (FM2 defer, #44-46 numbering, ADR-006, knowledge-work-plugins marketplace, /plugin unavailable in VSCode-extension env).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:07:26 +03:00
Дмитрий bcce4d9986 feat(a4): register Universal Icons MCP #45 in .mcp.json
Off-phase A4 design-tooling. Smoke verified post-reload: health_check OK, get_icon home/lucide returns a valid framework-neutral SVG. Comment ADR ref corrected ADR-004 -> ADR-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:07:25 +03:00
Дмитрий a718bb951f fix(a4): correct #46 Design plugin marketplace -> knowledge-work-plugins
The 'design' plugin lives in anthropics/knowledge-work-plugins (same marketplace as #42 product-management), not claude-plugins-official (which carries only frontend-design). Verified post-reload against the marketplace manifest. Pre-push fixup of 621498a's own error - v2.8/v3.8/v2.8 unchanged. Tooling 4.21 also completes the capability list (+Design System Management, +Dev Handoff).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:07:15 +03:00
Дмитрий 621498acc9 docs(a4): register #44-46 design-tooling — Tooling v2.8 / PSR_v1 v3.8 / Pravila v1.22 / CLAUDE.md v2.8 2026-05-17 13:03:58 +03:00
Дмитрий cafa8dfe2d fix(map): mcp_figma/mcp_icons → R10.1 блок 3 (MCP-серверы, не блок 1) 2026-05-17 12:40:49 +03:00
Дмитрий 8d9183c3ac feat(map): add mcp_figma/mcp_icons/design_plugin nodes — closes section A4 (3→6) 2026-05-17 12:35:00 +03:00
Дмитрий 0cea2cc320 docs(adr): ADR-006 — A4 design-tooling boundaries (FM1/DP1/DP2) 2026-05-17 12:29:09 +03:00
Дмитрий 9b63e27825 feat(map): deptrac node — extends section A6 to 4 nodes
automation-graph.html — new `deptrac` node (architecture-tooling),
NODE_SECTION → A6 (раздел «Архитектура систем» 3→4 узла), edge
psr_v1→deptrac, NODE_DETAILS + NODE_META entries. Smoke-tested:
113 nodes / 118 edges, 0 JS errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:32:37 +03:00
Дмитрий 0c98524357 docs(deptrac): register #43 deptrac architecture-tooling in 4 normative files
Tooling Прил. Н v2.6→v2.7 (§4.18 new, §0 counter 42→43, off-phase
+12→+13; footer v2.6 row restored — pre-existing C9 gap); PSR_v1
v3.6→v3.7 (R10.1 Блок 1 note — deptrac is a Composer dev-dep, not a
marketplace plugin, like mermaid-skill/CCPM); Pravila v1.20→v1.21
(§13.2 architecture-tooling para +deptrac); CLAUDE.md v2.6→v2.7
(§3 title, §1 row 2b, §3.3 +#43 row, §6 +para, §9 +entry, §0
cross-refs) via /claude-md-management:claude-md-improver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:28:08 +03:00
Дмитрий 431117087f docs(arch): code-derived C4 component-layer diagram from deptrac (gap 4)
docs/architecture/c4-component-layers.md — the Level-3 layer
dependency graph generated by `deptrac analyse --formatter=mermaidjs`
(code-derived, drift-proof). Closes the A6 «C4 drift» gap at the
component level. README diagram index + regenerate note updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:15:34 +03:00
Дмитрий 5deff727a4 ci(deptrac): wire deptrac as lefthook pre-commit job 10
Job 10 runs `deptrac analyse` (root: app/) when staged app/**/*.php
changes — the layer-dependency gate. Red-green verified: a
Model→Service dependency is flagged (DependsOnDisallowedLayer,
exit 1); a clean tree exits 0. app/.gitignore += /.deptrac.cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:13:27 +03:00
Дмитрий 554b59359c feat(deptrac): layer model + ruleset config + ADR-005
app/deptrac.yaml — 13 layers (Controller/Service/Model/Job/…),
conservative ruleset enforcing inward/upward-violating directions.
First `deptrac analyse`: 0 violations / 481 allowed / 977 uncovered
— the codebase already conforms, so no baseline file is needed.
ADR-005 records the decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:09:24 +03:00
Дмитрий 507c4d869a docs(plan): deptrac architecture-fitness integration plan
9-task plan closing the 4 open A6 architecture-fitness gaps
(conformance, layer-direction, C4 drift, active design) via
deptrac as a lefthook job-10 layer-dependency gate. + cspell vocab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:07:09 +03:00
Дмитрий f9bedb6aad build(deptrac): add deptrac 4.6.1 as a composer dev-dependency (DT1)
deptrac/deptrac ^4.6 + 5 transitive deps (symfony/config,
dependency-injection, var-exporter; phpdocumentor/graphviz;
jetbrains/phpstorm-stubs). Primary DT1 path — composer dev-dep;
no PHAR fallback needed (resolver clean, 0 security advisories).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:04:45 +03:00
Дмитрий 88eac07116 merge: origin/main (automation-graph C9 map + project-management tooling) в C9-интеграцию 2026-05-17 10:13:02 +03:00
Дмитрий b1e903f31a fix(projects): C9 code-review findings — ProjectResource отдаёт regions[] + покрытие
C1: ProjectResource не возвращал regions → edit-диалог/drawer затирали
    сохранённые регионы при сохранении. +поле в toArray().
C2: +integration-тест outbound regions[] через полный SyncSupplierProjectsJob::handle().
I1: расскип NewProjectDialog payload-теста (regions в POST).
I2: assert data.regions в ProjectsStore/UpdateTest (ловит C1 на backend-уровне).
I4: docblock — bulkUpdateRegions legacy (region_mask, не влияет на outbound до Plan 6.5).
M1: CHANGELOG v8.22 — исправлен неверный пример регионов (Москва=82).

Регрессия: Pest 905/902/3sk/0, Vitest 104f/884/3sk/0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:05:32 +03:00
Дмитрий ec6ebc57e0 merge: C9 — Plan 6 регионы субъект-уровня в портал
# Conflicts:
#	app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
#	db/CHANGELOG_schema.md
#	db/schema.sql
2026-05-17 09:30:21 +03:00
Дмитрий fad1c895a1 merge: Sprint 3E (D6/D7 — убрать placeholder-вкладки SettingsView) в портал 2026-05-17 09:03:21 +03:00
Дмитрий 7b04e7e752 feat(settings): D6/D7 — убрать placeholder-вкладки SettingsView
Audit findings D6/D7 (Sprint 3E): убраны 4 placeholder-вкладки
(Проекты/Команда/Интеграции/Тихие часы) из SettingsView — UI не должен
обещать неработающий функционал. Удалён PlaceholderTab.vue. Остались
4 рабочие вкладки: Профиль, Безопасность, API и Webhook, Уведомления.
Тесты: 8/8 SettingsView.spec.ts ✓, Vitest 100f/838/3sk/0 ✓.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 14:25:42 +03:00
Дмитрий 822e5346d8 docs(plan): Sprint 3E — Settings placeholder-tabs (D6/D7) 2026-05-16 14:21:56 +03:00
Дмитрий 4bdb996c6c feat(ui): subject-level regions autocomplete in NewProjectDialog + PDD (Plan 6 Task 5)
- projectsStore: Project.regions?: number[] interface field
- NewProjectDialog: replace interim placeholder с v-autocomplete (89
  subjects + federal district subtitle); form drops region_mask/region_mode
  (backend dual-writes)
- ProjectDetailsDrawer: replace maskToCodes/encode-watch с direct
  form.regions binding; same v-autocomplete component
- Vitest: +2 NewProjectDialog tests (count=89, POST payload includes regions[]);
  refactor 3 existing PDD region tests на regions[] model

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 05:54:05 +03:00
Дмитрий 830e7fc3d7 feat(supplier): outbound adapter direct-copy regions[] (Plan 6 Task 4)
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>
2026-05-15 05:43:49 +03:00
Дмитрий c1ecefafc0 feat(projects): backend support for subject-level regions array (Plan 6 Task 3)
- Project model: +regions in fillable + cast via PostgresIntArray
  (custom Eloquent cast for PG INT[] — Laravel stock 'array' uses JSON
  which Postgres rejects on native INT[] columns)
- StoreProjectRequest / UpdateProjectRequest: drop region_mask/mode rules,
  add regions array validation (1..89 each, present/sometimes)
- ProjectService::create: dual-write — regions источник истины + legacy
  region_mask=255 + region_mode='include' для PhonePrefixService/LeadRouter
  compatibility (Plan 6.5 cleanup will remove dual-write)
- +5 Pest tests covering create/update/dual-write/validation rejection
- Drive-by: SchemaDeltaTest indexes pin 117 → 118 (Plan 6 v8.20 carryover
  from Task 1; should ideally have landed in Task 1 commit c487641)
- phpstan-baseline: +3 entries for Project::$regions until next ide-helper
  regen; existing Pest actingAs counts bumped 9→12 / 6→8 for new tests

Verified: Pest --parallel 747/744/3sk/0/0 (5 new tests pass +
SchemaDeltaTest now green), phpstan 0 errors, pint clean, gitleaks 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 05:39:43 +03:00
Дмитрий f467409baf 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>
2026-05-15 05:01:12 +03:00
Дмитрий c4876410ea 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>
2026-05-15 04:52:19 +03:00
46 changed files with 4516 additions and 279 deletions
+14
View File
@@ -43,6 +43,20 @@
"command": "npx",
"args": ["-y", "ruflo@latest", "mcp", "start"],
"comment": "Off-phase orchestration MCP — exposes ~210 ruflo tools (Core/Intelligence/Agents/Memory/DevTools). Package: ruflo v3.7.0-alpha.38+ MIT (npm `ruflo`, repo ruvnet/claude-flow legacy after rename Jan-2026; plugin namespace @claude-flow/*). Plugin discovery via IPFS (CID QmeXmAdbWVvT84GfDXPD2Vg1HWhiTW2VdZfRLhkS96KkX2) — Pinata+Cloudflare gateways flaky 2026-05-15, only ipfs.io reliable. stdio mode (no port-conflict). Big-bang integration per spec/plan 2026-05-15-ruflo-integration-design.md (commit a68a0a0+). Pending формализация в Tooling §4.10 — Phase 3 Task 3.4."
},
"universal-icons": {
"command": "npx",
"args": ["-y", "mcp-universal-icons"],
"comment": "Off-phase A4 design-tooling #45 — Universal Icons MCP (npm mcp-universal-icons, awssat, MIT). Поиск/вставка SVG-иконок из 10 коллекций, включая Lucide (проектный icon-set, CTO-19). Tools: search_icons / get_icon / health_check. SVG framework-neutral по умолчанию — НЕ запрашивать jsx/Tailwind-формат (PSR_v1 R6.0). Формализация — Tooling §4.20. ADR-006 граница UI2: иконки UI; бренд-логотипы — за 21st logo_search. План docs/superpowers/plans/2026-05-17-a4-design-tooling-integration.md."
},
"openapi": {
"command": "npx",
"args": ["-y", "@ivotoby/openapi-mcp-server"],
"env": {
"API_BASE_URL": "http://localhost",
"OPENAPI_SPEC_PATH": "./docs/api/openapi.yaml"
},
"comment": "A3 integration-tooling #47 — OpenAPI MCP (ivo-toby/mcp-openapi-server, @ivotoby/openapi-mcp-server v1.14.0, MIT). Exposes Лидерра REST API endpoints (docs/api/openapi.yaml) as MCP tools. Config via env-vars API_BASE_URL + OPENAPI_SPEC_PATH (stdio transport default). READ scope: API discovery/introspection for Claude Code. Формализован в Tooling §4.22, PSR_v1 R10.1 блок 3, Pravila §13.2."
}
}
}
+24 -7
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -6,6 +6,7 @@
.env.production
.phpactor.json
.phpunit.result.cache
/.deptrac.cache
/.codex
/.cursor/
/.idea
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
/**
* Eloquent cast for PostgreSQL native INT[] columns.
*
* Laravel stock 'array' cast uses json_encode/json_decode and sends `[1,2,3]`
* (JSON), which Postgres rejects on INT[] columns (expects `{1,2,3}` array
* literal). This cast:
*
* - get(): parses Postgres array literal `{1,2,3}` (or empty `{}`) into PHP
* int array.
* - set(): serializes PHP array `[1,2,3]` into Postgres literal `{1,2,3}`.
*
* Used for projects.regions INT[] (Plan 6).
*
* @implements CastsAttributes<list<int>, list<int>|null>
*/
class PostgresIntArray implements CastsAttributes
{
/**
* @param array<string, mixed> $attributes
* @return list<int>
*/
public function get(Model $model, string $key, mixed $value, array $attributes): array
{
if ($value === null || $value === '' || $value === '{}') {
return [];
}
// PG returns literal like "{1,2,3}".
if (is_string($value)) {
$trimmed = trim($value, '{}');
if ($trimmed === '') {
return [];
}
return array_map('intval', explode(',', $trimmed));
}
// Defensive: if driver already gave array.
if (is_array($value)) {
return array_values(array_map('intval', $value));
}
return [];
}
/**
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
{
if ($value === null) {
return null;
}
// Defensive: interface phpdoc says list<int>|null, but $value is mixed at PHP level;
// protect against runtime misuse (e.g., string passed mistakenly).
// @phpstan-ignore function.alreadyNarrowedType
if (! is_array($value)) {
throw new \InvalidArgumentException(
"PostgresIntArray cast expects array for key '{$key}', got ".gettype($value)
);
}
if ($value === []) {
return '{}';
}
$ints = array_map('intval', $value);
return '{'.implode(',', $ints).'}';
}
}
@@ -22,8 +22,11 @@ class StoreProjectRequest extends FormRequest
'name' => ['required', 'string', 'max:255'],
'signal_type' => ['required', Rule::in(['site', 'call', 'sms'])],
'daily_limit_target' => ['required', 'integer', 'min:1', 'max:10000'],
'region_mask' => ['required', 'integer', 'min:0'],
'region_mode' => ['required', Rule::in(['include', 'exclude'])],
// Plan 6: subject-level regions[] заменил region_mask/region_mode на API-уровне.
// Empty array = "вся РФ" (паритет с legacy region_mask=255 + region_mode='include').
// present = поле должно быть в payload (даже если []), enforces explicit choice.
'regions' => ['present', 'array'],
'regions.*' => ['integer', 'between:1,89'],
'delivery_days_mask' => ['required', 'integer', 'min:1', 'max:127'],
];
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateProjectRequest extends FormRequest
{
@@ -20,8 +19,10 @@ class UpdateProjectRequest extends FormRequest
return [
'name' => ['sometimes', 'string', 'max:255'],
'daily_limit_target' => ['sometimes', 'integer', 'min:1', 'max:10000'],
'region_mask' => ['sometimes', 'integer', 'min:0'],
'region_mode' => ['sometimes', Rule::in(['include', 'exclude'])],
// Plan 6: subject-level regions[] заменил region_mask/region_mode на API-уровне.
// sometimes = поле omit-able (preserves prior DB value), массив + each 1..89.
'regions' => ['sometimes', 'array'],
'regions.*' => ['integer', 'between:1,89'],
'delivery_days_mask' => ['sometimes', 'integer', 'min:1', 'max:127'],
'sms_senders' => ['sometimes', 'array', 'min:1'],
'sms_senders.*' => ['string', 'max:11'],
@@ -31,6 +31,7 @@ class ProjectResource extends JsonResource
'archived_at' => $project->archived_at?->toIso8601String(),
'region_mask' => $this->region_mask,
'region_mode' => $this->region_mode,
'regions' => $this->regions,
'delivery_days_mask' => $this->delivery_days_mask,
'sync_status' => $this->aggregateSyncStatus(),
'last_synced_at' => $this->aggregateLastSyncedAt(),
@@ -207,7 +207,7 @@ class SyncSupplierProjectsJob implements ShouldQueue
* Маппинг:
* daily_limit daily_limit_target
* workdays биты delivery_days_mask (bit 0=Пн, , bit 6=Вс) ISO 1..7
* regions биты region_mask (bit 0=Центральный, , bit 7=Дальневосточный) 1..8
* regions projects.regions INT[] (subject codes 1..89) direct copy
*
* @param EloquentCollection<int, Project> $projects
* @return Collection<int, stdClass>
@@ -219,12 +219,11 @@ class SyncSupplierProjectsJob implements ShouldQueue
$obj->daily_limit = (int) $p->daily_limit_target;
$obj->workdays = $this->bitmaskToList((int) $p->delivery_days_mask, 7);
// region_mask=255 (все 8 ФО, default) — catch-all семантика → пустой массив
// у supplier ("без региональных ограничений"). Иначе — список выставленных битов.
$regionMask = (int) $p->region_mask;
$obj->regions = $regionMask === 255
? []
: $this->bitmaskToList($regionMask, 8);
// 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);
return $obj;
})->values();
+8
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models;
use App\Casts\PostgresIntArray;
use Carbon\CarbonInterface;
use Database\Factories\ProjectFactory;
use Illuminate\Database\Eloquent\Builder;
@@ -45,6 +46,9 @@ class Project extends Model
'effective_limit_calculated_at',
'region_mask',
'region_mode',
// Plan 6 (schema v8.20): Subject-level regions array (89 codes из resources/js/constants/regions.ts).
// Источник истины с Plan 6+; region_mask/region_mode — DEPRECATED (Plan 6.5 cleanup).
'regions',
'delivery_days_mask',
'assignment_strategy',
'ttfr_target_minutes',
@@ -69,6 +73,10 @@ class Project extends Model
'daily_limit_target' => 'integer',
'effective_daily_limit_today' => 'integer',
'region_mask' => 'integer',
// Plan 6: Subject-level regions array (89 codes). Используется кастомный
// PostgresIntArray cast — Laravel stock 'array' посылает JSON `[1,2,3]`,
// что Postgres отвергает на INT[] (ожидает literal `{1,2,3}`).
'regions' => PostgresIntArray::class,
'delivery_days_mask' => 'integer',
'ttfr_target_minutes' => 'integer',
'effective_limit_calculated_at' => 'datetime',
@@ -114,6 +114,13 @@ class ProjectService
return ['updated' => $updated, 'skipped' => [], 'warnings' => []];
}
/**
* LEGACY (Plan 6): обновляет только bitmask `region_mask` федеральных округов.
* После Plan 6 источник истины региональной фильтрации `regions` INT[];
* outbound SyncSupplierProjectsJob читает `regions[]`, НЕ `region_mask`. Значит
* этот bulk-action на реальную фильтрацию у поставщика не влияет. Субъект-уровневый
* bulk-edit `regions[]` запланирован в Plan 6.5 (spec §13 out of scope C9).
*/
private function bulkUpdateRegions($query, array $payload): array
{
$add = (int) ($payload['add'] ?? 0);
@@ -191,6 +198,11 @@ class ProjectService
$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);
+1
View File
@@ -17,6 +17,7 @@
},
"require-dev": {
"barryvdh/laravel-ide-helper": "*",
"deptrac/deptrac": "^4.6",
"fakerphp/faker": "^1.23",
"infection/infection": "^0.32.7",
"larastan/larastan": "*",
+427 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f6418ddc96f575de868a519b516c26d8",
"content-hash": "b859d747b77450b0917b3a7ae30284aa",
"packages": [
{
"name": "brick/math",
@@ -7279,6 +7279,91 @@
],
"time": "2024-05-06T16:37:16+00:00"
},
{
"name": "deptrac/deptrac",
"version": "4.6.1",
"source": {
"type": "git",
"url": "https://github.com/deptrac/deptrac.git",
"reference": "6ff20dec210f119a4ddebdf8e28603689f34eb67"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/deptrac/deptrac/zipball/6ff20dec210f119a4ddebdf8e28603689f34eb67",
"reference": "6ff20dec210f119a4ddebdf8e28603689f34eb67",
"shasum": ""
},
"require": {
"composer/xdebug-handler": "^3.0",
"jetbrains/phpstorm-stubs": "2024.3 || 2025.3 || 2026.1",
"nikic/php-parser": "^5",
"php": "^8.2",
"phpdocumentor/graphviz": "^2.1",
"phpdocumentor/type-resolver": "^1.9.0 || ^2.0.0",
"phpstan/phpdoc-parser": "^1.5.0 || ^2.1.0",
"phpstan/phpstan": "^2.0",
"psr/container": "^2.0",
"psr/event-dispatcher": "^1.0",
"symfony/config": "^6.4 || ^7.4 || ^8.0",
"symfony/console": "^6.4 || ^7.4 || ^8.0",
"symfony/dependency-injection": "^6.4 || ^7.4 || ^8.0",
"symfony/event-dispatcher": "^6.4 || ^7.4 || ^8.0",
"symfony/event-dispatcher-contracts": "^3.4",
"symfony/filesystem": "^6.4 || ^7.4 || ^8.0",
"symfony/finder": "^6.4 || ^7.4 || ^8.0",
"symfony/yaml": "^6.4 || ^7.4 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
"ergebnis/composer-normalize": "^2.45",
"ext-libxml": "*",
"symfony/stopwatch": "^6.4 || ^7.4 || ^8.0"
},
"suggest": {
"ext-dom": "For using the JUnit output formatter"
},
"bin": [
"deptrac"
],
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": false,
"forward-command": true,
"target-directory": "tools"
}
},
"autoload": {
"psr-4": {
"Deptrac\\Deptrac\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Tim Glabisch"
},
{
"name": "Simon Mönch"
},
{
"name": "Denis Brumann"
}
],
"description": "Deptrac is a static code analysis tool that helps to enforce rules for dependencies between software layers.",
"keywords": [
"dev",
"static analysis"
],
"support": {
"issues": "https://github.com/deptrac/deptrac/issues",
"source": "https://github.com/deptrac/deptrac/tree/4.6.1"
},
"time": "2026-05-13T08:23:06+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.6",
@@ -8042,6 +8127,50 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "jetbrains/phpstorm-stubs",
"version": "v2026.1",
"source": {
"type": "git",
"url": "https://github.com/JetBrains/phpstorm-stubs",
"reference": "2cdd054c4109dfb76667c9198bf9427606354243"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/2cdd054c4109dfb76667c9198bf9427606354243",
"reference": "2cdd054c4109dfb76667c9198bf9427606354243",
"shasum": ""
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^v3.86",
"nikic/php-parser": "^v5.6",
"phpdocumentor/reflection-docblock": "^5.6",
"phpunit/phpunit": "^12.3"
},
"type": "library",
"autoload": {
"files": [
"PhpStormStubsMap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "PHP runtime & extensions header files for PhpStorm",
"homepage": "https://www.jetbrains.com/phpstorm",
"keywords": [
"autocomplete",
"code",
"inference",
"inspection",
"jetbrains",
"phpstorm",
"stubs",
"type"
],
"time": "2026-02-19T20:12:01+00:00"
},
{
"name": "justinrainbow/json-schema",
"version": "6.8.2",
@@ -9674,6 +9803,59 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "phpdocumentor/graphviz",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/GraphViz.git",
"reference": "115999dc7f31f2392645aa825a94a6b165e1cedf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/GraphViz/zipball/115999dc7f31f2392645aa825a94a6b165e1cedf",
"reference": "115999dc7f31f2392645aa825a94a6b165e1cedf",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"ext-simplexml": "*",
"mockery/mockery": "^1.2",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^8.2 || ^9.2",
"psalm/phar": "^4.15"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\GraphViz\\": "src/phpDocumentor/GraphViz",
"phpDocumentor\\GraphViz\\PHPStan\\": "./src/phpDocumentor/PHPStan"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"description": "Wrapper for Graphviz",
"support": {
"issues": "https://github.com/phpDocumentor/GraphViz/issues",
"source": "https://github.com/phpDocumentor/GraphViz/tree/2.1.0"
},
"time": "2021-12-13T19:03:21+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
@@ -12674,6 +12856,169 @@
],
"time": "2024-10-20T05:08:20+00:00"
},
{
"name": "symfony/config",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-03T14:20:49+00:00"
},
{
"name": "symfony/dependency-injection",
"version": "v7.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d",
"reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/service-contracts": "^3.6",
"symfony/var-exporter": "^6.4.20|^7.2.5|^8.0"
},
"conflict": {
"ext-psr": "<1.1|>=2",
"symfony/config": "<6.4",
"symfony/finder": "<6.4",
"symfony/yaml": "<6.4"
},
"provide": {
"psr/container-implementation": "1.1|2.0",
"symfony/service-implementation": "1.1|2.0|3.0"
},
"require-dev": {
"symfony/config": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\DependencyInjection\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.10"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-06T11:55:30+00:00"
},
{
"name": "symfony/filesystem",
"version": "v7.4.9",
@@ -12744,6 +13089,87 @@
],
"time": "2026-04-18T13:18:21+00:00"
},
{
"name": "symfony/var-exporter",
"version": "v7.4.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "22e03a49c95ef054a43601cd159b222bfab1c701"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701",
"reference": "22e03a49c95ef054a43601cd159b222bfab1c701",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\VarExporter\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
"homepage": "https://symfony.com",
"keywords": [
"clone",
"construct",
"export",
"hydrate",
"instantiate",
"lazy-loading",
"proxy",
"serialize"
],
"support": {
"source": "https://github.com/symfony/var-exporter/tree/v7.4.9"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-18T13:18:21+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.4.10",
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Plan 6 (C9) subject-level regions.
*
* +1 колонка projects.regions INT[] (1..89 коды субъектов РФ; пустой массив = вся РФ).
* +1 GIN-индекс idx_projects_regions для outbound regions queries.
* region_mask/region_mode остаются (dual-write) удаление в Plan 6.5.
*
* Guard'ы: migrate:fresh грузит schema.sql v8.22 (где delta уже есть) до миграций,
* поэтому каждый кусок применяется только при отсутствии (как Sprint 4 миграция).
*/
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasColumn('projects', 'regions')) {
DB::statement("ALTER TABLE projects ADD COLUMN regions INT[] NOT NULL DEFAULT '{}'::INT[]");
}
DB::statement('CREATE INDEX IF NOT EXISTS idx_projects_regions ON projects USING GIN (regions)');
DB::statement(
'COMMENT ON COLUMN projects.regions IS '
."'Subject-level region filter (1..89 коды субъектов РФ). Пустой массив = вся РФ. Plan 6 (v8.22).'"
);
}
public function down(): void
{
DB::statement('DROP INDEX IF EXISTS idx_projects_regions');
if (Schema::hasColumn('projects', 'regions')) {
Schema::table('projects', fn ($table) => $table->dropColumn('regions'));
}
}
};
+46
View File
@@ -0,0 +1,46 @@
deptrac:
paths:
- ./app
layers:
- name: Controller
collectors: [{ type: directory, value: app/Http/Controllers/.* }]
- name: Request
collectors: [{ type: directory, value: app/Http/Requests/.* }]
- name: Resource
collectors: [{ type: directory, value: app/Http/Resources/.* }]
- name: Middleware
collectors: [{ type: directory, value: app/Http/Middleware/.* }]
- name: Service
collectors: [{ type: directory, value: app/Services/.* }]
- name: Job
collectors: [{ type: directory, value: app/Jobs/.* }]
- name: Console
collectors: [{ type: directory, value: app/Console/.* }]
- name: Repository
collectors: [{ type: directory, value: app/Repositories/.* }]
- name: Model
collectors: [{ type: directory, value: app/Models/.* }]
- name: Mail
collectors: [{ type: directory, value: app/Mail/.* }]
- name: Rule
collectors: [{ type: directory, value: app/Rules/.* }]
- name: Exception
collectors: [{ type: directory, value: app/Exceptions/.* }]
- name: Provider
collectors: [{ type: directory, value: app/Providers/.* }]
ruleset:
# Conservative ruleset — enforces only the architecturally-wrong directions
# (inward/upward deps). Whatever current code violates is captured by the
# baseline (deptrac.baseline.yaml); this gate then catches only NEW drift.
Controller: [Service, Request, Resource, Model, Job, Mail, Repository, Rule, Exception]
Middleware: [Service, Model, Exception]
Service: [Service, Model, Repository, Job, Mail, Rule, Exception]
Job: [Service, Model, Repository, Mail, Exception]
Console: [Service, Model, Repository, Job, Mail, Exception]
Repository: [Model, Exception]
Request: [Rule, Model]
Resource: [Model]
Rule: [Model]
Mail: [Model]
Model: []
Provider: [Controller, Service, Job, Console, Repository, Model, Mail, Middleware, Request, Resource, Rule, Exception]
+229 -7
View File
@@ -66,6 +66,96 @@ parameters:
count: 1
path: app/Http/Controllers/Api/ImpersonationController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$dry_run\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$error_message\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$filename\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$finished_at\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$rows_added\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$rows_skipped\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$rows_total\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$rows_updated\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$started_at\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$status\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$tenant_id\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$unknown_statuses_count\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$occurrences\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$status_ru\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Parameter \#1 \$callback of method Illuminate\\Database\\Eloquent\\Collection\<int,App\\Models\\ImportUnknownStatus\>\:\:map\(\) contains unresolvable type\.$#'
identifier: argument.unresolvableType
count: 1
path: app/Http/Controllers/Api/ImportController.php
-
message: '#^Using nullsafe method call on non\-nullable type Illuminate\\Support\\Carbon\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
@@ -78,12 +168,48 @@ parameters:
count: 1
path: app/Http/Middleware/SetTenantContext.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$file_path\.$#'
identifier: property.notFound
count: 3
path: app/Jobs/ImportLeadsJob.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$user_id\.$#'
identifier: property.notFound
count: 3
path: app/Jobs/ImportLeadsJob.php
-
message: '#^Parameter \#1 \$array \(array\{string\}\) of array_values is already a list, call has no effect\.$#'
identifier: arrayValues.list
count: 1
path: app/Jobs/Supplier/SyncSupplierProjectsJob.php
-
message: '#^Using nullsafe property access "\?\-\>name" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 2
path: app/Mail/NewLeadNotification.php
-
message: '#^PHPDoc tag @mixin contains unknown class App\\Models\\IdeHelperImportLog\.$#'
identifier: class.notFound
count: 1
path: app/Models/ImportLog.php
-
message: '#^PHPDoc tag @mixin contains unknown class App\\Models\\IdeHelperImportUnknownStatus\.$#'
identifier: class.notFound
count: 1
path: app/Models/ImportUnknownStatus.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$dry_run\.$#'
identifier: property.notFound
count: 1
path: app/Services/Import/HistoricalImportService.php
-
message: '#^Call to function is_array\(\) with array\<mixed\> will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
@@ -159,7 +285,7 @@ parameters:
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:postJson\(\)\.$#'
identifier: method.notFound
count: 6
count: 9
path: tests/Feature/Admin/AdminPricingTiersControllerTest.php
-
@@ -765,13 +891,13 @@ parameters:
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$otherTenant\.$#'
identifier: property.notFound
count: 7
count: 10
path: tests/Feature/DealIndexTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$project\.$#'
identifier: property.notFound
count: 26
count: 32
path: tests/Feature/DealIndexTest.php
-
@@ -783,7 +909,7 @@ parameters:
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
identifier: property.notFound
count: 30
count: 36
path: tests/Feature/DealIndexTest.php
-
@@ -801,7 +927,7 @@ parameters:
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:getJson\(\)\.$#'
identifier: method.notFound
count: 21
count: 24
path: tests/Feature/DealIndexTest.php
-
@@ -972,6 +1098,12 @@ parameters:
count: 9
path: tests/Feature/DealUpdateTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:seed\(\)\.$#'
identifier: method.notFound
count: 2
path: tests/Feature/DemoSeederTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:postJson\(\)\.$#'
identifier: method.notFound
@@ -1008,6 +1140,18 @@ parameters:
count: 17
path: tests/Feature/ImpersonationTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$mapped_to_slug\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/HistoricalImportServiceTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$occurrences\.$#'
identifier: property.notFound
count: 3
path: tests/Feature/Import/HistoricalImportServiceTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$service\.$#'
identifier: property.notFound
@@ -1038,6 +1182,18 @@ parameters:
count: 3
path: tests/Feature/Import/ImportCompletedNotificationTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$mapped_to_slug\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportControllerTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$resolved_at\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportControllerTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
identifier: property.notFound
@@ -1068,6 +1224,42 @@ parameters:
count: 5
path: tests/Feature/Import/ImportControllerTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$error_message\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$finished_at\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$rows_added\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$rows_skipped\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$status\.$#'
identifier: property.notFound
count: 3
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$unknown_statuses_count\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
identifier: property.notFound
@@ -1080,6 +1272,36 @@ parameters:
count: 4
path: tests/Feature/Import/ImportLeadsJobTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$dry_run\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportModelsTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$entity_type\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportModelsTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$mapping_config\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportModelsTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportLog\:\:\$status\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportModelsTest.php
-
message: '#^Access to an undefined property App\\Models\\ImportUnknownStatus\:\:\$status_ru\.$#'
identifier: property.notFound
count: 1
path: tests/Feature/Import/ImportModelsTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
identifier: property.notFound
@@ -1209,13 +1431,13 @@ parameters:
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#'
identifier: method.notFound
count: 9
count: 12
path: tests/Feature/Plan5/Projects/ProjectsStoreTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#'
identifier: method.notFound
count: 6
count: 8
path: tests/Feature/Plan5/Projects/ProjectsUpdateTest.php
-
@@ -3,7 +3,7 @@ import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import axios from 'axios';
import type { Project } from '../../stores/projectsStore';
import { useProjectsStore } from '../../stores/projectsStore';
import { REGIONS } from '../../constants/regions';
import { REGIONS, FEDERAL_DISTRICT_NAMES } from '../../constants/regions';
const props = defineProps<{ project: Project | null }>();
const emit = defineEmits<{ close: []; saved: [] }>();
@@ -11,8 +11,7 @@ const emit = defineEmits<{ close: []; saved: [] }>();
interface FormState {
name: string;
daily_limit_target: number;
region_mask: number;
region_mode: 'include' | 'exclude';
regions: number[];
delivery_days_mask: number;
sms_senders: string[];
sms_keyword: string;
@@ -21,48 +20,31 @@ interface FormState {
const form = reactive<FormState>({
name: '',
daily_limit_target: 50,
region_mask: 0,
region_mode: 'include',
regions: [],
delivery_days_mask: 127,
sms_senders: [],
sms_keyword: '',
});
const selectedRegions = ref<number[]>([]);
const selectableRegions = REGIONS.filter((r) => r.code !== 0);
function maskToCodes(mask: number): number[] {
const codes: number[] = [];
for (let i = 1; i <= 31; i++) if (mask & (1 << i)) codes.push(i);
return codes;
}
function reseedFromProject(p: Project | null): void {
if (!p) return;
form.name = p.name;
form.daily_limit_target = p.daily_limit_target;
form.region_mask = p.region_mask ?? 0;
form.region_mode = (p.region_mode ?? 'include') as 'include' | 'exclude';
form.regions = Array.isArray(p.regions) ? [...p.regions] : [];
form.delivery_days_mask = p.delivery_days_mask ?? 127;
form.sms_senders = p.sms_senders ?? [];
form.sms_keyword = p.sms_keyword ?? '';
selectedRegions.value = maskToCodes(form.region_mask);
}
reseedFromProject(props.project);
watch(() => props.project?.id, () => {
reseedFromProject(props.project);
});
watch(selectedRegions, (codes) => {
if (codes.length === 0) {
form.region_mask = 0;
form.region_mode = 'include';
} else {
form.region_mask = codes.reduce((acc, c) => (c >= 1 && c <= 31 ? acc | (1 << c) : acc), 0);
form.region_mode = 'exclude';
}
});
watch(
() => props.project?.id,
() => {
reseedFromProject(props.project);
},
);
const saving = ref(false);
const errors = reactive<Record<string, string[]>>({});
@@ -76,7 +58,9 @@ async function onPause(): Promise<void> {
async function onDelete(): Promise<void> {
if (!props.project) return;
const ok = window.confirm('Архивировать проект? Действие необратимо в Plan 5 (восстановление потребует ручного запроса).');
const ok = window.confirm(
'Архивировать проект? Действие необратимо в Plan 5 (восстановление потребует ручного запроса).',
);
if (!ok) return;
await store.archive(props.project.id);
emit('close');
@@ -90,8 +74,7 @@ async function onSave(): Promise<void> {
const payload: Record<string, unknown> = {
name: form.name,
daily_limit_target: form.daily_limit_target,
region_mask: form.region_mask,
region_mode: form.region_mode,
regions: form.regions,
delivery_days_mask: form.delivery_days_mask,
};
if (props.project.signal_type === 'sms') {
@@ -122,7 +105,7 @@ const activeDays = computed<boolean[]>(() => {
});
function toggleDay(i: number): void {
form.delivery_days_mask ^= (1 << i);
form.delivery_days_mask ^= 1 << i;
}
const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
@@ -159,7 +142,7 @@ const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
<div class="pdd-field">
<span class="pdd-label">Регионы (пусто = вся РФ)</span>
<v-autocomplete
v-model="selectedRegions"
v-model="form.regions"
:items="selectableRegions"
item-title="name"
item-value="code"
@@ -169,7 +152,15 @@ const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
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>
</div>
<div class="pdd-field">
@@ -197,13 +188,12 @@ const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
<button class="pdd-btn pdd-btn-error" data-testid="pdd-delete" @click="onDelete">🗄 Удалить</button>
</div>
<div class="pdd-foot-right">
<button class="pdd-btn pdd-btn-text" data-testid="pdd-cancel" @click="$emit('close')">Отмена</button>
<button
class="pdd-btn pdd-btn-primary"
data-testid="pdd-save"
:disabled="saving"
@click="onSave"
>Сохранить</button>
<button class="pdd-btn pdd-btn-text" data-testid="pdd-cancel" @click="$emit('close')">
Отмена
</button>
<button class="pdd-btn pdd-btn-primary" data-testid="pdd-save" :disabled="saving" @click="onSave">
Сохранить
</button>
</div>
</footer>
</div>
@@ -212,34 +202,123 @@ const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
<style scoped>
.project-details-drawer {
position: fixed; top: 0; right: 0; bottom: 0;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 480px;
background: var(--liderra-surface, #ffffff);
border-left: 1px solid var(--liderra-line, #e6e2d6);
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.06);
transform: translateX(100%);
transition: transform 240ms cubic-bezier(0.16, 1, 0.3, 1);
display: flex; flex-direction: column;
display: flex;
flex-direction: column;
z-index: 5;
}
.project-details-drawer.open { transform: translateX(0); }
.pdd-content { display: flex; flex-direction: column; height: 100%; }
.pdd-head { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--liderra-line, #e6e2d6); }
.pdd-title { font-weight: 600; font-size: 16px; }
.pdd-close { background: none; border: 0; cursor: pointer; font-size: 18px; padding: 4px; }
.pdd-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; flex: 1; overflow-y: auto; }
.pdd-field { display: flex; flex-direction: column; gap: 4px; }
.pdd-label { font-size: 12px; color: #6b6f72; }
.pdd-input { padding: 8px 10px; border: 1px solid var(--liderra-line, #e6e2d6); border-radius: 6px; font: inherit; }
.pdd-days { display: flex; gap: 4px; }
.pdd-day { padding: 6px 10px; border: 1px solid var(--liderra-line, #e6e2d6); background: #ffffff; border-radius: 4px; cursor: pointer; font: inherit; }
.pdd-day.active { background: #0f6e56; color: #ffffff; border-color: #0f6e56; }
.pdd-foot { display: flex; justify-content: space-between; padding: 12px 20px; border-top: 1px solid var(--liderra-line, #e6e2d6); }
.pdd-foot-left, .pdd-foot-right { display: flex; gap: 8px; }
.pdd-btn { padding: 6px 14px; border: 0; border-radius: 6px; cursor: pointer; font: inherit; }
.pdd-btn-text { background: transparent; color: #081319; }
.pdd-btn-primary { background: #0f6e56; color: #ffffff; }
.pdd-btn-warning { background: #f59e0b; color: #ffffff; }
.pdd-btn-error { background: #dc2626; color: #ffffff; }
.pdd-error { color: #dc2626; font-size: 12px; margin-top: 4px; }
.project-details-drawer.open {
transform: translateX(0);
}
.pdd-content {
display: flex;
flex-direction: column;
height: 100%;
}
.pdd-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--liderra-line, #e6e2d6);
}
.pdd-title {
font-weight: 600;
font-size: 16px;
}
.pdd-close {
background: none;
border: 0;
cursor: pointer;
font-size: 18px;
padding: 4px;
}
.pdd-body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 14px;
flex: 1;
overflow-y: auto;
}
.pdd-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.pdd-label {
font-size: 12px;
color: #6b6f72;
}
.pdd-input {
padding: 8px 10px;
border: 1px solid var(--liderra-line, #e6e2d6);
border-radius: 6px;
font: inherit;
}
.pdd-days {
display: flex;
gap: 4px;
}
.pdd-day {
padding: 6px 10px;
border: 1px solid var(--liderra-line, #e6e2d6);
background: #ffffff;
border-radius: 4px;
cursor: pointer;
font: inherit;
}
.pdd-day.active {
background: #0f6e56;
color: #ffffff;
border-color: #0f6e56;
}
.pdd-foot {
display: flex;
justify-content: space-between;
padding: 12px 20px;
border-top: 1px solid var(--liderra-line, #e6e2d6);
}
.pdd-foot-left,
.pdd-foot-right {
display: flex;
gap: 8px;
}
.pdd-btn {
padding: 6px 14px;
border: 0;
border-radius: 6px;
cursor: pointer;
font: inherit;
}
.pdd-btn-text {
background: transparent;
color: #081319;
}
.pdd-btn-primary {
background: #0f6e56;
color: #ffffff;
}
.pdd-btn-warning {
background: #f59e0b;
color: #ffffff;
}
.pdd-btn-error {
background: #dc2626;
color: #ffffff;
}
.pdd-error {
color: #dc2626;
font-size: 12px;
margin-top: 4px;
}
</style>
+114 -37
View File
@@ -1,42 +1,119 @@
export interface Region {
code: number;
name: string;
code: number; // 1..89, sequential по конституционному порядку (Art. 65)
name: string; // официальное название субъекта
federalDistrict: number; // 1..8 (см. FEDERAL_DISTRICT_NAMES)
}
// MVP: 31 региона (коды 1..31) ограничены 32-bit region_mask из Plan 5 Task 9.
// Sentinel code:0 = «Вся РФ» (включает все регионы, эквивалент пустой маски).
// Имена — официальные субъекты РФ по конституционному порядку нумерации.
// Конституционный порядок (ст. 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: 'Вся РФ' },
{ code: 1, name: 'Республика Адыгея' },
{ code: 2, name: 'Республика Башкортостан' },
{ code: 3, name: 'Республика Бурятия' },
{ code: 4, name: 'Республика Алтай' },
{ code: 5, name: 'Республика Дагестан' },
{ code: 6, name: 'Республика Ингушетия' },
{ code: 7, name: 'Кабардино-Балкарская Республика' },
{ code: 8, name: 'Республика Калмыкия' },
{ code: 9, name: 'Карачаево-Черкесская Республика' },
{ code: 10, name: 'Республика Карелия' },
{ code: 11, name: 'Республика Коми' },
{ code: 12, name: 'Республика Марий Эл' },
{ code: 13, name: 'Республика Мордовия' },
{ code: 14, name: 'Республика Саха (Якутия)' },
{ code: 15, name: 'Республика Северная Осетия — Алания' },
{ code: 16, name: 'Республика Татарстан' },
{ code: 17, name: 'Республика Тыва' },
{ code: 18, name: 'Удмуртская Республика' },
{ code: 19, name: 'Республика Хакасия' },
{ code: 20, name: 'Чеченская Республика' },
{ code: 21, name: 'Чувашская Республика' },
{ code: 22, name: 'Алтайский край' },
{ code: 23, name: 'Краснодарский край' },
{ code: 24, name: 'Красноярский край' },
{ code: 25, name: 'Приморский край' },
{ code: 26, name: 'Ставропольский край' },
{ code: 27, name: 'Хабаровский край' },
{ code: 28, name: 'Амурская область' },
{ code: 29, name: 'Архангельская область' },
{ code: 30, name: 'Астраханская область' },
{ code: 31, name: 'Белгородская область' },
{ 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: 'Дальневосточный',
};
+1
View File
@@ -16,6 +16,7 @@ export interface Project {
archived_at: string | null;
region_mask?: number;
region_mode?: string;
regions?: number[]; // Plan 6 — subject codes 1..89; пустой массив = вся РФ
delivery_days_mask?: number;
sync_status: 'ok' | 'pending' | 'failed';
last_synced_at?: string | null;
+8 -39
View File
@@ -1,16 +1,20 @@
<script setup lang="ts">
/**
* Settings настройки тенанта/пользователя. 8 вкладок (по v8.5 §13 + ТЗ §14).
* Settings настройки тенанта/пользователя. 4 рабочие вкладки.
*
* Источник дизайна: liderra_v8_handoff/concepts/v8_settings.html.
* Полностью реализованы (с UI-разводкой): Профиль, Безопасность, API и Webhook,
* Уведомления (матрица 8×3 по schema v8.7 §4 users.notification_preferences).
* Placeholder-заглушки: Проекты, Команда, Интеграции, Тихие часы.
*
* Аудит D6/D7 (Sprint 3E, 2026-05-16): placeholder-вкладки Проекты/Команда/
* Интеграции/Тихие часы убраны UI не должен обещать «в разработке».
* «Проекты» дублировали /projects; «Команда» и «Тихие часы» (ТЗ §17.8)
* требуют schema+backend (отдельные эпики); «Интеграции» внешне-блокированы (Б-1).
* Вкладки вернутся при реальной реализации соответствующих модулей.
*/
import { computed, ref } from 'vue';
import { ref } from 'vue';
import ApiTab from './settings/ApiTab.vue';
import NotificationsTab from './settings/NotificationsTab.vue';
import PlaceholderTab from './settings/PlaceholderTab.vue';
import ProfileTab from './settings/ProfileTab.vue';
import SecurityTab from './settings/SecurityTab.vue';
@@ -23,41 +27,11 @@ interface Tab {
const tabs: Tab[] = [
{ id: 'profile', label: 'Профиль', icon: 'mdi-account-outline' },
{ id: 'security', label: 'Безопасность', icon: 'mdi-shield-lock-outline' },
{ id: 'projects', label: 'Проекты', icon: 'mdi-folder-outline' },
{ id: 'team', label: 'Команда', icon: 'mdi-account-group-outline' },
{ id: 'api', label: 'API и Webhook', icon: 'mdi-api' },
{ id: 'integrations', label: 'Интеграции', icon: 'mdi-puzzle-outline' },
{ id: 'hours', label: 'Тихие часы', icon: 'mdi-clock-outline' },
{ id: 'notifications', label: 'Уведомления', icon: 'mdi-bell-outline' },
];
const activeTab = ref('profile');
const placeholderProps = computed(() => {
const map: Record<string, { title: string; description: string }> = {
projects: {
title: 'Проекты',
description:
'Управление проектами тенанта (макс. 10 на тарифе «Команда»). Для каждого проекта — поставщик ГЦК, цена за лид, активные UTM-кампании.',
},
team: {
title: 'Команда',
description:
'Менеджеры тенанта (макс. 4 + расширение). Назначение прав, автораспределение, ограничение доступа к проектам.',
},
integrations: {
title: 'Интеграции',
description:
'Подключение Telegram-бота для нотификаций, экспорт в 1С 8.3, JivoSite helpdesk, Yandex 360 SSO.',
},
hours: {
title: 'Тихие часы',
description:
'Расписание, в которое не приходят SMS/звонки автонапоминаний (например, 22:00-08:00 + выходные).',
},
};
return map[activeTab.value];
});
</script>
<template>
@@ -91,11 +65,6 @@ const placeholderProps = computed(() => {
<SecurityTab v-else-if="activeTab === 'security'" />
<ApiTab v-else-if="activeTab === 'api'" />
<NotificationsTab v-else-if="activeTab === 'notifications'" />
<PlaceholderTab
v-else-if="placeholderProps"
:title="placeholderProps.title"
:description="placeholderProps.description"
/>
</v-card>
</v-col>
</v-row>
@@ -76,12 +76,34 @@
:error-messages="errors.daily_limit_target"
/>
<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="mb-3"
class="mt-3"
closable
@click:close="generalError = null"
>
@@ -114,9 +136,12 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
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);
const props = defineProps<{
modelValue: boolean;
mode?: 'create' | 'edit';
@@ -124,9 +149,8 @@ const props = defineProps<{
}>();
const emit = defineEmits(['update:modelValue', 'saved']);
// region_mask=255 = все 8 ФО (schema default, см. db/schema.sql §projects).
// PDD regions UI отключён до закрытия Plan 6 конфликт с 8-битной ФО-маской
// в PhonePrefixService.php (1 phone prefix 1 ФО, не субъект).
// Plan 6: regions = subject codes (1..89) backend dual-writes region_mask/region_mode.
// Пустой массив = вся РФ.
const form = reactive({
name: '',
signal_type: 'site' as 'site' | 'call' | 'sms',
@@ -134,8 +158,7 @@ const form = reactive({
sms_senders: [] as string[],
sms_keyword: '',
daily_limit_target: 50,
region_mask: 255,
region_mode: 'include' as 'include' | 'exclude',
regions: [] as number[],
delivery_days_mask: 127,
});
const errors = reactive<Record<string, string[]>>({});
@@ -159,6 +182,7 @@ watch(
if (open) generalError.value = null;
if (open && props.mode === 'edit' && props.project) {
Object.assign(form, props.project);
form.regions = Array.isArray(props.project.regions) ? [...props.project.regions] : [];
const days: number[] = [];
for (let i = 0; i < 7; i++) if (form.delivery_days_mask & (1 << i)) days.push(i);
selectedDays.value = days;
@@ -170,8 +194,7 @@ watch(
sms_senders: [],
sms_keyword: '',
daily_limit_target: 50,
region_mask: 255,
region_mode: 'include',
regions: [],
delivery_days_mask: 127,
});
selectedDays.value = [0, 1, 2, 3, 4, 5, 6];
@@ -1,26 +0,0 @@
<script setup lang="ts">
/**
* Универсальный placeholder для ещё-не-реализованных вкладок Settings.
* Используется для вкладок: Проекты, Команда, Интеграции, Тихие часы.
*
* При реализации каждой вкладки заменяется на отдельный component.
*/
defineProps<{ title: string; description: string }>();
</script>
<template>
<div class="tab-content">
<h2 class="tab-title text-h6 mb-3">{{ title }}</h2>
<v-alert type="info" variant="tonal" density="compact" class="mb-4">
<strong>В разработке.</strong> Этот раздел реализуется в следующих коммитах.
</v-alert>
<p class="text-body-2 text-medium-emphasis">{{ description }}</p>
</div>
</template>
<style scoped>
.tab-title {
font-variation-settings: 'opsz' 18;
letter-spacing: -0.005em;
}
</style>
@@ -59,11 +59,12 @@ it('supplier_csv_reconcile_log table exists with required columns and status CHE
]))->toThrow(QueryException::class);
});
it('schema.sql v8.21 has correct metrics — 63 base tables, 118 indexes, 40 RLS policies', function () {
it('schema.sql v8.22 has correct metrics — 63 base tables, 119 indexes, 40 RLS policies', function () {
// Замена destructive `migrate:fresh` (cross-test coupling: после DROP CASCADE остальные
// Feature-тесты в той же сессии видели пустую БД). Static parse `db/schema.sql` —
// источник истины метрик из spec §2.4 / db/CHANGELOG_schema.md v8.21.
// источник истины метрик из spec §2.4 / db/CHANGELOG_schema.md v8.22.
// v8.21 (Sprint 4): +1 таблица import_unknown_statuses, +1 индекс, +1 RLS-политика.
// v8.22 (Plan 6/C9): +1 GIN-индекс idx_projects_regions.
$schemaPath = dirname(base_path()).DIRECTORY_SEPARATOR.'db'.DIRECTORY_SEPARATOR.'schema.sql';
expect(is_file($schemaPath) && is_readable($schemaPath))->toBeTrue();
$schema = file_get_contents($schemaPath);
@@ -76,7 +77,7 @@ it('schema.sql v8.21 has correct metrics — 63 base tables, 118 indexes, 40 RLS
expect($baseTables)->toBe(63);
$createIndexes = preg_match_all('/^CREATE\s+(?:UNIQUE\s+)?INDEX\b/m', $schema);
expect($createIndexes)->toBe(118);
expect($createIndexes)->toBe(119); // v8.22 (Plan 6/C9): +1 GIN idx_projects_regions
$createPolicies = preg_match_all('/^CREATE\s+POLICY\b/m', $schema);
expect($createPolicies)->toBe(40);
@@ -19,8 +19,7 @@ it('creates a site project with valid payload', function () {
'signal_type' => 'site',
'signal_identifier' => 'okna-spb.ru',
'daily_limit_target' => 50,
'region_mask' => 0,
'region_mode' => 'include',
'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -36,7 +35,7 @@ it('rejects invalid site domain', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'X', 'signal_type' => 'site', 'signal_identifier' => 'not a domain',
'daily_limit_target' => 50, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 50, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -50,7 +49,7 @@ it('creates a call project with valid 11-digit phone', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'Натяжные', 'signal_type' => 'call', 'signal_identifier' => '79161234567',
'daily_limit_target' => 30, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 30, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -63,7 +62,7 @@ it('rejects call signal_identifier not starting with 7', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'X', 'signal_type' => 'call', 'signal_identifier' => '89991234567',
'daily_limit_target' => 30, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 30, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -77,7 +76,7 @@ it('creates sms project with senders + keyword', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'Ипотека', 'signal_type' => 'sms',
'sms_senders' => ['TINKOFF'], 'sms_keyword' => 'ипотека',
'daily_limit_target' => 100, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 100, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -93,7 +92,7 @@ it('rejects sms project without sms_senders', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'X', 'signal_type' => 'sms',
'daily_limit_target' => 100, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 100, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -108,7 +107,7 @@ it('rejects when tenant exceeds max_projects limit', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'second', 'signal_type' => 'site', 'signal_identifier' => 'second.ru',
'daily_limit_target' => 10, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 10, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -123,7 +122,7 @@ it('forces tenant_id from auth user (not from payload)', function () {
$this->actingAs($userA)->postJson('/api/projects', [
'tenant_id' => $tenantB->id, // попытка инъекции
'name' => 'X', 'signal_type' => 'site', 'signal_identifier' => 'x.ru',
'daily_limit_target' => 10, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 10, 'regions' => [],
'delivery_days_mask' => 127,
]);
@@ -137,10 +136,67 @@ it('rejects site domain with consecutive dots', function () {
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'X', 'signal_type' => 'site', 'signal_identifier' => 'okna..spb.ru',
'daily_limit_target' => 50, 'region_mask' => 0, 'region_mode' => 'include',
'daily_limit_target' => 50, 'regions' => [],
'delivery_days_mask' => 127,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['signal_identifier']);
});
// Plan 6 — subject-level regions[] support.
it('creates project with subject-level regions array', function () {
$tenant = Tenant::factory()->create();
$user = 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);
$response->assertJsonPath('data.regions', [82, 83]);
$created = 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 = Tenant::factory()->create();
$user = 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 = 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 = Tenant::factory()->create();
$user = 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']);
});
@@ -78,14 +78,50 @@ it('cross-tenant update returns 404', function () {
])->assertStatus(404);
});
it('updates region_mask and delivery_days_mask', function () {
it('updates delivery_days_mask (region_mask now read-only — see regions[] tests below)', function () {
// Plan 6: region_mask/region_mode больше не клиент-controllable через UpdateProjectRequest
// (validation rules удалены, ProjectService::create dual-writes 255/include).
// Источник истины для региональной фильтрации — projects.regions INT[] (Plan 6).
// Этот тест адаптирован: проверяет, что delivery_days_mask остаётся writeable через PATCH.
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$project = Project::factory()->create(['tenant_id' => $tenant->id]);
$this->actingAs($user)->patchJson("/api/projects/{$project->id}", [
'region_mask' => 78, 'region_mode' => 'exclude', 'delivery_days_mask' => 31,
'delivery_days_mask' => 31,
])->assertOk();
expect($project->fresh()->region_mask)->toBe(78);
expect($project->fresh()->delivery_days_mask)->toBe(31);
});
// Plan 6 — subject-level regions[] support.
it('updates regions array via PATCH', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$project = Project::factory()->create(['tenant_id' => $tenant->id, 'regions' => []]);
$response = $this->actingAs($user)->patchJson("/api/projects/{$project->id}", [
'regions' => [82],
]);
$response->assertStatus(200);
$response->assertJsonPath('data.regions', [82]);
expect($project->fresh()->regions)->toBe([82]);
});
it('preserves regions when PATCH omits the field (sometimes rule)', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
$project = 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]);
});
@@ -11,6 +11,7 @@ use App\Models\SupplierProject;
use App\Models\SupplierSyncLog;
use App\Models\Tenant;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
@@ -309,6 +310,36 @@ test('respects time budget by stopping at 20:55 МСК', function (): void {
Http::assertNothingSent();
});
test('passes regions directly to allocator without bitmask conversion', function (): void {
$tenant = Tenant::factory()->create();
Project::factory()->create([
'tenant_id' => $tenant->id,
'regions' => [82, 83],
'region_mask' => 255,
]);
$job = new SyncSupplierProjectsJob;
$projects = Project::where('tenant_id', $tenant->id)->get();
$adapted = (fn (Collection $p) => $this->adaptProjectsForAllocator($p))->call($job, $projects);
expect($adapted->first()->regions)->toBe([82, 83]);
});
test('passes empty array to allocator when project has regions=[]', function (): void {
$tenant = Tenant::factory()->create();
Project::factory()->create([
'tenant_id' => $tenant->id,
'regions' => [],
'region_mask' => 255,
]);
$job = new SyncSupplierProjectsJob;
$projects = Project::where('tenant_id', $tenant->id)->get();
$adapted = (fn (Collection $p) => $this->adaptProjectsForAllocator($p))->call($job, $projects);
expect($adapted->first()->regions)->toBe([]);
});
test('sticky auth error throws and sends critical alert email', function (): void {
Mail::fake();
Bus::fake([RefreshSupplierSessionJob::class]);
@@ -346,3 +377,38 @@ test('sticky auth error throws and sends critical alert email', function (): voi
return $mail->alertType === 'sticky_auth';
});
});
test('outbound: copies project regions[] into supplier_project current_regions via full handle()', function (): void {
$tenant = Tenant::factory()->create();
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'regions-flow.example.com',
'supplier_external_id' => null,
'current_limit' => 0,
'current_workdays' => [],
'current_regions' => [],
]);
Project::factory()->create([
'tenant_id' => $tenant->id,
'is_active' => true,
'signal_type' => 'site',
'signal_identifier' => 'regions-flow.example.com',
'supplier_b1_project_id' => $sp->id,
'daily_limit_target' => 9,
'delivery_days_mask' => 127,
'regions' => [82, 83],
'region_mask' => 255,
'region_mode' => 'include',
]);
Http::fake([
'crm.bp-gr.ru/admin/rt-project-save' => Http::response(['id' => 556], 200),
]);
(new SyncSupplierProjectsJob)->handle();
$sp->refresh();
expect($sp->current_regions)->toBe([82, 83])
->and($sp->supplier_external_id)->toBe('556');
});
@@ -6,6 +6,16 @@ import axios from 'axios';
vi.mock('axios');
vi.mock('../../resources/js/api/client', () => ({
apiClient: {
post: vi.fn().mockResolvedValue({ data: {} }),
patch: vi.fn().mockResolvedValue({ data: {} }),
},
ensureCsrfCookie: vi.fn().mockResolvedValue(undefined),
extractErrorMessage: vi.fn(() => 'Произошла ошибка.'),
}));
import { apiClient } from '../../resources/js/api/client';
import NewProjectDialog from '../../resources/js/views/projects/NewProjectDialog.vue';
import type { Project } from '../../resources/js/stores/projectsStore';
@@ -74,4 +84,24 @@ describe('NewProjectDialog', () => {
it.skip('emits saved event after successful POST', async () => {
// TODO: см. предыдущий skip — те же причины.
});
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' });
autocomplete.vm.$emit('update:model-value', [82, 83]);
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(apiClient.post).toHaveBeenCalledWith('/api/projects', expect.objectContaining({ regions: [82, 83] }));
});
});
+18 -19
View File
@@ -24,6 +24,7 @@ const sampleProject: Project = {
archived_at: null,
region_mask: 0,
region_mode: 'include',
regions: [],
delivery_days_mask: 31, // Mon-Fri
sync_status: 'pending',
};
@@ -50,7 +51,7 @@ describe('ProjectDetailsDrawer', () => {
// Days mask 31 = bits 0..4 = Mon..Fri (5 days active)
const dayBtns = wrapper.findAll('button[data-testid^="pdd-day-"]');
expect(dayBtns.length).toBe(7);
const activeBtns = dayBtns.filter(b => b.classes().includes('active'));
const activeBtns = dayBtns.filter((b) => b.classes().includes('active'));
expect(activeBtns.length).toBe(5);
});
@@ -126,8 +127,12 @@ describe('ProjectDetailsDrawer', () => {
});
it('Pause button calls store.toggleActive', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ data: { data: { ...sampleProject, is_active: false } } });
(axios.get as unknown as ReturnType<typeof vi.fn> | undefined)?.mockResolvedValue?.({ data: { data: [], meta: { total: 0 } } });
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
data: { data: { ...sampleProject, is_active: false } },
});
(axios.get as unknown as ReturnType<typeof vi.fn> | undefined)?.mockResolvedValue?.({
data: { data: [], meta: { total: 0 } },
});
const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
const store = useProjectsStore();
const spy = vi.spyOn(store, 'toggleActive').mockResolvedValueOnce(undefined);
@@ -174,33 +179,30 @@ describe('ProjectDetailsDrawer', () => {
vi.unstubAllGlobals();
});
it('renders region chips when project has non-zero region_mask', async () => {
const withRegions: Project = { ...sampleProject, region_mask: 6, region_mode: 'exclude' };
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('Адыгея');
expect(text).toContain('Башкортостан');
expect(text).toContain('Адыгея'); // code 1
expect(text).toContain('Алтай'); // code 2 (Республика Алтай)
});
it('selecting regions encodes mask + sets mode=exclude on save', async () => {
it('selecting regions adds to regions array (no bitmask conversion)', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ data: { data: sampleProject } });
const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
const autocomplete = wrapper.getComponent({ name: 'VAutocomplete' });
await autocomplete.vm.$emit('update:model-value', [3, 5]);
await autocomplete.vm.$emit('update:model-value', [82, 83]);
await wrapper.vm.$nextTick();
await wrapper.get('[data-testid="pdd-save"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(axios.patch).toHaveBeenCalledWith(
'/api/projects/42',
expect.objectContaining({ region_mask: 40, region_mode: 'exclude' }),
);
expect(axios.patch).toHaveBeenCalledWith('/api/projects/42', expect.objectContaining({ regions: [82, 83] }));
});
it('clearing all regions resets mask=0 + mode=include on save', async () => {
it('clearing all regions sets regions=[] on save', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ data: { data: sampleProject } });
const withRegions: Project = { ...sampleProject, region_mask: 6, region_mode: 'exclude' };
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', []);
@@ -208,9 +210,6 @@ describe('ProjectDetailsDrawer', () => {
await wrapper.get('[data-testid="pdd-save"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(axios.patch).toHaveBeenCalledWith(
'/api/projects/42',
expect.objectContaining({ region_mask: 0, region_mode: 'include' }),
);
expect(axios.patch).toHaveBeenCalledWith('/api/projects/42', expect.objectContaining({ regions: [] }));
});
});
+11 -24
View File
@@ -15,28 +15,26 @@ describe('SettingsView.vue', () => {
expect(wrapper.find('h1').text()).toBe('Настройки');
});
it('содержит ровно 8 nav-tabs', () => {
it('содержит ровно 4 nav-tabs (placeholder-вкладки убраны, audit D6/D7)', () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
expect(items.length).toBe(8);
expect(items.length).toBe(4);
});
it('содержит все 8 названий вкладок', () => {
it('содержит все 4 названия рабочих вкладок', () => {
const wrapper = factory();
const text = wrapper.text();
const labels = [
'Профиль',
'Безопасность',
'Проекты',
'Команда',
'API и Webhook',
'Интеграции',
'Тихие часы',
'Уведомления',
];
const labels = ['Профиль', 'Безопасность', 'API и Webhook', 'Уведомления'];
labels.forEach((l) => expect(text).toContain(l));
});
it('не содержит placeholder-вкладок и текста «В разработке»', () => {
const wrapper = factory();
const railText = wrapper.find('.tabs-rail').text();
['Команда', 'Интеграции', 'Тихие часы'].forEach((l) => expect(railText).not.toContain(l));
expect(wrapper.text()).not.toContain('В разработке');
});
it('по умолчанию показывает вкладку «Профиль»', () => {
const wrapper = factory();
const text = wrapper.text();
@@ -46,17 +44,6 @@ describe('SettingsView.vue', () => {
expect(text).toContain('Тайм-зона');
});
it('placeholder-вкладки показывают «В разработке»', async () => {
const wrapper = factory();
// Кликаем по «Проекты» — placeholder-вкладка.
const items = wrapper.findAll('.tabs-rail .v-list-item');
const projectsItem = items.find((i) => i.text().includes('Проекты'));
expect(projectsItem).toBeDefined();
await projectsItem!.trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('В разработке');
});
it('переключение на «Уведомления» показывает матрицу 8×3', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
+20
View File
@@ -1,6 +1,9 @@
# Глоссарий проекта Лидерра
# Формат: одно слово на строке. Кириллица в нижнем регистре.
# A4 design-tooling integration (v2.8 / v3.8 / v1.22)
iconify
# Бренд и термины проекта
лидерра
liderra
@@ -1360,3 +1363,20 @@ PRD
automazeio
prds
Vivek
# deptrac architecture-fitness integration (2026-05-17)
deptrac
qossmic
mermaidjs
graphviz
# A3 integration-tooling design spec + plan (2026-05-17)
аудировал
JVM
хендлеров
ivo
redocly
ivotoby
ребейз
ребейзнута
ребейзом
+27 -2
View File
@@ -1,11 +1,36 @@
# CHANGELOG schema.sql — Лидерра
**Назначение:** консолидированный журнал изменений `schema.sql`. Содержит двадцать записей в обратном хронологическом порядке (v8.21 → v8.20 → v8.19 → v8.18 → v8.17 → v8.16 → v8.15 → v8.14 → v8.13 → v8.12 → v8.11 → v8.10 → v8.9 → v8.8 → v8.7 → v8.6 → v8.5 → v8.4 → v8.3 → v8.2), как принято в keep-a-changelog.
**Назначение:** консолидированный журнал изменений `schema.sql`. Содержит двадцать одну запись в обратном хронологическом порядке (v8.22 → v8.21 → v8.20 → v8.19 → v8.18 → v8.17 → v8.16 → v8.15 → v8.14 → v8.13 → v8.12 → v8.11 → v8.10 → v8.9 → v8.8 → v8.7 → v8.6 → v8.5 → v8.4 → v8.3 → v8.2), как принято в keep-a-changelog.
**Файл схемы:** `schema.sql` (текущая версия — v8.21, консолидированная — разворачивает БД с нуля).
**Файл схемы:** `schema.sql` (текущая версия — v8.22, консолидированная — разворачивает БД с нуля).
**История записей:**
## v8.22 — 2026-05-17 — Plan 6 (C9 — 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=[82,83]` → проект принимает лиды только из Москвы (82) и Санкт-Петербурга (83)
**Schema baseline после v8.22:** 64 базовых таблиц / 12 партиций / **119 индексов** (+1 GIN) / 40 RLS / 5 функций / 13 триггеров.
**Применение:** инкрементальная миграция `2026_05_17_100000_plan6_regions_subject_level.php` (`ALTER TABLE projects ADD COLUMN regions` + `CREATE INDEX ... USING GIN`, guard'ы `hasColumn` / `IF NOT EXISTS`).
**Связано:** docs/superpowers/specs/2026-05-14-plan-6-regions-subject-level-design.md
## v8.21 — 2026-05-16 — Sprint 4 (историческая миграция лидов §6)
- **+1 таблица** `import_unknown_statuses` (tenant-level маппинг неизвестных статусов CSV; RLS `tenant_isolation`; UNIQUE `(tenant_id, status_ru)`; partial index `idx_import_unknown_statuses_unresolved`).
+13 -3
View File
@@ -1,7 +1,7 @@
-- =============================================================================
-- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра»)
-- Версия: v8.21 (16.05.2026 — Sprint 4: import_unknown_statuses + import_log enrichment (+5 колонок))
-- Метрики: 64 базовые таблицы (62 regular + 2 partitioned parents: deals + supplier_lead_costs) + 12 партиций / 118 индексов / 40 RLS-политик / 5 функций / 13 триггеров
-- Версия: v8.22 (17.05.2026 — Plan 6 (C9): projects.regions INT[] subject-level filtering + GIN-индекс idx_projects_regions)
-- Метрики: 64 базовые таблицы (62 regular + 2 partitioned parents: deals + supplier_lead_costs) + 12 партиций / 119 индексов / 40 RLS-политик / 5 функций / 13 триггеров
-- Базовая версия: v8.20 (11.05.2026 — Plan 5 frontend projects UI: projects.archived_at TIMESTAMPTZ NULL для soft archive flow; tenants.limits JSONB NOT NULL DEFAULT '{}' для per-tenant project/user лимитов)
-- Базовая версия: v8.19 (11.05.2026 — Plan 4 billing+csv+admin: tenants.delivered_in_month, lead_charges.charge_source + CHECK, supplier_leads.recovered_from_csv_at, supplier_csv_reconcile_log)
-- Базовая версия: v8.18 (10.05.2026 — Plan 2/5 Task 1: supplier_leads SaaS-level + projects.delivered_today + 2 system_settings rows для supplier-webhook + IP allowlist defense-in-depth)
@@ -809,13 +809,18 @@ CREATE TABLE projects (
supplier_b3_project_id BIGINT,
effective_limit_calculated_at TIMESTAMPTZ,
-- РАСШИРЕНИЕ v8.2: регионы и дни (партия 10.3 секции 6, 7, 11)
region_mask INT NOT NULL DEFAULT 255,
region_mask INT NOT NULL DEFAULT 255, -- DEPRECATED Plan 6.5: см. regions INT[]
-- битмаска 8 ФО РФ: бит 1=Центральный, 2=Северо-Западный, 4=Южный,
-- 8=Северо-Кавказский, 16=Приволжский, 32=Уральский, 64=Сибирский,
-- 128=Дальневосточный. 255 = все 8 округов.
region_mode VARCHAR(10) NOT NULL DEFAULT 'include'
CHECK (region_mode IN ('include','exclude')),
-- 'include' = принимать только из выбранных, 'exclude' = принимать кроме выбранных
-- 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[],
delivery_days_mask INT NOT NULL DEFAULT 127,
-- битмаска дней недели: бит 1=Пн, 2=Вт, 4=Ср, 8=Чт, 16=Пт, 32=Сб, 64=Вс.
-- 127 = все 7 дней (паритет с формой создания нового проекта в оригинале).
@@ -863,6 +868,8 @@ CREATE INDEX idx_projects_tag ON projects(tag);
-- РАСШИРЕНИЕ v8.12: composite index для lookup по signal-полям (resolveSignalSource)
CREATE INDEX idx_projects_tenant_signal
ON projects(tenant_id, signal_type, signal_identifier);
-- v8.20 (Plan 6): GIN-индекс для outbound regions queries.
CREATE INDEX idx_projects_regions ON projects USING GIN (regions);
COMMENT ON COLUMN projects.daily_limit_target IS
'Целевой дневной лимит лидов, заданный клиентом. Фактический лимит на '
@@ -874,6 +881,9 @@ COMMENT ON COLUMN projects.effective_daily_limit_today IS
'MIN(daily_limit_target, FLOOR(balance / lead_cost)). Пересчитывается cron '
'limits:recalc в 00:00 МСК и при изменении баланса. NULL = не считалось.';
COMMENT ON COLUMN projects.regions IS
'Subject-level region filter (1..89 коды субъектов РФ). Пустой массив = вся РФ. Plan 6 (v8.22).';
-- -----------------------------------------------------------------------------
-- supplier_projects — SaaS-level агрегатные проекты у поставщиков (v8.13, Plan 1/5 Task 2)
+15 -1
View File
@@ -1,8 +1,12 @@
# Plugin Stack Rules — Superpowers + Frontend Design (v3.6)
# Plugin Stack Rules — Superpowers + Frontend Design (v3.9)
**Дата:** 17.05.2026
**Назначение:** свод правил совместного использования плагинов Claude Code в проекте Лидерра — paired-stack ядро `obra/superpowers` (14 skills) + `anthropics/frontend-design`, плюс расширенный пул UI-инструментов `ui-ux-pro-max` (skill, marketplace `nextlevelbuilder/ui-ux-pro-max-skill`) и `21st.dev Magic MCP` (MCP-сервер `magic`), плюс инфраструктурный `claude-md-management` (skills, marketplace `anthropics/claude-plugins-official`), плюс **debug-runtime MCP** `@sentry/mcp-server` + `@modelcontextprotocol/server-redis` (v2.1+, R10.1 Блок 3).
**v3.9** — A3 integration-tooling: R10.1 Блок 3 +1 строка **openapi-mcp-server** (категория integration-tooling, off-phase, раздел A3 карты, stdio MCP, server `openapi` в `.mcp.json`, Tooling §4.22 #47). Не UI → вне R6/R14. Содержательных изменений R0–R14: 0. Связано: Tooling v2.9, Pravila v1.23, CLAUDE.md v2.9; план `docs/superpowers/plans/2026-05-17-a3-integration-tooling-integration.md`.
**v3.7** — A6-расширение deptrac: R10.1 Блок 1 +note «Блок 1 — note (v3.7)» — **deptrac** (`deptrac/deptrac` v4.6.1, Composer dev-dependency, **не** marketplace-плагин и **не** в `enabledPlugins` — регистрируется нотой, как mermaid-skill/CCPM). Категория **architecture-tooling** (Tooling #43, раздел A6 карты) — 4-й инструмент подкатегории; не UI → вне R6.0/R6.1/R14. deptrac врезан как lefthook pre-commit job 10. Содержательных изменений R0–R14: 0. Связано: Tooling v2.7, Pravila v1.21, CLAUDE.md v2.7; план `docs/superpowers/plans/2026-05-17-deptrac-architecture-fitness-integration.md`.
**v3.6** — C9 project-management: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **CCPM** (`automazeio/ccpm`, вендорен в `.claude/skills/ccpm/`) + **product-management** (`anthropics/knowledge-work-plugins`, Anthropic Verified). Блок 1 +note про **CCPM** (вендоренный скил, аналог mermaid-skill). Новая категория **project-management** (Tooling #41-42, раздел C9 карты) — не UI → вне R6.0/R6.1/R14, как architecture-tooling/audit-security. Содержательных изменений R0–R14: 0. Связано: Tooling v2.6, Pravila v1.20, CLAUDE.md v2.6; план `docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md`.
**v3.5** — фактическая правка R10.1 Блок 1 строки **security-guidance**: это **блокирующий** PreToolUse-хук (`sys.exit(2)`, одноразовый speed-bump per «файл+правило» за сессию, retry проходит), не warn-only. Содержательных изменений R0–R14: 0. Связано: Tooling v2.5, Pravila v1.19, CLAUDE.md v2.5; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
@@ -406,11 +410,14 @@ Stack — **головной**. Все плагины вне stack'а — **ин
| **security-guidance** *(1 PreToolUse-хук, блокирующий)* | `anthropics/claude-plugins-official` | inline-предупреждения уязвимостей при правке кода — **блокирующий** хук (`sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию, retry проходит), 8 контентных правил + 1 path-правило. Категория: **audit-security** (Tooling #40) | автоматически — PreToolUse-хук на Write/Edit/MultiEdit. Не решатель, не UI → вне R6.0/R6.1/R14 |
| **CCPM** *(vendored standalone skill, `/pm` flow, 14 bash-скриптов)* | `automazeio/ccpm` (вендорен в `.claude/skills/ccpm/`) | PRD→эпик→GitHub-issue→код с полной трассируемостью. GitHub-issue-backed модель (ADR-004). PRD/epic store в `.claude/prds/`/`.claude/epics/`. Категория: **project-management** (Tooling #41, вне UI-пула). Bus-factor mitigation — вендорен (community-проект). 0 хуков | при авторинге PRD/epic и создании GitHub-issue из CCPM flow. Не UI → вне R6.0/R6.1/R14 |
| **product-management** *(6 команд `/write-spec`, `/roadmap-update` и др.)* | `anthropics/knowledge-work-plugins` (plugin `product-management@knowledge-work-plugins`, Anthropic Verified) | product-strategy церемонии (problem→spec, roadmap, stakeholder updates, research synthesis, competitive analysis, metrics review). Категория: **project-management** (Tooling #42). 0 хуков | при product-strategy work: написание спеки, обновление роадмапа, анализ конкурентов. Не UI → вне R6.0/R6.1/R14 |
| **Design plugin** *(Design Critique / Accessibility Audit / UX Writing / Research Synthesis)* | `anthropics/knowledge-work-plugins` (Anthropic Verified) | дизайн-критика и UX — ревью макетов, дизайн-уровневый a11y-аудит, UX-копирайт, research synthesis. Категория: **design-tooling** (Tooling #46, вне UI-пула) | при дизайн-критике макета, UX-анализе, написании микрокопирайта — pre-code (ADR-006). Не подменяет FD #30 (генерация) и `requesting-code-review`. Не UI → вне R6.0/R6.1/R14 |
**Блок 1 — note (v3.3):** **mermaid-skill** (Tooling #37, генератор C4/architecture-диаграмм) — вендоренный сторонний скил в `.claude/skills/mermaid/` (`WH-2099/mermaid-skill`, MIT), **не** через marketplace и **не** в `enabledPlugins`. Пассивная утилита (генерация Mermaid-исходника), не решатель — формально вне типологии трёх блоков; регистрируется здесь для полноты. Категория **architecture-tooling**, вне R6/R14.
**Блок 1 — note (v3.6):** **CCPM** (Tooling #41) — аналогично mermaid-skill: вендоренный сторонний скил в `.claude/skills/ccpm/` (`automazeio/ccpm`, MIT), **не** через marketplace и **не** в `enabledPlugins`. Активируется через `/pm` в Claude Code. Категория **project-management**, вне R6/R14.
**Блок 1 — note (v3.7):** **deptrac** (Tooling #43, архитектурный fitness-гейт) — Composer dev-dependency (`deptrac/deptrac` v4.6.1, BSD-3), **не** marketplace-плагин и **не** в `enabledPlugins`; врезан как lefthook pre-commit job 10 (`deptrac analyse` на staged `app/**/*.php`). CLI-инструмент статического анализа направления зависимостей между слоями — не решатель; формально вне типологии трёх блоков, регистрируется здесь для полноты. Категория **architecture-tooling** (как adr-kit/architecture-patterns), вне R6.0/R6.1/R14.
**Отмена:** через удаление из `enabledPlugins` в `~/.claude/settings.json` или через live-override `/имя-плагина` (R0.4.B) на одно действие.
#### Блок 2: Built-in skills Claude Code (всегда доступны через `Skill` tool по `/имя`)
@@ -440,6 +447,9 @@ Stack — **головной**. Все плагины вне stack'а — **ин
| **github** | `.mcp.json` (HTTP `api.githubcopilot.com/mcp`, требует `GITHUB_TOKEN`) | официальный hosted GitHub MCP — issues, PRs, search-code, list-commits и т.д. | при работе с GitHub-объектами (PR, issues, branches) |
| **sentry** *(`@sentry/mcp-server@0.33.0+`, official; tools `mcp__sentry__*`)* | `.mcp.json` (env `SENTRY_URL` + `SENTRY_AUTH_TOKEN` через PowerShell User scope) | **debug-runtime MCP** — production error investigation в self-hosted Sentry (Yandex Cloud per CLAUDE.md §2). Категория **debug-runtime** (v2.1+) — отдельная от UI-пула (UPM/21st) и инфраструктурного (claude-md-management). Tooling #34. Pending Sentry instance deployment (Б-1) | при investigation runtime error / post-incident debug. READ-ONLY scope (`org:read`/`project:read`/`event:read`). Не trigger'ит R6.0/R6.1 фильтры и не входит в R14 pipeline UI-генераторов |
| **redis** *(`@modelcontextprotocol/server-redis@2025.4.25`, deprecated Anthropic source)* | `.mcp.json` (`redis://localhost:6379` к Memurai Windows service) | **debug-runtime MCP** — Redis/Memurai inspection (очереди, кэш, Pest --parallel race conditions per quirk 72/77). Категория **debug-runtime** (v2.1+). Tooling #35. Memurai PONG verified Task 4. Migration plan на community alternative (e.g., `@easy-mcps/redis-mcp-server@1.0.8`) post-MVP | при debug Redis runtime. **READ-ONLY usage обязателен** — никаких DEL/FLUSHDB/SET/LPUSH из Claude. Manual mutations — через `memurai-cli` напрямую заказчиком. Cosmetic deprecation warning в stderr |
| **Universal Icons MCP** *(`universal-icons` сервер, tools `search_icons`/`get_icon`/`health_check`)* | `.mcp.json` (`npx -y mcp-universal-icons`, MIT) | поиск/вставка SVG-иконок — 10 коллекций включая Lucide (брендовый icon-set). Категория: **design-tooling** (Tooling #45) | при поиске иконки для Vue-компонента. НЕ запрашивать jsx/Tailwind-формат (R6.0). Материал, не решатель (R10.2). Вне R14 pipeline |
| **Figma MCP** *(remote `https://mcp.figma.com/mcp`)***DEFERRED** | `.mcp.json` (HTTP-транспорт, OAuth) — не установлен, precondition: Figma-аккаунт | извлечение дизайн-токенов/variables из Figma-источника (`get_variable_defs`). **Extract-only** (ADR-006) — code-gen не используется. Категория: **design-tooling** (Tooling #44) | DEFERRED (FM2 — у проекта нет Figma-файла). При появлении Figma-аккаунта. Extract-only — FD #30 остаётся UI-решателем. Вне R6.0/R6.1/R14 |
| **openapi-mcp-server** *(`openapi` сервер, tools `mcp__openapi__*`)* | `.mcp.json` (stdio MCP, env `OPENAPI_SPEC_URL` или локальный файл) | **integration-tooling MCP** — OpenAPI/Swagger-спецификации интеграций (inspect, introspect внешних API). Категория: **integration-tooling** (Tooling §4.22 #47). Раздел A3 карты «Программирование — интеграции (API, вебхуки)». Off-phase | при работе с внешними API-интеграциями (introspection спецификаций). **READ-ONLY introspection** — не мутировать внешние API из Claude. Не trigger'ит R6.0/R6.1 фильтры и не входит в R14 pipeline UI-генераторов. Вне R6/R14 |
**Отмена:** через удаление из `~/.claude.json` или `.mcp.json`. Live-override через `/команду` для MCP не предусмотрен — MCP-серверы не имеют slash-интерфейса.
@@ -765,6 +775,10 @@ Pipeline активируется при одновременном выполн
## История версий
- **v3.8 (2026-05-17)** — A4 design-tooling: R10.1 Блок 1 +Design plugin (`anthropics/claude-plugins-official`, Anthropic Verified) — дизайн-критика и UX, новая 8-я off-phase подкатегория design-tooling; Блок 3 +Universal Icons MCP (`npx -y mcp-universal-icons`, MIT) + Figma MCP (remote `https://mcp.figma.com/mcp`, DEFERRED). Не UI → вне R6.0/R6.1/R14. Содержательных изменений R0–R14: 0. Связано: Tooling v2.8, Pravila v1.22, CLAUDE.md v2.8. План `docs/superpowers/plans/2026-05-17-a4-design-tooling-integration.md`.
- **v3.7 (2026-05-17)** — A6-расширение deptrac: R10.1 Блок 1 +note «Блок 1 — note (v3.7)» — **deptrac** (`deptrac/deptrac` v4.6.1, BSD-3, Composer dev-dependency — **не** marketplace-плагин и **не** в `enabledPlugins`, регистрируется нотой как mermaid-skill/CCPM; врезан lefthook pre-commit job 10). Категория **architecture-tooling** (Tooling #43, раздел A6 карты) — 4-й инструмент подкатегории, не UI → вне R6.0/R6.1/R14. Содержательных изменений R0–R14: 0. Связано: Tooling v2.7, Pravila v1.21, CLAUDE.md v2.7. План `docs/superpowers/plans/2026-05-17-deptrac-architecture-fitness-integration.md`.
- **v3.6 (2026-05-17)** — C9 project-management: R10.1 Блок 1 (`enabledPlugins`) +2 строки (**CCPM** `automazeio/ccpm` вендорен в `.claude/skills/ccpm/`, **product-management** `anthropics/knowledge-work-plugins` Anthropic Verified) + Блок 1 note про CCPM (vendored скил, аналог mermaid-skill). Новая категория **project-management** (Tooling #41-42, раздел C9 карты «Управление проектами») — 7-я off-phase подкатегория, не UI → вне R6.0/R6.1/R14. Содержательных изменений R0–R14: 0. Связано: Tooling v2.6, Pravila v1.20, CLAUDE.md v2.6. План `docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md`.
- **v3.5 (2026-05-17)** — фактическая правка R10.1 Блок 1 (security-guidance): хук **блокирующий** (`sys.exit(2)`, одноразовый speed-bump per «файл+правило» за сессию), не warn-only. Содержательных изменений R0–R14: 0. Связано: Tooling v2.5, Pravila v1.19, CLAUDE.md v2.5. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
+16 -3
View File
@@ -1,10 +1,16 @@
# Правила работы Claude в проекте «Лидерра»
**Версия:** v1.20 (17.05.2026)
**Версия:** v1.23 (17.05.2026)
**Дата:** 17.05.2026
**Назначение:** настройки проекта (Project instructions) — Claude читает этот файл в каждом чате и следует правилам ниже.
**Статус документа:** ✅ утверждён. Содержимое скопировано в поле "Project instructions" Claude.ai. Файл хранится в архиве как служебный документ.
**Что изменилось в v1.23 относительно v1.22:** §13.2 +абзац «Off-phase integration-tooling» — формализованы инструменты раздела A3 карты «Программирование — интеграции (API, вебхуки)» (#47 openapi-mcp-server, api-docs agent) как девятая off-phase подкатегория; READ-ONLY introspection. Связано: Tooling v2.9 / PSR_v1 v3.9 / CLAUDE.md v2.9. План `docs/superpowers/plans/2026-05-17-a3-integration-tooling-integration.md`.
**Что изменилось в v1.22 относительно v1.21:** §13.2 +абзац «Off-phase design-tooling» — формализованы 3 инструмента раздела A4 карты «Дизайн (UI/UX, графика, бренд)» (#44 Figma MCP DEFERRED, #45 Universal Icons MCP, #46 Design plugin) как восьмая off-phase подкатегория; §13.2 PSR_v1 cross-ref синхронизирован → v3.8+. Связано: Tooling v2.8 / PSR_v1 v3.8 / CLAUDE.md v2.8. План `docs/superpowers/plans/2026-05-17-a4-design-tooling-integration.md`.
**Что изменилось в v1.21 относительно v1.20:** §13.2 абзац «Off-phase architecture-tooling» расширен — формализован 4-й инструмент раздела A6 карты «Архитектура систем» (#43 deptrac, Composer dev-dependency `deptrac/deptrac` v4.6.1; архитектурный fitness-гейт направления зависимостей / границ слоёв, врезан в lefthook pre-commit job 10). Категория architecture-tooling без изменений (та же пятая off-phase). Связано: Tooling v2.7 / PSR_v1 v3.7 / CLAUDE.md v2.7; план `docs/superpowers/plans/2026-05-17-deptrac-architecture-fitness-integration.md`.
**Что изменилось в v1.20 относительно v1.19:** §13.2 +абзац «Off-phase project-management» — формализованы 2 инструмента раздела C9 карты «Управление проектами» (#41 CCPM, #42 product-management) как седьмая off-phase категория; §13.2 PSR_v1 cross-ref v3.5+ → v3.6+. Связано: Tooling v2.6 / PSR_v1 v3.6 / CLAUDE.md v2.6; план `docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md`.
**Что изменилось в v1.19 относительно v1.18:** §13.2 абзац «Off-phase audit-security» — фактическая правка характеристики #40 Security Guidance: это **блокирующий** PreToolUse-хук (`sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию), не warn-only. §13.2 PSR_v1 cross-ref v3.4+ → v3.5+. Связано: Tooling v2.5 / PSR_v1 v3.5 / CLAUDE.md v2.5; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
@@ -567,6 +573,9 @@ P0 = блокер старта спринта или регуляторного
| **v1.18** | **17.05.2026** | D3 audit-security: §13.2 +абзац «Off-phase audit-security» — формализованы 2 инструмента раздела D3 карты «Аудит и управление рисками» (#39 Trail of Bits Skills, #40 Security Guidance) как шестая off-phase категория; §13.2 PSR_v1 cross-ref v3.3+ → v3.4+. Связано: Tooling v2.4 / PSR_v1 v3.4 / CLAUDE.md v2.4. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. |
| **v1.19** | **17.05.2026** | Фактическая правка §13.2 абзаца «Off-phase audit-security»: #40 Security Guidance — **блокирующий** PreToolUse-хук (`sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию), не warn-only; §13.2 PSR_v1 cross-ref v3.4+ → v3.5+. Связано: Tooling v2.5 / PSR_v1 v3.5 / CLAUDE.md v2.5. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. |
| **v1.20** | **17.05.2026** | C9 project-management: §13.2 +абзац «Off-phase project-management» — формализованы 2 инструмента раздела C9 карты «Управление проектами» (#41 CCPM, #42 product-management) как седьмая off-phase категория, отдельная от UI-пула / infrastructure / debug-runtime / orchestration / architecture-tooling / audit-security; не UI → вне R6.0/R6.1/R14. §13.2 PSR_v1 cross-ref v3.5+ → v3.6+. Связано: Tooling v2.6 (§4.16-4.17 + §0 счётчик 40→42), PSR_v1 v3.6 (R10.1 Блок 1 +2 строки + note), CLAUDE.md v2.6 (§3.3 +#41-42). Через manual Edit (Pravila/PSR_v1/Tooling) + `/claude-md-management:claude-md-improver` (CLAUDE.md per §5 п.10). План `docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md`. Архитектурных изменений в §§1–12 + §§13.1, 13.314: 0. |
| **v1.21** | **17.05.2026** | A6-расширение deptrac: §13.2 абзац «Off-phase architecture-tooling» расширен — формализован 4-й инструмент раздела A6 (#43 deptrac, Composer dev-dependency `deptrac/deptrac` v4.6.1 BSD-3; архитектурный fitness-гейт направления зависимостей / границ слоёв, врезан в lefthook pre-commit job 10, конфиг `app/deptrac.yaml` 13 слоёв, первый прогон 0 нарушений → baseline не нужен, red-green доказан). Категория architecture-tooling без изменений. Связано: Tooling v2.6→v2.7 (§4.18 + §0 счётчик 42→43), PSR_v1 v3.6→v3.7 (R10.1 Блок 1 note), CLAUDE.md v2.6→v2.7 (§3.3 +#43). Через manual Edit (Pravila/PSR_v1/Tooling) + `/claude-md-management:claude-md-improver` (CLAUDE.md per §5 п.10). План `docs/superpowers/plans/2026-05-17-deptrac-architecture-fitness-integration.md`. Архитектурных изменений в §§1–12 + §§13.1, 13.314: 0. |
| **v1.22** | **17.05.2026** | A4 design-tooling: §13.2 +абзац «Off-phase design-tooling» — формализованы 3 инструмента раздела A4 карты «Дизайн (UI/UX, графика, бренд)» (#44 Figma MCP / #45 Universal Icons MCP / #46 Design plugin) как восьмая off-phase подкатегория, отдельная от UI-пула / infrastructure / debug-runtime / orchestration / architecture-tooling / audit-security / project-management; не UI → вне R6.0/R6.1/R14. §13.2 PSR_v1 cross-ref v3.3+ → v3.8+ (текст застрял на v3.3+ — changelog v1.18-v1.20 заявлял bump'ы, но §13.2 не обновлялся; теперь синхронизирован). Связано: Tooling v2.8 / PSR_v1 v3.8 / CLAUDE.md v2.8. План `docs/superpowers/plans/2026-05-17-a4-design-tooling-integration.md`. |
| **v1.23** | **17.05.2026** | A3 integration-tooling: §13.2 +абзац «Off-phase integration-tooling» — формализованы инструменты раздела A3 карты «Программирование — интеграции (API, вебхуки)» (#47 `openapi-mcp-server`, Tooling §4.22; `api-docs` agent, claude-flow, без Tooling-номера) как девятая off-phase подкатегория, отдельная от всех предыдущих; не UI → вне R6.0/R6.1/R14. READ-ONLY introspection. Регулируются PSR_v1 R10.1 Блок 3. Связано: Tooling v2.9 / PSR_v1 v3.9 / CLAUDE.md v2.9. План `docs/superpowers/plans/2026-05-17-a3-integration-tooling-integration.md`. Архитектурных изменений в §§1–12 + §§13.1, 13.314: 0. |
---
@@ -682,7 +691,7 @@ P0 = блокер старта спринта или регуляторного
### 13.2. Парность со Superpowers + расширенный пул UI-инструментов (v1.8)
Frontend Design и `obra/superpowers` (v5.1.0, 14 skills) — **парный stack одного приоритетного уровня**. Оба плагина подключены к gate stack'а одновременно, между ними нет иерархии. Координация — через [docs/Plugin_stack_rules_v1.md](Plugin_stack_rules_v1.md) **v3.3+ (R0 — top-of-stack gate; ruflo big-bang 15.05.2026 + реколлаж 16.05.2026; полный детализированный реестр правил в PSR_v1)**.
Frontend Design и `obra/superpowers` (v5.1.0, 14 skills) — **парный stack одного приоритетного уровня**. Оба плагина подключены к gate stack'а одновременно, между ними нет иерархии. Координация — через [docs/Plugin_stack_rules_v1.md](Plugin_stack_rules_v1.md) **v3.8+ (R0 — top-of-stack gate; ruflo big-bang 15.05.2026 + реколлаж 16.05.2026; полный детализированный реестр правил в PSR_v1)**.
**Расширенный пул UI-инструментов (v1.8)** добавляет к paired-stack ядру два внешних плагина в роли **инструментов** (R10.1 PSR_v1, не решателей):
@@ -701,12 +710,16 @@ Frontend Design и `obra/superpowers` (v5.1.0, 14 skills) — **парный sta
**Off-phase MCP debug-runtime (отдельная категория, введена v1.13 Pravila, 13.05.2026 day +1):** `@sentry/mcp-server@0.33.0+` (Tooling #34, server `sentry` в `.mcp.json`) — отладка production errors в self-hosted Sentry (Yandex Cloud per CLAUDE.md §2; pending Б-1 ООО registration); `@modelcontextprotocol/server-redis@2025.4.25` (Tooling #35, server `redis` в `.mcp.json`; deprecated Anthropic source; Memurai PONG verified Task 4) — отладка Redis/Memurai runtime (очереди, кэш, Pest --parallel races per quirk 72/77). **Категория отдельная** от UI-пула (§13.2 paired-stack + UPM + 21st) и от infrastructure (claude-md-management §13.2 paragraph выше) — **не trigger'ит R6.0/R6.1 stack-фильтры** (READ-ONLY, не модифицируют code/UI/CLAUDE.md) и **не входит в R14 pipeline** UI-генераторов. Регулируется PSR_v1 R10.1 Блок 3 (`.mcp.json`-серверы) как debug-runtime off-phase tool. READ-ONLY usage обязателен — никаких mutation операций (DEL/FLUSHDB/SET/LPUSH для Redis; write actions для Sentry). Установлены retrospective на feat/claude-automation `6f7e7d7` (sentry) + `bd4ec48` (redis), merged через PR #3 (`cc5f63b`). PSR_v1 cross-ref: **v3.6+**, R10.1 Блок 3.
**Off-phase architecture-tooling (отдельная категория, v1.17, 17.05.2026):** три инструмента раздела A6 карты «Архитектура систем» — `adr-kit` (Tooling #36, marketplace `rvdbreemen/adr-kit`; ADR-решения в `docs/adr/`, `adr-judge` врезан в lefthook pre-commit job 9 декларативно, без `--llm`), `mermaid-skill` (Tooling #37, вендоренный сторонний скил `.claude/skills/mermaid/`; C4/architecture-диаграммы), `architecture-patterns` (Tooling #38, marketplace `secondsky/claude-skills`; knowledge-only справочник паттернов). **Категория отдельная** от UI-пула (UPM/21st), infrastructure (claude-md-management) и debug-runtime (Sentry/Redis) — не UI, **не trigger'ит R6.0/R6.1 stack-фильтры и не входит в R14 pipeline**. Регулируется PSR_v1 R10.1 Блок 1 (adr-kit, architecture-patterns) + Блок 1 note (mermaid-skill — вендоренный скил вне типологии трёх блоков). Установлены 17.05.2026 на ветке `feat/a6-architecture-tooling`; план `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`.
**Off-phase architecture-tooling (отдельная категория, v1.17, 17.05.2026; +deptrac v1.21):** четыре инструмента раздела A6 карты «Архитектура систем» — `adr-kit` (Tooling #36, marketplace `rvdbreemen/adr-kit`; ADR-решения в `docs/adr/`, `adr-judge` врезан в lefthook pre-commit job 9 декларативно, без `--llm`), `mermaid-skill` (Tooling #37, вендоренный сторонний скил `.claude/skills/mermaid/`; C4/architecture-диаграммы), `architecture-patterns` (Tooling #38, marketplace `secondsky/claude-skills`; knowledge-only справочник паттернов), `deptrac` (Tooling #43, Composer dev-dependency `deptrac/deptrac` v4.6.1 BSD-3; архитектурный fitness-гейт направления зависимостей / границ слоёв — врезан в lefthook pre-commit job 10, конфиг `app/deptrac.yaml` 13 слоёв, чистый PHP без вызовов LLM). **Категория отдельная** от UI-пула (UPM/21st), infrastructure (claude-md-management) и debug-runtime (Sentry/Redis) — не UI, **не trigger'ит R6.0/R6.1 stack-фильтры и не входит в R14 pipeline**. Регулируется PSR_v1 R10.1 Блок 1 (adr-kit, architecture-patterns) + Блок 1 notes (mermaid-skill — вендоренный скил, deptrac — composer dev-dep — оба вне типологии трёх блоков). Установлены 17.05.2026 (adr-kit/mermaid/architecture-patterns — ветка `feat/a6-architecture-tooling`, план `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`; deptrac — план `docs/superpowers/plans/2026-05-17-deptrac-architecture-fitness-integration.md`).
**Off-phase audit-security (отдельная категория, v1.18, 17.05.2026):** инструменты раздела D3 карты «Аудит и управление рисками» — `Trail of Bits Skills` (Tooling #39, marketplace `trailofbits/skills`; курированный субсет 8 audit-плагинов — security-аудит diff, supply-chain риск зависимостей; CC-BY-SA-4.0, marketplace-плагин не вендорен), `Security Guidance` (Tooling #40, marketplace `anthropics/claude-plugins-official`; один **блокирующий** PreToolUse-хук — inline-предупреждения уязвимостей, `sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию). Дополнительно `/security-review` (Anthropic built-in, customized в `.claude/commands/security-review.md` с проектным FP-фильтром RLS/ПДн/economy-хуки). **Категория отдельная** от UI-пула, infrastructure, debug-runtime, orchestration и architecture-tooling — не UI, **не trigger'ит R6.0/R6.1 stack-фильтры и не входит в R14 pipeline**. Регулируется PSR_v1 R10.1 Блок 1. Установлены 17.05.2026 на ветке `feat/d3-audit-risk-tooling`; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
**Off-phase project-management (отдельная категория, v1.20, 17.05.2026):** инструменты раздела C9 карты «Управление проектами» — `CCPM` (Tooling #41, вендоренный standalone-скил в `.claude/skills/ccpm/`, `automazeio/ccpm` MIT; PRD→эпик→GitHub-issue→код с трассируемостью через `/pm` flow; GitHub-issue-backed модель ADR-004; bus-factor — community-проект, mitigation — вендоринг), `product-management` (Tooling #42, marketplace `anthropics/knowledge-work-plugins`, Anthropic Verified; product-strategy церемонии: `/write-spec`, `/roadmap-update`, `/stakeholder-update`, `/synthesize-research`, `/competitive-brief`, `/metrics-review`). GitHub MCP (Tooling #3) reuse с `projects` toolset для GitHub Projects v2 (не новый слот). **Категория отдельная** от UI-пула, infrastructure, debug-runtime, orchestration, architecture-tooling и audit-security — не UI, **не trigger'ит R6.0/R6.1 stack-фильтры и не входит в R14 pipeline**. Регулируется PSR_v1 R10.1 Блок 1. Установлены 17.05.2026 на ветке `feat/c9-project-management-tooling`; план `docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md`.
**Off-phase design-tooling (A4):** #44 Figma MCP (extract-only, DEFERRED — у проекта нет Figma-аккаунта), #45 Universal Icons MCP, #46 Design plugin — раздел A4 карты «Дизайн (UI/UX, графика, бренд)». Восьмая off-phase подкатегория. Не UI-решатели → вне расширенного UI-пула, вне R6.0/R6.1/R14 PSR_v1. Границы — ADR-006 (Figma extract-only; Design plugin a11y дизайн-уровня — Pa11y остаётся техническим SoT; Design Critique pre-code). Регулируются PSR_v1 R10.1 (Блок 1 — Design plugin; Блок 3 — Figma MCP / Universal Icons MCP).
**Off-phase integration-tooling (A3):** Инструменты раздела A3 карты «Программирование — интеграции (API, вебхуки)» — #47 `openapi-mcp-server` (Tooling §4.22; введён A3-интеграцией 17.05.2026) и `api-docs` agent (claude-flow, узел карты A3 без отдельного Tooling-номера). Off-phase, не UI → вне R6/R14 PSR_v1. READ-ONLY introspection. Регулируются PSR_v1 R10.1 Блок 3.
### 13.3. Скоуп
| Тип задачи | Кто отвечает |
+72 -5
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,44 @@
# ADR-005: Architecture-fitness enforcement via deptrac
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
Map section A6 «Архитектура систем» had tools to *record* (adr-kit #36),
*visualize* (mermaid-skill #37) and *reference* (architecture-patterns #38)
architecture — but nothing enforced that the code keeps matching the layered
architecture (Controller → Service → Model …). `adr-judge` (adr-kit) enforces
only what is hand-written as a regex in an ADR `## Enforcement` block — too
narrow for dependency-direction rules.
## Decision
- Adopt **deptrac** (`deptrac/deptrac`, BSD-3, v4.x) — a declarative, zero-LLM
static-analysis tool — as the layer-dependency gate, wired as lefthook
pre-commit job 10.
- The layer model and ruleset live in `app/deptrac.yaml` (conservative — it
enforces only inward/upward-violating directions: a Model must not depend on
a Service, a Service must not depend on the Http layer, etc.).
- The first `deptrac analyse` reported **0 violations** — the codebase already
conforms to the layer model (481 allowed cross-layer dependencies, 0
violations) — so no baseline file is needed. If future drift is ever
intentionally accepted, a `deptrac.baseline.yaml` can be generated then; for
now the gate runs clean with no suppressions.
## Consequences
- Positive: layer drift is caught at commit time, deterministically, at zero
LLM cost (the AK6 principle adr-kit was built under).
- Positive: deptrac's code-derived graph output doubles as a drift-proof
C4-component diagram (`docs/architecture/c4-component-layers.md`).
- Risk: a too-strict ruleset produces noise — mitigated by the conservative
ruleset (verified: 0 violations against the current codebase).
- Risk: deptrac is third-party — bus-factor; mitigated by composer-lock pinning.
## Enforcement
The layer rules live in `app/deptrac.yaml`, enforced by lefthook pre-commit
job 10 (`deptrac analyse`) — not by an `adr-judge` regex. This ADR therefore
carries no `adr-judge`-parsed Enforcement clause.
@@ -0,0 +1,42 @@
# ADR-006: A4 design-tooling boundaries
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
The A4 «Дизайн» map section adds three tools to the existing FD #30 / UPM #31 /
21st #32: Figma MCP (#44), Universal Icons MCP (#45), Design plugin (#46). Two
overlaps with Frontend Design (#30) were identified during selection and must be
closed by explicit rule, not left implicit. Figma MCP install is deferred (no
Figma account yet); the boundary still applies the moment it is connected.
## Decision
1. **Figma MCP — extract-only.** Figma MCP is used solely for design-data and
design-token reads (e.g. `get_variable_defs`). Its design-to-code generation
capability is NOT used. Frontend Design (#30) remains the sole UI solver
(PSR_v1 R10.2 — external plugins are read-only for decisions). Figma MCP output
is material for FD/Claude, never a substitute solver.
2. **Design plugin a11y is design-level, pre-code.** The Design plugin's
Accessibility Audit operates at design-critique level. Pa11y remains the single
source of truth for technical a11y (CLAUDE.md §5 п.3, PSR_v1 R8 — Pa11y wins on
conflict). FD continues to cover a11y-principles during design. Three tiers, no
override.
3. **Design plugin critique runs in R2 phase 1.** The Design plugin's Design
Critique runs in the research / pre-code planning phase, not the phase-8 review.
Phase-8 review stays with the PSR_v1 R5 aspect-split (FD owns the UI/UX aspect)
plus the Superpowers review skills. The Design plugin does not replace
`superpowers:requesting-code-review`.
## Consequences
- A Figma MCP code-generation call is a process violation (CLAUDE.md §5 п.6).
- Universal Icons (#45) covers UI icons; 21st `logo_search` covers brand logos —
distinct, both retained.
- These boundaries are mirrored as PSR_v1 R10.1 rows + R6/R10/R14 notes.
## Enforcement
None — role boundaries, verified by code review, not by a regex gate.
+699
View File
@@ -0,0 +1,699 @@
# Стартовый OpenAPI-скелет (smoke A3-интеграции, 2026-05-17).
# Покрывает только группу /api/deals*. Полная спека REST API — отдельная задача вне scope A3.
openapi: 3.1.0
info:
title: Лидерра CRM — Deals API
version: 0.1.0
description: >
Частичная спека REST API Лидерры. Скелет покрывает только группу /api/deals*.
Все эндпоинты требуют аутентификации (Laravel Sanctum) и разрешают доступ
только к сделкам текущего тенанта (RLS + middleware tenant).
servers:
- url: https://app.liderra.ru
description: Production
security:
- sanctumCookie: []
tags:
- name: deals
description: Управление сделками (CRUD + bulk-операции + экспорт)
paths:
/api/deals:
get:
operationId: deals.index
tags: [deals]
summary: Список сделок тенанта
description: >
Возвращает сделки тенанта с пагинацией. Поддерживает два режима пагинации:
keyset (cursor) — O(1) глубины; offset-based — backward-совместимость.
При count_only=true возвращает только {"total": N} без строк.
parameters:
- name: status_in[]
in: query
description: Фильтр по статусам (можно несколько)
required: false
schema:
type: array
items:
type: string
style: form
explode: true
- name: project_id
in: query
required: false
schema:
type: integer
- name: manager_id
in: query
required: false
schema:
type: integer
- name: search
in: query
description: ILIKE-поиск по phone / contact_name
required: false
schema:
type: string
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 500
default: 100
- name: offset
in: query
description: Используется только без cursor (OFFSET-режим)
required: false
schema:
type: integer
minimum: 0
default: 0
- name: cursor
in: query
description: base64-encoded keyset cursor от предыдущей страницы
required: false
schema:
type: string
- name: only_deleted
in: query
description: Показывать только soft-deleted (корзина)
required: false
schema:
type: boolean
default: false
- name: count_only
in: query
description: 'Вернуть только {"total": N}, без строк (для бейджа сайдбара)'
required: false
schema:
type: boolean
default: false
responses:
'200':
description: Успех
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/DealsListResponse'
- $ref: '#/components/schemas/DealsCountResponse'
examples:
list:
summary: Обычный список
value:
deals:
- id: 42
tenant_id: 1
project_id: 5
project_name: "B1-Москва"
phone: "+79001234567"
contact_name: "Иван Иванов"
status: "new"
manager_id: 3
manager_name: "Мария К."
manager_initials: "МК"
received_at: "2026-05-17T10:00:00+03:00"
total: 1
offset: 0
limit: 100
next_cursor: null
count_only:
summary: count_only=true
value:
total: 157
'401':
$ref: '#/components/responses/Unauthorized'
post:
operationId: deals.store
tags: [deals]
summary: Создать сделку вручную
description: >
Ручное создание сделки из UI (не webhook). source_crm_id = NULL,
баланс не списывается. Project резолвится или создаётся по project_name.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DealStoreRequest'
example:
project_name: "B1-Москва"
phone: "+79001234567"
contact_name: "Иван Иванов"
status: "new"
manager_id: 3
comment: "Заинтересован, перезвонить в 15:00"
responses:
'201':
description: Сделка создана
content:
application/json:
schema:
$ref: '#/components/schemas/DealStoreResponse'
'422':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
delete:
operationId: deals.bulkDestroy
tags: [deals]
summary: Bulk soft-delete сделок
description: Мягкое удаление нескольких сделок. Идемпотентно.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BulkIdsRequest'
example:
ids: [42, 43, 44]
responses:
'200':
description: Результат удаления
content:
application/json:
schema:
$ref: '#/components/schemas/BulkDestroyResponse'
example:
deleted: 3
requested: 3
'422':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
/api/deals/{id}:
get:
operationId: deals.show
tags: [deals]
summary: Детали сделки + лог активности
description: >
Возвращает сделку с relations и до 50 последних событий activity_log.
Используется в DealDetailDrawer.
parameters:
- $ref: '#/components/parameters/DealId'
responses:
'200':
description: Успех
content:
application/json:
schema:
$ref: '#/components/schemas/DealShowResponse'
'404':
$ref: '#/components/responses/NotFound'
'401':
$ref: '#/components/responses/Unauthorized'
patch:
operationId: deals.update
tags: [deals]
summary: Редактировать сделку (частичное обновление)
description: >
Частичное обновление: comment / manager_id / status. Каждое изменение
пишется в activity_log. NO-OP (значение не изменилось) — лог не пишется.
parameters:
- $ref: '#/components/parameters/DealId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DealUpdateRequest'
example:
status: "in_progress"
manager_id: 5
comment: "Уточнить условия"
responses:
'200':
description: Сделка обновлена
content:
application/json:
schema:
$ref: '#/components/schemas/DealUpdateResponse'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
/api/deals/export:
post:
operationId: deals.export
tags: [deals]
summary: Экспорт сделок в CSV или XLSX
description: >
Streaming-экспорт через OpenSpout (O(1) memory). Формат по умолчанию — csv.
CSV: UTF-8 + BOM, разделитель ;. XLSX: bold-заголовок, sheet «Сделки».
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DealExportRequest'
example:
ids: [42, 43, 44]
format: csv
responses:
'200':
description: Файл экспорта (streamed)
content:
text/csv:
schema:
type: string
format: binary
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
schema:
type: string
format: binary
'422':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
/api/deals/transition:
post:
operationId: deals.bulkTransition
tags: [deals]
summary: Bulk смена статуса сделок
description: >
Массовая смена статуса. Bulk-UPDATE + bulk-INSERT в activity_log (2 запроса
вместо N). NO-OP (status уже совпадает) не считается.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DealTransitionRequest'
example:
ids: [42, 43]
status: "in_progress"
responses:
'200':
description: Результат перехода
content:
application/json:
schema:
$ref: '#/components/schemas/BulkTransitionResponse'
example:
updated: 2
requested: 2
status: "in_progress"
'422':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
/api/deals/restore:
post:
operationId: deals.bulkRestore
tags: [deals]
summary: Bulk восстановление soft-deleted сделок
description: Восстановление из корзины. Идемпотентно (уже живые — NO-OP).
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BulkIdsRequest'
example:
ids: [42, 43]
responses:
'200':
description: Результат восстановления
content:
application/json:
schema:
$ref: '#/components/schemas/BulkRestoreResponse'
example:
restored: 2
requested: 2
'422':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
components:
securitySchemes:
sanctumCookie:
type: apiKey
in: cookie
name: liderra_session
description: Laravel Sanctum session cookie (SPA auth)
parameters:
DealId:
name: id
in: path
required: true
description: ID сделки (только цифры, regex [0-9]+)
schema:
type: integer
minimum: 1
schemas:
DealSummary:
type: object
description: Краткое представление сделки (для списка)
properties:
id:
type: integer
tenant_id:
type: integer
project_id:
type: integer
project_name:
type: [string, "null"]
phone:
type: string
contact_name:
type: [string, "null"]
status:
type: string
description: Slug из таблицы lead_statuses
manager_id:
type: [integer, "null"]
manager_name:
type: [string, "null"]
manager_initials:
type: [string, "null"]
received_at:
type: [string, "null"]
format: date-time
DealDetail:
type: object
description: Полное представление сделки (для DealDetailDrawer)
properties:
id:
type: integer
tenant_id:
type: integer
project_id:
type: integer
project_name:
type: [string, "null"]
phone:
type: string
contact_name:
type: [string, "null"]
comment:
type: [string, "null"]
status:
type: string
manager_id:
type: [integer, "null"]
manager_name:
type: [string, "null"]
manager_initials:
type: [string, "null"]
received_at:
type: [string, "null"]
format: date-time
assigned_at:
type: [string, "null"]
format: date-time
ActivityEvent:
type: object
properties:
id:
type: integer
event:
type: string
description: >
Тип события: deal.created, deal.status_changed, deal.assigned,
deal.commented, deal.deleted, deal.restored
context:
type: object
description: Произвольный JSON-контекст (from/to/source и т.п.)
additionalProperties: true
created_at:
type: [string, "null"]
format: date-time
actor:
type: [object, "null"]
properties:
id:
type: integer
name:
type: string
initials:
type: string
DealsListResponse:
type: object
properties:
deals:
type: array
items:
$ref: '#/components/schemas/DealSummary'
total:
type: [integer, "null"]
description: Только в OFFSET-режиме (без cursor)
offset:
type: [integer, "null"]
description: Только в OFFSET-режиме
limit:
type: integer
next_cursor:
type: [string, "null"]
description: base64-encoded cursor для следующей страницы
DealsCountResponse:
type: object
description: Ответ при count_only=true
properties:
total:
type: integer
DealShowResponse:
type: object
properties:
deal:
$ref: '#/components/schemas/DealDetail'
events:
type: array
items:
$ref: '#/components/schemas/ActivityEvent'
DealStoreRequest:
type: object
required: [project_name, phone]
properties:
project_name:
type: string
maxLength: 255
phone:
type: string
maxLength: 20
contact_name:
type: [string, "null"]
maxLength: 255
status:
type: [string, "null"]
maxLength: 50
description: Slug из lead_statuses; по умолчанию "new"
manager_id:
type: [integer, "null"]
minimum: 1
comment:
type: [string, "null"]
maxLength: 5000
DealStoreResponse:
type: object
properties:
deal:
type: object
properties:
id:
type: integer
tenant_id:
type: integer
project_id:
type: integer
phone:
type: string
status:
type: string
contact_name:
type: [string, "null"]
manager_id:
type: [integer, "null"]
received_at:
type: string
format: date-time
message:
type: string
example: "Сделка создана."
DealUpdateRequest:
type: object
description: Все поля опциональны; хотя бы одно должно присутствовать
properties:
comment:
type: [string, "null"]
maxLength: 5000
manager_id:
type: [integer, "null"]
minimum: 1
status:
type: [string, "null"]
maxLength: 50
DealUpdateResponse:
type: object
properties:
deal:
type: object
properties:
id:
type: integer
tenant_id:
type: integer
project_id:
type: integer
phone:
type: string
contact_name:
type: [string, "null"]
comment:
type: [string, "null"]
status:
type: string
manager_id:
type: [integer, "null"]
received_at:
type: [string, "null"]
format: date-time
assigned_at:
type: [string, "null"]
format: date-time
DealExportRequest:
type: object
required: [ids]
properties:
ids:
type: array
items:
type: integer
minimum: 1
minItems: 1
maxItems: 10000
format:
type: string
enum: [csv, xlsx]
default: csv
DealTransitionRequest:
type: object
required: [ids, status]
properties:
ids:
type: array
items:
type: integer
minimum: 1
minItems: 1
maxItems: 1000
status:
type: string
maxLength: 50
description: Slug из lead_statuses
BulkIdsRequest:
type: object
required: [ids]
properties:
ids:
type: array
items:
type: integer
minimum: 1
minItems: 1
maxItems: 1000
BulkTransitionResponse:
type: object
properties:
updated:
type: integer
description: Реально изменённых (без NO-OP)
requested:
type: integer
status:
type: string
BulkDestroyResponse:
type: object
properties:
deleted:
type: integer
requested:
type: integer
BulkRestoreResponse:
type: object
properties:
restored:
type: integer
requested:
type: integer
ErrorMessage:
type: object
properties:
message:
type: string
ValidationErrorResponse:
type: object
properties:
message:
type: string
errors:
type: object
additionalProperties:
type: array
items:
type: string
responses:
Unauthorized:
description: Не аутентифицирован
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
example:
message: "Unauthenticated."
NotFound:
description: Сделка не найдена
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
example:
message: "Сделка не найдена."
ValidationError:
description: Ошибка валидации
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
example:
message: "The given data was invalid."
errors:
status:
- "Slug не найден в lead_statuses."
+6
View File
@@ -18,6 +18,7 @@ See [ADR-000](../adr/ADR-000-adr-process.md) for the full boundary rule.
| File | View | Mermaid type |
|---|---|---|
| [c4-context.md](c4-context.md) | System Context — Лидерра and its actors / external systems | `C4Context` |
| [c4-component-layers.md](c4-component-layers.md) | Component — backend layer dependency graph (deptrac-generated, drift-proof) | `flowchart` |
## Regenerating
@@ -28,3 +29,8 @@ block. No local renderer (`mmdc`) is required.
For pattern guidance when shaping a new subsystem, the `architecture-patterns`
skill covers Clean / Hexagonal / layered architecture and Domain-Driven Design.
The component-layer diagram ([c4-component-layers.md](c4-component-layers.md)) is
**not** hand-authored — deptrac generates it from code
(`cd app && php vendor/bin/deptrac analyse --formatter=mermaidjs`), so it cannot
drift. Regenerate it after any change to the layer model in `app/deptrac.yaml`.
+51
View File
@@ -0,0 +1,51 @@
# C4 — Component view: backend layers
The Level-3 (Component) view — the dependency directions between the backend
layers of `app/app/` (Controller, Service, Model, Job, …). Unlike the
hand-drawn [c4-context.md](c4-context.md), this diagram is **generated from
code** by deptrac, so it cannot drift. See [README](README.md) for the boundary
rule and [ADR-005](../adr/ADR-005-architecture-fitness-deptrac.md).
```mermaid
flowchart TD;
Console -->|3| Service;
Console -->|3| Model;
Console -->|2| Job;
Controller -->|190| Model;
Controller -->|11| Request;
Controller -->|8| Service;
Controller -->|1| Mail;
Controller -->|5| Job;
Controller -->|6| Resource;
Middleware -->|1| Model;
Resource -->|1| Model;
Job -->|75| Model;
Job -->|28| Service;
Job -->|4| Mail;
Job -->|8| Exception;
Mail -->|17| Model;
Provider -->|2| Service;
Repository -->|3| Model;
Service -->|87| Model;
Service -->|1| Repository;
Service -->|13| Exception;
Service -->|7| Mail;
Service -->|5| Job;
```
## Notes
- Edge labels are dependency counts. Every edge shown is an **allowed**
direction — `deptrac analyse` reports **0 violations** (481 allowed
cross-layer dependencies, 977 uncovered framework/vendor references). The
layer model and ruleset live in `app/deptrac.yaml`; the conformance gate is
lefthook pre-commit job 10.
- This is the **Component** level, derived from code. To regenerate after a
change to the layer model:
```bash
cd app && php vendor/bin/deptrac analyse --formatter=mermaidjs
```
- The **Context** level ([c4-context.md](c4-context.md) — external systems) is
not code-derived and stays hand-maintained with the `mermaid` skill.
+110 -6
View File
@@ -243,14 +243,17 @@ const NODES = [
{ id: 'claude_setup', label: 'claude-code-setup', group: 'plugins', size: 22, ring: 2, ...pos(2, 90) },
{ id: 'plugin_dev', label: 'plugin-dev', group: 'plugins', size: 22, ring: 2, ...pos(2, 290) },
{ id: 'context7', label: 'context7 (docs MCP)', group: 'plugins', size: 20, ring: 2, ...pos(2, 315) },
// A6 architecture-tooling (17.05.2026) — 2 плагина раздела «Архитектура систем»
// A6 architecture-tooling — adr-kit / architecture-patterns (плагины) + deptrac (composer dev-dep, job 10) — раздел «Архитектура систем»
{ id: 'adr_kit', label: 'adr-kit', group: 'plugins', size: 22, ring: 2, ...pos(2, 240) },
{ id: 'arch_patterns', label: 'architecture-patterns',group: 'plugins', size: 20, ring: 2, ...pos(2, 250) },
{ id: 'deptrac', label: 'deptrac', group: 'plugins', size: 20, ring: 2, ...pos(2, 260) },
// D3 audit-security (17.05.2026) — 2 плагина раздела «Аудит и управление рисками»
{ id: 'tob_skills', label: 'Trail of Bits\nskills', group: 'plugins', size: 22, ring: 2, ...pos(2, 330) },
{ id: 'sec_guidance', label: 'Security\nGuidance', group: 'plugins', size: 20, ring: 2, ...pos(2, 345) },
// C9 project-management-tooling (17.05.2026) — плагин раздела «Управление проектами»
{ id: 'product_mgmt', label: 'product-\nmanagement', group: 'plugins', size: 20, ring: 2, ...pos(2, 355) },
// A4 design-tooling (17.05.2026) — раздел «Дизайн (UI/UX, графика, бренд)» (плагины)
{ id: 'design_plugin', label: 'Design\nplugin', group: 'plugins', size: 20, ring: 2, ...pos(2, 155) },
// ── СКИЛЫ SUPERPOWERS (14) — N sector (090) ────
{ id: 'sk_brainstorm', label: 'brainstorming', group: 'skills_sp', size: 18, ring: 3, ...pos(3, 5) },
@@ -306,15 +309,22 @@ const NODES = [
{ id: 'ag_pvalid', label: 'plugin-dev:\nplugin-validator',group: 'agents', size: 16, ring: 4, ...pos(4, 260) },
{ id: 'ag_skreview', label: 'plugin-dev:\nskill-reviewer', group: 'agents', size: 16, ring: 4, ...pos(4, 275) },
{ id: 'ag_rls', label: 'rls-reviewer', group: 'agents', size: 22, ring: 4, ...pos(4, 315) },
// A3 integration-tooling (17.05.2026) — agent раздела «Программирование — интеграции»
{ id: 'ag_apidocs', label: 'api-docs (agent)', group: 'agents', size: 18, ring: 4, ...pos(4, 175) },
// ── MCP-СЕРВЕРЫ (7) — E (UI) + W (data) ───────
// ── MCP-СЕРВЕРЫ (9) — E (UI) + W (data) ───────
{ id: 'mcp_21st', label: 'MCP: 21st.dev Magic', group: 'mcp', size: 20, ring: 5, ...pos(5, 130) },
// A4 design-tooling (17.05.2026) — MCP-серверы раздела «Дизайн (UI/UX, графика, бренд)»
{ id: 'mcp_figma', label: 'MCP: Figma\n(DEFERRED)', group: 'mcp', size: 18, ring: 5, ...pos(5, 140) },
{ id: 'mcp_icons', label: 'MCP: Universal\nIcons', group: 'mcp', size: 18, ring: 5, ...pos(5, 120) },
{ id: 'mcp_pw', label: 'MCP: playwright', group: 'mcp', size: 22, ring: 5, ...pos(5, 110) },
{ id: 'mcp_gh', label: 'MCP: github', group: 'mcp', size: 22, ring: 5, ...pos(5, 75) },
{ id: 'mcp_boost', label: 'MCP: laravel-boost', group: 'mcp', size: 24, ring: 5, ...pos(5, 290) },
{ id: 'mcp_redis', label: 'MCP: redis', group: 'mcp', size: 22, ring: 5, ...pos(5, 310) },
{ id: 'mcp_sentry', label: 'MCP: sentry', group: 'mcp', size: 22, ring: 5, ...pos(5, 330) },
{ id: 'mcp_semgrep', label: 'MCP: semgrep', group: 'mcp', size: 20, ring: 5, ...pos(5, 350) },
// A3 integration-tooling (17.05.2026) — MCP-сервер раздела «Программирование — интеграции»
{ id: 'mcp_openapi', label: 'MCP: openapi', group: 'mcp', size: 20, ring: 5, ...pos(5, 5) },
// ── LEFTHOOK JOBS (10) — S+W (infra/data) ─────
{ id: 'lh_mdlint', label: 'lefthook:\nmarkdownlint', group: 'lefthook', size: 18, ring: 5, ...pos(5, 185) },
@@ -510,6 +520,12 @@ const EDGES = [
E('psr_v1', 'adr_kit', 'R10.1 блок 1:\narchitecture-tooling'),
E('psr_v1', 'arch_patterns', 'R10.1 блок 1:\narchitecture-tooling'),
E('tooling', 'mermaid_skill', '§4.12: реестр\n(вендоренный скил)'),
E('psr_v1', 'deptrac', 'R10.1 блок 1 note:\narchitecture-tooling'),
// ── A4 DESIGN-TOOLING 17.05.2026 — связи новых узлов ──
E('psr_v1', 'design_plugin', 'R10.1 блок 1:\ndesign-tooling'),
E('psr_v1', 'mcp_icons', 'R10.1 блок 3:\ndesign-tooling'),
E('psr_v1', 'mcp_figma', 'R10.1 блок 3:\ndesign-tooling (DEFERRED)'),
// ── D3 AUDIT-SECURITY 17.05.2026 — связи новых узлов ──
E('psr_v1', 'tob_skills', 'R10.1 блок 1:\naudit-security'),
@@ -521,6 +537,11 @@ const EDGES = [
E('sk_audit_portal', 'sk_regression', 'использует\nна фазе тестов'),
CONFLICT('tob_skills', 'mcp_semgrep', 'TB1: граница разграничена — Semgrep = inline SAST, Trail of Bits = глубокие on-demand аудит-кампании. Параллельное использование разрешено при разных сценариях.', 'GREEN'),
// ── A3 INTEGRATION-TOOLING 17.05.2026 — связи новых узлов ──
E('psr_v1', 'mcp_openapi', 'R10.1 блок 3:\nintegration-tooling'),
E('tooling', 'mcp_openapi', '§4.22 #47 — реестр'),
E('ag_apidocs', 'mcp_openapi', 'спека → MCP-ресурс'),
// ══════════════════════════════════════════════════
// КОНФЛИКТЫ — 3-color classification (iter2 §4)
// 🔴 не закрыт правилом / ⚫ возник на практике / 🟢 закрыт правилом
@@ -724,6 +745,40 @@ const NODE_DETAILS = {
[],
[{ name: 'docs/architecture/', cond: 'C4-диаграммы → c4-context.md' }]
),
deptrac: nd(
'Composer dev-dependency deptrac/deptrac v4.6.1 (BSD-3) — статический анализ направления зависимостей между слоями App\\ (Controller/Service/Model/Job/…). Чистый PHP, 0 вызовов LLM.',
'Архитектурный fitness-гейт: проверяет, что код не нарушает границы слоёв. Конфиг app/deptrac.yaml (13 слоёв) + ruleset; запускается автоматически как lefthook pre-commit job 10 на staged app/**/*.php.',
'Правило PSR_v1 R10.1 блок 1 note (architecture-tooling, off-phase — composer dev-dep, не marketplace-плагин). Первый прогон 0 нарушений → baseline-файл не нужен (red-green доказан). Не UI → вне фильтров R6.0/R6.1/R14. Tooling §4.18, CLAUDE.md §3.3 #43.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 1 note: architecture-tooling' }, { name: 'Tooling', cond: '§4.18 #43 — реестр' }],
[{ name: 'lefthook job 10 (deptrac)', cond: 'врезан как pre-commit гейт направления зависимостей' }],
[{ name: 'docs/architecture/', cond: 'mermaidjs-форматтер → c4-component-layers.md' }]
),
// ── A4 DESIGN-TOOLING (17.05.2026) ──────────────
mcp_figma: nd(
'MCP Figma (#44) — DEFERRED, precondition: Figma-аккаунт. Extract-only (ADR-006): извлечение токенов/variables из источника дизайна. FD #30 остаётся единственным UI-решателем.',
'При наличии Figma-аккаунта и нужде извлечь дизайн-токены/variables напрямую из Figma-файла.',
'DEFERRED — не установлен, требует Figma-аккаунт (Б-1). Не UI-решатель → вне R6.0/R6.1/R14. Extract-only, не генерирует UI-решения. PSR_v1 R10.1 блок 3.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 3: design-tooling (DEFERRED)' }],
[],
[{ name: 'Frontend Design', cond: 'FD остаётся единственным UI-решателем; Figma MCP — только источник токенов' }]
),
mcp_icons: nd(
'MCP Universal Icons (#45) — поиск/вставка SVG-иконок, 10 коллекций включая Lucide. Tools search_icons/get_icon. SVG framework-neutral (R6.0).',
'При поиске иконки из коллекции Lucide или других (Heroicons, Tabler, Phosphor и др.) — получить SVG-исходник для вставки в Vue-компонент.',
'SVG framework-neutral — результат нужно обернуть в Vue-компонент вручную. R6.0 фильтр не блокирует (SVG — не React/Tailwind). Lucide — branded иконочный стек проекта (CLAUDE.md §2). PSR_v1 R10.1 блок 3.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 3: design-tooling' }],
[],
[{ name: 'Frontend Design', cond: 'FD использует результат поиска как материал при UI-задачах' }]
),
design_plugin: nd(
'Плагин Design (#46, Anthropic Verified) — дизайн-критика, a11y-аудит дизайн-уровня, UX-копирайт, research synthesis. Pre-code (ADR-006); Pa11y остаётся техническим a11y SoT.',
'При дизайн-критике макета или компонента, при UX-анализе flow, при написании UX-копирайта, при synthesis пользовательских исследований.',
'Pre-code инструмент (ADR-006) — не генерирует финальный код, даёт дизайн-рекомендации. Pa11y остаётся техническим a11y SoT (§5 п.3). Не UI-решатель в смысле PSR_v1 R14 → вне R14 pipeline. PSR_v1 R10.1 блок 1.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 1: design-tooling' }],
[],
[{ name: 'Frontend Design', cond: 'Design plugin — pre-code критика; FD — post-spec UI-решатель; разные фазы' }]
),
// ── C9 PROJECT-MANAGEMENT-TOOLING (17.05.2026) ──
ccpm: nd(
@@ -778,6 +833,24 @@ const NODE_DETAILS = {
[{ name: 'скил security-review', cond: 'пара в D3 audit-security' }]
),
// ── A3 INTEGRATION-TOOLING (17.05.2026) ──────────
ag_apidocs: nd(
'Агент claude-flow — генерирует OpenAPI-спеку REST API по роутам и контроллерам Laravel. Pattern learning. 0 установки — агент доступен в сессии.',
'При фиксации контракта REST API: генерация/обновление OpenAPI-спеки группы эндпоинтов. Результат — docs/api/.',
'Sub-агент claude-flow — узел карты, но без отдельного номера в реестре Tooling Прил. Н (реестр — plugin-grain; 11 agent-узлов карты так же без Tooling-номеров). Не UI → вне фильтров R6.0/R6.1/R14.',
[{ name: 'CLAUDE.md', cond: '§3.3 — упомянут при #47 openapi-mcp' }],
[],
[{ name: 'MCP: openapi', cond: 'генерирует спеку → openapi-mcp отдаёт её как MCP-ресурс' }]
),
mcp_openapi: nd(
'MCP-сервер (npm, stdio) — отдаёт OpenAPI-спеку как MCP-ресурс/тулы; introspection своей и чужих API при интеграционной разработке.',
'При работе с интеграциями (API/вебхуки) — обращение к структуре OpenAPI-спеки из сессии Claude. READ-ONLY introspection.',
'Правило PSR_v1 R10.1 блок 3 (integration-tooling, off-phase — 9-я подкатегория). stdio-режим, без port-conflict. Не UI → вне фильтров R6.0/R6.1/R14. Tooling §4.22 #47, CLAUDE.md §3.3 #47.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 3: integration-tooling' }, { name: 'Tooling', cond: '§4.22 #47 — реестр' }],
[],
[{ name: 'docs/api/', cond: 'источник OpenAPI-спеки' }]
),
// ── СКИЛЫ SUPERPOWERS ────────────────────────────
sk_brainstorm: nd(
'Продумывает задачу вместе с заказчиком, формулирует варианты A/B/C и согласует дизайн до написания кода.',
@@ -1855,6 +1928,7 @@ const NODE_META = {
adr_kit: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
arch_patterns: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
mermaid_skill: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
deptrac: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
// ── D3 AUDIT-SECURITY 17.05.2026 ──
tob_skills: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
@@ -1865,6 +1939,15 @@ const NODE_META = {
// ── C9 PROJECT-MANAGEMENT-TOOLING 17.05.2026 ──
ccpm: { since: '17.05.2026', changed: '—', uses: null, usesSrc: 'скил' },
product_mgmt: { since: '17.05.2026', changed: '—', uses: null, usesSrc: 'плагин' },
// ── A4 DESIGN-TOOLING 17.05.2026 ──
mcp_figma: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
mcp_icons: { since: '17.05.2026', changed: '—', uses: null, usesSrc: 'MCP' },
design_plugin:{ since: '17.05.2026', changed: '—', uses: null, usesSrc: 'плагин' },
// ── A3 INTEGRATION-TOOLING (17.05.2026) ──
ag_apidocs: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
mcp_openapi: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
};
// Явные парные дубли (Фича 3) — попадают в кнопку «⧉ Дубли».
@@ -1947,7 +2030,7 @@ const SECTIONS = [
{ id: 'E7', bucket: 'E', label: 'Исследования' },
{ id: 'E8', bucket: 'E', label: 'Самообучение Claude' },
];
// Узел -> раздел. Покрывает все 112 узлов карты.
// Узел -> раздел. Покрывает все 118 узлов карты.
const NODE_SECTION = {
// правила (4)
pravila: 'E1', claude_md: 'E1', psr_v1: 'E1', tooling: 'E1',
@@ -1989,12 +2072,26 @@ const NODE_SECTION = {
sk_regression: 'A5',
mem_audit_b: 'E4', mem_audit_c: 'E4', mem_suppliercrm: 'E4', mem_audit12: 'E4',
mem_audit14: 'E4', mem_sprint1: 'E4', mem_sprint2: 'E4', mem_sprint3: 'E4',
// A6 architecture-tooling 17.05.2026 — раздел «Архитектура систем» наполнен
adr_kit: 'A6', arch_patterns: 'A6', mermaid_skill: 'A6',
// A6 architecture-tooling 17.05.2026 — раздел «Архитектура систем» наполнен (+deptrac)
adr_kit: 'A6', arch_patterns: 'A6', mermaid_skill: 'A6', deptrac: 'A6',
// D3 audit-security 17.05.2026 — раздел «Аудит и управление рисками» наполнен
tob_skills: 'D3', sec_guidance: 'D3', sk_security_review: 'D3', sk_audit_portal: 'D3',
// C9 project-management-tooling 17.05.2026 — раздел «Управление проектами» наполнен
ccpm: 'C9', product_mgmt: 'C9',
// A4 design-tooling 17.05.2026 — раздел «Дизайн (UI/UX, графика, бренд)» расширен (3→6 узлов)
mcp_figma: 'A4', mcp_icons: 'A4', design_plugin: 'A4',
// A3 integration-tooling 17.05.2026 — раздел «Программирование — интеграции» наполнен
ag_apidocs: 'A3', mcp_openapi: 'A3',
};
// Вторичная классификация: узел первично в NODE_SECTION, дополнительно — в этих
// разделах (кросс-реф). Введено A3-интеграцией 17.05.2026 — раздел A3 наполняется
// частично кросс-реф существующих интеграционных инструментов. NODE_SECTION 1:1 не трогается.
const NODE_SECTION_SECONDARY = {
mcp_boost: ['A3'],
context7: ['A3'],
ag_pest: ['A3'],
mcp_semgrep: ['A3'],
mcp_sentry: ['A3'],
};
// Производные индексы для рендера панели и Паспорта.
const SECTION_BY_ID = new Map(SECTIONS.map(s => [s.id, s]));
@@ -2002,6 +2099,9 @@ const SECTION_NODES = new Map(SECTIONS.map(s => [s.id, []]));
NODES.forEach(n => {
const sid = NODE_SECTION[n.id];
if (sid && SECTION_NODES.has(sid)) SECTION_NODES.get(sid).push(n.id);
(NODE_SECTION_SECONDARY[n.id] || []).forEach(secId => {
if (SECTION_NODES.has(secId)) SECTION_NODES.get(secId).push(n.id);
});
});
// ════════════════════════════════════════════════════
@@ -2108,7 +2208,11 @@ function showNodeLegend(nodeId) {
document.getElementById('ld-since').textContent = meta.since || '—';
document.getElementById('ld-changed').textContent = meta.changed || '—';
const _sec = NODE_SECTION[nodeId] ? SECTION_BY_ID.get(NODE_SECTION[nodeId]) : null;
document.getElementById('ld-section').textContent = _sec ? `${_sec.id} · ${_sec.label}` : '—';
const _secExtra = (NODE_SECTION_SECONDARY[nodeId] || [])
.map(id => SECTION_BY_ID.get(id)).filter(Boolean);
let _secText = _sec ? `${_sec.id} · ${_sec.label}` : '—';
if (_secExtra.length) _secText += ` (+${_secExtra.map(s => s.id).join(', ')})`;
document.getElementById('ld-section').textContent = _secText;
const usesEl = document.getElementById('ld-uses');
if (meta.uses === null || meta.uses === undefined) {
@@ -0,0 +1,240 @@
# Sprint 3E — Settings placeholder-tabs (D6/D7) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Убрать из `SettingsView` 4 placeholder-вкладки («Проекты», «Команда», «Интеграции», «Тихие часы»), которые показывают «В разработке» — UI не должен обещать нереализованный функционал.
**Architecture:** Чистое удаление. `SettingsView` оставляет 4 рабочие вкладки (Профиль, Безопасность, API и Webhook, Уведомления). Компонент `PlaceholderTab.vue` удаляется целиком. Spec-тест приводится к 4-вкладочному состоянию + добавляется регрессионная проверка, что placeholder'ы пропали.
**Tech Stack:** Vue 3 (`<script setup>` + TypeScript), Vuetify 3, Vitest 4 + @vue/test-utils.
---
## Контекст и per-tab решение (audit D6/D7)
Аудит портала ([docs/superpowers/specs/2026-05-15-portal-audit-design.md](../specs/2026-05-15-portal-audit-design.md)):
- **D6** — «PlaceholderTab × 4 — реализовать или скрыть (decide per-tab)».
- **D7** — «SettingsView left-rail: 8 tab'ов, 4 заглушки — Hide-if-not-implemented».
**Per-tab решение — скрыть все 4** (реализация каждой = отдельный эпик, вне scope Sprint 3E):
| Вкладка | Решение | Обоснование |
|---|---|---|
| Проекты | скрыть | Полноценный `/projects` view уже есть — вкладка чистый дубль. |
| Команда | скрыть | Нет ни `/team`-маршрута, ни backend; реализация = отдельный L-эпик со schema-работой, не в графике спринтов. |
| Интеграции | скрыть | Telegram/1С/JivoSite/Yandex SSO — все внешне-блокированы (Б-1 и пр.). |
| Тихие часы | скрыть | `quiet_hours` отсутствует в `db/schema.sql`; ТЗ §17.8 спецификацию даёт, но колонок/backend нет — отдельный эпик. |
«Импорт»-вкладка из предложения D7 — это H8 (Sprint 4, миграция §6), **вне scope Sprint 3E**.
Скрытие не отменяет ТЗ-требования (Команда / Тихие часы §17.8) — вкладки вернутся при реальной реализации соответствующих модулей.
---
## File Structure
- Modify: `app/resources/js/views/SettingsView.vue` — убрать 4 placeholder-вкладки, `placeholderProps` computed, импорт и использование `PlaceholderTab`, неиспользуемый импорт `computed`; обновить docblock.
- Delete: `app/resources/js/views/settings/PlaceholderTab.vue` — компонент больше не используется.
- Test: `app/tests/Frontend/SettingsView.spec.ts` — 8 → 4 вкладки, убрать placeholder-тест, добавить регрессию.
**НЕ трогать:** `app/dev-indices.json` (авто-генерируемый временной DevIndex-фичей, уже `M` в git status — не стейджить, не коммитить); `SettingsView.story.vue` (ссылается только на `SettingsView`, не на `PlaceholderTab` — изменений не требует).
---
## Task 1: Скрыть 4 placeholder-вкладки в SettingsView
**Files:**
- Modify: `app/resources/js/views/SettingsView.vue`
- Delete: `app/resources/js/views/settings/PlaceholderTab.vue`
- Test: `app/tests/Frontend/SettingsView.spec.ts`
- [ ] **Step 1: Привести spec-тест к 4-вкладочному состоянию (failing test first)**
Заменить весь файл `app/tests/Frontend/SettingsView.spec.ts` на:
```ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia } from 'pinia';
import { createVuetify } from 'vuetify';
import SettingsView from '../../resources/js/views/SettingsView.vue';
describe('SettingsView.vue', () => {
const factory = () =>
mount(SettingsView, {
global: { plugins: [createPinia(), createVuetify()] },
});
it('монтируется и содержит заголовок «Настройки»', () => {
const wrapper = factory();
expect(wrapper.find('h1').text()).toBe('Настройки');
});
it('содержит ровно 4 nav-tabs (placeholder-вкладки убраны, audit D6/D7)', () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
expect(items.length).toBe(4);
});
it('содержит все 4 названия рабочих вкладок', () => {
const wrapper = factory();
const text = wrapper.text();
const labels = ['Профиль', 'Безопасность', 'API и Webhook', 'Уведомления'];
labels.forEach((l) => expect(text).toContain(l));
});
it('не содержит placeholder-вкладок и текста «В разработке»', () => {
const wrapper = factory();
const railText = wrapper.find('.tabs-rail').text();
['Команда', 'Интеграции', 'Тихие часы'].forEach((l) => expect(railText).not.toContain(l));
expect(wrapper.text()).not.toContain('В разработке');
});
it('по умолчанию показывает вкладку «Профиль»', () => {
const wrapper = factory();
const text = wrapper.text();
// ProfileTab содержит поля Имя / Фамилия (split из «Полное имя» в audit D1) и Тайм-зона.
expect(text).toContain('Имя');
expect(text).toContain('Фамилия');
expect(text).toContain('Тайм-зона');
});
it('переключение на «Уведомления» показывает матрицу 8×3', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
const notifItem = items.find((i) => i.text().includes('Уведомления'));
await notifItem!.trigger('click');
await wrapper.vm.$nextTick();
const text = wrapper.text();
expect(text).toContain('События × каналы');
// 8 типов событий из schema users.notification_preferences.
['Новый лид', 'Напоминание', 'Низкий баланс', 'Нулевой баланс', 'Анонсы и промо'].forEach((e) =>
expect(text).toContain(e),
);
});
it('переключение на «Безопасность» показывает 2FA и сессии', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
const secItem = items.find((i) => i.text().includes('Безопасность'));
await secItem!.trigger('click');
await wrapper.vm.$nextTick();
const text = wrapper.text();
expect(text).toContain('Двухфакторная авторизация');
expect(text).toContain('Активные сессии');
});
it('переключение на «API и Webhook» показывает API-ключ и signing secret', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
const apiItem = items.find((i) => i.text().includes('API'));
await apiItem!.trigger('click');
await wrapper.vm.$nextTick();
const text = wrapper.text();
expect(text).toContain('API-ключ');
expect(text).toContain('Signing secret');
expect(text).toContain('HMAC');
});
});
```
Изменения относительно текущего файла: тест «ровно 8 nav-tabs» → 4; «8 названий вкладок» → 4 рабочих; тест «placeholder-вкладки показывают „В разработке"» удалён, вместо него — регрессия «не содержит placeholder-вкладок».
- [ ] **Step 2: Прогнать тест — убедиться, что падает**
Run: `cd app && npm run test:vue -- --run SettingsView`
Expected: FAIL — текущий `SettingsView.vue` рендерит 8 вкладок, тесты «ровно 4 nav-tabs» и «не содержит placeholder-вкладок» красные.
- [ ] **Step 3: Удалить 4 placeholder-вкладки из `SettingsView.vue`**
Заменить блок `<script setup>` (строки 161) на:
```vue
<script setup lang="ts">
/**
* Settings — настройки тенанта/пользователя. 4 рабочие вкладки.
*
* Источник дизайна: liderra_v8_handoff/concepts/v8_settings.html.
* Полностью реализованы (с UI-разводкой): Профиль, Безопасность, API и Webhook,
* Уведомления (матрица 8×3 по schema v8.7 §4 users.notification_preferences).
*
* Аудит D6/D7 (Sprint 3E, 2026-05-16): placeholder-вкладки Проекты/Команда/
* Интеграции/Тихие часы убраны — UI не должен обещать «в разработке».
* «Проекты» дублировали /projects; «Команда» и «Тихие часы» (ТЗ §17.8)
* требуют schema+backend (отдельные эпики); «Интеграции» внешне-блокированы (Б-1).
* Вкладки вернутся при реальной реализации соответствующих модулей.
*/
import { ref } from 'vue';
import ApiTab from './settings/ApiTab.vue';
import NotificationsTab from './settings/NotificationsTab.vue';
import ProfileTab from './settings/ProfileTab.vue';
import SecurityTab from './settings/SecurityTab.vue';
interface Tab {
id: string;
label: string;
icon: string;
}
const tabs: Tab[] = [
{ id: 'profile', label: 'Профиль', icon: 'mdi-account-outline' },
{ id: 'security', label: 'Безопасность', icon: 'mdi-shield-lock-outline' },
{ id: 'api', label: 'API и Webhook', icon: 'mdi-api' },
{ id: 'notifications', label: 'Уведомления', icon: 'mdi-bell-outline' },
];
const activeTab = ref('profile');
</script>
```
В `<template>` заменить блок `<v-card variant="outlined" class="tab-pane pa-6">…</v-card>` (строки 8999) на:
```vue
<v-card variant="outlined" class="tab-pane pa-6">
<ProfileTab v-if="activeTab === 'profile'" />
<SecurityTab v-else-if="activeTab === 'security'" />
<ApiTab v-else-if="activeTab === 'api'" />
<NotificationsTab v-else-if="activeTab === 'notifications'" />
</v-card>
```
`<style scoped>` — без изменений. Удаляются: импорт `PlaceholderTab`, импорт `computed` (становится неиспользуемым — остаётся только `ref`), `placeholderProps` computed, 4 строки placeholder-вкладок в `tabs`, `<PlaceholderTab>` в шаблоне.
- [ ] **Step 4: Удалить `PlaceholderTab.vue`**
Удалить файл `app/resources/js/views/settings/PlaceholderTab.vue` (`git rm`). Компонент больше нигде не импортируется (grep `PlaceholderTab` по `app/resources/js` → только `SettingsView.vue`, который мы уже почистили).
- [ ] **Step 5: Прогнать тест — убедиться, что зелёный**
Run: `cd app && npm run test:vue -- --run SettingsView`
Expected: PASS — все 8 тестов SettingsView зелёные.
- [ ] **Step 6: Проверить vue-tsc и ESLint**
Run: `cd app && npm run type-check` → 0 ошибок (важно: неиспользуемый импорт `computed` удалён, иначе vue-tsc/ESLint ругнётся).
Run: `cd app && npm run lint:vue` → 0 ошибок.
- [ ] **Step 7: Полный прогон Vitest (регрессия)**
Run: `cd app && npm run test:vue`
Expected: 0 failed. Базовый объём перед изменением — 100 файлов / 838 passed / 3 skipped; после Sprint 3E удалён 1 тест → ожидается 100 файлов / 837 passed / 3 skipped (точное число — из реального вывода, не экстраполировать).
- [ ] **Step 8: Commit**
```bash
git add app/resources/js/views/SettingsView.vue app/tests/Frontend/SettingsView.spec.ts
git rm app/resources/js/views/settings/PlaceholderTab.vue
git commit -m "feat(settings): D6/D7 — убрать placeholder-вкладки SettingsView"
```
**НЕ стейджить** `app/dev-indices.json` (авто-генерируемый, pre-existing `M`).
---
## Self-Review
- Spec coverage: D6 (4 placeholder-вкладки убраны) ✅; D7 (left-rail 8→4) ✅. «Импорт»-вкладка из D7 — H8/Sprint 4, явно вне scope.
- Placeholder scan: нет TODO/TBD; весь код приведён дословно.
- Type consistency: `tabs` остаётся `Tab[]`; `activeTab``ref('profile')`; `computed` удалён вместе с единственным потребителем `placeholderProps`.
@@ -0,0 +1,536 @@
# A3 Integration-Tooling 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:** Наполнить пустой раздел A3 «Программирование — интеграции (API, вебхуки)» карты `automation-graph.html` — формализовать 2 новых интеграционных инструмента + кросс-реф 5 существующих, синхронизировать 4 нормативных файла.
**Architecture:** Параллельно A6/D3. Новый аддитивный слой `NODE_SECTION_SECONDARY` в карте (`NODE_SECTION` 1:1 не трогается) даёт кросс-реф существующих узлов в A3. `openapi-mcp-server` — реальная установка (`.mcp.json`), `api-docs` agent — 0-install (claude-flow). Нормативка — A6/D3-паттерн (Tooling §4.22 + PSR_v1 R10.1 + Pravila §13.2 + CLAUDE.md через `claude-md-improver`).
**Tech Stack:** `automation-graph.html` (vis.js, vanilla JS), `.mcp.json` (stdio MCP), Markdown (Tooling/PSR_v1/Pravila/CLAUDE.md), claude-flow `api-docs` agent, npm/npx.
**Спецификация:** [docs/superpowers/specs/2026-05-17-a3-integration-tooling-design.md](../specs/2026-05-17-a3-integration-tooling-design.md)
---
## Verification Approach
Интеграция — **документация + конфиг + vis.js-карта**, прикладного кода (Laravel/Vue) не трогает → unit-TDD-поверхности нет (как у A6/D3). Верификация по задачам:
- lefthook pre-commit (gitleaks / markdownlint / cspell / adr-judge) — на каждом коммите;
- MCP-smoke — `openapi-mcp-server` поднимается в stdio;
- визуальный smoke карты — 118 узлов, 0 JS-ошибок в консоли, панель «Разделы» показывает A3;
- регрессия `quick` (lint/format/type-check) — перед финальным коммитом нормативки.
Если cspell блокирует новый валидный термин — добавить в `cspell-words.txt` (в том же коммите).
**Изоляция:** работа в worktree `.claude/worktrees/a3-integration-tooling` (ветка `feat/a3-integration-tooling`). В native-worktree `lefthook` может быть не в PATH (квирк #97) → pre-commit хуки молча пропускаются; cspell/gitleaks гонять вручную перед коммитом/push.
---
## File Structure
- Create: `docs/api/openapi.yaml` — стартовый OpenAPI-скелет (smoke, deals API)
- Modify: `.mcp.json` — +`openapi` server entry
- Modify: `docs/automation-graph.html``NODE_SECTION_SECONDARY` слой + 2 узла + рендер
- Modify: `docs/Tooling_v8_3.md` — §4.22 + §0 счётчик, v2.8→v2.9
- Modify: `docs/Plugin_stack_rules_v1.md` — R10.1 Блок 3, v3.8→v3.9
- Modify: `docs/Pravila_raboty_Claude_v1_1.md` — §13.2, v1.22→v1.23
- Modify: `CLAUDE.md` — через `/claude-md-management:claude-md-improver`, v2.8→v2.9
- Modify: `cspell-words.txt` — новые термины по мере надобности
- Modify: `memory/project_automation_map.md`, `memory/reference_archive.md` — после push
---
## Task 1: OpenAPI-скелет через api-docs agent (smoke)
**Files:**
- Create: `docs/api/openapi.yaml`
- Read (контекст для агента): `app/routes/api.php`, `app/app/Http/Controllers/` (deals-контроллеры)
- [ ] **Step 1: Найти роуты deals API**
Run: `Grep` по `app/routes/api.php` паттерн `deals` (output_mode content). Зафиксировать список эндпоинтов `/api/deals*` (index/show/store/update/transition/destroy/restore/export).
- [ ] **Step 2: Dispatch api-docs agent**
Через `Agent` tool, `subagent_type: api-docs`, model `sonnet` (механическая генерация). Промпт: «Сгенерируй OpenAPI 3.1 скелет ТОЛЬКО для группы эндпоинтов `/api/deals*` проекта Лидерра (Laravel 13). Источник — `app/routes/api.php` + контроллеры. Только paths + базовые request/response shapes, без полной схемы компонентов. Результат — валидный YAML, верни raw-текст файла.»
- [ ] **Step 3: Записать результат**
Записать вывод агента в `docs/api/openapi.yaml`. В шапку добавить комментарий:
```yaml
# Стартовый OpenAPI-скелет (smoke A3-интеграции, 2026-05-17).
# Покрывает только группу /api/deals*. Полная спека REST API — отдельная задача вне scope A3.
```
- [ ] **Step 4: Валидировать YAML**
Run: `npx --yes @redocly/cli@latest lint docs/api/openapi.yaml` (или `npx --yes @stoplight/spectral-cli lint`).
Expected: парсится без fatal-ошибок (warning'и о неполноте допустимы — это скелет).
Если CLI недоступен — минимум: `node -e "require('js-yaml').load(require('fs').readFileSync('docs/api/openapi.yaml','utf8'))"` → без исключения.
- [ ] **Step 5: Commit**
```bash
git add docs/api/openapi.yaml
git commit -m "docs(a3): OpenAPI skeleton for /api/deals — A3 smoke artifact"
```
Если cspell блокирует — добавить термины (`openapi`, `redocly`, `spectral` и т.п.) в `cspell-words.txt`, `git add` его, повторить коммит.
---
## Task 2: Установить и сконфигурировать openapi-mcp-server
**Files:**
- Modify: `.mcp.json` (корень репозитория)
- [ ] **Step 1: Определить точное имя npm-пакета**
Run: `npm view @ivotoby/openapi-mcp-server version` — основной кандидат (GitHub `ivo-toby/mcp-openapi-server`).
Fallback при ошибке: `npm view openapi-mcp-server version`, затем `npm view mcp-openapi-server version`.
Зафиксировать имя пакета с непустой версией и репозиторием `ivo-toby/mcp-openapi-server`. Обозначить как `<PKG>`.
- [ ] **Step 2: Прочитать текущий `.mcp.json`**
Run: `Read` по `.mcp.json`. Зафиксировать формат существующих stdio-серверов (`redis`, `sentry`) — `command` / `args` / `env`.
- [ ] **Step 3: Добавить server-блок `openapi`**
В объект `mcpServers` добавить рядом с `redis`/`sentry` (stdio, через `npx`, без глобальной установки — как redis MCP):
```json
"openapi": {
"command": "npx",
"args": ["-y", "<PKG>"],
"env": {
"API_BASE_URL": "http://localhost",
"OPENAPI_SPEC_PATH": "./docs/api/openapi.yaml"
}
}
```
Точные имена env-переменных свериться с README пакета (Step 1 дал репозиторий) — `API_BASE_URL` / `OPENAPI_SPEC_PATH` либо CLI-флаги `--api-base-url` / `--openapi-spec`. Использовать тот вариант, что в README пакета.
- [ ] **Step 4: Smoke — сервер поднимается**
Run: `npx -y <PKG> --help` (проверка, что пакет ставится и запускается на native-Windows).
Expected: печатает usage без краша. Если падает (native-Windows несовместимость / кириллица в пути, квирк #26) — **fallback:** оставить server-блок в `.mcp.json` закомментированным-эквивалентом не выйдет (JSON не поддерживает комментарии) → задокументировать в Tooling §4.22 статус «pending: native-Windows install не верифицирован» (прецедент — Sentry MCP «pending Б-1»), узел карты остаётся. Зафиксировать факт в коммите Step 5.
- [ ] **Step 5: Commit**
```bash
git add .mcp.json
git commit -m "feat(a3): register openapi-mcp-server in .mcp.json"
```
---
## Task 3: Карта — слой NODE_SECTION_SECONDARY + интеграция в рендер
**Files:**
- Modify: `docs/automation-graph.html` (3 точки: после `NODE_SECTION`, build `SECTION_NODES`, `ld-section` в Паспорте, `showSectionsLegend`)
- [ ] **Step 1: Добавить объект `NODE_SECTION_SECONDARY`**
После закрывающей `};` объекта `NODE_SECTION` (локализовать Grep'ом: закрывающая `};` объекта `NODE_SECTION` перед `const SECTION_BY_ID`) вставить:
```js
// Вторичная классификация: узел первично в NODE_SECTION, дополнительно — в этих
// разделах (кросс-реф). Введено A3-интеграцией 17.05.2026 — раздел A3 наполняется
// частично кросс-реф существующих интеграционных инструментов. NODE_SECTION 1:1 не трогается.
const NODE_SECTION_SECONDARY = {
mcp_boost: ['A3'],
context7: ['A3'],
ag_pest: ['A3'],
mcp_semgrep: ['A3'],
mcp_sentry: ['A3'],
};
```
- [ ] **Step 2: Модифицировать build `SECTION_NODES`**
Текущий код (локализовать Grep'ом `const SECTION_NODES = new Map`):
```js
const SECTION_NODES = new Map(SECTIONS.map(s => [s.id, []]));
NODES.forEach(n => {
const sid = NODE_SECTION[n.id];
if (sid && SECTION_NODES.has(sid)) SECTION_NODES.get(sid).push(n.id);
});
```
Заменить на:
```js
const SECTION_NODES = new Map(SECTIONS.map(s => [s.id, []]));
NODES.forEach(n => {
const sid = NODE_SECTION[n.id];
if (sid && SECTION_NODES.has(sid)) SECTION_NODES.get(sid).push(n.id);
(NODE_SECTION_SECONDARY[n.id] || []).forEach(secId => {
if (SECTION_NODES.has(secId)) SECTION_NODES.get(secId).push(n.id);
});
});
```
- [ ] **Step 3: Модифицировать строку «Раздел» Паспорта**
Текущий код (локализовать Grep'ом `ld-section` в `showNodeLegend`):
```js
const _sec = NODE_SECTION[nodeId] ? SECTION_BY_ID.get(NODE_SECTION[nodeId]) : null;
document.getElementById('ld-section').textContent = _sec ? `${_sec.id} · ${_sec.label}` : '—';
```
Заменить на:
```js
const _sec = NODE_SECTION[nodeId] ? SECTION_BY_ID.get(NODE_SECTION[nodeId]) : null;
const _secExtra = (NODE_SECTION_SECONDARY[nodeId] || [])
.map(id => SECTION_BY_ID.get(id)).filter(Boolean);
let _secText = _sec ? `${_sec.id} · ${_sec.label}` : '—';
if (_secExtra.length) _secText += ` (+${_secExtra.map(s => s.id).join(', ')})`;
document.getElementById('ld-section').textContent = _secText;
```
- [ ] **Step 4: Проверить, что счётчик в `showSectionsLegend` уже корректен**
(Локализовать Grep'ом `nodeIds.length` в `showSectionsLegend`): `nodeIds.length` берётся из `SECTION_NODES.get(sec.id)` — после Step 2 счётчик автоматически учитывает кросс-реф. Правок не требуется. Зафиксировать факт (no-op проверка).
- [ ] **Step 5: Визуальный smoke (промежуточный)**
Открыть `docs/automation-graph.html` через Playwright MCP (`browser_navigate` file:// — квирк #90: file:// отвергается → использовать локальный сервер `npx -y serve docs -l 8123` + `browser_navigate http://localhost:8123/automation-graph.html`). Нажать «📂 Разделы». Раздел A3 пока показывает 5 узлов (context7/Boost/Pest/Semgrep/Sentry) — новых узлов ещё нет. Консоль (`browser_console_messages`) — 0 ошибок.
- [ ] **Step 6: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): NODE_SECTION_SECONDARY layer — cross-ref nodes into A3"
```
---
## Task 4: Карта — 2 новых узла A3
**Files:**
- Modify: `docs/automation-graph.html` (5 точек: `NODES`, `NODE_SECTION`, `NODE_DETAILS` блок `nd()`, `NODE_TIMELINE`, `EDGES`, комментарий-счётчик)
- [ ] **Step 1: Добавить узел `ag_apidocs` в `NODES`**
В секции агентов (после `ag_rls`, формат-образец `ag_pest` — Grep `id: 'ag_pest'``group: 'agents'`, `ring: 4`) вставить:
```js
// A3 integration-tooling (17.05.2026) — agent раздела «Программирование — интеграции»
{ id: 'ag_apidocs', label: 'api-docs (agent)', group: 'agents', size: 18, ring: 4, ...pos(4, 85) },
```
`pos(4, 85)` — свободный угол между `ag_guide` (`pos(4,70)`) и `hk_session` (`pos(4,100)`); при перекрытии на визуальном smoke (Step 6 Task 5) сдвинуть на ±5.
- [ ] **Step 2: Добавить узел `mcp_openapi` в `NODES`**
В секции MCP-серверов (после `mcp_semgrep`, формат-образец `mcp_boost` — Grep `id: 'mcp_boost'``group: 'mcp'`, `ring: 5`) вставить:
```js
// A3 integration-tooling (17.05.2026) — MCP-сервер раздела «Программирование — интеграции»
{ id: 'mcp_openapi', label: 'MCP: openapi', group: 'mcp', size: 20, ring: 5, ...pos(5, 330) },
```
`pos(5, 330)` — свободный угол после `mcp_redis` (`pos(5,310)`); при перекрытии сдвинуть.
- [ ] **Step 2a: Свериться с фактическим положением узлов MCP/agents**
Run: `Grep` по `automation-graph.html` паттерн `id: 'mcp_semgrep'|id: 'ag_rls'` — подтвердить точку вставки и отсутствие конфликта углов `pos()` с соседями.
- [ ] **Step 3: Добавить записи в `NODE_SECTION`**
В объекте `NODE_SECTION` после строки с A6-узлами (`adr_kit: 'A6', arch_patterns: 'A6', mermaid_skill: 'A6',`) или после D3-узлов — добавить:
```js
// A3 integration-tooling 17.05.2026 — раздел «Программирование — интеграции» наполнен
ag_apidocs: 'A3', mcp_openapi: 'A3',
```
- [ ] **Step 4: Добавить `nd()`-детали в `NODE_DETAILS`**
После D3-блока `nd()` (`tob_skills`/`sec_guidance`, локализовать Grep'ом `tob_skills`) добавить (формат-образец `adr_kit` — Grep `adr_kit: nd\(` — 6 аргументов `nd()`: что делает / когда / ограничения / кому подчиняется / кто подчиняется / с кем работает):
```js
// ── A3 INTEGRATION-TOOLING (17.05.2026) ──────────
ag_apidocs: nd(
'Агент claude-flow — генерирует OpenAPI-спеку REST API по роутам и контроллерам Laravel. Pattern learning. 0 установки — агент доступен в сессии.',
'При фиксации контракта REST API: генерация/обновление OpenAPI-спеки группы эндпоинтов. Результат — docs/api/.',
'Sub-агент claude-flow — узел карты, но без отдельного номера в реестре Tooling Прил. Н (реестр — plugin-grain; 11 agent-узлов карты так же без Tooling-номеров). Не UI → вне фильтров R6.0/R6.1/R14.',
[{ name: 'CLAUDE.md', cond: '§3.3 — упомянут при #47 openapi-mcp' }],
[],
[{ name: 'MCP: openapi', cond: 'генерирует спеку → openapi-mcp отдаёт её как MCP-ресурс' }]
),
mcp_openapi: nd(
'MCP-сервер (npm, stdio) — отдаёт OpenAPI-спеку как MCP-ресурс/тулы; introspection своей и чужих API при интеграционной разработке.',
'При работе с интеграциями (API/вебхуки) — обращение к структуре OpenAPI-спеки из сессии Claude. READ-ONLY introspection.',
'Правило PSR_v1 R10.1 блок 3 (integration-tooling, off-phase — 9-я подкатегория). stdio-режим, без port-conflict. Не UI → вне фильтров R6.0/R6.1/R14. Tooling §4.22 #47, CLAUDE.md §3.3 #47.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 3: integration-tooling' }, { name: 'Tooling', cond: '§4.22 #47 — реестр' }],
[],
[{ name: 'docs/api/', cond: 'источник OpenAPI-спеки' }]
),
```
- [ ] **Step 5: Добавить записи в `NODE_TIMELINE`**
После A6/D3-записей в `NODE_TIMELINE` (формат-образец `skill_creator` — Grep `skill_creator:.*since``{ since, changed, uses, usesSrc }`) добавить:
```js
// ── A3 INTEGRATION-TOOLING (17.05.2026) ──
ag_apidocs: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
mcp_openapi: { since: '17.05.2026', changed: '—', uses: null, usesSrc: '—' },
```
- [ ] **Step 6: Добавить рёбра в `EDGES`**
После D3-блока рёбер (локализовать Grep'ом `E('psr_v1', 'sec_guidance'`) добавить:
```js
// ── A3 INTEGRATION-TOOLING 17.05.2026 — связи новых узлов ──
E('psr_v1', 'mcp_openapi', 'R10.1 блок 3:\nintegration-tooling'),
E('tooling', 'mcp_openapi', '§4.22 #47 — реестр'),
E('ag_apidocs', 'mcp_openapi', 'спека → MCP-ресурс'),
```
- [ ] **Step 7: Обновить комментарий-счётчик `NODE_SECTION`**
(Grep-комментарий `Покрывает все NNN узлов карты` — сейчас 116, ставим 118): `// Узел -> раздел. Покрывает все 116 узлов карты.``118 узлов`.
Run: `Grep` по `automation-graph.html` паттерн `\b(116|117|118)\b.*узлов` и `рёбер` — найти прочие счётчики/метрики, если есть, инкрементировать (узлы +2, рёбра +3). Если иных счётчиков нет — зафиксировать факт.
- [ ] **Step 8: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): A3 nodes — api-docs agent + openapi MCP, section «Программирование — интеграции»"
```
---
## Task 5: Визуальный smoke карты
**Files:** нет правок — только проверка.
- [ ] **Step 1: Поднять локальный сервер и открыть карту**
Run (background): `npx -y serve docs -l 8123`. Через Playwright MCP: `browser_navigate http://localhost:8123/automation-graph.html`.
- [ ] **Step 2: Проверить граф**
`browser_console_messages` — 0 ошибок JS. Граф рендерится, 2 новых узла (`api-docs (agent)`, `MCP: openapi`) видны в секторах agents/mcp, без перекрытий. При перекрытии — вернуться в Task 4 Step 1/2, скорректировать угол `pos()`.
- [ ] **Step 3: Проверить панель «Разделы»**
Нажать «📂 Разделы». Раздел **A3** не пустой, показывает **7 узлов** (api-docs, openapi + 5 кросс-реф). Клик по узлу `MCP: openapi` → Паспорт, строка «Раздел» = `A3 · Программирование — интеграции (API, вебхуки)`. Клик по `MCP: laravel-boost` → строка «Раздел» = `A1 · … (+A3)`.
- [ ] **Step 4: Скриншот-доказательство**
`browser_take_screenshot` панели «Разделы» с раскрытым A3 → сохранить как `a3-section-smoke.png` (корень, как `iter-recollage-smoke.png`; **не коммитить** — артефакт smoke).
- [ ] **Step 5: Остановить сервер**
Завершить background-процесс `serve`.
Коммита нет — задача проверочная.
---
## Task 6: Tooling Прил. Н — §4.22 + счётчик
**Files:**
- Modify: `docs/Tooling_v8_3.md`
- [ ] **Step 1: Прочитать §4.21/§4.20 (A4-записи)**
Run: `Read` `docs/Tooling_v8_3.md`, `Grep` паттерн `§4.21|§4.20|§4.19` — зафиксировать структуру/стиль off-phase subsection (#44/#45/#46 design-tooling) для зеркалирования.
- [ ] **Step 2: Добавить §4.22**
После §4.21 добавить новый подраздел (зеркалируя стиль §4.19/§4.20/§4.21):
```markdown
### §4.22. #47 openapi-mcp-server — off-phase integration-tooling
**Пакет:** `<PKG>` (npm, репозиторий `ivo-toby/mcp-openapi-server`), stdio MCP, server `openapi` в `.mcp.json`, tools `mcp__openapi__*`.
**Категория:** off-phase, **integration-tooling** (9-я off-phase подкатегория — после UI-пула / infrastructure / debug-runtime / orchestration / architecture-tooling / audit-security / project-management / design-tooling). Раздел A3 карты «Программирование — интеграции (API, вебхуки)».
**Назначение:** отдаёт OpenAPI-спеку как MCP-ресурс/тулы; introspection своей и чужих API при интеграционной разработке. READ-ONLY.
**Парный узел карты:** `api-docs` agent (claude-flow) — генератор OpenAPI-спеки; узел карты A3 без отдельного Tooling-номера (sub-агент, реестр — plugin-grain).
**Координация:** PSR_v1 R10.1 Блок 3 (MCP-серверы). Не UI → не trigger'ит R6.0/R6.1, вне R14 pipeline.
**Статус установки:** [подставить из Task 2 Step 4 — «verified» либо «pending: native-Windows install не верифицирован»].
```
`<PKG>` — подставить точное имя из Task 2 Step 1.
- [ ] **Step 3: Обновить счётчик §0**
Run: `Grep` по `docs/Tooling_v8_3.md` паттерн `46|формализованных позиций|off-phase` — найти строку-счётчик §0 (после A4 = «46 формализованных позиций: 29 active + 16 off-phase + 1 historic» либо аналог). Инкрементировать: позиций 46→**47**, off-phase 16→**17**. Обновить перечисление off-phase subsections (добавить §4.22) и упоминание подкатегорий (добавить integration-tooling).
- [ ] **Step 4: Bump версии Прил. Н**
Шапка/колонтитул Прил. Н: v2.8 → **v2.9**. Добавить changelog-строку «v2.9 от 17.05.2026 — A3 integration-tooling: §4.22 #47 openapi-mcp-server, 9-я off-phase подкатегория integration-tooling; §0 счётчик 46→47. Связано: PSR_v1 v3.9, Pravila v1.23, CLAUDE.md v2.9.»
- [ ] **Step 5: Commit**
```bash
git add docs/Tooling_v8_3.md
git commit -m "docs(a3): Tooling Прил. Н v2.9 — register #47 openapi-mcp-server (§4.22)"
```
cspell-блок → добавить термины в `cspell-words.txt`, повторить.
---
## Task 7: PSR_v1 — R10.1 Блок 3
**Files:**
- Modify: `docs/Plugin_stack_rules_v1.md`
- [ ] **Step 1: Прочитать R10.1 Блок 3**
Run: `Grep` `docs/Plugin_stack_rules_v1.md` паттерн `R10.1|Блок 3|sentry|redis` — зафиксировать структуру Блока 3 (MCP-серверы; sentry/redis с категорией debug-runtime).
- [ ] **Step 2: Добавить строку в Блок 3**
В таблицу/список Блока 3 R10.1 добавить строку (зеркалируя строки sentry/redis):
```markdown
| openapi-mcp-server | integration-tooling | off-phase. stdio MCP, server `openapi` в `.mcp.json`. Раздел A3 карты. Не trigger'ит R6.0/R6.1, вне R14 pipeline. Tooling §4.22 #47. |
```
Точный формат строки — по факту таблицы (колонки сверить в Step 1).
- [ ] **Step 3: Bump версии**
Шапка PSR_v1: v3.8 → **v3.9**. Changelog-строка «v3.9 от 17.05.2026 — R10.1 Блок 3 +1 строка openapi-mcp-server (категория integration-tooling, off-phase, раздел A3). Не UI → вне R6/R14. Связано: Tooling v2.9, Pravila v1.23, CLAUDE.md v2.9.» Обновить cross-refs шапки, если они перечисляют версии родственных файлов.
- [ ] **Step 4: Commit**
```bash
git add docs/Plugin_stack_rules_v1.md
git commit -m "docs(a3): PSR_v1 v3.9 — R10.1 Блок 3 +openapi-mcp (integration-tooling)"
```
---
## Task 8: Pravila — §13.2
**Files:**
- Modify: `docs/Pravila_raboty_Claude_v1_1.md`
- [ ] **Step 1: Прочитать §13.2**
Run: `Grep` `docs/Pravila_raboty_Claude_v1_1.md` паттерн `§13.2|Off-phase|audit-security|architecture-tooling` — зафиксировать структуру абзацев off-phase подкатегорий (последний добавленный — audit-security, D3).
- [ ] **Step 2: Добавить абзац**
После абзаца «Off-phase audit-security» добавить:
```markdown
**Off-phase integration-tooling.** Инструменты раздела A3 карты «Программирование — интеграции (API, вебхуки)» — #47 `openapi-mcp-server` (Tooling §4.22; введён A3-интеграцией 17.05.2026) и `api-docs` agent (claude-flow, узел карты A3 без отдельного Tooling-номера). Off-phase, не UI → вне R6/R14 PSR_v1. READ-ONLY introspection. Регулируются PSR_v1 R10.1 Блок 3.
```
- [ ] **Step 3: Bump версии + счётчик правил**
Шапка Pravila: v1.22 → **v1.23**. Если §13.2 (или §11.5) содержит счётчик подкатегорий/правил — свериться `Grep`'ом и инкрементировать. Changelog-строка «v1.23 от 17.05.2026 — §13.2 +абзац Off-phase integration-tooling (#47 openapi-mcp-server / api-docs agent — раздел A3 карты). Связано: Tooling v2.9, PSR_v1 v3.9, CLAUDE.md v2.9.»
- [ ] **Step 4: Commit**
```bash
git add docs/Pravila_raboty_Claude_v1_1.md
git commit -m "docs(a3): Pravila v1.23 — §13.2 +Off-phase integration-tooling"
```
---
## Task 9: CLAUDE.md — через claude-md-management
**Files:**
- Modify: `CLAUDE.md` (только через скил — §5 п.10)
- [ ] **Step 1: Инвокировать claude-md-improver**
Invoke `/claude-md-management:claude-md-improver` со списком targeted-правок:
- §3 title «Карта 46 инструментов» → «47»;
- §3.3 +строка #47 openapi-mcp-server (integration-tooling, off-phase; Tooling §4.22) + упоминание `api-docs` agent как парного узла карты A3 без Tooling-номера;
- §1 priority-chain row 2b «реестр 46» → «47»;
- §3.3 footer count 46→47 + integration-tooling как 9-я off-phase подкатегория;
- §3.4 нумерационная сноска — добавить #47 в перечисление off-phase, обновить арифметику;
- §0 cross-refs: Pravila v1.22→**v1.23**, PSR_v1 v3.8→**v3.9**, Tooling v2.8→**v2.9**;
- §6 +абзац A3 integration-tooling (по образцу абзацев A6/D3/A4);
- шапка v2.8 → **v2.9** + §9 changelog-запись.
- [ ] **Step 2: Проверить синхронность (§5 п.7)**
Убедиться, что внутри flow скила Tooling (Task 6) и Pravila (Task 8) уже синхронизированы — версии в §0 cross-refs CLAUDE.md совпадают с фактическими шапками файлов.
- [ ] **Step 3: Commit**
Скил/ручной коммит:
```bash
git add CLAUDE.md
git commit -m "docs(a3): CLAUDE.md v2.9 — register #47 openapi-mcp-server (A3 integration-tooling)"
```
---
## Task 10: Регрессия + память + handoff на push
**Files:**
- Modify: `memory/project_automation_map.md`, `memory/reference_archive.md` (после успешной регрессии)
- [ ] **Step 1: Регрессия quick**
Invoke skill `regression` с аргументом `quick` (lint/format/type-check). Зафиксировать канонический статус-лайн + вердикт. Ожидание GREEN — правки только в `.md`/`.html`/`.json`/`.yaml`, прикладной код не тронут.
- [ ] **Step 2: Финальная сверка счётчика (риск нумерации)**
Run: `git log --oneline origin/main..HEAD` + `Grep` Tooling §0 — подтвердить, что #47/§4.22 не пересёкся с A11, если та смёрджилась в main (C9/deptrac/A4 уже влиты). При коллизии — перенумеровать openapi-mcp на следующий свободный номер во всех 4 файлах + карте, повторить затронутые коммиты.
- [ ] **Step 3: Обновить memory**
- `memory/project_automation_map.md` — метрики 116→118 узлов / +3 ребра, раздел A3 наполнен (7 узлов: 2 новых + 5 кросс-реф), `NODE_SECTION_SECONDARY` слой.
- `memory/reference_archive.md` — версии Tooling v2.9 / PSR_v1 v3.9 / Pravila v1.23 / CLAUDE.md v2.9.
- `MEMORY.md` — обновить строки-указатели при необходимости.
- [ ] **Step 4: Commit memory**
```bash
git add memory/
git commit -m "docs(a3): memory sync — A3 integration-tooling closed"
```
- [ ] **Step 5: Handoff на push**
Не пушить автоматически. Представить заказчику: ветка `feat/a3-integration-tooling`, перечень коммитов, напоминание про pre-push (`gitleaks` full-history + `lychee`) и про порядок merge относительно D3/A11/C9. Push — по явному «пушь» (паттерн `git push origin feat/a3-integration-tooling:main`).
---
## Self-Review
**Spec coverage:**
- spec §3.1 (2 новых узла) → Task 1 (api-docs smoke), Task 2 (openapi-mcp install), Task 4 (узлы карты). ✓
- spec §3.2 (5 кросс-реф) → Task 3 (`NODE_SECTION_SECONDARY`). ✓
- spec §4 (правка модели карты) → Task 3 + Task 4 + Task 5. ✓
- spec §5 (нормативка 4 файла) → Task 6 (Tooling), 7 (PSR_v1), 8 (Pravila), 9 (CLAUDE.md). ✓
- spec §6 (smoke/верификация) → Task 1 Step 4, Task 2 Step 4, Task 5, Task 10 Step 1. ✓
- spec §7 (риск нумерации) → Task 10 Step 2. ✓
- spec §8 (ветка/артефакты) → ветка создана (`feat/a3-integration-tooling`), spec/plan на месте, push-handoff Task 10 Step 5. ✓
**Placeholder scan:** `<PKG>` — намеренный плейсхолдер, разрешается в Task 2 Step 1 (`npm view`) и подставляется в Task 2 Step 3 / Task 6 Step 2; «[подставить из Task 2 Step 4]» — статус установки, разрешается в рамках Task 2. Иных плейсхолдеров нет.
**Type consistency:** имена узлов `ag_apidocs` / `mcp_openapi` — единообразны во всех задачах (NODES, NODE_SECTION, NODE_DETAILS, NODE_TIMELINE, EDGES, NODE_SECTION_SECONDARY не содержит их — они первичны в A3). Объект `NODE_SECTION_SECONDARY` — одно имя везде. Версии: Tooling v2.9 / PSR_v1 v3.9 / Pravila v1.23 / CLAUDE.md v2.9 — консистентны в Tasks 6-9 и §0 cross-refs.
@@ -0,0 +1,509 @@
# A4 Design Tooling Integration 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:** Close the gaps in the `A4 «Дизайн (UI/UX, графика, бренд)»` map section by adding three tools to the current three (FD #30 / UPM #31 / 21st #32) — #41 **Figma MCP** (design source + brand tokens), #42 **Universal Icons MCP** (graphics / icons), #43 **Design plugin** (design review / a11y critique / UX writing) — so A4 grows from 3 → 6 nodes with full UI/UX + графика + бренд coverage.
**Architecture:** Three tools, three install modes. **Design plugin** = Anthropic-Verified marketplace plugin (review-side, like FD — no code-gen). **Universal Icons MCP** = stdio npm MCP server, MIT, framework-neutral SVG. **Figma MCP** = official remote MCP server (`https://mcp.figma.com/mcp`), used in **extract-only** role. The two pre-identified overlaps (Figma MCP code-gen ↔ FD solver; Design plugin a11y/review ↔ FD/Pa11y/R5) are closed by **ADR-004** boundary decisions + PSR_v1 rows — not left implicit.
**Tech Stack:** Figma MCP (official, remote HTTP transport, OAuth); `mcp-universal-icons` (npm, MIT, stdio); Design plugin (`anthropics/claude-plugins-official` marketplace, Anthropic Verified); adr-kit v0.13.1 (already installed #36 — used to author ADR-004); project normative docs; `docs/automation-graph.html` (vis.js).
---
## Plan Correction (2026-05-17, after Task 1 pre-flight)
Facts verified on branch `feat/a4-design-tooling` (from `origin/main` `9b63e27`) supersede assumptions in the tasks below:
- **FM2 resolved — no Figma account.** Figma MCP install (Task 4) is **deferred**, precondition «Figma account + file created». It is still registered (Task 6) as **deferred-pending** (precedent: Sentry MCP #34 «pending Б-1») and appears on the map (Task 7) marked deferred. A4 ships **5 live nodes now**; the 6th (Figma) activates later.
- **Registry numbering.** deptrac already took #43 (Tooling §4.18, §0 counter **43**). A4 tools are therefore **#44 Figma MCP / #45 Universal Icons MCP / #46 Design plugin**, Tooling subsections **§4.19 / §4.20 / §4.21**, §0 counter **43 → 46**. Every `#41 / #42 / #43` in the tasks below reads as `#44 / #45 / #46`.
- **Plugins / marketplace.** `~/.claude/settings.json` `enabledPlugins` holds **21** plugins (not 9); installing Design plugin → **22**. Marketplace `claude-plugins-official` is **already present** — no `/plugin marketplace add` needed.
- **lefthook** has **10** jobs (deptrac = job 10). Task 1 baseline: all 10 green / "no staged files".
- **ADR id — ADR-006, not ADR-004.** ADR-004/005 were already taken (project-management / deptrac epics). The A4 boundaries ADR is `docs/adr/ADR-006-a4-design-tooling-boundaries.md`. Every `ADR-004` in the tasks / self-review below reads as `ADR-006`.
- **Design plugin marketplace — `knowledge-work-plugins`, not `claude-plugins-official`** (verified post-reload, 2026-05-17, against the marketplace manifest). The `design` plugin lives in `anthropics/knowledge-work-plugins` (same marketplace as #42 product-management); `claude-plugins-official` has only `frontend-design`. The `enabledPlugins` entry is `design@knowledge-work-plugins`. Every `design@claude-plugins-official` / `claude-plugins-official` reference to the **Design plugin** in the tasks below reads accordingly (`claude-plugins-official` for #33 claude-md-management / #40 security-guidance stays correct). The `/plugin install` path (Task 2 Steps 1-3) is unavailable in the VSCode-extension environment — the plugin is enabled by editing `enabledPlugins` directly.
---
## Tool Identity (verified 2026-05-17 — re-fact-check in Task 1)
| # | Tool | Install mode | Source | Hooks? |
|---|---|---|---|---|
| 41 | **Figma MCP** | Remote MCP server → `.mcp.json` (`http` transport, `https://mcp.figma.com/mcp`) | Figma official; Figma↔Claude Code integration announced Feb 2026 | None (MCP server, no CC lifecycle hooks) |
| 42 | **Universal Icons MCP** (`mcp-universal-icons`) | stdio MCP server → `.mcp.json` (`npx -y mcp-universal-icons`) | GitHub `awssat/mcp-universal-icons`, **MIT** | None (MCP server) |
| 43 | **Design plugin** | Marketplace plugin → `~/.claude/settings.json` `enabledPlugins` | `anthropics/claude-plugins-official`, **Anthropic Verified** | Verify on install (DP4) |
Kept from the A4 top-6 (already in the map): #30 Frontend Design, #31 UI UX Pro Max, #32 21st Magic MCP.
---
## Design Decisions & Conflict Audit
Gap analysis behind the tool choice (the "что закрываем"):
| A4 подзона | Покрыто до | Пробел → закрывает |
|---|---|---|
| UI/UX (компоненты, экраны, паттерны) | FD #30 + UPM #31 + 21st #32 | — (уже плотно) |
| Графика (иконки, SVG) | 21st `logo_search` (только логотипы) | иконки/SVG → **#42 Universal Icons** |
| Бренд (палитра, типографика, токены, источник) | handoff-доки Платона (статика) + ручной перенос в `vuetify.ts` | живой источник + извлечение токенов → **#41 Figma MCP** |
| Сквозное: design-review / визуальный аудит | Pa11y (только технический a11y) | дизайн-критика, UX-копирайт, дизайн-a11y → **#43 Design plugin** |
Conflict audit (pattern follows the AK/MK/CC audit used for A6). Verified against the project `.mcp.json`, `~/.claude/settings.json`, project `.claude/settings.json`, `lefthook.yml`, `.gitleaks.toml`.
| # | Tool | Sev | Conflict | Resolution (locked) |
|---|---|---|---|---|
| FM1 | Figma MCP | 🔴 | Figma MCP can **generate UI code** (design-to-code) → duplicates FD #30 the UI solver → CLAUDE.md §5 п.6 (no two tools on one task). | **ADR-004 Decision 1 + PSR_v1 R10.1 row:** Figma MCP used **extract-only** (design-data + token reads, e.g. `get_variable_defs`). Its code-gen mode is **never invoked**. FD remains the sole UI solver (PSR_v1 R10.2). |
| FM2 | Figma MCP | 🔴 | Figma MCP's value (token extraction from source) needs an **accessible live Figma file**. The handoff is currently static (`liderra_v8_handoff/` = `.md` + 13 HTML). No live file → the tool degrades to "describe screenshots". | **Hard gate in Task 1 Step 5.** If no accessible Figma file exists → STOP, surface to user: defer #41 (A4 set drops to 5 — the "в" variant) or obtain access from Платон. Do not install #41 blind. |
| FM3 | Figma MCP | 🟡 | Remote MCP auth — if a static token is required it must not leak into the repo. | Prefer remote OAuth flow (no repo secret). If a token IS required → env-var via PowerShell User scope (same pattern as Sentry `SENTRY_AUTH_TOKEN`), referenced in `.mcp.json`, never inlined. gitleaks pre-push must stay 0. |
| FM4 | Figma MCP | 🟡 | Figma MCP default code-gen targets React/Tailwind → wrong stack. | Sidestepped by FM1 (extract-only). The PSR_v1 row still states the R6.0 stack-filter applies if any Figma output is consumed as material. |
| FM5 | Figma MCP | 🟡 | MCP server #41 unregistered = PSR_v1 R0.2/R10 violation on use. | Register in 4 normative homes (Task 6). |
| FM6 | Figma MCP | 🟢 | Lifecycle-hook collision with the 6 economy + 2 ruflo + skill-discipline hooks. | None — MCP server, zero CC lifecycle hooks. Re-verify Task 4 Step 4. |
| UI1 | Universal Icons | 🟢 | Overlap with FD #30. | None — icons are an asset-primitive (material); FD decides *which/where*, the MCP only fetches SVG (PSR_v1 R10.2). |
| UI2 | Universal Icons | 🟡 | Slight overlap with 21st `logo_search` (logos). | Boundary in ADR-004 note: Universal Icons = UI **icons** (Lucide-first); 21st `logo_search` = brand **logos**. Both kept; no task-level duplication. |
| UI3 | Universal Icons | 🟢 | MIT, stdio, hooks/registration footprint. | None — MCP server, zero hooks. Tailwind injection is optional; default SVG output is framework-neutral (R6.0 satisfied by default — never request `jsx`/Tailwind format). Re-verify Task 3 Step 4. |
| UI4 | Universal Icons | 🟡 | MCP server #42 unregistered. | Register in 4 normative homes (Task 6). |
| DP1 | Design plugin | 🟡 | "Accessibility Audit WCAG 2.1 AA" is a 3-way overlap: Design plugin audit × FD "a11y-принципы" (Tooling §4.4) × Pa11y (technical a11y SoT, CLAUDE.md §5 п.3). | **ADR-004 Decision 2:** Design plugin a11y = design-level critique, **pre-code**. **Pa11y stays the single source of truth** for technical a11y (PSR_v1 R8 — Pa11y wins). FD keeps a11y-principles during design. Three-tier, no override. |
| DP2 | Design plugin | 🟡 | "Design Critique" overlaps PSR_v1 R5 (review-by-aspect — UI/UX aspect → FD) and `superpowers:requesting-code-review`/`receiving-code-review`. | **ADR-004 Decision 3:** Design Critique runs in **R2 phase 1** (research / pre-code planning), not phase-8 review. Phase-8 review stays R5 aspect-split + Superpowers review skills. Design plugin does not replace `requesting-code-review`. |
| DP3 | Design plugin | 🟡 | Plugin #43 unregistered. | Register in 4 normative homes (Task 6). |
| DP4 | Design plugin | 🟢 | May register CC lifecycle hooks → would touch the economy/ruflo chain. | Verify on install (Task 2 Step 4). If it injects a `hooks` entry → STOP, re-audit like A6 AK5. claude-md-management #33 ships zero hooks; security-guidance #40 ships one PreToolUse hook — Design plugin is unknown until installed. |
| DP5 | Design plugin | 🟢 | R6.0 stack-filter applicability. | None — Design plugin is review/planning, produces no code → no R6.0 needed (treated like FD's review side). UX-writing output is Russian microcopy — fine. |
| CC1 | all 3 | 🟡 | Bus-factor — Universal Icons is a single-maintainer community repo. | Figma MCP (Figma official) + Design plugin (Anthropic) are low-risk. Universal Icons is MIT and trivially replaceable (`iconify-mcp-server` is a drop-in alternative). Note in Tooling §4.x as a known risk. |
**Severable scope:** Task 4 (Figma MCP) is gated on the FM2 spike. If the spike fails, **skip Task 4 entirely** — Tasks 1-3, 5-8 still deliver A4 at 5 nodes (Design plugin + Universal Icons added). Primary path below = full 6-node integration.
---
## File Structure
| File | Created / Modified | Responsibility |
|---|---|---|
| `.mcp.json` | Modify | += `figma` server (Task 4) + `universal-icons` server (Task 3) |
| `~/.claude/settings.json` | Modify | `enabledPlugins` += `design@claude-plugins-official`; `extraKnownMarketplaces` unchanged (Anthropic marketplace already present from #33/#40) |
| `docs/adr/ADR-004-a4-design-tooling-boundaries.md` | Create | The 2 boundary decisions (FM1 / DP1 / DP2) — authored via adr-kit, no Enforcement block |
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new §4.16/§4.17/§4.18 + §0 counter `40 → 43` |
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — 3 new rows; R6/R10/R14 boundary notes for Figma MCP + Universal Icons |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — design-tooling note |
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, §3.3 rows #41/#42/#43, §6 integration paragraph |
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
| `docs/automation-graph.html` | Modify | 3 new nodes (`mcp_figma`, `mcp_icons`, `design_plugin`) → `NODE_SECTION` A4 |
---
## Task 1: Pre-flight — branch, baseline, fact-check, FM2 spike
**Files:** none modified (read-only) except branch creation
- [ ] **Step 1: Resolve working-tree state and create the branch**
```bash
cd "c:/моя/проекты/портал crm/Документация"
git status --short
git branch --show-current
git rev-parse --short HEAD
```
If `CLAUDE.md` or other files are modified from prior D3 work — confirm with the user whether to commit/stash before branching. With a clean tree:
```bash
git checkout -b feat/a4-design-tooling
```
Expected: on `feat/a4-design-tooling`; record HEAD SHA as the regression baseline.
- [ ] **Step 2: Baseline the pre-commit chain**
```bash
npx lefthook run pre-commit
```
Expected: all 9 jobs (gitleaks, markdownlint, cspell, stylelint, pint, larastan, squawk, eslint-vue, adr-judge) green / "no staged files". Record the count.
- [ ] **Step 3: Snapshot MCP + plugin state**
```bash
node -e "const c=require('./.mcp.json');console.log(Object.keys(c.mcpServers||c.servers||c))"
```
Read `~/.claude/settings.json` `enabledPlugins` — record the current plugin count (expected 11 after A6). Read the `hooks` block of both `~/.claude/settings.json` and project `.claude/settings.json` — record as the DP4 baseline.
- [ ] **Step 4: Fact-check the 3 tools**
Confirm assumptions still hold:
- Figma MCP — remote endpoint `https://mcp.figma.com/mcp`, `http` transport, OAuth. Confirm the current Claude Code `claude mcp add --transport http` syntax and whether a token is needed (FM3). Source: `help.figma.com` "Claude Code and Figma: Set up the MCP server".
- `https://github.com/awssat/mcp-universal-icons` — npm `mcp-universal-icons`, MIT, tools `search_icons`/`get_icon`/`health_check`, Lucide in the collection set, no CC hooks.
- `https://claude.com/plugins/design` — confirm the exact marketplace id and plugin name (expected `anthropics/claude-plugins-official`, plugin `design`), Anthropic Verified, and whether it ships hooks.
If any tool now registers CC lifecycle hooks → note it; DP4/FM6/UI3 re-audit in the install task.
- [ ] **Step 5: FM2 spike — confirm an accessible live Figma file exists (HARD GATE)**
```bash
grep -ri "figma.com" liderra_v8_handoff/ docs/ 2>/dev/null
```
Then ask the user directly: **does Платон's v8 Forest design exist as a Figma file this project can open (view/inspect access)?**
- **Accessible Figma file confirmed** → proceed to Task 4 (full 6-node path).
- **No accessible Figma file** → STOP. Surface to the user: Figma MCP (#41) loses its core value; either (a) defer #41, ship A4 at 5 nodes (Design plugin + Universal Icons), or (b) obtain Figma access first. Do not proceed to Task 4 until the user decides.
No repo files changed in Task 1 → no commit (branch creation aside).
---
## Task 2: Install the Design plugin (#43, marketplace)
**Files:** Modify `~/.claude/settings.json` (the `Edit` triggers the `ask` permission — expected)
- [ ] **Step 1: Add the marketplace if absent**
The Anthropic marketplace is already present (used by #33 claude-md-management and #40 security-guidance). If `extraKnownMarketplaces` lacks it:
```
/plugin marketplace add anthropics/claude-plugins-official
```
- [ ] **Step 2: Install the plugin**
```
/plugin install design@claude-plugins-official
```
(Use the exact plugin id confirmed in Task 1 Step 4.)
- [ ] **Step 3: Reload**
```
/reload-plugins
```
- [ ] **Step 4: Verify `enabledPlugins` and the hook baseline (DP4)**
Read `~/.claude/settings.json`. `enabledPlugins` must now contain `design@claude-plugins-official: true`**12 plugins total** (was 11). Read the `hooks` block of `~/.claude/settings.json` AND project `.claude/settings.json` — both must be **unchanged** vs the Task 1 Step 3 baseline. If the Design plugin injected a `hooks` entry → **STOP**, re-audit DP4 (the economy/ruflo chain must not be perturbed).
- [ ] **Step 5: Smoke-test the plugin**
Invoke a Design-plugin capability — e.g. ask for a design critique of one existing screen description, or a UX-writing pass on a short microcopy string. Confirm the plugin's skill activates and returns design-review output (not code).
- [ ] **Step 6: Confirm economy/ruflo chain intact**
Submit a trivial prompt; confirm the economy marker still appears, no hook errors. No repo files changed in Task 2 → no commit.
---
## Task 3: Install Universal Icons MCP (#42, stdio)
**Files:** Modify `.mcp.json`
- [ ] **Step 1: Add the MCP server**
```bash
claude mcp add universal-icons -- npx -y mcp-universal-icons
```
If `claude mcp add` writes to the user scope instead of the project `.mcp.json`, add the block manually to project `.mcp.json` alongside the existing servers:
```json
"universal-icons": {
"command": "npx",
"args": ["-y", "mcp-universal-icons"]
}
```
- [ ] **Step 2: Reload and verify the server connects**
```
/reload-plugins
```
Confirm a `universal-icons` MCP server appears and its tools (`mcp__universal-icons__search_icons`, `mcp__universal-icons__get_icon`, `mcp__universal-icons__health_check`) are listed.
- [ ] **Step 3: Smoke-test — Lucide icon search**
Call `search_icons` for an icon known to exist in Лидерра's set (e.g. `bell`, `chevron-down`) restricted to the Lucide collection, then `get_icon` for one result. Confirm: a result is returned, the SVG is **framework-neutral** (no Tailwind classes, no JSX) — default `svg` format. Never request `jsx`/Tailwind output (UI3 / R6.0).
- [ ] **Step 4: Verify no hooks / no settings drift (UI3)**
Read the `hooks` block of `~/.claude/settings.json` and project `.claude/settings.json` — unchanged vs Task 1 baseline. `enabledPlugins` unchanged (MCP server ≠ plugin).
- [ ] **Step 5: Commit**
```bash
git add .mcp.json
git commit -m "feat(a4): add Universal Icons MCP server (#42, design graphics)"
```
---
## Task 4: Install Figma MCP (#41, remote — gated on Task 1 Step 5)
**Files:** Modify `.mcp.json`
> Skip this entire task if the FM2 spike (Task 1 Step 5) failed and the user chose to defer #41.
- [ ] **Step 1: Add the remote MCP server**
Using the syntax confirmed in Task 1 Step 4:
```bash
claude mcp add --transport http figma https://mcp.figma.com/mcp
```
Or manually in project `.mcp.json`:
```json
"figma": {
"type": "http",
"url": "https://mcp.figma.com/mcp"
}
```
If Task 1 found a static token is required (FM3): set it as a PowerShell User-scope env var (e.g. `FIGMA_MCP_TOKEN`) and reference it via env in `.mcp.json`**never inline the token**.
- [ ] **Step 2: Authenticate**
Complete the Figma OAuth flow (browser) when prompted on first server use. Confirm the project's Figma account has at least view/inspect access to the v8 Forest file confirmed in Task 1 Step 5.
- [ ] **Step 3: Reload and verify the server connects**
```
/reload-plugins
```
Confirm the `figma` MCP server appears and its tools are listed (expect a `get_variable_defs`-class tool and design-data read tools).
- [ ] **Step 4: Smoke-test — extract-only (FM1)**
Point Figma MCP at a node in the Forest file and call the variable/token-extraction tool (`get_variable_defs` or equivalent). Confirm color/spacing/typography variables are returned. **Do NOT invoke any code-generation tool** — extract-only is the locked role (FM1). Verify no `hooks` drift in either `settings.json` (FM6).
- [ ] **Step 5: Cross-check one token against the source of truth**
Compare one extracted color against `app/resources/js/plugins/vuetify.ts` (Teal `#0F6E56` / ivory `#F6F3EC`). They should agree — this proves the Figma file is the genuine Forest source and the extraction is usable. Note any drift for the user; do not "fix" `vuetify.ts` in this plan.
- [ ] **Step 6: Commit**
```bash
git add .mcp.json
git commit -m "feat(a4): add Figma MCP server (#41, extract-only — FM1)"
```
---
## Task 5: Author ADR-004 — A4 design-tooling boundaries
**Files:** Create `docs/adr/ADR-004-a4-design-tooling-boundaries.md`
- [ ] **Step 1: Generate the ADR via adr-kit**
Preferred: run `/adr-kit:adr "A4 design-tooling boundaries"` and let the `adr-generator` agent emit the Nygard 7-section skeleton. Then fill it with the content below.
- [ ] **Step 2: Write the ADR**
Create `docs/adr/ADR-004-a4-design-tooling-boundaries.md`:
```markdown
# ADR-004: A4 design-tooling boundaries
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
The A4 «Дизайн» map section adds three tools to the existing FD #30 / UPM #31 /
21st #32: Figma MCP (#41), Universal Icons MCP (#42), Design plugin (#43). Two
overlaps with Frontend Design (#30) were identified during selection and must be
closed by explicit rule, not left implicit.
## Decision
1. **Figma MCP — extract-only.** Figma MCP is used solely for design-data and
design-token reads (e.g. `get_variable_defs`). Its design-to-code generation
capability is NOT used. Frontend Design (#30) remains the sole UI solver
(PSR_v1 R10.2 — external plugins are read-only for decisions). Figma MCP output
is material for FD/Claude, never a substitute solver.
2. **Design plugin a11y is design-level, pre-code.** The Design plugin's
Accessibility Audit operates at design-critique level. Pa11y remains the single
source of truth for technical a11y (CLAUDE.md §5 п.3, PSR_v1 R8 — Pa11y wins on
conflict). FD continues to cover a11y-principles during design. Three tiers, no
override.
3. **Design plugin critique runs in R2 phase 1.** The Design plugin's Design
Critique runs in the research / pre-code planning phase, not the phase-8 review.
Phase-8 review stays with the PSR_v1 R5 aspect-split (FD owns the UI/UX aspect)
plus the Superpowers review skills. The Design plugin does not replace
`superpowers:requesting-code-review`.
## Consequences
- A Figma MCP code-generation call is a process violation (CLAUDE.md §5 п.6).
- Universal Icons (#42) covers UI icons; 21st `logo_search` covers brand logos —
distinct, both retained.
- These boundaries are mirrored as PSR_v1 R10.1 rows + R6/R10/R14 notes (Task 6).
## Enforcement
None — role boundaries, verified by code review, not by a regex gate.
```
- [ ] **Step 3: Lint and validate**
```bash
npx markdownlint-cli2 "docs/adr/ADR-004-a4-design-tooling-boundaries.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/adr/ADR-004-a4-design-tooling-boundaries.md"
```
Add flagged valid terms to `cspell-words.txt`. Then `/adr-kit:lint docs/adr/` — expected: all ADRs pass structural checks.
- [ ] **Step 4: Commit**
```bash
git add docs/adr/ADR-004-a4-design-tooling-boundaries.md cspell-words.txt
git commit -m "docs(adr): ADR-004 — A4 design-tooling boundaries (FM1/DP1/DP2)"
```
---
## Task 6: Unified normative registry sync (FM5 / UI4 / DP3)
**Files:** Modify `docs/Tooling_v8_3.md`, `docs/Plugin_stack_rules_v1.md`, `docs/Pravila_raboty_Claude_v1_1.md`, `CLAUDE.md`, `docs/CHANGELOG_claude_md.md`
- [ ] **Step 1: Read the registry homes**
Read for exact insertion points and counters: `docs/Tooling_v8_3.md` Прил. Н §0 (counter "40") + the last subsection §4.15; `docs/Plugin_stack_rules_v1.md` R10.1 (3 blocks); `docs/Pravila_raboty_Claude_v1_1.md` §13.2.
- [ ] **Step 2: Add Tooling Прил. Н §4.16–§4.18**
Edit `docs/Tooling_v8_3.md`: add three subsections — `§4.16 #41 Figma MCP`, `§4.17 #42 Universal Icons MCP`, `§4.18 #43 Design plugin`. Category: off-phase **design-tooling** (extends the UI-pool). Per tool:
- **#41 Figma MCP** — Figma official remote MCP (`https://mcp.figma.com/mcp`, `http`), **extract-only** role per ADR-004 (FM1); R6.0 stack-filter applies to any consumed material (FM4); needs an accessible Figma file (FM2 — note the dependency).
- **#42 Universal Icons MCP** — `mcp-universal-icons` (npm, MIT, `awssat`), stdio; UI icons (Lucide-first); default SVG framework-neutral (never request Tailwind/JSX — UI3); distinct from 21st `logo_search` (UI2); bus-factor note (CC1).
- **#43 Design plugin** — `anthropics/claude-plugins-official`, Anthropic Verified; design review / a11y critique / UX writing / research synthesis; review-side (no R6.0, outside R14 — like FD); a11y is design-level, Pa11y stays technical SoT (DP1).
State: Figma MCP + Universal Icons are material tools → R6.0/R6.1 + R14 pipeline apply (same as #32 21st); Design plugin is a review tool → treated like FD. Bump §0 counter `40 → 43`; bump the Прил. Н version header.
- [ ] **Step 3: Add 3 PSR_v1 R10.1 rows + boundary notes**
Edit `docs/Plugin_stack_rules_v1.md`: add Figma MCP, Universal Icons MCP, Design plugin rows to R10.1, category **design-tooling** (off-phase). Add the ADR-004 boundary references — Figma MCP extract-only (R10.2), Design plugin a11y/review positioning (R8 / R5). Confirm Figma MCP + Universal Icons fall under R6.0/R6.1/R14 (material-generators, like #32); Design plugin outside R14 (review). Bump the PSR_v1 version header.
- [ ] **Step 4: Add the Pravila §13.2 note**
Edit `docs/Pravila_raboty_Claude_v1_1.md` §13.2: add a one-line design-tooling note covering the three tools, alongside the existing infrastructure / architecture-tooling / audit-security notes. Re-read Pravila §0/§13 first to keep section numbering consistent. Bump the Pravila version header.
- [ ] **Step 5: Update CLAUDE.md via the governed channel**
Invoke `/claude-md-management:claude-md-improver`. Apply: §3 title count (`40``43`), §1 priority-chain row 2b count (`40``43`), three new §3.3 rows `#41 Figma MCP` / `#42 Universal Icons MCP` / `#43 Design plugin`, §6 integration paragraph (A4 closure). The plugin also writes the `docs/CHANGELOG_claude_md.md` entry and bumps §0 cross-ref versions (Tooling / PSR_v1 / Pravila).
- [ ] **Step 6: Lint + commit**
```bash
npx markdownlint-cli2 "docs/Tooling_v8_3.md" "docs/Plugin_stack_rules_v1.md" "docs/Pravila_raboty_Claude_v1_1.md" "docs/CHANGELOG_claude_md.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/Tooling_v8_3.md" "docs/Plugin_stack_rules_v1.md" "docs/Pravila_raboty_Claude_v1_1.md"
git add docs/Tooling_v8_3.md docs/Plugin_stack_rules_v1.md docs/Pravila_raboty_Claude_v1_1.md CLAUDE.md docs/CHANGELOG_claude_md.md cspell-words.txt
git commit -m "docs(a4): register Figma MCP/Universal Icons/Design plugin #41-43 (FM5/UI4/DP3)"
```
---
## Task 7: Reflect the 3 tools on the map (close A4 — 3→6)
**Files:** Modify `docs/automation-graph.html`
- [ ] **Step 1: Read the structures to replicate**
In `docs/automation-graph.html` read, as templates: an existing MCP-server node — `mcp_21st` — across `NODES`, `NODE_DETAILS`, `NODE_SECTION`, the MCP-серверы group, and the "Паспорт узла" date fields; and an existing plugin node — `fd_plugin` — for the Design-plugin template. Record the current node/edge counts (expected ~106 nodes / ~109 edges after A6) and the MCP-серверы + плагины group sizes.
- [ ] **Step 2: Add three nodes**
Add to `NODES`, replicating the template shapes:
- `mcp_figma` — label `Figma MCP`, MCP-серверы group.
- `mcp_icons` — label `Universal Icons MCP`, MCP-серверы group.
- `design_plugin` — label `Design plugin`, плагины group.
Add matching `NODE_DETAILS`: `mcp_figma` — "MCP Figma (extract-only, ADR-004): извлечение токенов/variables из источника дизайна."; `mcp_icons` — "MCP Universal Icons: поиск/вставка SVG-иконок (Lucide-first)."; `design_plugin` — "Плагин Design (Anthropic): дизайн-критика, a11y-аудит, UX-копирайт — pre-code." Паспорт: дата внедрения `2026-05-17` for all three.
- [ ] **Step 3: Map all three to section A4**
In `NODE_SECTION` add:
```js
mcp_figma: 'A4', mcp_icons: 'A4', design_plugin: 'A4',
```
A4 «Дизайн (UI/UX, графика, бренд)» goes from 3 → 6 nodes.
- [ ] **Step 4: Add edges + update header metrics**
Add edges replicating how `fd_plugin`/`upm`/`mcp_21st` connect (e.g. to governing rules / the design-tooling cluster) — mirror the A6 pattern of 3 nodes / +3 edges. Bump the node count in the map header/legend by 3 (`106 → 109`) and the edge count accordingly. Update the MCP-серверы (+2) and плагины (+1) group-count comments in `NODE_SECTION`.
- [ ] **Step 5: Smoke-test the map**
```bash
npx stylelint docs/automation-graph.html
```
Open `docs/automation-graph.html` in a browser (Playwright MCP or local `python -m http.server`): 0 JS console errors; the 3 new nodes render; clicking section `A4` highlights all six (FD/UPM/21st + Figma/Icons/Design).
- [ ] **Step 6: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): add mcp_figma/mcp_icons/design_plugin nodes — closes section A4 (3→6)"
```
---
## Task 8: Final regression & branch finish
**Files:** none modified
- [ ] **Step 1: Full pre-commit chain**
```bash
npx lefthook run pre-commit
```
Expected: all 9 jobs green.
- [ ] **Step 2: Confirm app code untouched — run the suites**
These tools change no `app/` code → suites must match the Task 1 baseline:
```bash
cd app && php artisan test --parallel
npm run test:vue
```
Expected: Pest and Vitest counts unchanged vs the Task 1 baseline (0 regressions). Record exact counts; write out any failure with file:line.
- [ ] **Step 3: Confirm the economy/ruflo hook chain is intact**
Economy marker still appears; Stop verifier still runs; no plugin/server leaked a `hooks` entry into either `settings.json` (DP4/FM6/UI3 final check).
- [ ] **Step 4: Pre-push checks**
```bash
./bin/gitleaks.exe detect --source . --no-banner --config .gitleaks.toml --redact
./bin/lychee.exe --config .lychee.toml "docs/**/*.md" "db/**/*.md" "*.md"
```
Expected: gitleaks 0 leaks (FM3 — confirm no Figma token leaked); lychee 0 broken (the new `docs/adr/ADR-004*.md` is scanned — fix or `.lychee.toml`-exclude any link).
- [ ] **Step 5: Finish the branch**
Invoke `superpowers:finishing-a-development-branch` — present the standard options. Do **not** push without an explicit user choice.
---
## Self-Review
**1. Spec coverage** — A4 gap analysis: графика → #42 (Task 3), бренд/источник → #41 (Task 4), design-review → #43 (Task 2). Conflict audit: FM1→ADR-004 D1 (Task 5) + PSR_v1 row (Task 6.3); FM2→Task 1.5 hard gate; FM3→Task 4.1; FM4→Task 6.2; FM5/UI4/DP3→Task 6; FM6/UI3/DP4→install-task verify steps; UI1→no task (no conflict); UI2→ADR-004 + Tooling note; DP1→ADR-004 D2; DP2→ADR-004 D3; DP5→no task; CC1→Tooling §4.x note. Map A4 closure→Task 7. No gaps.
**2. Placeholder scan** — Task 1 Step 4/Step 5 and Task 7 Step 1 are *fact-check / decision / read-template* steps with concrete criteria, not "TBD" (exact MCP-add syntax, the FM2 prerequisite, and the live 2400-line map node shapes are not knowable without install / without reading the file). All commands exact; ADR-004 content shown in full; the `.mcp.json` blocks shown in full.
**3. Consistency** — registry numbers `#41 Figma / #42 Universal Icons / #43 Design` consistent across Tasks 5-7; counter `40 → 43` consistent Task 6.2↔6.5; node ids `mcp_figma`/`mcp_icons`/`design_plugin` consistent Task 7 Steps 2-3; map count `106 → 109` consistent; ADR id `ADR-004` consistent Task 5↔6↔7; "extract-only" wording for Figma MCP consistent FM1↔ADR-004↔Tooling↔map.
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-17-a4-design-tooling-integration.md`. Two execution options:
1. **Subagent-Driven** — fresh subagent per task, two-stage review. *Caveat:* Tasks 2-5 (slash commands `/plugin …`, `/reload-plugins`, `claude mcp add`, `/adr-kit:adr`, `/adr-kit:lint`), Task 4 Step 2 (Figma OAuth) and Task 6 Step 5 (`claude-md-management`) are main-session-bound — those steps stay with the controller.
2. **Inline Execution**`superpowers:executing-plans`, batch with checkpoints. **Recommended here** — the integration is install/config/docs-heavy with many interactive main-session steps and one user-decision gate (FM2).
Open gate for the user: the **FM2 spike (Task 1 Step 5)** decides full 6-node (`#41` included) vs 5-node (`#41` deferred). Surface the result before Task 4.
@@ -0,0 +1,555 @@
# deptrac Architecture-Fitness Integration 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:** Close the four open architecture-fitness gaps inside map section A6 — active feature-architecture design, architecture conformance, layer/dependency-direction enforcement, and C4-component-diagram drift — by integrating **deptrac** as a declarative, zero-LLM layer-dependency gate wired into the lefthook pre-commit chain.
**Architecture:** A6 «Архитектура систем» currently has three nodes (adr-kit #36 / mermaid-skill #37 / architecture-patterns #38) covering *record / visualize / reference* — but nothing **enforces** that code keeps matching the chosen layered architecture beyond `adr-judge`'s narrow regex scope. deptrac is a deterministic static-analysis tool (no LLM, matches the AK6 principle adr-kit was built under): it parses the `App\` source into named layers (Controller / Service / Model / Job / …) and fails the build on disallowed dependency directions. Pre-existing violations are captured once into a baseline so the gate is green from day one and only catches *new* drift. deptrac's code-derived graph output doubles as a drift-proof C4-component diagram. deptrac becomes the 4th architecture-tooling tool — A6 goes 3 → 4 nodes.
**Tech Stack:** deptrac (`qossmic/deptrac` — PHP static analysis, declarative YAML, BSD-3); composer dev-dependency in `app/` (PHAR-in-`bin/` fallback — DT1); lefthook (pre-commit job 10); the project normative docs; `docs/automation-graph.html` (vis.js).
---
## Sequencing (locked 2026-05-17)
This is the **4th** tooling-integration epic. The A6, D3, and C9 plans lock a strict, never-interleaved order because all four epics touch the same shared files (the map, the 4 normative docs, the Tooling counter, `cspell-words.txt`). deptrac joins that queue:
- **A6** architecture-tooling — ✅ complete, on `origin/main`.
- **D3** audit-risk-tooling — must run to completion and push **first**.
- **C9** project-management-tooling — runs after D3.
- **deptrac** (this plan) — runs after D3; its slot **relative to C9** (before or after) is the user's call at the Execution Handoff. Both orders are valid — deptrac shares no logical dependency with C9.
Task 1 forks the working branch from the **then-current** `origin/main` (not hard-coded). The Tooling registry number is **runtime-resolved** (DT-NUM) — never hard-code a number before reading the live counter; A6 took #36-38, D3 claims #39-40, C9 claims #41(-42).
---
## Tool Identity (verified 2026-05-17 — re-verify in Task 1)
| Item | Value |
|---|---|
| Tool | **deptrac** — PHP architecture layer/dependency enforcement |
| Package | `qossmic/deptrac` (Composer) — **Task 1 re-confirms the current package name, major version, PHP 8.3 / Laravel 13 compatibility, and the collector-config syntax** |
| License | BSD-3-Clause (permissive) |
| Install mode | Composer **dev-dependency** in `app/`; **fallback** = PHAR in `bin/deptrac.phar` if the dependency resolver conflicts (DT1) |
| Hooks | None — deptrac is a CLI tool, registers no Claude Code lifecycle hooks |
| LLM cost | Zero — pure static analysis (AST), no API calls (AK6-aligned) |
| Category | off-phase, **architecture-tooling** — 4th tool of that subcategory (with #36-38) |
**Out of scope (deliberate — YAGNI).** JS-side module-boundary enforcement for `resources/js` (`eslint-plugin-boundaries` / `dependency-cruiser`) is a separate future epic. This plan closes the **PHP/backend** architecture-fitness gaps only. Context/container-level C4 drift (external systems — Yandex Cloud, Unisender, JivoSite, Sentry) is inherently hand-maintained; it is covered by a §8 self-review checklist line (Task 7), not by tooling.
---
## Design Decisions & Conflict Audit
Pattern follows the AK1-AK6 (A6) / TB1 (D3) / CP-PG (C9) audits. Verified against `lefthook.yml`, `app/composer.json`, the `app/app/` tree, `app/phpstan.neon`, and the A6/D3/C9 plans.
| # | Sev | Conflict | Resolution (locked) |
|---|---|---|---|
| DT1 | 🟡 | `composer require --dev qossmic/deptrac` may conflict with Laravel 13's dependency tree (deptrac historically ships isolated). | Task 2: try the composer dev-dep first. If the resolver conflicts → fallback to a pinned `bin/deptrac.phar` (the project already vendors `bin/gitleaks.exe`, `bin/squawk.exe`, `bin/lychee.exe` this way). Decision criterion + both paths are in Task 2. |
| DT2 | 🟢 | deptrac becomes the 10th pre-commit job — adds latency per commit. | deptrac on a codebase this size analyses in ~1-3 s. Measured in Task 5 Step 4; recorded. `glob: "app/**/*.php"` scopes the job to PHP changes only. |
| DT3 | 🟡 | The **first** `deptrac analyse` will report pre-existing layer violations in current `app/` code → job 10 would block **every** commit. | **The headline risk.** Task 4 generates `app/deptrac.baseline.yaml` (`deptrac analyse --formatter=baseline`) capturing all current violations; job 10 then fails only on **new** drift. Same philosophy as `app/phpstan-baseline.neon`. The baseline count is recorded as architecture debt in `docs/architecture/`. |
| DT4 | 🟢 | Overlap with Larastan #12 (§5 п.6 — no two tools per task). | Different tasks: Larastan = type-level bug detection (PHPStan); deptrac = layer-dependency graph. Not a duplicate. Stated explicitly in the Tooling entry (Task 7). |
| DT5 | 🟢 | Overlap with adr-kit's `adr-judge` #36 — both "architecture enforcement" in lefthook. | Complementary, different mechanism: `adr-judge` = declarative regex on ADR `## Enforcement` text blocks; deptrac = AST-level layer-dependency graph. Documented in the Tooling entry + an ADR (Task 3 Step 5). |
| DT6 | 🟢 | The project nests Laravel at `app/`, so the `App\` source is `app/app/`. | deptrac config lives at `app/deptrac.yaml`; `paths: [app]` resolves to `app/app/`. lefthook job 10 uses `root: "app/"`. |
| DT7 | 🟢 | New `deptrac.yaml` / `deptrac.baseline.yaml` files. | `.yaml` — no pre-commit lint job touches it (squawk = `*.sql`, cspell/markdownlint = `*.md`). No lint conflict. |
| DT8 | 🟢 | Native Windows (no Docker / nested-virt — see `project_phase1_strategy`). | deptrac is pure PHP, runs via `php`. No extension/virtualization dependency (unlike pg_partman). |
| DT-NUM | 🟡 | A6 = #36-38, D3 = #39-40, C9 = #41(-42). deptrac must not collide. | Task 7 reads the **live** `docs/Tooling_v8_3.md` Прил. Н §0 counter and assigns `counter + 1`. Never hard-code. |
---
## File Structure
| File | Created / Modified | Responsibility |
|---|---|---|
| `app/deptrac.yaml` | Create | deptrac layer definitions + dependency ruleset |
| `app/deptrac.baseline.yaml` | Create | Baseline of pre-existing violations (DT3) |
| `app/composer.json` + `app/composer.lock` | Modify | deptrac dev-dependency (DT1 primary path) |
| `bin/deptrac.phar` | Create *(conditional — DT1 fallback only)* | Vendored deptrac PHAR if the composer resolver conflicts |
| `lefthook.yml` | Modify | New pre-commit **job 10** `deptrac` |
| `docs/architecture/c4-component-layers.md` | Create | Code-derived layer/component diagram (gap 4) + baseline-debt note |
| `docs/adr/ADR-005-architecture-fitness-deptrac.md` | Create | ADR recording the layer model + deptrac decision (reuses adr-kit from A6) |
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new architecture-tooling subsection + §0 counter bump |
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 Блок 1 — new deptrac row |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — extend the architecture-tooling category note |
| `CLAUDE.md` | Modify (**via claude-md-management only** — §5 п.10) | §3 title count, §1 row 2b count, new §3.3 row, §6 paragraph |
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
| `docs/automation-graph.html` | Modify | New `deptrac` node → `NODE_SECTION` A6; header metrics |
| `cspell-words.txt` | Modify | deptrac vocabulary (`deptrac`, `qossmic`, `Deptrac`, …) |
---
## Task 1: Pre-flight — branch, baseline, fact-check, spike
**Files:** none modified (read-only) except a new branch.
- [ ] **Step 1: Confirm tree state and create the working branch**
```bash
cd "c:/моя/проекты/портал crm/Документация"
git status --short
git fetch origin
git checkout -b feat/deptrac-architecture-fitness origin/main
git rev-parse --short HEAD
```
Expected: D3 (and, per the user's chosen slot, C9) are already on `origin/main`. Record the `origin/main` HEAD SHA as the regression baseline. Push pattern at the end: `git push origin feat/deptrac-architecture-fitness:main`.
- [ ] **Step 2: Baseline regression**
Run `/regression quick`. Expected: GREEN. Record the last green Pest / Vitest counts from memory `project_state.md`. This epic changes **no** `app/` runtime code → the Task 9 run must match exactly.
- [ ] **Step 3: Snapshot the hook chain**
Read `.claude/settings.json` and `~/.claude/settings.json`; record every lifecycle hook. deptrac registers none — this snapshot is the Task 9 comparison baseline.
- [ ] **Step 4: Fact-check deptrac (DT1)**
WebFetch `https://github.com/qossmic/deptrac` and the Packagist page. Confirm and record:
- Current Composer package name + latest major version.
- PHP 8.3 / Laravel 13 compatibility (deptrac analyses source, does not load the app — compatibility is just the PHP runtime version).
- The collector config syntax for the installed major (`type: directory` path-regex vs `type: classLike` namespace-regex) — Task 3 writes the config for **this** syntax.
- The available `--formatter` values (specifically whether `mermaidjs` exists — Task 6 uses it; fallback `graphviz`).
If the current major's config schema differs materially from the Task 3 example below → adjust Task 3's config to match; note the deviation.
- [ ] **Step 5: Spike — run deptrac once on the real codebase**
Install deptrac into a throwaway location (do **not** commit yet) and run it once to learn the real violation count — this sizes DT3:
```bash
cd app
composer require --dev qossmic/deptrac --no-interaction --dry-run
```
Record whether `--dry-run` reports a dependency conflict (drives Task 2's path). Then do a real throwaway install + a single `vendor/bin/deptrac analyse` against a *minimal* temp config (one `Service`/`Model` layer pair) just to confirm the binary runs on Windows and prints a violation report. Record the rough violation count. Revert the throwaway install (`git checkout app/composer.json app/composer.lock`) — Task 2 does the real, committed install.
No repo files committed in Task 1.
---
## Task 2: Install deptrac
**Files:** Modify `app/composer.json`, `app/composer.lock`**or** Create `bin/deptrac.phar` (DT1 fallback).
- [ ] **Step 1: Install — primary path (composer dev-dependency)**
If Task 1 Step 5 reported **no** resolver conflict:
```bash
cd app
composer require --dev qossmic/deptrac --no-interaction
php vendor/bin/deptrac --version
```
Expected: deptrac added under `require-dev` in `app/composer.json`; `vendor/bin/deptrac` exists; `--version` prints the version. Record it.
- [ ] **Step 2: Install — fallback path (PHAR in `bin/`)**
Run this **only if** Step 1's `composer require` failed the dependency resolver. Download the pinned PHAR release asset from `https://github.com/qossmic/deptrac/releases` into `bin/deptrac.phar`:
```bash
curl -L -o bin/deptrac.phar https://github.com/qossmic/deptrac/releases/download/<version>/deptrac.phar
php bin/deptrac.phar --version
```
Expected: `php bin/deptrac.phar --version` prints the version. Record which path (Step 1 or Step 2) was taken — every later `deptrac` invocation in this plan uses `php vendor/bin/deptrac` (Step 1) **or** `php ../bin/deptrac.phar` run from `app/` (Step 2).
- [ ] **Step 3: Verify the offline security audit still passes**
```bash
cd app && composer audit --locked
```
Expected: 0 advisories (Roave already gates installs; this confirms deptrac added none).
- [ ] **Step 4: Commit**
Primary path:
```bash
git add app/composer.json app/composer.lock
git commit -m "build(deptrac): add deptrac as a composer dev-dependency (DT1)"
```
Fallback path:
```bash
git add bin/deptrac.phar
git commit -m "build(deptrac): vendor deptrac PHAR into bin/ (DT1 fallback)"
```
---
## Task 3: Author `deptrac.yaml` — layers + conservative ruleset
**Files:** Create `app/deptrac.yaml`; Create `docs/adr/ADR-005-architecture-fitness-deptrac.md`.
- [ ] **Step 1: Write the deptrac config**
Create `app/deptrac.yaml` (adjust the collector `type` to the syntax recorded in Task 1 Step 4 — the example uses the `type: directory` path-regex form):
```yaml
deptrac:
paths:
- ./app
layers:
- name: Controller
collectors: [{ type: directory, value: app/Http/Controllers/.* }]
- name: Request
collectors: [{ type: directory, value: app/Http/Requests/.* }]
- name: Resource
collectors: [{ type: directory, value: app/Http/Resources/.* }]
- name: Middleware
collectors: [{ type: directory, value: app/Http/Middleware/.* }]
- name: Service
collectors: [{ type: directory, value: app/Services/.* }]
- name: Job
collectors: [{ type: directory, value: app/Jobs/.* }]
- name: Console
collectors: [{ type: directory, value: app/Console/.* }]
- name: Repository
collectors: [{ type: directory, value: app/Repositories/.* }]
- name: Model
collectors: [{ type: directory, value: app/Models/.* }]
- name: Mail
collectors: [{ type: directory, value: app/Mail/.* }]
- name: Rule
collectors: [{ type: directory, value: app/Rules/.* }]
- name: Exception
collectors: [{ type: directory, value: app/Exceptions/.* }]
- name: Provider
collectors: [{ type: directory, value: app/Providers/.* }]
ruleset:
# Conservative ruleset — enforces only the architecturally-wrong directions
# (inward/upward deps). Whatever current code violates is captured by the
# Task 4 baseline; this gate then catches only NEW drift.
Controller: [Service, Request, Resource, Model, Job, Mail, Repository, Rule, Exception]
Middleware: [Service, Model, Exception]
Service: [Service, Model, Repository, Job, Mail, Rule, Exception]
Job: [Service, Model, Repository, Mail, Exception]
Console: [Service, Model, Repository, Job, Mail, Exception]
Repository: [Model, Exception]
Request: [Rule, Model]
Resource: [Model]
Rule: [Model]
Mail: [Model]
Model: ~ # bottom layer — depends on nothing in App\
Provider: [Controller, Service, Job, Console, Repository, Model, Mail, Middleware, Request, Resource, Rule, Exception]
```
- [ ] **Step 2: Run deptrac — see the current state (the "failing test")**
Primary: `cd app && php vendor/bin/deptrac analyse --no-progress` (fallback: `cd app && php ../bin/deptrac.phar analyse --no-progress`).
Expected: deptrac parses the config and prints a violation report. If the config has a **schema error**, deptrac prints the offending key — fix per its message and re-run. Record the violation count + the uncovered-class count. A non-zero violation count here is expected and correct (it is the architecture debt the baseline will capture).
- [ ] **Step 3: Write ADR-005**
Create `docs/adr/ADR-005-architecture-fitness-deptrac.md` (Nygard format — mirror the shape of `docs/adr/ADR-000`..`ADR-003`):
```markdown
# ADR-005: Architecture-fitness enforcement via deptrac
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
Map section A6 had tools to *record* (adr-kit), *visualize* (mermaid-skill) and
*reference* (architecture-patterns) architecture — but nothing enforced that the
code keeps matching the layered architecture (Controller → Service → Model …).
`adr-judge` enforces only what is hand-written as a regex in an ADR Enforcement
block — too narrow for dependency-direction rules.
## Decision
- Adopt **deptrac** — a declarative, zero-LLM static-analysis tool — as the
layer-dependency gate, wired as lefthook pre-commit job 10.
- The layer model and ruleset are defined in `app/deptrac.yaml` (conservative —
enforces only inward/upward-violating directions).
- Pre-existing violations are captured in `app/deptrac.baseline.yaml`; the gate
fails only on NEW drift. Reducing the baseline is tracked architecture debt.
## Consequences
- Positive: layer drift is caught at commit time, deterministically, free.
- Risk: a too-strict ruleset produces noise — mitigated by the conservative
ruleset + the baseline.
## Enforcement
The layer rules live in `app/deptrac.yaml`, enforced by lefthook job 10 — not by
an `adr-judge` regex. This ADR has no `adr-judge` Enforcement clause.
```
- [ ] **Step 4: Lint + commit**
```bash
npx markdownlint-cli2 "docs/adr/ADR-005-*.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/adr/ADR-005-*.md"
```
Add valid flagged terms (`deptrac`, `qossmic`, `Nygard`, …) to `cspell-words.txt`. Then:
```bash
git add app/deptrac.yaml docs/adr/ADR-005-*.md cspell-words.txt
git commit -m "feat(deptrac): layer model + ruleset config + ADR-005"
```
---
## Task 4: Generate the baseline (DT3 — the headline risk)
**Files:** Create `app/deptrac.baseline.yaml`; Modify `app/deptrac.yaml`.
- [ ] **Step 1: Generate the baseline file**
```bash
cd app && php vendor/bin/deptrac analyse --formatter=baseline --output=deptrac.baseline.yaml
```
(Fallback binary: `php ../bin/deptrac.phar …`.) Expected: `app/deptrac.baseline.yaml` is created, listing every current violation as a skipped entry.
- [ ] **Step 2: Wire the baseline into the config**
Edit `app/deptrac.yaml` — add under the `deptrac:` key:
```yaml
baseline_file: ./deptrac.baseline.yaml
```
- [ ] **Step 3: Run deptrac — confirm GREEN (the "passing test")**
```bash
cd app && php vendor/bin/deptrac analyse --no-progress
```
Expected: **0 reported violations** (all pre-existing ones absorbed by the baseline). Exit code 0. If any violation still reports → the baseline did not capture it; regenerate (Step 1) and re-check.
- [ ] **Step 4: Commit**
```bash
git add app/deptrac.yaml app/deptrac.baseline.yaml
git commit -m "feat(deptrac): baseline pre-existing violations — gate green from day 1 (DT3)"
```
---
## Task 5: Wire lefthook pre-commit job 10
**Files:** Modify `lefthook.yml`.
- [ ] **Step 1: Add job 10**
Edit `lefthook.yml` — append after job 9 (`adr-judge`), inside `pre-commit.jobs`:
```yaml
# 10. deptrac — архитектурный гейт направления зависимостей / границ слоёв
# (Прил. Н — architecture-tooling). Анализирует app/app/** по слоям из
# app/deptrac.yaml; baseline_file гасит унаследованные нарушения (DT3) —
# job падает только на НОВОМ дрейфе. Без glob точечного анализа: deptrac
# строит граф классов, нужен весь app/. Чистый PHP, без LLM (AK6).
- name: deptrac
glob: "app/**/*.php"
root: "app/"
run: php vendor/bin/deptrac analyse --no-progress
fail_text: |
deptrac: staged-изменение нарушает направление зависимостей между
слоями (см. file:line выше и app/deptrac.yaml ruleset).
Если это осознанное архитектурное изменение — сначала обнови
app/deptrac.yaml (и при необходимости ADR-005), затем коммить.
```
Fallback-PHAR variant of the `run:` line: `run: php ../bin/deptrac.phar analyse --no-progress` — use whichever Task 2 path was taken.
- [ ] **Step 2: Verify the full chain runs**
```bash
npx lefthook run pre-commit
```
Expected: all 10 jobs execute; job 10 `deptrac` runs and reports 0 violations (baseline applied). Record job 10's wall-time (DT2).
- [ ] **Step 3: Red-green — prove the gate actually catches drift**
Introduce a deliberate violation, confirm deptrac flags it, then revert:
```bash
# pick a Model and make it import a Service (a forbidden upward dependency)
```
Edit any file under `app/app/Models/` to add `use App\Services\NotificationService;` and reference it. Run `cd app && php vendor/bin/deptrac analyse --no-progress`.
Expected: **deptrac reports 1 violation** (`Model must not depend on Service`), exit code non-zero. Then revert the edit (`git checkout app/app/Models/<file>`) and re-run — expected 0 violations again. This is the red-green proof that job 10 works.
- [ ] **Step 4: Commit**
```bash
git add lefthook.yml
git commit -m "ci(deptrac): wire deptrac as lefthook pre-commit job 10"
```
---
## Task 6: Code-derived C4-component diagram (gap 4)
**Files:** Create `docs/architecture/c4-component-layers.md`; Modify the `docs/architecture/` index.
- [ ] **Step 1: Generate the layer graph from code**
If Task 1 Step 4 confirmed a `mermaidjs` formatter:
```bash
cd app && php vendor/bin/deptrac analyse --formatter=mermaidjs --output=../docs/architecture/_deptrac-layers.mmd
```
Else use `--formatter=graphviz-dot --output=../docs/architecture/_deptrac-layers.dot`. The graph is **derived from the actual code** — it cannot drift.
- [ ] **Step 2: Create the diagram doc**
Create `docs/architecture/c4-component-layers.md`: a short intro, then the generated Mermaid graph embedded in a ```mermaid fence (or, for the graphviz fallback, a note + the committed `.dot` file). Include the recorded baseline violation count (Task 4 Step 1) as a one-line "architecture debt" figure. State that this is the **component-level** C4 view, derived from `app/deptrac.yaml`, regenerate via the Task 6 Step 1 command — and that **context/container-level** C4 (`c4-context.md`, external systems) stays hand-maintained.
- [ ] **Step 3: Link it from the architecture index**
Read the `docs/architecture/` index/README created by the A6 epic; add a link to `c4-component-layers.md` next to `c4-context.md`.
- [ ] **Step 4: Lint + commit**
```bash
npx markdownlint-cli2 "docs/architecture/c4-component-layers.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/architecture/c4-component-layers.md"
git add docs/architecture/
git commit -m "docs(arch): code-derived C4 component-layer diagram from deptrac (gap 4)"
```
---
## Task 7: Normative registry sync (DT-NUM)
**Files:** Modify `docs/Tooling_v8_3.md`, `docs/Plugin_stack_rules_v1.md`, `docs/Pravila_raboty_Claude_v1_1.md`, `CLAUDE.md`, `docs/CHANGELOG_claude_md.md`, `cspell-words.txt`.
- [ ] **Step 1: Read the registry homes + the live counter (DT-NUM)**
Read for exact insertion points and the **current** counter: `docs/Tooling_v8_3.md` Прил. Н §0 + the last `§4.x` subsection (architecture-tooling = §4.11-4.13); `docs/Plugin_stack_rules_v1.md` R10.1 Блок 1; `docs/Pravila_raboty_Claude_v1_1.md` §13.2. Assign deptrac the number `counter + 1`**after** the A6/D3/C9 entries already present. Record `#N`.
- [ ] **Step 2: Add the Tooling Прил. Н subsection**
Edit `docs/Tooling_v8_3.md`: add a `§4.x` subsection for `#N deptrac`, category **architecture-tooling** (off-phase — the 4th tool of that subcategory, with adr-kit/mermaid/architecture-patterns). Record: `qossmic/deptrac` BSD-3; install mode (composer dev-dep or `bin/deptrac.phar` — whichever Task 2 took); config `app/deptrac.yaml` + `app/deptrac.baseline.yaml`; lefthook job 10; zero-LLM (AK6-aligned); the DT4 boundary vs Larastan (#12 — type analysis ≠ layer graph) and DT5 vs adr-judge (#36 — regex ≠ AST graph). Bump the §0 counter and the Прил. Н version header. Mirror the shape of the §4.11 adr-kit entry.
- [ ] **Step 3: Add the PSR_v1 R10.1 row**
Edit `docs/Plugin_stack_rules_v1.md`: add a deptrac row to R10.1 Блок 1, category **architecture-tooling** — explicitly *outside* the UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline (same treatment as adr-kit/architecture-patterns). Bump the PSR_v1 version header.
- [ ] **Step 4: Extend the Pravila §13.2 note**
Edit `docs/Pravila_raboty_Claude_v1_1.md` §13.2: the **architecture-tooling** category note already exists (added by A6 for #36-38) — extend it to include `#N deptrac`. Re-read Pravila §0/§13 first to keep numbering consistent. Bump the Pravila version header.
- [ ] **Step 5: Update CLAUDE.md via the governed channel**
Invoke `/claude-md-management:claude-md-improver`. Apply: §3 title count bump (+1); §1 priority-chain row 2b count bump (+1); a new §3.3 row for `#N deptrac` (architecture-tooling, lefthook job 10, mirrors the #36 adr-kit row); a §6 paragraph noting the deptrac architecture-fitness integration. The plugin also writes the `docs/CHANGELOG_claude_md.md` entry and bumps the §0 cross-ref versions. **Do not** edit `CLAUDE.md` directly (§5 п.10).
- [ ] **Step 6: Lint + commit**
```bash
npx markdownlint-cli2 "docs/Tooling_v8_3.md" "docs/Plugin_stack_rules_v1.md" "docs/Pravila_raboty_Claude_v1_1.md" "docs/CHANGELOG_claude_md.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/Tooling_v8_3.md" "docs/Plugin_stack_rules_v1.md" "docs/Pravila_raboty_Claude_v1_1.md"
git add docs/Tooling_v8_3.md docs/Plugin_stack_rules_v1.md docs/Pravila_raboty_Claude_v1_1.md CLAUDE.md docs/CHANGELOG_claude_md.md cspell-words.txt
git commit -m "docs(deptrac): register #N deptrac architecture-tooling (DT-NUM)"
```
---
## Task 8: Reflect deptrac on the map — extend section A6
**Files:** Modify `docs/automation-graph.html`.
- [ ] **Step 1: Read the structures to replicate**
In `docs/automation-graph.html` read, as templates, the A6 node `adr_kit` across `NODES`, `NODE_DETAILS`/`nd(...)`, `NODE_META`, `NODE_SECTION`, and the lefthook-job nodes (`lh_larastan`) — deptrac is both an architecture-tooling node and a lefthook job. Record the current node/edge counts from the header comment.
- [ ] **Step 2: Add the `deptrac` node**
Add to `NODES` a `deptrac` node replicating the `adr_kit` shape (plugins/tooling group, `ring`/`pos` near the other A6 nodes). Add a matching `nd(...)` / `NODE_DETAILS` entry (Russian, per the file's convention) and a `NODE_META` entry with `since: '17.05.2026'`:
> "deptrac — архитектурный гейт: статический анализ направления зависимостей между слоями (Controller/Service/Model/…), lefthook job 10, baseline на унаследованные нарушения. Декларативно, без LLM (AK6). Закрывает A6-пробелы conformance + границы слоёв + дрейф C4."
Add an edge from `tooling` (or `psr_v1`) to `deptrac`, mirroring the A6 edges.
- [ ] **Step 3: Map the node to section A6**
In `NODE_SECTION` add `deptrac: 'A6',`. Section A6 «Архитектура систем» goes 3 → 4 nodes. Update the group-count comment (plugins/tooling) and the header node/edge metrics (+1 node, +1 edge).
- [ ] **Step 4: Smoke-test the map**
```bash
npx stylelint docs/automation-graph.html
```
Open `docs/automation-graph.html` via a local `http.server` (quirk 90 — `file://` rejected) with Playwright MCP. Verify: 0 JS console errors (favicon 404 is harmless); `NODES.length` increased by 1; the `deptrac` node renders; clicking section A6 highlights 4 nodes.
- [ ] **Step 5: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): deptrac node — extends section A6 to 4 nodes"
```
---
## Task 9: Final regression & branch finish
**Files:** none modified.
- [ ] **Step 1: Full pre-commit chain**
```bash
npx lefthook run pre-commit
```
Expected: all **10** jobs green (job 10 `deptrac` = 0 violations, baseline applied).
- [ ] **Step 2: Confirm app runtime code untouched — run the suites**
This epic adds only `deptrac.yaml` + a composer dev-dep + docs; it changes **no** `app/` runtime code:
```bash
cd app && composer test:parallel
npm run test:vue
```
Expected: Pest and Vitest counts **identical** to the Task 1 Step 2 baseline (0 regressions). Record exact counts; write out any failure with file:line.
- [ ] **Step 3: Confirm the hook chain is intact**
Compare `.claude/settings.json` + `~/.claude/settings.json` to the Task 1 Step 3 snapshot — unchanged. The economy marker still appears; the Stop verifier still runs. deptrac leaked no lifecycle hook.
- [ ] **Step 4: Pre-push checks**
```bash
./bin/gitleaks.exe detect --source . --no-banner --config .gitleaks.toml --redact
./bin/lychee.exe --config .lychee.toml "docs/**/*.md" "*.md"
```
Expected: gitleaks 0 leaks; lychee 0 broken (new `docs/architecture/c4-component-layers.md` + `docs/adr/ADR-005-*.md` are scanned — fix or `.lychee.toml`-exclude any link).
- [ ] **Step 5: Finish the branch**
Invoke `superpowers:finishing-a-development-branch` — present the standard options. Do **not** push without an explicit user choice. Push pattern: `git push origin feat/deptrac-architecture-fitness:main`.
---
## Self-Review
**1. Spec coverage (the 4 A6 gaps).** Gap «conformance / fitness» + gap «направление зависимостей / границ слоёв» → Tasks 3-5 (deptrac.yaml ruleset + baseline + lefthook job 10, red-green-proven). Gap «дрейф C4 vs код» → Task 6 (code-derived component diagram; context-level explicitly carried to a §8 checklist line — Task 7 Step 5 via the §6 paragraph / acknowledged out-of-tooling). Gap «активное проектирование архитектуры фичи» → **deliberately not a tool**: it is covered by the existing cross-cutting `brainstorming`/`writing-plans`/SPARC-architect + architecture-patterns #38, and ADR-005 (Task 3) records the layer model that design work now references — closing the gap by wiring, not by an install (per the agreed analysis; a 4th overlapping design agent would violate §5 п.6). Conflict audit: DT1→T2, DT2→T5.2, DT3→T4, DT4/DT5→T7.2, DT6→T3.1, DT7→(no task — no lint conflict), DT8→T2, DT-NUM→T1/T7.1. No gaps.
**2. Placeholder scan.** `#N` (Task 7) and the deptrac package version / collector syntax / formatter list (Task 3, Task 6) are **runtime-resolved by design** — Task 1 Steps 4-5 carry concrete resolution criteria (same pattern as the A6/D3/C9 plans). The `deptrac.yaml`, `lefthook.yml` job 10, and ADR-005 contents are shown in full. No "TBD" / "handle edge cases".
**3. Consistency.** Branch `feat/deptrac-architecture-fitness` consistent T1↔T9. Node id `deptrac` consistent T8 Steps 2-3. Category **architecture-tooling** consistent T7 Steps 2-4. Paths consistent: `app/deptrac.yaml`, `app/deptrac.baseline.yaml`, `docs/architecture/c4-component-layers.md`, `docs/adr/ADR-005-*.md`. The DT1 primary/fallback fork (composer dep vs `bin/deptrac.phar`) is flagged uniformly (T2, T5.1 `run:` line, T7.2). lefthook job count 9 → 10 consistent T5↔T9.
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-17-deptrac-architecture-fitness-integration.md`.
**Sequencing decision required before execution** (see the "Sequencing" header): this epic runs **after D3**. Its slot relative to **C9** is the user's call — `… → D3 → deptrac → C9` or `… → D3 → C9 → deptrac`. Both are valid.
**Execution method:**
1. **Subagent-Driven (recommended)** — fresh subagent per task, two-stage review. *Caveat:* Task 1 Step 4-5 (WebFetch), Task 7 Step 5 (`claude-md-management`), Task 8 Step 4 (Playwright smoke) are main-session-bound — those steps stay with the controller.
2. **Inline Execution**`superpowers:executing-plans`, batch with checkpoints (same as the A6/D3/C9 plans — install/config/docs-heavy with interactive steps).
@@ -0,0 +1,112 @@
# A3 Integration-Tooling — дизайн интеграции
**Дата:** 2026-05-17
**Раздел карты:** A3 «Программирование — интеграции (API, вебхуки)»
**Параллель:** A6 architecture-tooling (17.05.2026), D3 audit-security (17.05.2026)
**Ветка:** `feat/a3-integration-tooling` (rebased на origin/main `1313d89`, CLAUDE.md v2.8)
## 1. Проблема
Раздел A3 карты `docs/automation-graph.html` пуст — 0 узлов в `NODE_SECTION`
(строки 1868-1911, ни одного значения `'A3'`). Интеграционная работа физически
идёт — REST-эндпоинты (deals/lookup/billing/admin), webhook-приём
(HMAC + per-token rate-limit), ~5 внешних API (Unisender Go / Yandex Cloud /
Yandex 360 / JivoSite / Sentry) — но инструментами разделов A1/A5/A7/A8/E7.
Ни один инструмент не классифицирован как A3. Состояние — как у A6 до 17.05.
(строки 1868-1911 в оригинальном форке — после ребейза локализовать Grep'ом по `'A3'` в `NODE_SECTION`).
## 2. Цель и scope
Наполнить раздел A3 карты — параллельно A6/D3.
**Scope = формализация инструментов**, не их применение. A6-прецедент: поставил
adr-kit, но не аудировал весь код; создал ADR-000/001/002 + одну C4-диаграмму как
smoke.
**Вне scope:** полная OpenAPI-спека REST API проекта; рефактор webhook-кода;
mock-инфраструктура внешних API в тестах (Microcks отклонён — требует
Docker/JVM+Mongo, несовместим с native-Windows-no-Docker стеком проекта).
## 3. Состав узлов A3 (7)
### 3.1. Новые узлы карты (2)
| Узел (id карты) | Что | Установка | Tooling-реестр |
|---|---|---|---|
| **api-docs agent** (`ag_apidocs`, claude-flow) | генерация OpenAPI-спеки REST API, pattern learning | 0 — агент уже доступен в сессии | **нет номера** — claude-flow sub-агент; реестр Прил. Н — plugin-grain (как 11 уже существующих agent-узлов карты, ни один не имеет Tooling-номера) |
| **openapi-mcp-server** (`mcp_openapi`, npm, stdio MCP) | отдаёт OpenAPI-спеку как MCP-ресурс/тулы; introspection своей и чужих API | `npm i` + запись в `.mcp.json` | **#47**, Tooling §4.22 |
### 3.2. Кросс-реф вторичным тегом A3 (5 — первично остаются в своих разделах)
| Узел | Первичный раздел | Роль в A3 |
|---|---|---|
| `context7` | E7 | актуальная дока внешних API при интеграции |
| `mcp_boost` / Boost #10 | A1 | серверные REST-эндпоинты, HTTP-клиент, Sanctum-auth |
| `ag_pest` / Pest 4 #18 | A5 | contract/integration-тесты эндпоинтов и вебхук-приёма |
| `mcp_semgrep` / Semgrep #25 | A8 | безопасность интеграций (hardcoded webhook URL, API-key leak) |
| `mcp_sentry` / Sentry MCP #34 | A7 | runtime-ошибки вебхук-хендлеров и внешних вызовов |
## 4. Правка модели карты (`docs/automation-graph.html`)
Развилка: `NODE_SECTION` строго 1:1 (узел → один раздел), кросс-реф 5
существующих узлов в A3 механически невозможен. Решение — аддитивный слой:
- **Новый объект `NODE_SECTION_SECONDARY`** (`NODE_SECTION` 1:1 не трогается):
```js
const NODE_SECTION_SECONDARY = {
mcp_boost: ['A3'], context7: ['A3'], ag_pest: ['A3'],
mcp_semgrep: ['A3'], mcp_sentry: ['A3'],
};
```
- **Цикл `SECTION_NODES`** (строка ~1915): узел добавляется в первичный раздел
И в каждый раздел из `NODE_SECTION_SECONDARY`.
- **Панель «Разделы»** — узел показывается под всеми своими разделами.
- **Паспорт узла, строка «Раздел»** (`#ld-section`, строка 147): формат
`A1 (+A3)` для кросс-реф узлов.
- **2 новых объекта `NODES`:** `ag_apidocs` (group `agents`), `mcp_openapi`
(group `mcp`) + позиционирование `pos()`.
- **`NODE_SECTION`:** `ag_apidocs: 'A3', mcp_openapi: 'A3'` (первичный раздел).
- **`NODE_TIMELINE`:** `ag_apidocs` / `mcp_openapi``since: '17.05.2026'`.
- **Рёбра (~2-3):** новые узлы → governing-правила, напр.
`E('psr_v1', 'mcp_openapi', 'R10.1 блок 3:\nintegration-tooling')`.
- **Счётчики:** 116→118 узлов, +3 ребра. Новых конфликтов не ожидается.
## 5. Нормативка (4 файла — как A6/D3)
| Файл | Правка | Версия |
|---|---|---|
| Tooling Прил. Н | §4.22 (новый) — #47 openapi-mcp-server; §0 счётчик 46→47; 9-я off-phase подкатегория **integration-tooling** | v2.8→v2.9 |
| PSR_v1 | R10.1 Блок 3 (MCP-серверы) +1 строка openapi-mcp; integration-tooling — не UI → вне R6/R14 | v3.8→v3.9 |
| Pravila | §13.2 +абзац «Off-phase integration-tooling» | v1.22→v1.23 |
| CLAUDE.md | §3 title 46→47; §3.3 +строка #47 (+упоминание api-docs agent); §1 row 2b 46→47; §3.3 footer; §0 cross-refs; §6 +абзац A3 | v2.8→v2.9 |
CLAUDE.md — через `/claude-md-management:claude-md-improver` (§5 п.10). Кросс-реф
5 существующих инструментов — **только map-модель**; в нормативный реестр не
попадает (инструменты уже зарегистрированы по своей идентичности).
## 6. Smoke / верификация
- Аудит установки openapi-mcp: точное имя npm-пакета (кандидат
`ivo-toby/mcp-openapi-server`), native-Windows совместимость, stdio-режим
(без port-conflict), кириллица в пути (квирк #26).
- Smoke: openapi-mcp поднят на тестовой спеке (PONG-эквивалент) + dispatch
api-docs agent на одну группу эндпоинтов (deals API) → стартовый
OpenAPI-скелет как proof. Полную спеку не генерируем.
- Регрессия `quick` (lint/format/type-check) перед коммитом нормативки.
- Визуальный smoke карты: открыть `automation-graph.html` — 118 узлов,
0 JS-ошибок, панель «Разделы» показывает A3 с 7 узлами.
- `git` без bypass хуков.
## 7. Нумерация (риск реализовался — закрыт)
Ветка `feat/a3-integration-tooling` исходно форкнулась от D3-эры (`7c12b74`). За это время в origin/main влиты C9 (#41 CCPM / #42 product-management), deptrac (#43), A4 (#44/#45/#46). Риск из исходной редакции §7 материализовался. Закрыт ребейзом feat/a3 на актуальный origin/main `1313d89`: openapi-mcp-server подтверждён как **#47**, Tooling **§4.22**, integration-tooling — **9-я** off-phase подкатегория. Остаточный риск: если ещё одна интеграция (напр. A11) смёрджится в main раньше A3 — финально сверить счётчик Tooling §0 перед коммитом нормативки (план Task 10 Step 2).
## 8. Ветка и артефакты
- Ветка `feat/a3-integration-tooling` rebased на origin/main `1313d89`.
- Spec: этот файл.
- План: `docs/superpowers/plans/2026-05-17-a3-integration-tooling-integration.md`.
- Merge: `git push origin feat/a3-integration-tooling:main` после D3 (ветка A3
содержит D3-коммиты как предков — корректный порядок merge).
+16
View File
@@ -132,6 +132,22 @@ pre-commit:
решение (ADR). Смотри file:line выше и docs/adr/ADR-*.md.
Если ADR устарел — сначала обнови ADR (и его Enforcement-блок).
# 10. deptrac — архитектурный гейт направления зависимостей / границ слоёв
# (Прил. Н #43, architecture-tooling). Анализирует app/app/** по 13 слоям
# из app/deptrac.yaml. Первый прогон: 0 нарушений — кодовая база уже
# конформна, baseline-файл не нужен; job падает на НОВОМ дрейфе (Model→
# Service, Service→Http и т.п.). Без glob точечного анализа: deptrac строит
# граф классов, нужен весь app/. Чистый PHP, без LLM (AK6).
- name: deptrac
glob: "app/**/*.php"
root: "app/"
run: php vendor/bin/deptrac analyse --no-progress
fail_text: |
deptrac: staged-изменение нарушает направление зависимостей между
слоями (см. вывод выше и app/deptrac.yaml ruleset).
Если это осознанное архитектурное изменение — сначала обнови
app/deptrac.yaml (и при необходимости ADR-005), затем коммить.
# Pre-push: проверки перед git push (медленнее, но реже запускаются)
pre-push:
parallel: false