Compare commits

...

57 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
Дмитрий 3b7023809f feat(map): C9 nodes ccpm + product_mgmt — closes section «Управление проектами» 2026-05-17 09:10:44 +03:00
Дмитрий d733ad0a2f docs(c9): CLAUDE.md v2.6 — register C9 project-management tooling (#41-42) 2026-05-17 09:10:44 +03:00
Дмитрий 2cf7471687 docs(c9): register CCPM + product-management #41-42 (project-management category) 2026-05-17 09:10:44 +03:00
Дмитрий 6b4e7441c9 feat(c9): bootstrap docs/projects + CCPM store + ADR-004 2026-05-17 09:10:44 +03:00
Дмитрий a7b207e689 feat(c9): vendor CCPM skill into .claude/skills + lint-ignore (CP1) 2026-05-17 09:10:44 +03:00
Дмитрий 6b2da83851 docs(plan): C9 project-management tooling integration plan 2026-05-17 09:10:44 +03:00
Дмитрий cc3f2e5b13 feat(c9): enable GitHub MCP projects toolset for Projects v2 (GH1) 2026-05-17 09:10:44 +03:00
Дмитрий fad1c895a1 merge: Sprint 3E (D6/D7 — убрать placeholder-вкладки SettingsView) в портал 2026-05-17 09:03:21 +03:00
Дмитрий 1c217fae43 chore(cleanup): снять устаревший MDI clearable-workaround (CTO-19 tail) — Sprint 6 I5 2026-05-17 08:18:44 +03:00
Дмитрий 6230c0fa61 fix(a11y): aria-label с ключом на edit-кнопках AdminSystem — Sprint 6 G9 2026-05-17 08:18:44 +03:00
Дмитрий 7a537105e3 docs(polling): семантический doc-комментарий вместо списка call-site'ов — Sprint 6 F4 review-fixup 2026-05-17 08:18:44 +03:00
Дмитрий 8a7314d198 refactor(polling): вынести интервалы в constants/polling.ts — Sprint 6 F4 2026-05-17 08:18:44 +03:00
Дмитрий e41844a13b test(admin): явный stubEnv DEV=true в dev-баннер тесте — Sprint 6 B6 review-fixup 2026-05-17 08:18:43 +03:00
Дмитрий 11baaefe21 feat(admin): DEV-only баннер о застабленном auth-gate — Sprint 6 B6 2026-05-17 08:18:43 +03:00
Дмитрий 97a27fdfbf fix(a11y): focus-visible ring + keyboard-activation тест на eye-toggle — Sprint 6 A9 review-fixup 2026-05-17 08:18:43 +03:00
Дмитрий d41471c818 fix(a11y): accessible eye-toggle на полях пароля — Sprint 6 A9 2026-05-17 08:18:43 +03:00
Дмитрий 3360e6f023 docs(sprint6): implementation plan — P3 polish + cleanup tail 2026-05-17 08:18:43 +03:00
Дмитрий 7d84959c15 docs(d3): mark stale warn-only claim in D3 plan as corrected (v2.5)
The D3 plan still describes Security Guidance #40 as warn-only (the pre-correction belief). Plan body kept as a historical snapshot; added a one-line NB pointing to the v2.5 correction (Tooling §4.15 / ADR-003 / CLAUDE.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 07:36:26 +03:00
Дмитрий ded07d3a6b docs(d3): correct Security Guidance #40 — blocking hook, not warn-only
SG #40 was characterised across all D3 docs as warn-only / does not block. Verified end-to-end: security_reminder_hook.py does sys.exit(2) — a BLOCKING PreToolUse hook (one-time speed-bump per file+rule per session, the retry passes).

SG2: on this Windows host the bundled hooks.json hardcodes python3, absent from PATH — the hook never spawned (inert). Fixed with a python3.exe shim in the Python install dir (env-only, not in repo).

Normative sync: Tooling v2.5, PSR_v1 v3.5, Pravila v1.19, CLAUDE.md v2.5; ADR-003 amended; automation-graph sec_guidance nd(). Tool counts unchanged (40 positions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 07:29:42 +03:00
Дмитрий 608f4b2231 docs(a11): implementation plan — ML/AI tooling integration (A11)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 07:29:42 +03:00
Дмитрий 6a64a98fbf docs(a11): brainstorming spec — ML/AI tooling integration (A11)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 07:29:42 +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
96 changed files with 9083 additions and 342 deletions
+1
View File
@@ -0,0 +1 @@
# CCPM epic/task store — see docs/projects/README.md
+1
View File
@@ -0,0 +1 @@
# CCPM PRD store — see docs/projects/README.md
+87
View File
@@ -0,0 +1,87 @@
---
name: ccpm
description: "CCPM - spec-driven project management: PRD → Epic → GitHub Issues → parallel agents → shipped code. Use this skill for anything in the software delivery lifecycle: writing a PRD ('write a PRD for X', 'let's plan X', 'scope this out'), parsing a PRD into an epic, decomposing an epic into tasks, syncing to GitHub ('sync the X epic', 'push tasks to github'), starting work on an issue ('start working on issue N', 'let's work on issue N'), analyzing parallel work streams, running standups ('standup', 'run the standup'), checking status ('what's next', 'what's blocked', 'what are we working on'), closing issues, or merging an epic. Use ccpm any time the user is talking about shipping a feature, managing work, or tracking progress — even if they don't say 'ccpm' or 'PRD'. Do NOT use for: debugging code, writing tests, reviewing PRs, or raw GitHub issue/PR operations with no delivery context."
---
# CCPM - Claude Code Project Manager
A spec-driven development workflow: PRD → Epic → GitHub Issues → Parallel Agents → Shipped Code.
## Core Philosophy
Requirements live in files, not heads. Every feature starts as a PRD, becomes a technical epic, decomposes into GitHub issues, and gets executed by parallel agents with full traceability.
## File Conventions
Before doing anything, read `references/conventions.md` for path standards, frontmatter schemas, and GitHub operation rules. These apply to all phases.
## The Five Phases
### 1. Plan — Capture requirements
**When**: User wants to define a new feature, product requirement, or scope of work.
**Read**: `references/plan.md`
**Covers**: Writing PRDs through guided brainstorming, converting PRDs to technical epics.
### 2. Structure — Break it down
**When**: An epic exists and needs to be decomposed into concrete tasks.
**Read**: `references/structure.md`
**Covers**: Epic decomposition into numbered task files with dependencies and parallelization.
### 3. Sync — Push to GitHub
**When**: Local epic/tasks need to become GitHub issues, progress needs to be posted as comments, or a bug is found and needs a linked issue created.
**Read**: `references/sync.md`
**Covers**: Epic sync (epic + tasks → GitHub issues), issue sync (progress comments), closing issues/epics, bug reporting against completed issues.
### 4. Execute — Start building
**When**: User wants to start working on one or more GitHub issues with parallel agents.
**Read**: `references/execute.md`
**Covers**: Issue analysis (parallel work stream identification), launching parallel agents, coordinating worktrees.
### 5. Track — Know where things stand
**When**: User asks for status, standup report, what's blocked, what's next, or needs to validate state.
**Read**: `references/track.md`
**Covers**: Status, standup, search, in-progress, next priority, blocked items, validation.
---
## Script-First Rule
For deterministic operations — anything that reads and reports without needing reasoning — always run the bash script directly rather than doing the work manually:
| What the user wants | Script to run |
|---|---|
| Project status | `bash references/scripts/status.sh` |
| Standup report | `bash references/scripts/standup.sh` |
| List all epics | `bash references/scripts/epic-list.sh` |
| Show epic details | `bash references/scripts/epic-show.sh <name>` |
| Epic status | `bash references/scripts/epic-status.sh <name>` |
| List PRDs | `bash references/scripts/prd-list.sh` |
| PRD status | `bash references/scripts/prd-status.sh` |
| Search issues/tasks | `bash references/scripts/search.sh <query>` |
| What's in progress | `bash references/scripts/in-progress.sh` |
| What's next | `bash references/scripts/next.sh` |
| What's blocked | `bash references/scripts/blocked.sh` |
| Validate project state | `bash references/scripts/validate.sh` |
Use the LLM for work that requires reasoning: writing PRDs, analyzing parallelism, launching agents, synthesizing updates.
---
## Quick Reference
```
Plan a feature: "I want to build X" or "create a PRD for X"
Parse to epic: "turn the X PRD into an epic"
Decompose: "break down the X epic into tasks"
Sync to GitHub: "push the X epic to GitHub"
Start an issue: "start working on issue 42"
Check status: "what's our status" / "standup"
What's next: "what should I work on next"
Merge epic: "merge the X epic"
Report a bug: "found a bug in issue 42" / "testing issue 42 revealed X"
```
@@ -0,0 +1,178 @@
# Conventions — File Formats, Paths & Rules
Read this before doing any file operations across all phases.
---
## Directory Structure
```
.claude/
├── prds/
│ └── <feature-name>.md # Product requirement documents
├── epics/
│ ├── <feature-name>/
│ │ ├── epic.md # Technical epic
│ │ ├── <N>.md # Task files (named by GitHub issue number after sync)
│ │ ├── <N>-analysis.md # Parallel work stream analysis
│ │ ├── github-mapping.md # Issue number → URL mapping
│ │ ├── execution-status.md # Active agents tracker
│ │ └── updates/
│ │ └── <issue_N>/
│ │ ├── stream-A.md # Per-agent progress
│ │ ├── progress.md # Overall issue progress
│ │ └── execution.md # Execution state
│ └── archived/
│ └── <feature-name>/ # Completed epics
└── context/ # Project context docs (separate system)
```
---
## Frontmatter Schemas
### PRD (.claude/prds/<name>.md)
```yaml
---
name: <feature-name> # kebab-case, matches filename
description: <one-liner> # used in lists and summaries
status: backlog | active | completed
created: <ISO 8601> # date -u +"%Y-%m-%dT%H:%M:%SZ"
---
```
### Epic (.claude/epics/<name>/epic.md)
```yaml
---
name: <feature-name>
status: backlog | in-progress | completed
created: <ISO 8601>
updated: <ISO 8601>
progress: 0% # recalculated when tasks close
prd: .claude/prds/<name>.md
github: https://github.com/<owner>/<repo>/issues/<N> # set on sync
---
```
### Task (.claude/epics/<name>/<N>.md)
```yaml
---
name: <Task Title>
status: open | in-progress | closed
created: <ISO 8601>
updated: <ISO 8601>
github: https://github.com/<owner>/<repo>/issues/<N> # set on sync
depends_on: [] # issue numbers this must wait for
parallel: true # can run concurrently with non-conflicting tasks
conflicts_with: [] # issue numbers that touch the same files
---
```
### Progress (.claude/epics/<name>/updates/<N>/progress.md)
```yaml
---
issue: <N>
started: <ISO 8601>
last_sync: <ISO 8601>
completion: 0%
---
```
---
## Datetime Rule
Always get real current datetime from the system — never use placeholder text:
```bash
date -u +"%Y-%m-%dT%H:%M:%SZ"
```
---
## Frontmatter Update Pattern
When updating a single frontmatter field in an existing file:
```bash
sed -i.bak "/^<field>:/c\\<field>: <value>" <file>
rm <file>.bak
```
When stripping frontmatter to get body content for GitHub:
```bash
sed '1,/^---$/d; 1,/^---$/d' <file> > /tmp/body.md
```
---
## GitHub Operations
### Repository Safety Check (run before any write operation)
```bash
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$remote_url" == *"automazeio/ccpm"* ]]; then
echo "❌ Cannot write to the CCPM template repository."
echo "Update remote: git remote set-url origin https://github.com/YOUR/REPO.git"
exit 1
fi
REPO=$(echo "$remote_url" | sed 's|.*github.com[:/]||' | sed 's|\.git$||')
```
### Authentication
Don't pre-check authentication. Run the `gh` command and handle failure:
```bash
gh <command> || echo "❌ GitHub CLI failed. Run: gh auth login"
```
### Getting Issue Numbers
```bash
# From a task file's github field:
grep 'github:' <file> | grep -oE '[0-9]+$'
```
---
## Git / Worktree Conventions
- One branch per epic: `epic/<name>`
- Worktrees live at `../epic-<name>/` (sibling to project root)
- Always start branches from an up-to-date main:
```bash
git checkout main && git pull origin main
git worktree add ../epic-<name> -b epic/<name>
```
- Commit format inside epics: `Issue #<N>: <description>`
- Never use `--force` in any git operation
---
## Naming Conventions
- Feature names: kebab-case, lowercase, letters/numbers/hyphens, starts with a letter
- Task files before sync: `001.md`, `002.md`, ... (sequential)
- Task files after sync: renamed to GitHub issue number (e.g., `1234.md`)
- Labels applied on sync: `epic`, `epic:<name>`, `feature` (for epics); `task`, `epic:<name>` (for tasks)
---
## Epic Progress Calculation
```bash
total=$(ls .claude/epics/<name>/[0-9]*.md 2>/dev/null | wc -l)
closed=$(grep -l '^status: closed' .claude/epics/<name>/[0-9]*.md 2>/dev/null | wc -l)
progress=$((closed * 100 / total))
```
Update epic frontmatter when any task closes.
+223
View File
@@ -0,0 +1,223 @@
# Execute — Start Building with Parallel Agents
This phase covers analyzing GitHub issues for parallel work streams and launching agents to execute them.
---
## Issue Analysis
**Trigger**: User wants to understand how to parallelize work on an issue before starting.
### Preflight
- Find the local task file: check `.claude/epics/*/<N>.md` first, then search for `github:.*issues/<N>` in frontmatter.
- If not found: "❌ No local task for issue #<N>. Run a sync first."
### Process
Get issue details: `gh issue view <N> --json title,body,labels`
Read the local task file fully. Identify independent work streams by asking:
- Which files will be created/modified?
- Which changes can happen simultaneously without conflict?
- What are the dependencies between changes?
**Common stream patterns:**
- Database Layer: schema, migrations, models
- Service Layer: business logic, data access
- API Layer: endpoints, validation, middleware
- UI Layer: components, pages, styles
- Test Layer: unit tests, integration tests
Create `.claude/epics/<epic_name>/<N>-analysis.md`:
```markdown
---
issue: <N>
title: <title>
analyzed: <run: date -u +"%Y-%m-%dT%H:%M:%SZ">
estimated_hours: <total>
parallelization_factor: <1.0-5.0>
---
# Parallel Work Analysis: Issue #<N>
## Overview
## Parallel Streams
### Stream A: <Name>
**Scope**:
**Files**:
**Can Start**: immediately
**Estimated Hours**:
**Dependencies**: none
### Stream B: <Name>
**Scope**:
**Files**:
**Can Start**: after Stream A
**Dependencies**: Stream A
## Coordination Points
### Shared Files
### Sequential Requirements
## Conflict Risk Assessment
## Parallelization Strategy
## Expected Timeline
- With parallel execution: <max_stream_hours>h wall time
- Without: <sum_all_hours>h
- Efficiency gain: <pct>%
```
**Output**: "✅ Analysis complete for issue #<N> — N parallel streams identified. Ready to start? Say: start issue <N>"
---
## Starting an Issue
**Trigger**: User wants to begin work on a specific GitHub issue.
### Preflight
1. Verify issue exists and is open: `gh issue view <N> --json state,title,labels,body`
2. Find local task file (as above).
3. Check for analysis file: `.claude/epics/*/<N>-analysis.md` — if missing, run analysis first (or do both in sequence: analyze then start).
4. Verify epic worktree exists: `git worktree list | grep "epic-<name>"` — if not: "❌ No worktree. Sync the epic first."
### Process
**Step 1 — Read the analysis**, identify which streams can start immediately vs. which have dependencies.
**Step 2 — Create progress tracking:**
```bash
mkdir -p .claude/epics/<epic>/updates/<N>
current_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
```
Create `.claude/epics/<epic>/updates/<N>/stream-<X>.md` for each stream:
```markdown
---
issue: <N>
stream: <stream_name>
started: <datetime>
status: in_progress
---
## Scope
## Progress
- Starting implementation
```
**Step 3 — Launch parallel agents** for each stream that can start immediately:
```yaml
Task:
description: "Issue #<N> Stream <X>"
subagent_type: "general-purpose"
prompt: |
You are working on Issue #<N> in the epic worktree at: ../epic-<name>/
Your stream: <stream_name>
Your scope — files to modify: <file_patterns>
Work to complete: <stream_description>
Instructions:
1. Read full task from: .claude/epics/<epic>/<N>.md
2. Read analysis from: .claude/epics/<epic>/<N>-analysis.md
3. Work ONLY in your assigned files
4. Commit frequently: "Issue #<N>: <specific change>"
5. Update progress in: .claude/epics/<epic>/updates/<N>/stream-<X>.md
6. If you need to touch files outside your scope, note it in your progress file and wait
7. Never use --force on git operations
Complete your stream's work and mark status: completed when done.
```
Streams with unmet dependencies are queued — launch them as their dependencies complete.
**Step 4 — Assign on GitHub:**
```bash
gh issue edit <N> --add-assignee @me --add-label "in-progress"
```
**Step 5 — Create execution status file** at `.claude/epics/<epic>/updates/<N>/execution.md`:
```markdown
## Active Streams
- Stream A: <name> — Started <time>
- Stream B: <name> — Started <time>
## Queued
- Stream C: <name> — Waiting on Stream A
## Completed
(none yet)
```
**Output:**
```
✅ Started work on issue #<N>
Launched N agents:
Stream A: <name> ✓ Started
Stream B: <name> ✓ Started
Stream C: <name> ⏸ Waiting (depends on A)
Monitor: check progress in .claude/epics/<epic>/updates/<N>/
Sync updates: "sync issue <N>"
```
---
## Starting a Full Epic
**Trigger**: User wants to launch parallel agents across all ready issues in an epic at once.
### Preflight
- Verify `.claude/epics/<name>/epic.md` exists and has a `github:` field (i.e., it's been synced).
- Check for uncommitted changes: `git status --porcelain` — block if dirty.
- Verify epic branch exists: `git branch -a | grep "epic/<name>"`
### Process
**Step 1 — Read all task files** in `.claude/epics/<name>/`. Parse frontmatter for `status`, `depends_on`, `parallel`.
**Step 2 — Categorize tasks:**
- Ready: status=open, no unmet depends_on
- Blocked: has unmet depends_on
- In Progress: already has an execution file
- Complete: status=closed
**Step 3 — Analyze any ready tasks** that don't have an analysis file yet (run issue analysis inline).
**Step 4 — Launch agents** for all ready tasks following the same per-issue agent launch pattern above.
**Step 5 — Create/update** `.claude/epics/<name>/execution-status.md` with all active agents and queued issues.
**Step 6 — As agents complete**, check if blocked issues are now unblocked and launch those agents.
---
## Agent Coordination Rules
When multiple agents work in the same worktree simultaneously:
- Each agent works only on files in its assigned stream scope.
- Agents commit frequently with `Issue #<N>: <description>` format.
- Before modifying a shared file, check `git status <file>` — if another agent has it modified, wait and pull first.
- Agents sync via commits: `git pull --rebase origin epic/<name>` before starting new file work.
- Conflicts are never auto-resolved — agents report them and pause.
- No `--force` flags ever.
Shared files that commonly need coordination (types, config, package.json) should be handled by one designated stream; others pull after that commit.
+111
View File
@@ -0,0 +1,111 @@
# Plan — Capture Requirements
This phase turns an idea into a structured PRD, then converts the PRD into a technical epic ready for decomposition.
---
## Writing a PRD
**Trigger**: User wants to plan a new feature, product requirement, or area of work.
### Preflight
- Check if `.claude/prds/<name>.md` already exists — if so, confirm overwrite before proceeding.
- Ensure `.claude/prds/` directory exists; create it if not.
- Feature name must be kebab-case (lowercase, letters/numbers/hyphens, starts with a letter). If not: "❌ Feature name must be kebab-case. Example: user-auth, payment-v2"
### Process
Conduct a genuine brainstorming session before writing anything. Ask the user:
- What problem does this solve?
- Who are the users affected?
- What does success look like?
- What's explicitly out of scope?
- What are the constraints (tech, time, resources)?
Then write `.claude/prds/<name>.md` with this frontmatter and structure:
```markdown
---
name: <feature-name>
description: <one-line summary>
status: backlog
created: <run: date -u +"%Y-%m-%dT%H:%M:%SZ">
---
# PRD: <feature-name>
## Executive Summary
## Problem Statement
## User Stories
## Functional Requirements
## Non-Functional Requirements
## Success Criteria
## Constraints & Assumptions
## Out of Scope
## Dependencies
```
**Quality gates before saving:**
- No placeholder text in any section
- User stories include acceptance criteria
- Success criteria are measurable
- Out of scope is explicitly listed
**After creation**: Confirm "✅ PRD created: `.claude/prds/<name>.md`" and suggest: "Ready to create technical epic? Say: parse the <name> PRD"
---
## Parsing a PRD into a Technical Epic
**Trigger**: User wants to convert an existing PRD into a technical implementation plan.
### Preflight
- Verify `.claude/prds/<name>.md` exists with valid frontmatter (name, description, status, created).
- Check if `.claude/epics/<name>/epic.md` already exists — confirm overwrite if so.
### Process
Read the PRD fully, then produce `.claude/epics/<name>/epic.md`:
```markdown
---
name: <feature-name>
status: backlog
created: <run: date -u +"%Y-%m-%dT%H:%M:%SZ">
progress: 0%
prd: .claude/prds/<name>.md
github: (will be set on sync)
---
# Epic: <feature-name>
## Overview
## Architecture Decisions
## Technical Approach
### Frontend Components
### Backend Services
### Infrastructure
## Implementation Strategy
## Task Breakdown Preview
## Dependencies
## Success Criteria (Technical)
## Estimated Effort
```
**Key constraints:**
- Aim for ≤10 tasks total — prefer simplicity over completeness.
- Look for ways to leverage existing functionality before creating new code.
- Identify parallelization opportunities in the task breakdown preview.
**After creation**: Confirm "✅ Epic created: `.claude/epics/<name>/epic.md`" and suggest: "Ready to decompose into tasks? Say: decompose the <name> epic"
---
## Editing a PRD or Epic
Read the file first, make targeted edits preserving all frontmatter. Update the `updated` frontmatter field with current datetime.
@@ -0,0 +1,67 @@
#!/bin/bash
echo "Getting tasks..."
echo ""
echo ""
echo "🚫 Blocked Tasks"
echo "================"
echo ""
found=0
for epic_dir in .claude/epics/*/; do
[ -d "$epic_dir" ] || continue
epic_name=$(basename "$epic_dir")
for task_file in "$epic_dir"/[0-9]*.md; do
[ -f "$task_file" ] || continue
# Check if task is open
status=$(grep "^status:" "$task_file" | head -1 | sed 's/^status: *//')
if [ "$status" != "open" ] && [ -n "$status" ]; then
continue
fi
# Check for dependencies
deps_line=$(grep "^depends_on:" "$task_file" | head -1)
if [ -n "$deps_line" ]; then
deps=$(echo "$deps_line" | sed 's/^depends_on: *//' | sed 's/^\[//' | sed 's/\]$//' | sed 's/,/ /g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$deps" ] && deps=""
else
deps=""
fi
if [ -n "$deps" ] && [ "$deps" != "depends_on:" ]; then
task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
task_num=$(basename "$task_file" .md)
echo "⏸️ Task #$task_num - $task_name"
echo " Epic: $epic_name"
echo " Blocked by: [$deps]"
# Check status of dependencies
open_deps=""
for dep in $deps; do
dep_file="$epic_dir$dep.md"
if [ -f "$dep_file" ]; then
dep_status=$(grep "^status:" "$dep_file" | head -1 | sed 's/^status: *//')
[ "$dep_status" = "open" ] && open_deps="$open_deps #$dep"
fi
done
[ -n "$open_deps" ] && echo " Waiting for:$open_deps"
echo ""
((found++))
fi
done
done
if [ $found -eq 0 ]; then
echo "No blocked tasks found!"
echo ""
echo "💡 All tasks with dependencies are either completed or in progress."
else
echo "📊 Total blocked: $found tasks"
fi
exit 0
@@ -0,0 +1,94 @@
#!/bin/bash
echo "Getting epics..."
echo ""
echo ""
[ ! -d ".claude/epics" ] && echo "📁 No epics directory found. Create your first epic with: /pm:prd-parse <feature-name>" && exit 0
[ -z "$(ls -d .claude/epics/*/ 2>/dev/null)" ] && echo "📁 No epics found. Create your first epic with: /pm:prd-parse <feature-name>" && exit 0
echo "📚 Project Epics"
echo "================"
echo ""
# Initialize arrays to store epics by status
planning_epics=""
in_progress_epics=""
completed_epics=""
# Process all epics
for dir in .claude/epics/*/; do
[ -d "$dir" ] || continue
[ -f "$dir/epic.md" ] || continue
# Extract metadata
n=$(grep "^name:" "$dir/epic.md" | head -1 | sed 's/^name: *//')
s=$(grep "^status:" "$dir/epic.md" | head -1 | sed 's/^status: *//' | tr '[:upper:]' '[:lower:]')
p=$(grep "^progress:" "$dir/epic.md" | head -1 | sed 's/^progress: *//')
g=$(grep "^github:" "$dir/epic.md" | head -1 | sed 's/^github: *//')
# Defaults
[ -z "$n" ] && n=$(basename "$dir")
[ -z "$p" ] && p="0%"
# Count tasks
t=$(ls "$dir"/[0-9]*.md 2>/dev/null | wc -l)
# Format output with GitHub issue number if available
if [ -n "$g" ]; then
i=$(echo "$g" | grep -o '/[0-9]*$' | tr -d '/')
entry=" 📋 ${dir}epic.md (#$i) - $p complete ($t tasks)"
else
entry=" 📋 ${dir}epic.md - $p complete ($t tasks)"
fi
# Categorize by status (handle various status values)
case "$s" in
planning|draft|"")
planning_epics="${planning_epics}${entry}\n"
;;
in-progress|in_progress|active|started)
in_progress_epics="${in_progress_epics}${entry}\n"
;;
completed|complete|done|closed|finished)
completed_epics="${completed_epics}${entry}\n"
;;
*)
# Default to planning for unknown statuses
planning_epics="${planning_epics}${entry}\n"
;;
esac
done
# Display categorized epics
echo "📝 Planning:"
if [ -n "$planning_epics" ]; then
echo -e "$planning_epics" | sed '/^$/d'
else
echo " (none)"
fi
echo ""
echo "🚀 In Progress:"
if [ -n "$in_progress_epics" ]; then
echo -e "$in_progress_epics" | sed '/^$/d'
else
echo " (none)"
fi
echo ""
echo "✅ Completed:"
if [ -n "$completed_epics" ]; then
echo -e "$completed_epics" | sed '/^$/d'
else
echo " (none)"
fi
# Summary
echo ""
echo "📊 Summary"
total=$(ls -d .claude/epics/*/ 2>/dev/null | wc -l)
tasks=$(find .claude/epics -name "[0-9]*.md" 2>/dev/null | wc -l)
echo " Total epics: $total"
echo " Total tasks: $tasks"
exit 0
@@ -0,0 +1,91 @@
#!/bin/bash
epic_name="$1"
if [ -z "$epic_name" ]; then
echo "❌ Please provide an epic name"
echo "Usage: /pm:epic-show <epic-name>"
exit 1
fi
echo "Getting epic..."
echo ""
echo ""
epic_dir=".claude/epics/$epic_name"
epic_file="$epic_dir/epic.md"
if [ ! -f "$epic_file" ]; then
echo "❌ Epic not found: $epic_name"
echo ""
echo "Available epics:"
for dir in .claude/epics/*/; do
[ -d "$dir" ] && echo "$(basename "$dir")"
done
exit 1
fi
# Display epic details
echo "📚 Epic: $epic_name"
echo "================================"
echo ""
# Extract metadata
status=$(grep "^status:" "$epic_file" | head -1 | sed 's/^status: *//')
progress=$(grep "^progress:" "$epic_file" | head -1 | sed 's/^progress: *//')
github=$(grep "^github:" "$epic_file" | head -1 | sed 's/^github: *//')
created=$(grep "^created:" "$epic_file" | head -1 | sed 's/^created: *//')
echo "📊 Metadata:"
echo " Status: ${status:-planning}"
echo " Progress: ${progress:-0%}"
[ -n "$github" ] && echo " GitHub: $github"
echo " Created: ${created:-unknown}"
echo ""
# Show tasks
echo "📝 Tasks:"
task_count=0
open_count=0
closed_count=0
for task_file in "$epic_dir"/[0-9]*.md; do
[ -f "$task_file" ] || continue
task_num=$(basename "$task_file" .md)
task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
task_status=$(grep "^status:" "$task_file" | head -1 | sed 's/^status: *//')
parallel=$(grep "^parallel:" "$task_file" | head -1 | sed 's/^parallel: *//')
if [ "$task_status" = "closed" ] || [ "$task_status" = "completed" ]; then
echo " ✅ #$task_num - $task_name"
((closed_count++))
else
echo " ⬜ #$task_num - $task_name"
[ "$parallel" = "true" ] && echo -n " (parallel)"
((open_count++))
fi
((task_count++))
done
if [ $task_count -eq 0 ]; then
echo " No tasks created yet"
echo " Run: /pm:epic-decompose $epic_name"
fi
echo ""
echo "📈 Statistics:"
echo " Total tasks: $task_count"
echo " Open: $open_count"
echo " Closed: $closed_count"
[ $task_count -gt 0 ] && echo " Completion: $((closed_count * 100 / task_count))%"
# Next actions
echo ""
echo "💡 Actions:"
[ $task_count -eq 0 ] && echo " • Decompose into tasks: /pm:epic-decompose $epic_name"
[ -z "$github" ] && [ $task_count -gt 0 ] && echo " • Sync to GitHub: /pm:epic-sync $epic_name"
[ -n "$github" ] && [ "$status" != "completed" ] && echo " • Start work: /pm:epic-start $epic_name"
exit 0
@@ -0,0 +1,90 @@
#!/bin/bash
echo "Getting status..."
echo ""
echo ""
epic_name="$1"
if [ -z "$epic_name" ]; then
echo "❌ Please specify an epic name"
echo "Usage: /pm:epic-status <epic-name>"
echo ""
echo "Available epics:"
for dir in .claude/epics/*/; do
[ -d "$dir" ] && echo "$(basename "$dir")"
done
exit 1
else
# Show status for specific epic
epic_dir=".claude/epics/$epic_name"
epic_file="$epic_dir/epic.md"
if [ ! -f "$epic_file" ]; then
echo "❌ Epic not found: $epic_name"
echo ""
echo "Available epics:"
for dir in .claude/epics/*/; do
[ -d "$dir" ] && echo "$(basename "$dir")"
done
exit 1
fi
echo "📚 Epic Status: $epic_name"
echo "================================"
echo ""
# Extract metadata
status=$(grep "^status:" "$epic_file" | head -1 | sed 's/^status: *//')
progress=$(grep "^progress:" "$epic_file" | head -1 | sed 's/^progress: *//')
github=$(grep "^github:" "$epic_file" | head -1 | sed 's/^github: *//')
# Count tasks
total=0
open=0
closed=0
blocked=0
# Use find to safely iterate over task files
for task_file in "$epic_dir"/[0-9]*.md; do
[ -f "$task_file" ] || continue
((total++))
task_status=$(grep "^status:" "$task_file" | head -1 | sed 's/^status: *//')
deps=$(grep "^depends_on:" "$task_file" | head -1 | sed 's/^depends_on: *\[//' | sed 's/\]//')
if [ "$task_status" = "closed" ] || [ "$task_status" = "completed" ]; then
((closed++))
elif [ -n "$deps" ] && [ "$deps" != "depends_on:" ]; then
((blocked++))
else
((open++))
fi
done
# Display progress bar
if [ $total -gt 0 ]; then
percent=$((closed * 100 / total))
filled=$((percent * 20 / 100))
empty=$((20 - filled))
echo -n "Progress: ["
[ $filled -gt 0 ] && printf '%0.s█' $(seq 1 $filled)
[ $empty -gt 0 ] && printf '%0.s░' $(seq 1 $empty)
echo "] $percent%"
else
echo "Progress: No tasks created"
fi
echo ""
echo "📊 Breakdown:"
echo " Total tasks: $total"
echo " ✅ Completed: $closed"
echo " 🔄 Available: $open"
echo " ⏸️ Blocked: $blocked"
[ -n "$github" ] && echo ""
[ -n "$github" ] && echo "🔗 GitHub: $github"
fi
exit 0
@@ -0,0 +1,71 @@
#!/bin/bash
echo "Helping..."
echo ""
echo ""
echo "📚 Claude Code PM - Project Management System"
echo "============================================="
echo ""
echo "🎯 Quick Start Workflow"
echo " 1. /pm:prd-new <name> - Create a new PRD"
echo " 2. /pm:prd-parse <name> - Convert PRD to epic"
echo " 3. /pm:epic-decompose <name> - Break into tasks"
echo " 4. /pm:epic-sync <name> - Push to GitHub"
echo " 5. /pm:epic-start <name> - Start parallel execution"
echo ""
echo "📄 PRD Commands"
echo " /pm:prd-new <name> - Launch brainstorming for new product requirement"
echo " /pm:prd-parse <name> - Convert PRD to implementation epic"
echo " /pm:prd-list - List all PRDs"
echo " /pm:prd-edit <name> - Edit existing PRD"
echo " /pm:prd-status - Show PRD implementation status"
echo ""
echo "📚 Epic Commands"
echo " /pm:epic-decompose <name> - Break epic into task files"
echo " /pm:epic-sync <name> - Push epic and tasks to GitHub"
echo " /pm:epic-oneshot <name> - Decompose and sync in one command"
echo " /pm:epic-list - List all epics"
echo " /pm:epic-show <name> - Display epic and its tasks"
echo " /pm:epic-status [name] - Show epic progress"
echo " /pm:epic-close <name> - Mark epic as complete"
echo " /pm:epic-edit <name> - Edit epic details"
echo " /pm:epic-refresh <name> - Update epic progress from tasks"
echo " /pm:epic-start <name> - Launch parallel agent execution"
echo ""
echo "📝 Issue Commands"
echo " /pm:issue-show <num> - Display issue and sub-issues"
echo " /pm:issue-status <num> - Check issue status"
echo " /pm:issue-start <num> - Begin work with specialized agent"
echo " /pm:issue-sync <num> - Push updates to GitHub"
echo " /pm:issue-close <num> - Mark issue as complete"
echo " /pm:issue-reopen <num> - Reopen closed issue"
echo " /pm:issue-edit <num> - Edit issue details"
echo " /pm:issue-analyze <num> - Analyze for parallel work streams"
echo ""
echo "🔄 Workflow Commands"
echo " /pm:next - Show next priority tasks"
echo " /pm:status - Overall project dashboard"
echo " /pm:standup - Daily standup report"
echo " /pm:blocked - Show blocked tasks"
echo " /pm:in-progress - List work in progress"
echo ""
echo "🔗 Sync Commands"
echo " /pm:sync - Full bidirectional sync with GitHub"
echo " /pm:import <issue> - Import existing GitHub issues"
echo ""
echo "🔧 Maintenance Commands"
echo " /pm:validate - Check system integrity"
echo " /pm:clean - Archive completed work"
echo " /pm:search <query> - Search across all content"
echo ""
echo "⚙️ Setup Commands"
echo " /pm:init - Install dependencies and configure GitHub"
echo " /pm:help - Show this help message"
echo ""
echo "💡 Tips"
echo " • Use /pm:next to find available work"
echo " • Run /pm:status for quick overview"
echo " • Epic workflow: prd-new → prd-parse → epic-decompose → epic-sync"
echo " • View README.md for complete documentation"
exit 0
@@ -0,0 +1,74 @@
#!/bin/bash
echo "Getting status..."
echo ""
echo ""
echo "🔄 In Progress Work"
echo "==================="
echo ""
# Check for active work in updates directories
found=0
if [ -d ".claude/epics" ]; then
for updates_dir in .claude/epics/*/updates/*/; do
[ -d "$updates_dir" ] || continue
issue_num=$(basename "$updates_dir")
epic_name=$(basename $(dirname $(dirname "$updates_dir")))
if [ -f "$updates_dir/progress.md" ]; then
completion=$(grep "^completion:" "$updates_dir/progress.md" | head -1 | sed 's/^completion: *//')
[ -z "$completion" ] && completion="0%"
# Get task name from the task file
task_file=".claude/epics/$epic_name/$issue_num.md"
if [ -f "$task_file" ]; then
task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
else
task_name="Unknown task"
fi
echo "📝 Issue #$issue_num - $task_name"
echo " Epic: $epic_name"
echo " Progress: $completion complete"
# Check for recent updates
if [ -f "$updates_dir/progress.md" ]; then
last_update=$(grep "^last_sync:" "$updates_dir/progress.md" | head -1 | sed 's/^last_sync: *//')
[ -n "$last_update" ] && echo " Last update: $last_update"
fi
echo ""
((found++))
fi
done
fi
# Also check for in-progress epics
echo "📚 Active Epics:"
for epic_dir in .claude/epics/*/; do
[ -d "$epic_dir" ] || continue
[ -f "$epic_dir/epic.md" ] || continue
status=$(grep "^status:" "$epic_dir/epic.md" | head -1 | sed 's/^status: *//')
if [ "$status" = "in-progress" ] || [ "$status" = "active" ]; then
epic_name=$(grep "^name:" "$epic_dir/epic.md" | head -1 | sed 's/^name: *//')
progress=$(grep "^progress:" "$epic_dir/epic.md" | head -1 | sed 's/^progress: *//')
[ -z "$epic_name" ] && epic_name=$(basename "$epic_dir")
[ -z "$progress" ] && progress="0%"
echo "$epic_name - $progress complete"
fi
done
echo ""
if [ $found -eq 0 ]; then
echo "No active work items found."
echo ""
echo "💡 Start work with: /pm:next"
else
echo "📊 Total active items: $found"
fi
exit 0
@@ -0,0 +1,192 @@
#!/bin/bash
echo "Initializing..."
echo ""
echo ""
echo " ██████╗ ██████╗██████╗ ███╗ ███╗"
echo "██╔════╝██╔════╝██╔══██╗████╗ ████║"
echo "██║ ██║ ██████╔╝██╔████╔██║"
echo "╚██████╗╚██████╗██║ ██║ ╚═╝ ██║"
echo " ╚═════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝"
echo "┌─────────────────────────────────┐"
echo "│ Claude Code Project Management │"
echo "│ by https://x.com/aroussi │"
echo "└─────────────────────────────────┘"
echo "https://github.com/automazeio/ccpm"
echo ""
echo ""
echo "🚀 Initializing Claude Code PM System"
echo "======================================"
echo ""
# Check for required tools
echo "🔍 Checking dependencies..."
# Check gh CLI
if command -v gh &> /dev/null; then
echo " ✅ GitHub CLI (gh) installed"
else
echo " ❌ GitHub CLI (gh) not found"
echo ""
echo " Installing gh..."
if command -v brew &> /dev/null; then
brew install gh
elif command -v apt-get &> /dev/null; then
sudo apt-get update && sudo apt-get install gh
else
echo " Please install GitHub CLI manually: https://cli.github.com/"
exit 1
fi
fi
# Check gh auth status
echo ""
echo "🔐 Checking GitHub authentication..."
if gh auth status &> /dev/null; then
echo " ✅ GitHub authenticated"
else
echo " ⚠️ GitHub not authenticated"
echo " Running: gh auth login"
gh auth login
fi
# Check for gh-sub-issue extension
echo ""
echo "📦 Checking gh extensions..."
if gh extension list | grep -q "yahsan2/gh-sub-issue"; then
echo " ✅ gh-sub-issue extension installed"
else
echo " 📥 Installing gh-sub-issue extension..."
gh extension install yahsan2/gh-sub-issue
fi
# Create directory structure
echo ""
echo "📁 Creating directory structure..."
mkdir -p .claude/prds
mkdir -p .claude/epics
mkdir -p .claude/rules
mkdir -p .claude/agents
mkdir -p .claude/scripts/pm
echo " ✅ Directories created"
# Copy scripts if in main repo
if [ -d "scripts/pm" ] && [ ! "$(pwd)" = *"/.claude"* ]; then
echo ""
echo "📝 Copying PM scripts..."
cp -r scripts/pm/* .claude/scripts/pm/
chmod +x .claude/scripts/pm/*.sh
echo " ✅ Scripts copied and made executable"
fi
# Check for git
echo ""
echo "🔗 Checking Git configuration..."
if git rev-parse --git-dir > /dev/null 2>&1; then
echo " ✅ Git repository detected"
# Check remote
if git remote -v | grep -q origin; then
remote_url=$(git remote get-url origin)
echo " ✅ Remote configured: $remote_url"
# Check if remote is the CCPM template repository
if [[ "$remote_url" == *"automazeio/ccpm"* ]] || [[ "$remote_url" == *"automazeio/ccpm.git"* ]]; then
echo ""
echo " ⚠️ WARNING: Your remote origin points to the CCPM template repository!"
echo " This means any issues you create will go to the template repo, not your project."
echo ""
echo " To fix this:"
echo " 1. Fork the repository or create your own on GitHub"
echo " 2. Update your remote:"
echo " git remote set-url origin https://github.com/YOUR_USERNAME/YOUR_REPO.git"
echo ""
else
# Create GitHub labels if this is a GitHub repository
if gh repo view &> /dev/null; then
echo ""
echo "🏷️ Creating GitHub labels..."
# Create base labels with improved error handling
epic_created=false
task_created=false
if gh label create "epic" --color "0E8A16" --description "Epic issue containing multiple related tasks" --force 2>/dev/null; then
epic_created=true
elif gh label list 2>/dev/null | grep -q "^epic"; then
epic_created=true # Label already exists
fi
if gh label create "task" --color "1D76DB" --description "Individual task within an epic" --force 2>/dev/null; then
task_created=true
elif gh label list 2>/dev/null | grep -q "^task"; then
task_created=true # Label already exists
fi
# Report results
if $epic_created && $task_created; then
echo " ✅ GitHub labels created (epic, task)"
elif $epic_created || $task_created; then
echo " ⚠️ Some GitHub labels created (epic: $epic_created, task: $task_created)"
else
echo " ❌ Could not create GitHub labels (check repository permissions)"
fi
else
echo " ️ Not a GitHub repository - skipping label creation"
fi
fi
else
echo " ⚠️ No remote configured"
echo " Add with: git remote add origin <url>"
fi
else
echo " ⚠️ Not a git repository"
echo " Initialize with: git init"
fi
# Create CLAUDE.md if it doesn't exist
if [ ! -f "CLAUDE.md" ]; then
echo ""
echo "📄 Creating CLAUDE.md..."
cat > CLAUDE.md << 'EOF'
# CLAUDE.md
> Think carefully and implement the most concise solution that changes as little code as possible.
## Project-Specific Instructions
Add your project-specific instructions here.
## Testing
Always run tests before committing:
- `npm test` or equivalent for your stack
## Code Style
Follow existing patterns in the codebase.
EOF
echo " ✅ CLAUDE.md created"
fi
# Summary
echo ""
echo "✅ Initialization Complete!"
echo "=========================="
echo ""
echo "📊 System Status:"
gh --version | head -1
echo " Extensions: $(gh extension list | wc -l) installed"
echo " Auth: $(gh auth status 2>&1 | grep -o 'Logged in to [^ ]*' || echo 'Not authenticated')"
echo ""
echo "🎯 Next Steps:"
echo " 1. Create your first PRD: /pm:prd-new <feature-name>"
echo " 2. View help: /pm:help"
echo " 3. Check status: /pm:status"
echo ""
echo "📚 Documentation: README.md"
exit 0
@@ -0,0 +1,61 @@
#!/bin/bash
echo "Getting status..."
echo ""
echo ""
echo "📋 Next Available Tasks"
echo "======================="
echo ""
# Find tasks that are open and have no dependencies or whose dependencies are closed
found=0
for epic_dir in .claude/epics/*/; do
[ -d "$epic_dir" ] || continue
epic_name=$(basename "$epic_dir")
for task_file in "$epic_dir"/[0-9]*.md; do
[ -f "$task_file" ] || continue
# Check if task is open
status=$(grep "^status:" "$task_file" | head -1 | sed 's/^status: *//')
if [ "$status" != "open" ] && [ -n "$status" ]; then
continue
fi
# Check dependencies
deps_line=$(grep "^depends_on:" "$task_file" | head -1)
if [ -n "$deps_line" ]; then
deps=$(echo "$deps_line" | sed 's/^depends_on: *//' | sed 's/^\[//' | sed 's/\]$//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$deps" ] && deps=""
else
deps=""
fi
# If no dependencies or empty, task is available
if [ -z "$deps" ] || [ "$deps" = "depends_on:" ]; then
task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
task_num=$(basename "$task_file" .md)
parallel=$(grep "^parallel:" "$task_file" | head -1 | sed 's/^parallel: *//')
echo "✅ Ready: #$task_num - $task_name"
echo " Epic: $epic_name"
[ "$parallel" = "true" ] && echo " 🔄 Can run in parallel"
echo ""
((found++))
fi
done
done
if [ $found -eq 0 ]; then
echo "No available tasks found."
echo ""
echo "💡 Suggestions:"
echo " • Check blocked tasks: /pm:blocked"
echo " • View all tasks: /pm:epic-list"
fi
echo ""
echo "📊 Summary: $found tasks ready to start"
exit 0
@@ -0,0 +1,89 @@
# !/bin/bash
# Check if PRD directory exists
if [ ! -d ".claude/prds" ]; then
echo "📁 No PRD directory found. Create your first PRD with: /pm:prd-new <feature-name>"
exit 0
fi
# Check for PRD files
if ! ls .claude/prds/*.md >/dev/null 2>&1; then
echo "📁 No PRDs found. Create your first PRD with: /pm:prd-new <feature-name>"
exit 0
fi
# Initialize counters
backlog_count=0
in_progress_count=0
implemented_count=0
total_count=0
echo "Getting PRDs..."
echo ""
echo ""
echo "📋 PRD List"
echo "==========="
echo ""
# Display by status groups
echo "🔍 Backlog PRDs:"
for file in .claude/prds/*.md; do
[ -f "$file" ] || continue
status=$(grep "^status:" "$file" | head -1 | sed 's/^status: *//')
if [ "$status" = "backlog" ] || [ "$status" = "draft" ] || [ -z "$status" ]; then
name=$(grep "^name:" "$file" | head -1 | sed 's/^name: *//')
desc=$(grep "^description:" "$file" | head -1 | sed 's/^description: *//')
[ -z "$name" ] && name=$(basename "$file" .md)
[ -z "$desc" ] && desc="No description"
# echo " 📋 $name - $desc"
echo " 📋 $file - $desc"
((backlog_count++))
fi
((total_count++))
done
[ $backlog_count -eq 0 ] && echo " (none)"
echo ""
echo "🔄 In-Progress PRDs:"
for file in .claude/prds/*.md; do
[ -f "$file" ] || continue
status=$(grep "^status:" "$file" | head -1 | sed 's/^status: *//')
if [ "$status" = "in-progress" ] || [ "$status" = "active" ]; then
name=$(grep "^name:" "$file" | head -1 | sed 's/^name: *//')
desc=$(grep "^description:" "$file" | head -1 | sed 's/^description: *//')
[ -z "$name" ] && name=$(basename "$file" .md)
[ -z "$desc" ] && desc="No description"
# echo " 📋 $name - $desc"
echo " 📋 $file - $desc"
((in_progress_count++))
fi
done
[ $in_progress_count -eq 0 ] && echo " (none)"
echo ""
echo "✅ Implemented PRDs:"
for file in .claude/prds/*.md; do
[ -f "$file" ] || continue
status=$(grep "^status:" "$file" | head -1 | sed 's/^status: *//')
if [ "$status" = "implemented" ] || [ "$status" = "completed" ] || [ "$status" = "done" ]; then
name=$(grep "^name:" "$file" | head -1 | sed 's/^name: *//')
desc=$(grep "^description:" "$file" | head -1 | sed 's/^description: *//')
[ -z "$name" ] && name=$(basename "$file" .md)
[ -z "$desc" ] && desc="No description"
# echo " 📋 $name - $desc"
echo " 📋 $file - $desc"
((implemented_count++))
fi
done
[ $implemented_count -eq 0 ] && echo " (none)"
# Display summary
echo ""
echo "📊 PRD Summary"
echo " Total PRDs: $total_count"
echo " Backlog: $backlog_count"
echo " In-Progress: $in_progress_count"
echo " Implemented: $implemented_count"
exit 0
@@ -0,0 +1,63 @@
#!/bin/bash
echo "📄 PRD Status Report"
echo "===================="
echo ""
if [ ! -d ".claude/prds" ]; then
echo "No PRD directory found."
exit 0
fi
total=$(ls .claude/prds/*.md 2>/dev/null | wc -l)
[ $total -eq 0 ] && echo "No PRDs found." && exit 0
# Count by status
backlog=0
in_progress=0
implemented=0
for file in .claude/prds/*.md; do
[ -f "$file" ] || continue
status=$(grep "^status:" "$file" | head -1 | sed 's/^status: *//')
case "$status" in
backlog|draft|"") ((backlog++)) ;;
in-progress|active) ((in_progress++)) ;;
implemented|completed|done) ((implemented++)) ;;
*) ((backlog++)) ;;
esac
done
echo "Getting status..."
echo ""
echo ""
# Display chart
echo "📊 Distribution:"
echo "================"
echo ""
echo " Backlog: $(printf '%-3d' $backlog) [$(printf '%0.s█' $(seq 1 $((backlog*20/total))))]"
echo " In Progress: $(printf '%-3d' $in_progress) [$(printf '%0.s█' $(seq 1 $((in_progress*20/total))))]"
echo " Implemented: $(printf '%-3d' $implemented) [$(printf '%0.s█' $(seq 1 $((implemented*20/total))))]"
echo ""
echo " Total PRDs: $total"
# Recent activity
echo ""
echo "📅 Recent PRDs (last 5 modified):"
ls -t .claude/prds/*.md 2>/dev/null | head -5 | while read file; do
name=$(grep "^name:" "$file" | head -1 | sed 's/^name: *//')
[ -z "$name" ] && name=$(basename "$file" .md)
echo "$name"
done
# Suggestions
echo ""
echo "💡 Next Actions:"
[ $backlog -gt 0 ] && echo " • Parse backlog PRDs to epics: /pm:prd-parse <name>"
[ $in_progress -gt 0 ] && echo " • Check progress on active PRDs: /pm:epic-status <name>"
[ $total -eq 0 ] && echo " • Create your first PRD: /pm:prd-new <name>"
exit 0
@@ -0,0 +1,71 @@
#!/bin/bash
query="$1"
if [ -z "$query" ]; then
echo "❌ Please provide a search query"
echo "Usage: /pm:search <query>"
exit 1
fi
echo "Searching for '$query'..."
echo ""
echo ""
echo "🔍 Search results for: '$query'"
echo "================================"
echo ""
# Search in PRDs
if [ -d ".claude/prds" ]; then
echo "📄 PRDs:"
results=$(grep -l -i "$query" .claude/prds/*.md 2>/dev/null)
if [ -n "$results" ]; then
for file in $results; do
name=$(basename "$file" .md)
matches=$(grep -c -i "$query" "$file")
echo "$name ($matches matches)"
done
else
echo " No matches"
fi
echo ""
fi
# Search in Epics
if [ -d ".claude/epics" ]; then
echo "📚 Epics:"
results=$(find .claude/epics -name "epic.md" -exec grep -l -i "$query" {} \; 2>/dev/null)
if [ -n "$results" ]; then
for file in $results; do
epic_name=$(basename $(dirname "$file"))
matches=$(grep -c -i "$query" "$file")
echo "$epic_name ($matches matches)"
done
else
echo " No matches"
fi
echo ""
fi
# Search in Tasks
if [ -d ".claude/epics" ]; then
echo "📝 Tasks:"
results=$(find .claude/epics -name "[0-9]*.md" -exec grep -l -i "$query" {} \; 2>/dev/null | head -10)
if [ -n "$results" ]; then
for file in $results; do
epic_name=$(basename $(dirname "$file"))
task_num=$(basename "$file" .md)
echo " • Task #$task_num in $epic_name"
done
else
echo " No matches"
fi
fi
# Summary
total=$(find .claude -name "*.md" -exec grep -l -i "$query" {} \; 2>/dev/null | wc -l)
echo ""
echo "📊 Total files with matches: $total"
exit 0
@@ -0,0 +1,86 @@
#!/bin/bash
echo "📅 Daily Standup - $(date '+%Y-%m-%d')"
echo "================================"
echo ""
today=$(date '+%Y-%m-%d')
echo "Getting status..."
echo ""
echo ""
echo "📝 Today's Activity:"
echo "===================="
echo ""
# Find files modified today
recent_files=$(find .claude -name "*.md" -mtime -1 2>/dev/null)
if [ -n "$recent_files" ]; then
# Count by type
prd_count=$(echo "$recent_files" | grep -c "/prds/" 2>/dev/null | tr -d '[:space:]')
epic_count=$(echo "$recent_files" | grep -c "/epic.md" 2>/dev/null | tr -d '[:space:]')
task_count=$(echo "$recent_files" | grep -c "/[0-9]*.md" 2>/dev/null | tr -d '[:space:]')
update_count=$(echo "$recent_files" | grep -c "/updates/" 2>/dev/null | tr -d '[:space:]')
prd_count=${prd_count:-0}; epic_count=${epic_count:-0}; task_count=${task_count:-0}; update_count=${update_count:-0}
[ "$prd_count" -gt 0 ] && echo " • Modified $prd_count PRD(s)"
[ "$epic_count" -gt 0 ] && echo " • Updated $epic_count epic(s)"
[ "$task_count" -gt 0 ] && echo " • Worked on $task_count task(s)"
[ "$update_count" -gt 0 ] && echo " • Posted $update_count progress update(s)"
else
echo " No activity recorded today"
fi
echo ""
echo "🔄 Currently In Progress:"
# Show active work items
for updates_dir in .claude/epics/*/updates/*/; do
[ -d "$updates_dir" ] || continue
if [ -f "$updates_dir/progress.md" ]; then
issue_num=$(basename "$updates_dir")
epic_name=$(basename $(dirname $(dirname "$updates_dir")))
completion=$(grep "^completion:" "$updates_dir/progress.md" | head -1 | sed 's/^completion: *//')
echo " • Issue #$issue_num ($epic_name) - ${completion:-0%} complete"
fi
done
echo ""
echo "⏭️ Next Available Tasks:"
# Show top 3 available tasks
count=0
for epic_dir in .claude/epics/*/; do
[ -d "$epic_dir" ] || continue
for task_file in "$epic_dir"/[0-9]*.md; do
[ -f "$task_file" ] || continue
status=$(grep "^status:" "$task_file" | head -1 | sed 's/^status: *//')
if [ "$status" != "open" ] && [ -n "$status" ]; then
continue
fi
deps_line=$(grep "^depends_on:" "$task_file" | head -1)
if [ -n "$deps_line" ]; then
deps=$(echo "$deps_line" | sed 's/^depends_on: *//' | sed 's/^\[//' | sed 's/\]$//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$deps" ] && deps=""
else
deps=""
fi
if [ -z "$deps" ] || [ "$deps" = "depends_on:" ]; then
task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
task_num=$(basename "$task_file" .md)
echo " • #$task_num - $task_name"
((count++))
[ $count -ge 3 ] && break 2
fi
done
done
echo ""
echo "📊 Quick Stats:"
total_tasks=$(find .claude/epics -name "[0-9]*.md" 2>/dev/null | wc -l)
open_tasks=$(find .claude/epics -name "[0-9]*.md" -exec grep -l "^status: *open" {} \; 2>/dev/null | wc -l)
closed_tasks=$(find .claude/epics -name "[0-9]*.md" -exec grep -l "^status: *closed" {} \; 2>/dev/null | wc -l)
echo " Tasks: $open_tasks open, $closed_tasks closed, $total_tasks total"
exit 0
@@ -0,0 +1,42 @@
#!/bin/bash
echo "Getting status..."
echo ""
echo ""
echo "📊 Project Status"
echo "================"
echo ""
echo "📄 PRDs:"
if [ -d ".claude/prds" ]; then
total=$(ls .claude/prds/*.md 2>/dev/null | wc -l)
echo " Total: $total"
else
echo " No PRDs found"
fi
echo ""
echo "📚 Epics:"
if [ -d ".claude/epics" ]; then
total=$(ls -d .claude/epics/*/ 2>/dev/null | grep -v '/archived/$' | wc -l)
echo " Total: $total"
else
echo " No epics found"
fi
echo ""
echo "📝 Tasks:"
if [ -d ".claude/epics" ]; then
total=$(find .claude/epics -path "*/archived/*" -prune -o -name "[0-9]*.md" -print 2>/dev/null | wc -l)
open=$(find .claude/epics -path "*/archived/*" -prune -o -name "[0-9]*.md" -print 2>/dev/null | xargs grep -l "^status: *open" 2>/dev/null | wc -l)
closed=$(find .claude/epics -path "*/archived/*" -prune -o -name "[0-9]*.md" -print 2>/dev/null | xargs grep -l "^status: *closed" 2>/dev/null | wc -l)
echo " Open: $open"
echo " Closed: $closed"
echo " Total: $total"
else
echo " No tasks found"
fi
exit 0
@@ -0,0 +1,96 @@
#!/bin/bash
echo "Validating PM System..."
echo ""
echo ""
echo "🔍 Validating PM System"
echo "======================="
echo ""
errors=0
warnings=0
# Check directory structure
echo "📁 Directory Structure:"
[ -d ".claude" ] && echo " ✅ .claude directory exists" || { echo " ❌ .claude directory missing"; ((errors++)); }
[ -d ".claude/prds" ] && echo " ✅ PRDs directory exists" || echo " ⚠️ PRDs directory missing"
[ -d ".claude/epics" ] && echo " ✅ Epics directory exists" || echo " ⚠️ Epics directory missing"
[ -d ".claude/rules" ] && echo " ✅ Rules directory exists" || echo " ⚠️ Rules directory missing"
echo ""
# Check for orphaned files
echo "🗂️ Data Integrity:"
# Check epics have epic.md files
for epic_dir in .claude/epics/*/; do
[ -d "$epic_dir" ] || continue
if [ ! -f "$epic_dir/epic.md" ]; then
echo " ⚠️ Missing epic.md in $(basename "$epic_dir")"
((warnings++))
fi
done
# Check for tasks without epics
orphaned=$(find .claude -name "[0-9]*.md" -not -path ".claude/epics/*/*" 2>/dev/null | wc -l)
[ $orphaned -gt 0 ] && echo " ⚠️ Found $orphaned orphaned task files" && ((warnings++))
# Check for broken references
echo ""
echo "🔗 Reference Check:"
for task_file in .claude/epics/*/[0-9]*.md; do
[ -f "$task_file" ] || continue
deps_line=$(grep "^depends_on:" "$task_file" | head -1)
if [ -n "$deps_line" ]; then
deps=$(echo "$deps_line" | sed 's/^depends_on: *//' | sed 's/^\[//' | sed 's/\]$//' | sed 's/,/ /g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$deps" ] && deps=""
else
deps=""
fi
if [ -n "$deps" ] && [ "$deps" != "depends_on:" ]; then
epic_dir=$(dirname "$task_file")
for dep in $deps; do
if [ ! -f "$epic_dir/$dep.md" ]; then
echo " ⚠️ Task $(basename "$task_file" .md) references missing task: $dep"
((warnings++))
fi
done
fi
done
if [ $warnings -eq 0 ] && [ $errors -eq 0 ]; then
echo " ✅ All references valid"
fi
# Check frontmatter
echo ""
echo "📝 Frontmatter Validation:"
invalid=0
for file in $(find .claude -name "*.md" -path "*/epics/*" -o -path "*/prds/*" 2>/dev/null); do
if ! grep -q "^---" "$file"; then
echo " ⚠️ Missing frontmatter: $(basename "$file")"
((invalid++))
fi
done
[ $invalid -eq 0 ] && echo " ✅ All files have frontmatter"
# Summary
echo ""
echo "📊 Validation Summary:"
echo " Errors: $errors"
echo " Warnings: $warnings"
echo " Invalid files: $invalid"
if [ $errors -eq 0 ] && [ $warnings -eq 0 ] && [ $invalid -eq 0 ]; then
echo ""
echo "✅ System is healthy!"
else
echo ""
echo "💡 Run /pm:clean to fix some issues automatically"
fi
exit 0
+111
View File
@@ -0,0 +1,111 @@
# Structure — Break Down an Epic
This phase converts a technical epic into concrete, numbered task files with dependency and parallelization metadata.
---
## Epic Decomposition
**Trigger**: User wants to break an epic into actionable tasks.
### Preflight
- Verify `.claude/epics/<name>/epic.md` exists with valid frontmatter.
- If numbered task files (001.md, 002.md...) already exist in the epic directory, list them and confirm deletion before recreating.
- If epic status is "completed", warn the user before proceeding.
### Process
Read the epic fully. Analyze for parallelism — which pieces of work can happen simultaneously without file conflicts?
**Task types to consider:**
- Setup: environment, scaffolding, dependencies
- Data: models, schemas, migrations
- API: endpoints, services, integration
- UI: components, pages, styling
- Tests: unit, integration, e2e
- Docs: README, API docs, changelogs
**Parallelization strategy by epic size:**
- Small (<5 tasks): create sequentially
- Medium (510 tasks): batch into 23 groups, spawn parallel Task agents
- Large (>10 tasks): analyze dependencies first, launch parallel agents (max 5 concurrent), create dependent tasks after prerequisites
For parallel creation, use the Task tool:
```yaml
Task:
description: "Create task files batch N"
subagent_type: "general-purpose"
prompt: |
Create task files for epic: <name>
Tasks to create: [list 3-4 tasks]
Save to: .claude/epics/<name>/001.md, 002.md, etc.
Follow the task file format exactly.
Return: list of files created.
```
### Task File Format
```markdown
---
name: <Task Title>
status: open
created: <run: date -u +"%Y-%m-%dT%H:%M:%SZ">
updated: <same as created>
github: (will be set on sync)
depends_on: []
parallel: true
conflicts_with: []
---
# Task: <Task Title>
## Description
## Acceptance Criteria
- [ ]
## Technical Details
## Dependencies
## Effort Estimate
- Size: XS/S/M/L/XL
- Hours: N
## Definition of Done
- [ ] Code implemented
- [ ] Tests written and passing
- [ ] Code reviewed
```
**Numbering**: sequential 001.md, 002.md, etc. Tasks are renamed to GitHub issue numbers after sync — do not hard-code dependencies by filename, use the `depends_on` array.
### After Creating All Tasks
Append a summary to the epic file:
```markdown
## Tasks Created
- [ ] 001.md - <Title> (parallel: true/false)
- [ ] 002.md - <Title> (parallel: true/false)
Total tasks: N
Parallel tasks: N
Sequential tasks: N
Estimated total effort: N hours
```
**After completion**: Confirm "✅ Created N tasks for epic: <name>" and suggest: "Ready to push to GitHub? Say: sync the <name> epic"
---
## Dependency Rules
- `depends_on` lists task numbers that must complete before this task can start.
- `parallel: true` means the task can run concurrently with others it doesn't conflict with.
- `conflicts_with` lists tasks that touch the same files — these cannot run in parallel.
- Circular dependencies are an error — check before finalizing.
+315
View File
@@ -0,0 +1,315 @@
# Sync — Push to GitHub & Track Progress
This phase covers pushing local epics/tasks to GitHub as issues, syncing progress as comments, and closing issues when work is done.
---
## Repository Safety Check
**Always run this before any GitHub write operation:**
```bash
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$remote_url" == *"automazeio/ccpm"* ]]; then
echo "❌ Cannot sync to the CCPM template repository."
echo "Update remote: git remote set-url origin https://github.com/YOUR/REPO.git"
exit 1
fi
REPO=$(echo "$remote_url" | sed 's|.*github.com[:/]||' | sed 's|\.git$||')
```
---
## Epic Sync — Push Epic + Tasks to GitHub
**Trigger**: User wants to push a local epic and its tasks to GitHub as issues.
### Preflight
- Verify `.claude/epics/<name>/epic.md` exists.
- Verify numbered task files exist — if none: "❌ No tasks to sync. Decompose the epic first."
### Process
**Step 1 — Create epic issue:**
Strip frontmatter from epic.md, then:
```bash
sed '1,/^---$/d; 1,/^---$/d' .claude/epics/<name>/epic.md > /tmp/epic-body.md
epic_number=$(gh issue create \
--repo "$REPO" \
--title "Epic: <name>" \
--body-file /tmp/epic-body.md \
--label "epic,epic:<name>,feature" \
--json number -q .number)
```
**Step 2 — Create task sub-issues:**
Check if `gh-sub-issue` extension is available:
```bash
if gh extension list | grep -q "yahsan2/gh-sub-issue"; then
use_subissues=true
fi
```
For <5 tasks: create sequentially.
For ≥5 tasks: use parallel Task agents (3-4 tasks per batch).
Per task:
```bash
sed '1,/^---$/d; 1,/^---$/d' <task_file> > /tmp/task-body.md
task_number=$(gh issue create \
--repo "$REPO" \
--title "<task_name>" \
--body-file /tmp/task-body.md \
--label "task,epic:<name>" \
--json number -q .number)
# or with sub-issues:
# gh sub-issue create --parent $epic_number ...
```
**Step 3 — Rename task files and update references:**
After all issues are created, rename `001.md``<issue_number>.md` and update all `depends_on`/`conflicts_with` arrays to use real issue numbers (not sequential numbers).
```bash
# Build old→new mapping, then for each task file:
sed -i.bak "s/\b001\b/<new_num_1>/g" <file> # repeat for each mapping
mv 001.md <new_num>.md
```
**Step 4 — Update frontmatter:**
```bash
current_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Update github: and updated: fields in epic.md and each task file
github_url="https://github.com/$REPO/issues/<number>"
sed -i.bak "/^github:/c\\github: $github_url" <file>
sed -i.bak "/^updated:/c\\updated: $current_date" <file>
rm <file>.bak
```
**Step 5 — Create worktree for the epic:**
```bash
git checkout main && git pull origin main
git worktree add ../epic-<name> -b epic/<name>
```
**Step 6 — Create github-mapping.md:**
```markdown
# GitHub Issue Mapping
Epic: #<N> - https://github.com/<repo>/issues/<N>
Tasks:
- #<N>: <title> - https://github.com/<repo>/issues/<N>
Synced: <datetime>
```
**Output:**
```
✅ Synced epic <name> to GitHub
Epic: #<N>
Tasks: N sub-issues
Worktree: ../epic-<name>
Next: "start working on issue <N>" or "start the <name> epic"
```
---
## Issue Sync — Post Progress to GitHub
**Trigger**: User wants to sync local development progress to a GitHub issue as a comment.
### Preflight
- Verify issue exists: `gh issue view <N> --json state`
- Check `.claude/epics/*/updates/<N>/` exists with a `progress.md` file.
- Check `last_sync` in progress.md — if synced <5 minutes ago, confirm before proceeding.
### Process
Gather updates from `.claude/epics/<epic>/updates/<N>/` (progress.md, notes.md, commits.md).
Format and post a comment:
```bash
gh issue comment <N> --body-file /tmp/update-comment.md
```
Comment format:
```markdown
## 🔄 Progress Update - <date>
### ✅ Completed Work
### 🔄 In Progress
### 📝 Technical Notes
### 📊 Acceptance Criteria Status
### 🚀 Next Steps
### ⚠️ Blockers
---
*Progress: N% | Synced at <timestamp>*
```
After posting: update `last_sync` in progress.md frontmatter, update `updated` in the task file.
Add sync marker to local files to prevent duplicate comments:
```markdown
<!-- SYNCED: <datetime> -->
```
---
## Closing an Issue
**Trigger**: User marks a task complete.
### Process
1. Find the local task file (`.claude/epics/*/<N>.md`).
2. Update frontmatter: `status: closed`, `updated: <now>`.
3. Post completion comment:
```bash
echo "✅ Task completed — all acceptance criteria met." | gh issue comment <N> --body-file -
gh issue close <N>
```
1. Check off the task in the epic issue body:
```bash
gh issue view <epic_N> --json body -q .body > /tmp/epic-body.md
sed -i "s/- \[ \] #<N>/- [x] #<N>/" /tmp/epic-body.md
gh issue edit <epic_N> --body-file /tmp/epic-body.md
```
1. Recalculate and update epic progress: `progress = closed_tasks / total_tasks * 100`
---
## Merging an Epic
**Trigger**: User wants to merge a completed epic back to main.
### Preflight
- Verify worktree `../epic-<name>` exists.
- Check for uncommitted changes in the worktree — block if dirty.
- Warn if any task issues are still open.
### Process
```bash
# From worktree: run project tests if detectable
cd ../epic-<name>
# detect and run: npm test / pytest / cargo test / go test / etc.
# From main repo:
git checkout main && git pull origin main
git merge epic/<name> --no-ff -m "Merge epic: <name>"
git push origin main
# Cleanup
git worktree remove ../epic-<name>
git branch -d epic/<name>
git push origin --delete epic/<name>
# Archive
mkdir -p .claude/epics/archived/
mv .claude/epics/<name> .claude/epics/archived/
# Close GitHub issues
epic_issue=$(grep 'github:' .claude/epics/archived/<name>/epic.md | grep -oE '[0-9]+$')
gh issue close $epic_issue -c "Epic completed and merged to main"
```
Update epic.md frontmatter: `status: completed`.
---
## Reporting a Bug Against a Completed Issue
**Trigger**: User finds a bug while testing a completed or in-progress issue — e.g. "found a bug in issue 42", "email validation is broken, came up while testing issue 42".
The workflow should stay automated: create a linked bug task without losing context from the original issue.
### Process
**Step 1 — Read the original issue for context:**
```bash
gh issue view <original_N> --json title,body,labels
```
Also read the local task file if it exists: `.claude/epics/*/<original_N>.md`
**Step 2 — Create a local bug task file:**
```markdown
---
name: Bug: <short description>
status: open
created: <run: date -u +"%Y-%m-%dT%H:%M:%SZ">
updated: <same>
github: (will be set on sync)
depends_on: []
parallel: false
conflicts_with: []
bug_for: <original_N>
---
# Bug: <short description>
## Context
Found while working on / testing issue #<original_N>: <original title>
## Description
<what's broken>
## Steps to Reproduce
<steps>
## Expected vs Actual
- Expected:
- Actual:
## Acceptance Criteria
- [ ] Bug is fixed
- [ ] Original issue #<original_N> behaviour is unaffected
## Effort Estimate
- Size: XS/S
```
Save to `.claude/epics/<same_epic_as_original>/bug-<original_N>-<slug>.md`
**Step 3 — Create a linked GitHub issue:**
```bash
gh issue create \
--repo "$REPO" \
--title "Bug: <short description>" \
--body "$(cat /tmp/bug-body.md)" \
--label "bug,epic:<epic_name>" \
--json number -q .number
```
The issue body should open with `Fixes / follow-up to #<original_N>` so GitHub auto-links them.
**Step 4 — Update the local file** with the GitHub issue number and rename to `<new_N>.md`.
**Output:**
```
✅ Bug issue created: #<new_N> — "Bug: <short description>"
Linked to: #<original_N>
Epic: <epic_name>
Start fixing it: "start working on issue <new_N>"
```
+165
View File
@@ -0,0 +1,165 @@
# Track — Know Where Things Stand
Tracking operations use bash scripts directly for speed and consistency. The LLM is not needed for these — just run the script and present the output.
---
## Script-First Rule
All tracking operations have a corresponding bash script. Run the script; do not reconstruct the output manually.
Scripts live in `references/scripts/` relative to this skill, but need to run from the **project root** (where `.claude/` lives). Run them as:
```bash
bash <skill_path>/references/scripts/<script>.sh [args]
```
Or if ccpm is installed project-locally:
```bash
bash ccpm/scripts/pm/<script>.sh [args]
```
---
## Project Status
**Trigger**: "what's our status", "project status", "overview"
```bash
bash references/scripts/status.sh
```
Shows: active epics, open issues count, recent activity.
---
## Standup Report
**Trigger**: "standup", "daily standup", "what did we do", "morning update"
```bash
bash references/scripts/standup.sh
```
Shows: what was completed yesterday, what's in progress today, any blockers.
---
## List Epics
**Trigger**: "list epics", "show epics", "what epics do we have"
```bash
bash references/scripts/epic-list.sh
```
---
## Show Epic Details
**Trigger**: "show the <name> epic", "epic details for <name>"
```bash
bash references/scripts/epic-show.sh <name>
```
---
## Epic Status
**Trigger**: "status of the <name> epic", "how far along is <name>"
```bash
bash references/scripts/epic-status.sh <name>
```
Shows: task completion breakdown, active agents, blocking issues.
---
## List PRDs
**Trigger**: "list PRDs", "what PRDs do we have", "show backlog"
```bash
bash references/scripts/prd-list.sh
```
---
## PRD Status
**Trigger**: "PRD status", "which PRDs are parsed", "what's in backlog"
```bash
bash references/scripts/prd-status.sh
```
---
## Search
**Trigger**: "search for <query>", "find issues about <topic>", "look for <term>"
```bash
bash references/scripts/search.sh "<query>"
```
Searches local task files, PRDs, and epics for the query term.
---
## What's In Progress
**Trigger**: "what's in progress", "what are we working on", "active work"
```bash
bash references/scripts/in-progress.sh
```
---
## What's Next
**Trigger**: "what should I work on next", "what's next", "next priority"
```bash
bash references/scripts/next.sh
```
Shows highest-priority open tasks with no blocking dependencies.
---
## What's Blocked
**Trigger**: "what's blocked", "any blockers", "what can't we move on"
```bash
bash references/scripts/blocked.sh
```
---
## Validate Project State
**Trigger**: "validate", "check project state", "is everything consistent"
```bash
bash references/scripts/validate.sh
```
Checks: frontmatter consistency, orphaned files, missing GitHub links, dependency integrity.
---
## When Scripts Fail
If a script fails or the output needs interpretation (e.g., an error in the output, or the user asks "what does this mean"), then step in to explain. But always run the script first — don't guess at what status/standup output would look like.
If `.claude/` directory doesn't exist at all, the project hasn't been initialized. Direct the user to run:
```bash
bash references/scripts/init.sh
```
+1
View File
@@ -3,3 +3,4 @@ node_modules/
bin/
CLAUDE.md
.claude/skills/mermaid/
.claude/skills/ccpm/
+17 -2
View File
@@ -10,9 +10,10 @@
"type": "http",
"url": "https://api.githubcopilot.com/mcp",
"headers": {
"Authorization": "Bearer ${GITHUB_TOKEN}"
"Authorization": "Bearer ${GITHUB_TOKEN}",
"X-MCP-Toolsets": "actions,code_security,context,dependabot,discussions,gists,issues,notifications,orgs,projects,pull_requests,repos,secret_protection,security_advisories,stargazers,users"
},
"comment": "Фаза 0 #3 — официальный hosted GitHub MCP (https://github.com/github/github-mcp-server). Требует env GITHUB_TOKEN с PAT (scopes: repo, read:org, не давать admin/delete). Раньше использовали deprecated @modelcontextprotocol/server-github — заменён 06.05.2026."
"comment": "Фаза 0 #3 — официальный hosted GitHub MCP (https://github.com/github/github-mcp-server). Требует env GITHUB_TOKEN с PAT (scopes: repo, read:org, не давать admin/delete). Раньше использовали deprecated @modelcontextprotocol/server-github — заменён 06.05.2026. X-MCP-Toolsets явно перечисляет toolset'ы, включая `projects` (GitHub Projects v2 — доски/спринты/milestones) для раздела C9 «Управление проектами» — план docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md (GH1). Заголовок заменяет default-набор: список явный, чтобы не сузить поверхность."
},
"laravel-boost": {
"command": "php",
@@ -42,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."
}
}
}
+34 -9
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
-
@@ -14,6 +14,7 @@
import { computed, onMounted, ref } from 'vue';
import { impersonationActive, type ImpersonationActiveSession } from '../../api/admin';
import { usePolling } from '../../composables/usePolling';
import { POLLING_INTERVAL_MS } from '../../constants/polling';
const sessions = ref<ImpersonationActiveSession[]>([]);
@@ -37,7 +38,7 @@ const label = computed(() => {
});
onMounted(load);
usePolling(load, { intervalMs: 30_000 });
usePolling(load, { intervalMs: POLLING_INTERVAL_MS });
defineExpose({ sessions, load });
</script>
@@ -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>
+3 -2
View File
@@ -1,4 +1,5 @@
import { onBeforeUnmount, onMounted } from 'vue';
import { POLLING_INTERVAL_MS } from '../constants/polling';
/**
* Polling-composable для авто-обновления view-данных.
@@ -15,14 +16,14 @@ import { onBeforeUnmount, onMounted } from 'vue';
* Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
*/
export interface PollingOptions {
/** Период polling в миллисекундах. По умолчанию 30_000. */
/** Период polling в миллисекундах. По умолчанию POLLING_INTERVAL_MS (30 с). */
intervalMs?: number;
/** Если false — composable не стартует interval (для disable-флага). */
enabled?: boolean;
}
export function usePolling(loader: () => void | Promise<void>, options: PollingOptions = {}): void {
const intervalMs = options.intervalMs ?? 30_000;
const intervalMs = options.intervalMs ?? POLLING_INTERVAL_MS;
const enabled = options.enabled ?? true;
if (!enabled) return;
+11
View File
@@ -0,0 +1,11 @@
/**
* Интервалы polling-обновления view-данных единый источник «магических»
* чисел для usePolling. До приезда SSE/WebSocket в production это покрывает
* «real-time»-паттерн (см. composables/usePolling.ts).
*/
/** Базовый интервал авто-обновления для большинства view-данных (30 с). */
export const POLLING_INTERVAL_MS = 30_000;
/** Интервал для менее срочных счётчиков (напоминания в сайдбаре). */
export const POLLING_REMINDERS_INTERVAL_MS = 60_000;
+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: 'Дальневосточный',
};
+16
View File
@@ -38,6 +38,9 @@ const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
/** DEV-режим: показываем баннер о застабленном auth-gate админки (B6). */
const isDevEnv = import.meta.env.DEV;
const userInitials = computed(() => {
const u = auth.user;
if (!u) return 'АО';
@@ -131,6 +134,19 @@ const currentPageTitle = computed(() => {
</v-app-bar>
<v-main class="admin-main">
<v-alert
v-if="isDevEnv"
type="warning"
variant="tonal"
density="compact"
class="ma-4"
data-testid="dev-auth-gap-banner"
>
DEV-режим: доступ к админке открыт без SSO-проверки middleware
<code>EnsureSaasAdmin</code> в dev пропускает все запросы. В production
требуется вход через Yandex 360 + роль <code>super_admin</code> (Б-1);
неавторизованные запросы получают 503.
</v-alert>
<ImpersonationBanner />
<RouterView />
</v-main>
+3 -2
View File
@@ -15,6 +15,7 @@ import { useAuthStore } from '../stores/auth';
import { useNotificationsStore } from '../stores/notifications';
import { useRemindersStore } from '../stores/reminders';
import { usePolling } from '../composables/usePolling';
import { POLLING_INTERVAL_MS, POLLING_REMINDERS_INTERVAL_MS } from '../constants/polling';
import AppSidebar from '../components/layout/AppSidebar.vue';
import AppTopbar from '../components/layout/AppTopbar.vue';
import DevIndexBadge from '../components/DevIndexBadge.vue';
@@ -57,8 +58,8 @@ onMounted(() => {
void loadNotifications();
void loadReminderCounts();
});
usePolling(loadNotifications, { intervalMs: 30_000, enabled: true });
usePolling(loadReminderCounts, { intervalMs: 60_000, enabled: true });
usePolling(loadNotifications, { intervalMs: POLLING_INTERVAL_MS, enabled: true });
usePolling(loadReminderCounts, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true });
</script>
<template>
+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;
-27
View File
@@ -167,33 +167,6 @@ onUnmounted(() => store.stopPolling());
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
/* Workaround: MDI-шрифт не подключён в проекте (Диз-4),
`<i class="mdi-close-circle">` рендерится пустым. Подменяем глиф на Unicode ``
и показываем только когда поле имеет значение (Vuetify ставит `.v-field--dirty`). */
.projects-view :deep(.v-field__clearable) {
position: relative;
}
.projects-view :deep(.v-field__clearable .v-icon) {
color: transparent;
width: 20px;
height: 20px;
}
.projects-view :deep(.v-field--dirty .v-field__clearable)::after {
content: '✕';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: rgba(1, 32, 25, 0.55);
font-size: 14px;
font-family: 'Inter', system-ui, sans-serif;
pointer-events: none;
transition: color 150ms ease;
}
.projects-view :deep(.v-field--dirty .v-field__clearable:hover)::after {
color: var(--liderra-noir, #012019);
}
.toolbar-check {
display: inline-flex;
align-items: center;
+2 -1
View File
@@ -12,6 +12,7 @@ import { computed, onMounted, ref } from 'vue';
import { cancelReportJob, createReportJob, deleteReportJob, listReportJobs, retryReportJob } from '../api/reports';
import { extractErrorMessage, extractValidationErrors } from '../api/client';
import { usePolling } from '../composables/usePolling';
import { POLLING_INTERVAL_MS } from '../constants/polling';
import { type ReportFormat, type ReportJob, type ReportType } from '../composables/mockReports';
import { mapApiReportJob, uiTypeToApi } from '../composables/reportsMapper';
import ReportRequestForm from '../components/reports/ReportRequestForm.vue';
@@ -59,7 +60,7 @@ onMounted(() => {
void loadJobs();
});
usePolling(loadJobs, { intervalMs: 30_000 });
usePolling(loadJobs, { intervalMs: POLLING_INTERVAL_MS });
async function submitForm(): Promise<void> {
submitting.value = true;
+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>
@@ -163,6 +163,7 @@ defineExpose({ settingsState, editOpen, editSetting, openEdit, onSettingUpdated,
size="small"
density="comfortable"
prepend-icon="mdi-pencil"
:aria-label="`Изменить настройку ${setting.key}`"
:data-testid="`edit-${setting.key}-btn`"
@click="openEdit(setting)"
>
+20 -3
View File
@@ -86,11 +86,22 @@ async function handleSubmit() {
placeholder="Минимум 8 символов"
variant="outlined"
density="comfortable"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
required
:error-messages="errors.password"
@click:append-inner="showPassword = !showPassword"
/>
>
<template #append-inner>
<v-icon
class="password-toggle"
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
role="button"
tabindex="0"
@click="showPassword = !showPassword"
@keydown.enter.prevent="showPassword = !showPassword"
@keydown.space.prevent="showPassword = !showPassword"
/>
</template>
</v-text-field>
<div class="d-flex justify-end mb-2">
<RouterLink to="/forgot" class="text-body-2 text-primary"> Забыли пароль? </RouterLink>
@@ -141,4 +152,10 @@ async function handleSubmit() {
.yandex-sso-wrap {
width: 100%;
}
.password-toggle:focus-visible {
outline: 2px solid currentColor;
outline-offset: 1px;
border-radius: 2px;
}
</style>
+20 -3
View File
@@ -102,11 +102,22 @@ async function handleSubmit() {
placeholder="Минимум 8 символов"
variant="outlined"
density="comfortable"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
required
:error-messages="errors.password"
@click:append-inner="showPassword = !showPassword"
/>
>
<template #append-inner>
<v-icon
class="password-toggle"
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
role="button"
tabindex="0"
@click="showPassword = !showPassword"
@keydown.enter.prevent="showPassword = !showPassword"
@keydown.space.prevent="showPassword = !showPassword"
/>
</template>
</v-text-field>
<div v-if="password" class="strength-block mb-2">
<v-progress-linear
@@ -184,4 +195,10 @@ async function handleSubmit() {
gap: 4px;
margin-bottom: 8px;
}
.password-toggle:focus-visible {
outline: 2px solid currentColor;
outline-offset: 1px;
border-radius: 2px;
}
</style>
@@ -112,11 +112,22 @@ async function handleSubmit() {
placeholder="Минимум 10 символов"
variant="outlined"
density="comfortable"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
required
:error-messages="errors.password"
@click:append-inner="showPassword = !showPassword"
/>
>
<template #append-inner>
<v-icon
class="password-toggle"
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
role="button"
tabindex="0"
@click="showPassword = !showPassword"
@keydown.enter.prevent="showPassword = !showPassword"
@keydown.space.prevent="showPassword = !showPassword"
/>
</template>
</v-text-field>
<v-text-field
v-model="passwordConfirmation"
@@ -165,4 +176,10 @@ async function handleSubmit() {
flex-direction: column;
gap: 8px;
}
.password-toggle:focus-visible {
outline: 2px solid currentColor;
outline-offset: 1px;
border-radius: 2px;
}
</style>
@@ -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');
});
+16
View File
@@ -60,6 +60,10 @@ const mountAdminLayout = async (path = '/admin/tenants', user: AuthUser | null =
};
describe('AdminLayout.vue', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('монтируется без ошибок', async () => {
const { wrapper } = await mountAdminLayout();
expect(wrapper.exists()).toBe(true);
@@ -214,4 +218,16 @@ describe('AdminLayout.vue', () => {
const nav = wrapper.find('[aria-label="Админ навигация"]');
expect(nav.exists()).toBe(true);
});
it('B6: показывает DEV-баннер auth-gap в dev-режиме', async () => {
vi.stubEnv('DEV', true);
const { wrapper } = await mountAdminLayout();
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(true);
});
it('B6: скрывает DEV-баннер в production-режиме', async () => {
vi.stubEnv('DEV', false);
const { wrapper } = await mountAdminLayout();
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(false);
});
});
@@ -149,4 +149,14 @@ describe('AdminSystemView.vue', () => {
expect(row?.value).toBe('7');
expect(row?.updated_at).toBe('2026-05-09T11:30:00');
});
it('G9: edit-кнопки имеют aria-label с ключом настройки', async () => {
const wrapper = await mountView();
const editBtns = wrapper.findAll('[data-testid^="edit-"]');
expect(editBtns.length).toBeGreaterThan(0);
for (const btn of editBtns) {
const label = btn.attributes('aria-label') ?? '';
expect(label).toMatch(/^Изменить настройку .+/);
}
});
});
+13
View File
@@ -87,4 +87,17 @@ describe('LoginView.vue', () => {
expect(ssoBtn).toBeDefined();
expect(ssoBtn!.classes()).toContain('v-btn--disabled');
});
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
const wrapper = await mountLoginView();
const toggle = wrapper.find('[aria-label="Показать пароль"]');
expect(toggle.exists()).toBe(true);
expect(toggle.attributes('role')).toBe('button');
await toggle.trigger('click');
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
// keyboard activation (Enter) — toggle back
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
});
});
@@ -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: [] }));
});
});
+13
View File
@@ -53,4 +53,17 @@ describe('RegisterView.vue', () => {
const links = wrapper.findAll('a').map((a) => a.text());
expect(links.some((t) => t.includes('Войдите'))).toBe(true);
});
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
const wrapper = await mountRegister();
const toggle = wrapper.find('[aria-label="Показать пароль"]');
expect(toggle.exists()).toBe(true);
expect(toggle.attributes('role')).toBe('button');
await toggle.trigger('click');
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
// keyboard activation (Enter) — toggle back
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
});
});
@@ -112,4 +112,17 @@ describe('ResetPasswordView.vue', () => {
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('Пароли не совпадают');
});
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
const wrapper = await mountReset();
const toggle = wrapper.find('[aria-label="Показать пароль"]');
expect(toggle.exists()).toBe(true);
expect(toggle.attributes('role')).toBe('button');
await toggle.trigger('click');
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
// keyboard activation (Enter) — toggle back
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
});
});
+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');
+50
View File
@@ -1,6 +1,9 @@
# Глоссарий проекта Лидерра
# Формат: одно слово на строке. Кириллица в нижнем регистре.
# A4 design-tooling integration (v2.8 / v3.8 / v1.22)
iconify
# Бренд и термины проекта
лидерра
liderra
@@ -1321,6 +1324,7 @@ mmdc
inertiajs
Sev
вендоренный
вендорен
# D3 audit-risk tooling integration (Прил. Н #39-40)
unvetted
@@ -1330,3 +1334,49 @@ triada
trailofbits
hackathon
субсет
# A11 ML/AI tooling integration — brainstorming spec + plan (2026-05-17)
CCPM
REU
promptfoo
promptfooconfig
datalayer
scikit
XGBoost
Jupyter
pandas
alirezarezvani
Anthropic
RAG
venv
Helicone
Langfuse
# SG #40 Security Guidance correction (2026-05-17)
резолва
шим
characterisation
Arclio
Cowork
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
ребейз
ребейзнута
ребейзом
+2 -1
View File
@@ -23,7 +23,8 @@
"package-lock.json",
"*.svg",
"**/*.sql",
".claude/skills/mermaid/**"
".claude/skills/mermaid/**",
".claude/skills/ccpm/**"
],
"ignoreRegExpList": [
"Email",
+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)
+28 -2
View File
@@ -1,8 +1,16 @@
# Plugin Stack Rules — Superpowers + Frontend Design (v3.4)
# 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`.
**v3.4** — D3 audit-security: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **Trail of Bits Skills** (`trailofbits/skills`, субсет 8 плагинов) + **security-guidance** (`anthropics/claude-plugins-official`). Новая категория **audit-security** (Tooling #39-40, раздел D3 карты) — не UI → вне R6.0/R6.1/R14, как debug-runtime/infrastructure/architecture-tooling. Содержательных изменений R0–R9/R11–R14: 0. Связано: Tooling v2.4, Pravila v1.18, CLAUDE.md v2.4.
**v3.3** — A6 architecture-tooling: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **adr-kit** (`rvdbreemen/adr-kit`) + **architecture-patterns** (`secondsky/claude-skills`); Блок 1 +note про **mermaid-skill** (вендоренный сторонний скил). Новая категория **architecture-tooling** (Tooling #36-38, раздел A6 карты) — не UI → вне R6.0/R6.1/R14, как debug-runtime/infrastructure. Содержательных изменений R0–R9/R11–R14: 0. Связано: Tooling v2.3, Pravila v1.17, CLAUDE.md v2.3.
@@ -399,10 +407,17 @@ Stack — **головной**. Все плагины вне stack'а — **ин
| **adr-kit** *(8 skills + агент `adr-generator`)* | `rvdbreemen/adr-kit` | Architecture Decision Records — `/adr-kit:adr` авторинг, `/adr-kit:lint` проверка, `adr-judge` enforcement. Категория: **architecture-tooling** (Tooling #36, вне UI-пула) | при авторинге/ревизии архитектурного решения в `docs/adr/`. `adr-judge` врезан в lefthook job 9 (декларативно, без `--llm`). Не UI → вне R6.0/R6.1/R14 |
| **architecture-patterns** *(1 skill)* | `secondsky/claude-skills` | справочник архитектурных паттернов (Clean / Hexagonal / layered / DDD). Категория: **architecture-tooling** (Tooling #38). Knowledge-only, не решатель | при проектировании/рефакторинге подсистемы — справка по паттернам. Не источник истины (R11), как UPM |
| **Trail of Bits Skills** *(субсет 8 плагинов)* | `trailofbits/skills` (marketplace `trailofbits`) | аудит безопасности — security-аудит diff, supply-chain риск зависимостей, поиск вариантов уязвимостей. Категория: **audit-security** (Tooling #39, вне UI-пула). CC-BY-SA-4.0, marketplace-плагин (не вендорен) | при глубокой аудит-кампании раздела D3 «Аудит и управление рисками». Не UI → вне R6.0/R6.1/R14 |
| **security-guidance** *(1 PreToolUse warn-хук)* | `anthropics/claude-plugins-official` | inline-предупреждения уязвимостей при правке кода (warn-only, не блокирует, 8 категорий). Категория: **audit-security** (Tooling #40) | автоматически — PreToolUse-хук на Write/Edit. Не решатель, не UI → вне R6.0/R6.1/R14 |
| **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 по `/имя`)
@@ -432,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-интерфейса.
@@ -757,6 +775,14 @@ 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`.
- **v3.4 (2026-05-17)** — D3 audit-security: R10.1 Блок 1 (`enabledPlugins`) +2 строки (Trail of Bits Skills #39, security-guidance #40) — новая 6-я off-phase подкатегория audit-security. Связано: Tooling v2.4, Pravila v1.18, CLAUDE.md v2.4. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
- **v3.3 (2026-05-17)** — A6 architecture-tooling: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **adr-kit** (`rvdbreemen/adr-kit`, 8 skills + агент `adr-generator`; `adr-judge` врезан в lefthook pre-commit job 9 декларативно, без `--llm` → 0 вызовов Claude API) + **architecture-patterns** (`secondsky/claude-skills`, knowledge-only справочник паттернов). Блок 1 +note про **mermaid-skill** (вендоренный сторонний скил `.claude/skills/mermaid/`, генератор C4-диаграмм — пассивная утилита вне типологии 3 блоков). Новая категория **architecture-tooling** (Tooling #36-38, раздел A6 карты «Архитектура систем») — не UI → вне R6.0/R6.1/R14 pipeline, как debug-runtime и infrastructure. Содержательных изменений R0–R9/R11–R14: 0. Связано: Tooling v2.2→v2.3, Pravila v1.16→v1.17, CLAUDE.md v2.2→v2.3. Через manual Edit. План `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`.
+27 -5
View File
@@ -1,10 +1,20 @@
# Правила работы Claude в проекте «Лидерра»
**Версия:** v1.18 (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`.
**Что изменилось в v1.18 относительно v1.17:** §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.17 относительно v1.16:** §13.2 +абзац «Off-phase architecture-tooling» — формализованы 3 инструмента раздела A6 карты «Архитектура систем» (#36 adr-kit, #37 mermaid-skill, #38 architecture-patterns) как пятая off-phase категория; §13.2 PSR_v1 cross-ref v3.2+ → v3.3+. Связано: Tooling v2.3 / PSR_v1 v3.3 / CLAUDE.md v2.3; план `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`.
@@ -560,6 +570,12 @@ P0 = блокер старта спринта или регуляторного
| **v1.15** | **15.05.2026** | Новый §14 «Ruflo Queen routing — hard rule»: триггер queen/королева → безусловный route через ruflo Queen (`hive-mind spawn --claude`), enforcement-хук `tools/ruflo-queen-hook.mjs`. §13.6 tier-таблица +строка §14 (explicit hard-rule). §0 priority chain +§14 +note. §14.3 — проактивное предложение ruflo-spawn на нетривиальных задачах. Связано: spec/plan 2026-05-15-ruflo-queen-trigger-and-delegation, CLAUDE.md v2.1, PSR_v1 v3.1, Tooling v2.1. Через `superpowers:brainstorming``writing-plans``subagent-driven-development`. |
| **v1.16** | **16.05.2026** | Реколлаж ruflo — приведение декларации к фактическому рантайму: §12 Superpowers переведён из sub-policy обратно в explicit hard-rule; §0 priority note и §14.6 cross-ref — убраны упоминания ruflo как «уровня 1»; §11.5/§13.2/§13.9/§13.10 cross-refs на PSR_v1 v3.2. Связано: CLAUDE.md v2.2 / PSR_v1 v3.2 / Tooling v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`. |
| **v1.17** | **17.05.2026** | A6 architecture-tooling: §13.2 +абзац «Off-phase architecture-tooling» — формализованы 3 инструмента раздела A6 карты «Архитектура систем» (#36 adr-kit, #37 mermaid-skill, #38 architecture-patterns) как пятая off-phase категория, отдельная от UI-пула / infrastructure / debug-runtime / orchestration; не UI → вне R6.0/R6.1/R14. §13.2 PSR_v1 cross-ref v3.2+ → v3.3+. Связано: Tooling v2.2→v2.3 (§4.11-4.13 + §0 счётчик 35→38), PSR_v1 v3.2→v3.3 (R10.1 Блок 1 +2 строки + note), CLAUDE.md v2.2→v2.3 (§3.3 +#36-38). Через manual Edit (Pravila/PSR_v1/Tooling) + `/claude-md-management:claude-md-improver` (CLAUDE.md per §5 п.10). План `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`. Архитектурных изменений в §§1–12 + §§13.1, 13.314: 0. |
| **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. |
---
@@ -675,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, не решателей):
@@ -692,11 +708,17 @@ Frontend Design и `obra/superpowers` (v5.1.0, 14 skills) — **парный sta
**Инфраструктурные плагины (вне расширенного UI-пула, v1.9+):** `claude-md-management` (skills `claude-md-improver` + `revise-claude-md`, marketplace `anthropics/claude-plugins-official`) — единственный интерфейс правок CLAUDE.md (CLAUDE.md §5 п.10). Категория **инфраструктурная**, не UI — поэтому не попадает под §13 (расширенный UI-пул) и не проходит R6.0/R6.1 фильтр / R14 pipeline. Регулируется PSR_v1 R10.1 блок 1 (`enabledPlugins`-плагины) как off-pool tool. Аналогичные инфраструктурные категории — built-in skills Claude Code (`review`, `security-review`, `init`, `simplify`, `update-config`, `keybindings-help`, `fewer-permission-prompts`, `loop`, `schedule`, `claude-api`) — активируются по явному `/имя` от пользователя; PSR_v1 R10.1 блок 2.
**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.4+**, R10.1 Блок 3.
**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 warn-only хук — inline-предупреждения уязвимостей, не блокирует). Дополнительно `/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 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. Скоуп
+98 -8
View File
File diff suppressed because one or more lines are too long
+12 -4
View File
@@ -2,7 +2,9 @@
## Status
Accepted, 2026-05-17.
Accepted, 2026-05-17. Amended 2026-05-17 — corrected the Security Guidance
characterisation (a blocking `PreToolUse` hook, not warn-only) and recorded the
`python3.exe` shim needed on the Windows dev host.
## Context
@@ -26,8 +28,10 @@ The D3 audit and risk-management toolset is:
(`differential-review`, `audit-context-building`, `supply-chain-risk-auditor`,
`insecure-defaults`, `sharp-edges`, `static-analysis`, `variant-analysis`,
`agentic-actions-auditor`) for deep, on-demand audit campaigns.
- **Security Guidance** — the Anthropic warn-only `PreToolUse` hook plugin, for
inline vulnerability reminders while editing.
- **Security Guidance** — the Anthropic `PreToolUse` hook plugin, for inline
vulnerability reminders while editing. The hook is **blocking** (`sys.exit(2)`):
the first edit per session whose content matches a vulnerable pattern in a
given file is blocked once — a one-time speed-bump, the retry passes.
- **adr-kit** — reused, not re-installed. The decision and risk register is the
set of ADRs in `docs/adr/`: each ADR's `## Consequences` records the residual
risks of a decision, and the `docs/Открытые_вопросы` registry holds the
@@ -70,7 +74,11 @@ The D3 audit and risk-management toolset is:
and supply-chain risk; mitigated by marketplace-cache pinning and re-checked
on plugin upgrades.
- Security Guidance adds one `PreToolUse` hook to a chain that already carries
four — a small per-edit latency cost, accepted because the hook is warn-only.
four — a small per-edit latency cost. The hook is **blocking** (`sys.exit(2)`),
not warn-only; the block is a one-time per-file-and-rule speed-bump, so the
cost is bounded. On this Windows host the bundled `hooks.json` hardcodes the
`python3` interpreter, which is absent — fixed by a `python3.exe` shim in the
Python install directory on PATH (the plugin cache is not modified).
- The toolchain attack surface still depends on a manual procedure until a
vetted auto-auditor exists.
@@ -0,0 +1,33 @@
# ADR-004: Project-management tooling (C9)
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
The `C9 «Управление проектами»` map section had zero tooling. Planning was done
ad-hoc via `docs/superpowers/plans/` files, the `Открытые_вопросы` registry, and
sprint notes in memory. GitHub Issues were barely used (FF-push workflow).
## Decision
- Project management adopts a **GitHub-issue-backed** model: CCPM
(vendored skill) drives PRD→epic→issue→code with traceability; epics are
parent issues, tasks are sub-issues.
- The **GitHub Projects v2** board (official GitHub MCP `projects` toolset) is
the sprint/iteration board.
- Execution plans stay as `docs/superpowers/plans/` files (Superpowers).
- The `Открытые_вопросы` registry remains the home of *open* questions; an ADR
records *closed* decisions; CCPM tracks *features in flight*.
- product-management (Anthropic) is added only if Claude-Code-installable.
## Consequences
- Positive: C9 populated; planning traceable; sprints visible on a board.
- Risk: CCPM is third-party (bus-factor) — mitigated by vendoring.
- Risk: a workflow shift toward GitHub Issues — accepted, this is the decision.
## Enforcement
None — C9 tools are advisory; verified by use and code review.
@@ -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.
+2 -1
View File
@@ -9,7 +9,8 @@ procedures and their artifacts.
- `/security-review` — the customized Anthropic security-review command
(`.claude/commands/security-review.md`).
- Trail of Bits Skills — the `trailofbits` marketplace audit plugins.
- Security Guidance — the Anthropic warn-only inline-vulnerability hook.
- Security Guidance — the Anthropic inline-vulnerability hook (blocking
`PreToolUse`, a one-time per-file-and-rule speed-bump).
- `audit-portal` — the project skill encoding the 14-phase portal audit.
## Boundaries
+141 -9
View File
@@ -243,12 +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) },
@@ -275,6 +280,8 @@ const NODES = [
// D3 audit-security (17.05.2026) — скилы раздела «Аудит и управление рисками»
{ id: 'sk_security_review', label: 'security-review', group: 'skills_proj', size: 20, ring: 3, ...pos(3, 315) },
{ id: 'sk_audit_portal', label: 'audit-portal', group: 'skills_proj', size: 20, ring: 3, ...pos(3, 325) },
// C9 project-management-tooling (17.05.2026) — вендоренный скил раздела «Управление проектами»
{ id: 'ccpm', label: 'CCPM\n(skill)', group: 'skills_proj', size: 18, ring: 3, ...pos(3, 335) },
// ── ХУКИ (12) — S+infra + E (economy/skill) ───
{ id: 'hk_session', label: 'SessionStart:\ncontext-inject', group: 'hooks', size: 24, ring: 4, ...pos(4, 100) },
@@ -302,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) },
@@ -506,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'),
@@ -517,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)
// 🔴 не закрыт правилом / ⚫ возник на практике / 🟢 закрыт правилом
@@ -720,6 +745,58 @@ 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(
'Vendored-скил CCPM: PRD→эпик→GitHub-issue→код с трассируемостью. Раздел C9 опирается также на reuse — GitHub MCP (issues/Projects v2) + Superpowers writing-plans + q-item-add.',
'При создании PRD, декомпозиции на эпики/задачи, связывании с GitHub Issues — стек CCPM + GitHub MCP + writing-plans.',
'Вендорен в .claude/skills/ccpm/ (C9 project-management-tooling, off-phase). Раздел C9. Tooling #41, CLAUDE.md §3.3 #41.',
[{ name: 'Tooling', cond: '§4.16 #41 — реестр' }],
[],
[{ name: 'GitHub MCP', cond: 'CCPM использует GitHub Issues/Projects v2 как source-of-truth' }]
),
product_mgmt: nd(
'Плагин Anthropic (product-management@knowledge-work-plugins): PRD/роадмап/метрики — product-strategy церемонии.',
'При написании PRD, обновлении роадмапа, анализе метрик продукта — skills product-management плагина.',
'Правило PSR_v1 R10.1 блок 1 (project-management-tooling, off-phase). Не UI → вне фильтров R6.0/R6.1/R14. Tooling #42, CLAUDE.md §3.3 #42.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 1: project-management-tooling' }, { name: 'Tooling', cond: '§4.17 #42 — реестр' }],
[],
[]
),
// ── D3 AUDIT-SECURITY (17.05.2026) ───────────────
tob_skills: nd(
@@ -732,9 +809,9 @@ const NODE_DETAILS = {
[{ name: 'MCP: semgrep', desc: 'TB1: граница разграничена регламентом — Semgrep = inline SAST в процессе работы, Trail of Bits = глубокие аудит-кампании по запросу. Параллельное использование разрешено при разных сценариях.', type: 'GREEN' }]
),
sec_guidance: nd(
'Anthropic-плагин (`security-guidance@claude-plugins-official`, Anthropic Verified) — один warn-only PreToolUse-хук, inline-предупреждения уязвимостей при правке кода (8 категорий). Не блокирует, только предупреждает. Раздел D3. Tooling #40.',
'Активен автоматически при каждом Write/Edit/MultiEdit — печатает предупреждение об уязвимом паттерне перед правкой файла.',
'Правило PSR_v1 R10.1 блок 1 (audit-security, off-phase). SG1: 5-й PreToolUse-хук, +~34 мс/правку, economy/ruflo-цепочка не нарушается. Warn-only — не блокирует. Не UI → вне R6.0/R6.1/R14. Tooling §4.15, CLAUDE.md §3.3 #40.',
'Anthropic-плагин (`security-guidance@claude-plugins-official`, Anthropic Verified) — один блокирующий PreToolUse-хук, inline-предупреждения уязвимостей при правке кода (8 контентных правил + 1 path-правило). При первом за сессию совпадении уязвимого паттерна в файле — sys.exit(2), блокирует правку (одноразовый speed-bump, retry проходит). Раздел D3. Tooling #40.',
'Активен автоматически при каждом Write/Edit/MultiEdit — при уязвимом паттерне печатает предупреждение и блокирует первую такую правку файла за сессию; повторная попытка проходит.',
'Правило PSR_v1 R10.1 блок 1 (audit-security, off-phase). SG1: 5-й PreToolUse-хук, блокирующий (sys.exit 2), одноразовый per «файл+правило» за сессию — economy/ruflo-цепочка не нарушается, +~34 мс/правку. SG2: Windows-починка — bundled hooks.json зовёт python3 (нет в PATH), решено python3.exe-шимом в каталоге Python. Не UI → вне R6.0/R6.1/R14. Tooling §4.15, CLAUDE.md §3.3 #40.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 1: audit-security' }, { name: 'Tooling', cond: '§4.15 #40 — реестр' }],
[],
[{ name: 'скил security-review', cond: 'оба — D3 audit-security; sec_guidance inline, sk_security_review ручной' }]
@@ -756,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 и согласует дизайн до написания кода.',
@@ -1833,12 +1928,26 @@ 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: '—' },
sec_guidance: { since: '17.05.2026', changed: '—', uses: null, usesSrc: 'хук' },
sk_security_review: { since: '17.05.2026', changed: '—', uses: null, usesSrc: 'скил' },
sk_audit_portal: { since: '17.05.2026', changed: '—', uses: null, usesSrc: 'скил' },
// ── 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) — попадают в кнопку «⧉ Дубли».
@@ -1921,7 +2030,7 @@ const SECTIONS = [
{ id: 'E7', bucket: 'E', label: 'Исследования' },
{ id: 'E8', bucket: 'E', label: 'Самообучение Claude' },
];
// Узел -> раздел. Покрывает все 110 узлов карты.
// Узел -> раздел. Покрывает все 118 узлов карты.
const NODE_SECTION = {
// правила (4)
pravila: 'E1', claude_md: 'E1', psr_v1: 'E1', tooling: 'E1',
@@ -1963,10 +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]));
@@ -1974,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);
});
});
// ════════════════════════════════════════════════════
@@ -2080,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) {
+40
View File
@@ -0,0 +1,40 @@
# docs/projects — project-management playbook (map section C9)
Home of the `C9 «Управление проектами»` section. Defines how the Лидерра
development project is planned, tracked, and reported.
## Toolset
- **CCPM** (`.claude/skills/ccpm/`) — the PRD→epic→GitHub-issue→code traceability
layer. PRDs live in `.claude/prds/`, epics/tasks in `.claude/epics/<feature>/`.
Invoked via the `/pm` flow / the `ccpm` skill.
- **GitHub MCP** — issues, sub-issues (epic = parent issue, task = sub-issue),
and **Projects v2** boards used as the sprint/iteration board.
- **Superpowers `writing-plans` / `executing-plans` / `subagent-driven-development`**
— authoring and running execution plan-files in `docs/superpowers/plans/`.
- **`q-item-add` skill + `docs/Открытые_вопросы_v8_3.md`** — the open-question
registry (Б-/CTO-/Ю-/Диз-/DO-/OPEN- codes).
- **product-management plugin** (if installed — see Task 5) — PRD/roadmap/metrics
product-strategy ceremonies.
## Boundaries (which tool for which artifact)
- **Open product/business/legal question**`Открытые_вопросы` registry. Not an
epic, not an ADR — it has no resolution yet.
- **A closed technical/architecture decision**`docs/adr/` (adr-kit, A6).
- **A feature to build, with engineering traceability** → a CCPM PRD → epic →
GitHub issues.
- **The step-by-step execution plan for that epic** → a `docs/superpowers/plans/`
file (Superpowers `writing-plans`). CCPM tracks *what & status*; the plan
file tracks *how, task-by-task*.
- **Product-strategy spec / roadmap** → product-management `/write-spec` /
`/roadmap-update` (if installed); else a registry/WISHLIST entry.
## Workflow
1. Idea → CCPM PRD (`.claude/prds/`).
2. PRD → epic + GitHub issues (CCPM `sync`, via GitHub MCP `issue_write` /
`sub_issue_write`).
3. Epic → a `docs/superpowers/plans/` plan-file → execution.
4. Status → CCPM `status.sh` / `standup.sh`; the GitHub Projects v2 board is the
sprint view.
@@ -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,672 @@
# A11 ML / AI 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:** Populate the empty `A11 «ML / AI-разработка»` map section with a conflict-minimal ML/AI toolset — document a **reuse core** (claude-api skill + context7 MCP + Sentry MCP), install **two new light tools** (promptfoo, a vendored Data Scientist skill), and register **Jupyter MCP as a deferred reserved slot** — so A11 becomes a working playbook.
**Architecture:** A11 is an **empty** functional section — `NODE_SECTION` in `docs/automation-graph.html` tags zero nodes `A11`. Approach А (chosen 2026-05-17): a **reuse layer** (claude-api skill, context7 MCP, Sentry MCP — all already installed; A11 documents the coverage, never re-tags their nodes) plus **two new installs** — promptfoo as a root `package.json` devDependency, and a Data Scientist skill vendored as a standalone skill into `.claude/skills/data-scientist/` (no plugin, no marketplace, no hooks — the A6 mermaid pattern). **Jupyter MCP** (executable notebooks) is **deferred** — registered now as a reserved slot, installed later by a separate severable task gated on a Python ML environment + a concrete model to train. All A11 tools are non-UI → a new **ml-ai-tooling** off-phase category, outside the PSR_v1 UI-pool. A11 artifacts live in `docs/ml/`.
**Tech Stack:** promptfoo (`promptfoo/promptfoo`, npm package `promptfoo`, MIT); a Data Scientist skill (vendored standalone skill, MIT/permissive — exact repo resolved in Task 1); Jupyter MCP (`datalayer/jupyter-mcp-server` — deferred, not installed); the already-installed claude-api skill / context7 MCP / Sentry MCP (reuse); project normative docs; `docs/automation-graph.html` (vis.js).
**Sequencing (2026-05-17):** the A6 / D3 / C9 epics land and push **first** (they touch the same shared files: the map, 4 normative docs, the Tooling counter). A11 then rebases onto the updated `origin/main` (Task 8 Step 1). The working branch `feat/a11-ml-ai-tooling` already exists and holds the brainstorming spec commit (`ae423be`). A11's Tooling numbers are runtime-resolved (NUM1) — never hard-coded before reading the live counter. Push pattern: `git push origin feat/a11-ml-ai-tooling:main`.
---
## Tool Identity (verified 2026-05-17 via WebSearch)
| # | Tool | Install mode | Source / License | Hooks? |
|---|---|---|---|---|
| 1 | **promptfoo** — CLI test-suite for LLM prompts/agents/RAG: declarative `promptfooconfig.yaml`, assertions (`equals`/`contains`/`llm-rubric`/cost/latency), model comparison, red-teaming. Invoked `npx promptfoo`. | npm **devDependency** in the root `package.json` (`npm i -D promptfoo`) — version-pinned via `package-lock.json` (ML9) | GitHub `promptfoo/promptfoo`, **MIT** (OpenAI-owned since 2026, remains OSS) | None — npm CLI, no Claude Code lifecycle hooks |
| 2 | **Data Scientist skill** — knowledge-only skill: business objective → ML task, algorithm selection (Linear Regression … XGBoost), feature engineering, experiment-tracking + A/B-analysis guidance. | Standalone skill — **vendored** copy into `.claude/skills/data-scientist/` (no plugin, no marketplace) | Exact repo **resolved Task 1 Step 4** — candidates: `secondsky/claude-skills` (the A6 architecture-patterns marketplace), `alirezarezvani/claude-skills` (263+ skills), or a dedicated data-science skill repo. MIT/permissive required. | None disclosed (skills-only) — **verify on vendor** |
| — | **Jupyter MCP** (`datalayer/jupyter-mcp-server`) — executable notebooks: insert/run cells, read outputs, plots. | **NOT installed** — deferred reserved slot (see "Deferred Task"). | GitHub `datalayer/jupyter-mcp-server` | n/a — not installed |
| — | **claude-api skill** / **context7 MCP** / **Sentry MCP** | **Reuse** — already installed | claude-api: plugin skill; context7: MCP (`mcp__plugin_context7_context7__*`); Sentry: Tooling #34 | n/a |
**Verification status:** promptfoo — repo, MIT, npm package name, config-file convention confirmed via WebSearch. Data Scientist skill — the *category* (151+ data-science skills in the marketplace) and the "Data Scientist" skill *concept* confirmed; the **exact source repo is NOT pinned** → Task 1 Step 4 resolves it with concrete criteria. Jupyter MCP — `datalayer/jupyter-mcp-server` confirmed to exist; **not installed by this plan**.
**Deferred (with reason — no task in this plan):**
- **Jupyter MCP** — would be the 8th `.mcp.json` server; experimental (Notebook 6.x only); requires a Python ML environment that the native-Windows machine deliberately lacks; and there is no model to train. Registered in the Tooling registry as a **pending** slot (Task 6); installed later by the severable task described in "Deferred Task" below.
**Dropped (with reason — no task, no slot):**
- **A dedicated LLM-observability tool** (Langfuse / Helicone) — redundant with Sentry's AI/LLM monitoring on the already-installed Sentry MCP (#34); CLAUDE.md §5 п.6 (no two tools for one job).
- **A standalone Claude-API plugin** — the claude-api skill is already available; installing a second is a §5 п.6 duplication.
- **An `ml-developer`-style agent plugin** — the agent layer is already crowded; promptfoo + the Data Scientist skill + claude-api cover the workflow without a new agent.
---
## Design Decisions & Conflict Audit
Pattern follows the K1K8 / AK1CC1 / CP1NUM1 audits used for claude-mem and the A6 / C9 / D3 plans. Verified against the promptfoo repo, project `.claude/settings.json`, `~/.claude/settings.json`, `.mcp.json`, `lefthook.yml`, `cspell.json`, `.markdownlintignore`, root `package.json`, and the A6 plan.
| # | Tool | Sev | Conflict | Resolution (locked) |
|---|---|---|---|---|
| ML1 | promptfoo | 🟡 | A real eval run needs an Anthropic API key and makes **paid** LLM calls. Putting it in a hook / pre-commit would cost money on every commit and could break the economy chain. | promptfoo runs **manually / CI-only** — never in a hook, never a lefthook job, never auto. No `lefthook.yml` change. The API key lives in an env var (`ANTHROPIC_API_KEY`, PowerShell User scope — the Sentry `SENTRY_AUTH_TOKEN` pattern), never committed. Documented in `docs/ml/README.md` (Task 4) + the Tooling entry (Task 6). |
| ML2 | promptfoo | 🟢 | promptfoo's red-team module overlaps the D3 audit-security tools. | None — promptfoo red-team tests *LLM prompts* for jailbreak/injection; D3 Trail of Bits (#39) + Semgrep (#25) are SAST of *code*. Different objects. Boundary stated in `docs/ml/README.md` (Task 4) + the Tooling entry (Task 6). |
| ML3 | Data Scientist skill | 🟡 | Vendored `.claude/skills/data-scientist/**/*.md` (third-party English files) is caught by the cspell + markdownlint pre-commit jobs. | Add `.claude/skills/data-scientist/**` to `cspell.json` `ignorePaths` and `.claude/skills/data-scientist/` to `.markdownlintignore` (Task 3 Step 4) — the A6 MK1 pattern. The project's own skills (`q-item-add`, `rls-check`, `regression`, `mermaid`) stay linted. |
| ML4 | reuse layer | 🟢 | Re-tagging the existing `context7` (E7) / `mcp_sentry` (A7) map nodes to A11 would empty their current sections — `NODE_SECTION` is 1-node→1-section. | Reuse nodes **stay** in their sections. A11 gets its **own** new nodes (`claude_api`, `promptfoo`, `data_scientist`). The reuse coverage is documented in `docs/ml/README.md` (Task 4) and noted in the new nodes' details (Task 7). Same as A6/D3/C9 REU1. |
| ML5 | all | 🟢 | A11 is non-UI tooling. | New off-phase category **ml-ai-tooling**, outside the PSR_v1 UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline — same treatment as `claude-md-management` (infrastructure), the A6 architecture-tooling, and the D3 audit-security categories. Registered in PSR_v1 R10.1 + Pravila §13.2 (Task 6). |
| ML6 | promptfoo | 🟢 | 8 PreToolUse/UserPromptSubmit/Stop economy + skill-discipline + ruflo hooks — does promptfoo touch them? | None — promptfoo is an npm CLI invoked on demand; it registers zero Claude Code lifecycle hooks. Re-verified Task 2 Step 5. |
| ML7 | all | 🟡 | Bus-factor — the Data Scientist skill is a community project; Jupyter MCP is community + experimental. | The Data Scientist skill is **vendored** → immune to upstream loss (the A6 mermaid pattern). promptfoo is MIT, OpenAI-owned, stable. Jupyter MCP is deferred and will be version-pinned at install. Noted in the Tooling entry (Task 6). No alpha-substrate spike needed for the core scope — neither promptfoo nor the vendored skill has a known-broken core (unlike ruflo K7). |
| ML8 | reuse layer | 🟡 | claude-api skill / context7 MCP are used but may not be **formalized** Tooling-registry positions → using them unregistered is a PSR_v1 R0.2/R10 gap. | Task 1 Step 5 audits whether claude-api and context7 are already in `~/.claude/settings.json` `enabledPlugins` + the Tooling registry. Task 6 registers whatever is missing (as reuse positions). Sentry MCP is already Tooling #34. |
| ML9 | Jupyter MCP | 🟡 | Registering a tool that is not installed could mislead a future reader into thinking it is available. | The Tooling entry + `docs/ml/README.md` + the map mark Jupyter MCP explicitly **«pending — не установлен, severable-задача»** (the Sentry #34 "pending Sentry deployment" precedent). No `jupyter_mcp` map node is added until the deferred task runs. |
| NUM1 | normative sync | 🟡 | The A6 plan claimed Tooling #36-#38, D3 claimed #39-#40, C9 claims the next after that. A11 must not collide. | Task 1 Step 6 + Task 6 Step 1 read the **live** `docs/Tooling_v8_3.md` Прил. Н §0 counter and assign A11's numbers sequentially after whatever is current. Never hard-code a number before reading the live counter. |
**Severable scope.** Core A11 = Tasks 1-8 (reuse documentation + promptfoo + Data Scientist skill + normative + map + finish) — already populates and closes the section. There is **no severable task inside this plan** (unlike A6's Task 5): the only deferred piece, Jupyter MCP, is a *future* task outside this plan (see "Deferred Task"). A11 adds **no lefthook job** and **no `.mcp.json` change** — fewer conflicts by design (the C9 shape).
---
## File Structure
| File | Created / Modified | Responsibility |
|---|---|---|
| `docs/ml/` | Create dir | A11 home — the ML/AI playbook |
| `docs/ml/README.md` | Create | The ML/AI convention: tool boundaries (claude-api skill = *build* / promptfoo = *test* prompts / Data Scientist skill = classical-ML *workflow* / Jupyter MCP = *execute* — deferred); the reuse-layer map; the promptfoo API-key + manual-run note (ML1) |
| `docs/ml/promptfoo-example/promptfooconfig.yaml` | Create | One seed eval config — a worked lead-qualification prompt example |
| `docs/ml/promptfoo-example/README.md` | Create | How to run the example (`npx promptfoo eval`), the API-key requirement, the "never in CI/hooks" rule |
| `.claude/skills/data-scientist/` | Create (vendored) | The Data Scientist skill — `SKILL.md` + `references/` |
| `docs/adr/ADR-005-ml-ai-tooling.md` | Create (conditional — adr-kit/A6 landed) | Seed ADR documenting the A11 tooling decision + the Python/Jupyter defer |
| `package.json` (repo root) | Modify | `promptfoo` devDependency |
| `package-lock.json` (repo root) | Modify | promptfoo dependency tree (written by `npm i`) |
| `cspell.json` | Modify | `ignorePaths` += `.claude/skills/data-scientist/**` (ML3) |
| `.markdownlintignore` | Modify | += `.claude/skills/data-scientist/` (ML3) |
| `cspell-words.txt` | Modify (conditional) | New ML/AI vocabulary |
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new ml-ai-tooling subsection(s) + §0 counter bump |
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — new ml-ai-tooling rows |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — ml-ai-tooling category note |
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, new §3.3 ml-ai-tooling row(s) |
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
| `docs/automation-graph.html` | Modify | 3 new A11 nodes (`claude_api`, `promptfoo`, `data_scientist`) → `NODE_SECTION` A11; header metrics |
| `.mcp.json` | **NOT modified** | Jupyter MCP deferred — `.mcp.json` is untouched by this plan |
---
## Task 1: Pre-flight — baseline, branch, snapshot, fact-check
**Files:** none modified (read-only)
- [ ] **Step 1: Confirm tree state and branch**
```bash
cd "c:/моя/проекты/портал crm/Документация"
git status --short
git rev-parse --short HEAD
git branch --show-current
```
Expected: branch `feat/a11-ml-ai-tooling`, HEAD at the spec commit `ae423be` (or later). Record the HEAD SHA as the regression baseline. (If a different branch — `git checkout feat/a11-ml-ai-tooling`.)
- [ ] **Step 2: Snapshot the hook chain**
Read `.claude/settings.json`, `.claude/settings.local.json` (if present), and `~/.claude/settings.json`. Record every hook on `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `PostCompact`, `Stop`. This is the ML6 baseline — Task 2 compares against it.
Expected (`~/.claude/settings.json`): SessionStart economy-self-check; PreToolUse skill-marker/skill-check/economy-state-guard/CLAUDE.md-warn/security-guidance; UserPromptSubmit economy-mode; PostCompact economy-postcompact; Stop economy-verifier. Project `.claude/settings.json`: ruflo-recall + ruflo-queen (UserPromptSubmit), markdownlint-fix + schema-CHANGELOG-reminder (PostToolUse).
- [ ] **Step 3: Baseline regression**
```
/regression quick
```
Expected: GREEN. Record the current Pest / Vitest counts from the last green run (memory `project_state.md`). A11 touches no `app/` code → the final run (Task 8) must match.
- [ ] **Step 4: Fact-check promptfoo + resolve the Data Scientist skill repo**
promptfoo — open `https://github.com/promptfoo/promptfoo` and confirm: npm package `promptfoo`, MIT license, `promptfooconfig.yaml` config convention, the Anthropic provider id form (`anthropic:messages:<model>`), no Claude Code lifecycle hooks.
Data Scientist skill — resolve the exact source. WebFetch the candidates and pick **one** repo that satisfies ALL of:
- ships a real standalone **skill** (`SKILL.md` + optional `references/`), not a plugin requiring marketplace machinery;
- MIT or other permissive license;
- covers classical-ML workflow (algorithm selection, feature engineering, evaluation, experiment tracking);
- no `hooks` block / no Claude Code lifecycle hooks.
Candidates, in priority order: (a) `secondsky/claude-skills` (already a project marketplace from A6 — check for a `data-science`/`data-scientist` skill dir), (b) `alirezarezvani/claude-skills`, (c) a dedicated data-science skill repo found via WebSearch. Record the chosen repo URL + license + the in-repo path to the skill directory. If a candidate registers CC lifecycle hooks → reject it, try the next.
If **no** candidate qualifies → **stop**, report to the user; A11 falls back to a project-authored minimal `data-scientist` skill (out of this plan's scope).
- [ ] **Step 5: Audit reuse-tool registration (ML8)**
Check whether the reuse tools are already formalized:
- `~/.claude/settings.json` `enabledPlugins` — is the claude-api skill's backing plugin listed? Record yes/no + the plugin id.
- `.mcp.json` / `~/.claude/settings.json` — is context7 configured? (It is — `mcp__plugin_context7_context7__*` tools exist.) Record the server/plugin id.
- `docs/Tooling_v8_3.md` Прил. Н — grep for `claude-api`, `context7`. Record which (if any) already have a Tooling number.
This drives Task 6: register whatever reuse tool is missing a Tooling position.
- [ ] **Step 6: Check A6 landed + read the live Tooling counter (NUM1)**
```bash
git log --oneline | grep -iE "adr-kit|architecture-patterns|mermaid|ADR-00" | head
ls docs/adr/ 2>/dev/null
```
Read `docs/Tooling_v8_3.md` Прил. Н §0 — record the **live** tool counter. Record: **A6 landed?** yes/no (drives Task 4's conditional ADR-005 — `docs/adr/ADR-000-adr-process.md` exists ⇒ yes) and the counter value (drives Task 6 numbering). (Note: the Task-0 pre-commit run already showed `adr-judge` as lefthook job 9 + "1 ADR with Enforcement" — A6 has very likely landed; confirm here.)
No repo files changed → no commit.
---
## Task 2: Install promptfoo (npm devDependency — ML1/ML9)
**Files:** Modify `package.json`, `package-lock.json` (repo root)
- [ ] **Step 1: Inspect the root `package.json`**
Read the repo-root `package.json`. Confirm it is the doc-tooling project (scripts `lint:md`, `spell`, `links`, `a11y`; devDependencies `markdownlint-cli2`, `cspell`, etc.). Record the current `devDependencies` shape.
- [ ] **Step 2: Install promptfoo as a devDependency**
```bash
cd "c:/моя/проекты/портал crm/Документация"
npm install --save-dev promptfoo
```
Expected: `promptfoo` appears in `package.json` `devDependencies` with a pinned `^`-range; `package-lock.json` updated. If `npm install` warns about peer deps, record the warning — do NOT use `--force`/`--legacy-peer-deps` unless a hard failure requires it (record the reason if so).
- [ ] **Step 3: Verify the CLI runs**
```bash
npx promptfoo --version
```
Expected: prints a version number (a real run with no API key still reports `--version`). No paid LLM call is made here.
- [ ] **Step 4: Add a convenience npm script (optional but recommended)**
Edit `package.json` `scripts` — add:
```json
"eval:llm": "promptfoo eval -c docs/ml/promptfoo-example/promptfooconfig.yaml"
```
This documents the manual invocation. It is **not** wired into any other script, hook, or CI step (ML1).
- [ ] **Step 5: Verify NO lifecycle hooks were added (ML6)**
Read the `hooks` block of `~/.claude/settings.json` AND project `.claude/settings.json`. Both must be **unchanged** vs the Task 1 Step 2 snapshot — npm installs do not touch Claude Code settings, but confirm. If anything changed → stop and re-audit.
- [ ] **Step 6: Lint + commit**
```bash
git add package.json package-lock.json
npx lefthook run pre-commit
```
Expected: all jobs green (`package.json`/`package-lock.json` are JSON — not linted by markdownlint/cspell; gitleaks scans them — promptfoo's tree has no secrets).
```bash
git commit -m "feat(a11): add promptfoo as devDependency for LLM prompt eval (ML1)"
```
---
## Task 3: Vendor the Data Scientist skill (standalone, vendored — ML3/ML7)
**Files:**
- Create: `.claude/skills/data-scientist/` (vendored skill tree)
- Modify: `cspell.json`, `.markdownlintignore`
- [ ] **Step 1: Clone the resolved source to a temp location**
Using the repo URL + in-repo skill path resolved in Task 1 Step 4:
```bash
git clone --depth 1 <RESOLVED_REPO_URL> /tmp/ds-skill-src
ls -R /tmp/ds-skill-src/<RESOLVED_SKILL_PATH>/
```
Expected: a `SKILL.md` + optional `references/` directory.
- [ ] **Step 2: Verify the skill ships no Claude Code hooks (ML6/ML7)**
```bash
grep -rIl "hooks" /tmp/ds-skill-src --include="*.json" || echo "no hooks json"
```
Expected: no `settings.json` with a `hooks` block. If the skill ships hooks → **stop**, re-audit (pick another candidate from Task 1 Step 4).
- [ ] **Step 3: Vendor the skill into the project**
```bash
mkdir -p ".claude/skills/data-scientist"
cp -r /tmp/ds-skill-src/<RESOLVED_SKILL_PATH>/. ".claude/skills/data-scientist/"
ls -R ".claude/skills/data-scientist/"
rm -rf /tmp/ds-skill-src
```
Expected: `.claude/skills/data-scientist/SKILL.md` (+ `references/**` if present). The `SKILL.md` frontmatter `name:` should be `data-scientist` (or a clear ML name) — if the upstream `name:` clashes with an existing skill, edit it to `data-scientist`. (Vendoring — not a submodule — keeps it on Windows+Cyrillic paths and immune to upstream loss, ML7.)
- [ ] **Step 4: Exclude the vendored skill from the doc-lint chain (ML3)**
Edit `.markdownlintignore` — append:
```
.claude/skills/data-scientist/
```
Edit `cspell.json` — add `.claude/skills/data-scientist/**` to the `ignorePaths` array. Do **not** ignore `.claude/skills/` wholesale — `q-item-add`, `rls-check`, `regression`, `mermaid` stay linted.
- [ ] **Step 5: Reload and verify the skill is discoverable**
```
/reload-plugins
```
Confirm a `data-scientist` skill is now listed among available skills (project `.claude/skills/` is auto-discovered, like the other project skills). Confirm neither `settings.json` `hooks` block changed (ML6).
- [ ] **Step 6: Verify lint exclusion works, then commit**
```bash
git add .claude/skills/data-scientist/ cspell.json .markdownlintignore
npx lefthook run pre-commit
```
Expected: cspell + markdownlint do NOT report errors from `.claude/skills/data-scientist/**`; all jobs green.
```bash
git commit -m "feat(a11): vendor Data Scientist skill into .claude/skills + lint-ignore (ML3)"
```
---
## Task 4: Bootstrap the A11 home — `docs/ml/` + seed promptfoo example + ADR-005
**Files:**
- Create: `docs/ml/README.md`, `docs/ml/promptfoo-example/promptfooconfig.yaml`, `docs/ml/promptfoo-example/README.md`
- Create (conditional — A6 landed): `docs/adr/ADR-005-ml-ai-tooling.md`
- Modify (conditional): `cspell-words.txt`
- [ ] **Step 1: Create the A11 home + the ML/AI convention**
```bash
mkdir -p "docs/ml/promptfoo-example"
```
Create `docs/ml/README.md`:
```markdown
# docs/ml — ML / AI playbook (map section A11)
Home of the `A11 «ML / AI-разработка»` section. Defines the tooling Лидерра uses
to build and test ML/AI capability. The portal currently ships no ML/AI code —
this section is the toolset, ready for when AI features are scoped.
## Toolset
| Tool | Role | Status |
|---|---|---|
| **claude-api skill** | Build AI features on the Anthropic SDK (lead qualification, call summaries, email drafts) with prompt caching. | reuse — already available |
| **context7 MCP** | Up-to-date docs for AI/ML libraries and SDKs. | reuse — already installed |
| **Sentry MCP** | Debug AI features in production via Sentry AI/LLM monitoring (read-only). | reuse — Tooling #34, pending the Sentry deployment (Б-1) |
| **promptfoo** | Test suite for LLM prompts/agents: assertions, regression, LLM-graded eval, red-team. | installed — `npx promptfoo` |
| **Data Scientist skill** | Classical-ML workflow: business objective → ML task, algorithm selection, feature engineering, evaluation. | installed — vendored skill |
| **Jupyter MCP** | Executable notebooks for real model training. | **deferred** — see below |
## Boundaries (which tool for which job)
- **Building an AI feature** (a prompt-backed endpoint) → the **claude-api skill**.
- **Testing / regression-checking an LLM prompt****promptfoo** (`docs/ml/promptfoo-example/`).
- **A classical-ML modelling question** (which algorithm, how to evaluate) → the
**Data Scientist skill**.
- **Executing a notebook / training a model****Jupyter MCP***deferred*.
- promptfoo's **red-team** tests *prompts*; the D3 Trail of Bits / Semgrep tools do
SAST of *code*. Different objects — not a duplication.
## promptfoo — running an eval
promptfoo makes **paid** Anthropic API calls. It runs **manually or in CI only**
never in a git hook, never in pre-commit, never automatically.
- API key: `ANTHROPIC_API_KEY` env var (PowerShell User scope — the Sentry
`SENTRY_AUTH_TOKEN` pattern). Never commit a key.
- Run the seed example: `npm run eval:llm` (or
`npx promptfoo eval -c docs/ml/promptfoo-example/promptfooconfig.yaml`).
## Jupyter MCP — why deferred
Jupyter MCP executes notebooks; it needs a Python ML environment (pandas /
scikit-learn / Jupyter). The machine is native Windows, deliberately runtime-minimal
(no Docker), and there is no model to train yet. Jupyter MCP is a **reserved slot**:
registered in the Tooling registry as *pending*, installed by a separate severable
task when a concrete ML model is scoped. See the A11 plan's "Deferred Task".
```
- [ ] **Step 2: Create the seed promptfoo example config**
Create `docs/ml/promptfoo-example/promptfooconfig.yaml`:
```yaml
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
# Seed example — A11. Lead-qualification prompt eval.
# Run manually: npm run eval:llm (needs ANTHROPIC_API_KEY — never in CI/hooks)
description: "Лидерра — lead-qualification prompt eval (example)"
prompts:
- |
Классифицируй обращение лида как HOT, WARM или COLD.
Ответь РОВНО одним словом — HOT, WARM или COLD.
Обращение: {{message}}
providers:
- id: anthropic:messages:claude-haiku-4-5-20251001
tests:
- vars:
message: "Нужно срочно, бюджет согласован, готовы подписать договор сегодня."
assert:
- type: equals
value: HOT
- vars:
message: "Интересно, расскажите подробнее про условия и сроки."
assert:
- type: equals
value: WARM
- vars:
message: "Просто смотрю что есть на рынке, ничего конкретного."
assert:
- type: equals
value: COLD
```
- [ ] **Step 3: Create the example README**
Create `docs/ml/promptfoo-example/README.md`:
```markdown
# promptfoo example — lead-qualification eval
A worked promptfoo eval: a HOT/WARM/COLD lead-classification prompt with three
assertion cases. Demonstrates the A11 prompt-testing workflow.
## Run
```bash
# from the repo root; ANTHROPIC_API_KEY must be set (PowerShell User scope)
npm run eval:llm
```
This makes **paid** Anthropic API calls. Run it manually or in CI only — never
in a git hook or pre-commit (A11 rule ML1). See `docs/ml/README.md`.
## Adapt
Copy `promptfooconfig.yaml` next to a real prompt when an AI feature is built.
Swap the model, add `tests`, use richer assertions (`contains`, `llm-rubric`,
cost/latency thresholds). Full reference: <https://promptfoo.dev/docs/>.
```
- [ ] **Step 4: Decide on the conditional ADR (uses Task 1 Step 6 result)**
If **A6 landed** (`docs/adr/ADR-000-adr-process.md` exists, adr-kit present) → do Step 5. If A6 has **not** landed → skip Step 5, and add a line to `docs/ml/README.md` noting "the A11 tooling decision will be recorded as an ADR once adr-kit (A6) lands". Record the branch taken.
- [ ] **Step 5: Write ADR-005 (conditional)**
Create `docs/adr/ADR-005-ml-ai-tooling.md`:
```markdown
# ADR-005: ML / AI tooling (A11)
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
The `A11 «ML / AI-разработка»` map section had zero tooling. Лидерра ships no
ML/AI code; `calc_lead_score` is a deterministic SQL function. A toolset is needed
for the day AI features (LLM-backed) or a scoring model are scoped.
## Decision
A11 adopts a six-position toolset in two subcategories:
- **LLM integration** — the claude-api skill (build), promptfoo (test prompts),
Sentry MCP (observe). All reuse or light.
- **Classical ML** — a vendored Data Scientist skill (workflow knowledge). The
executable part, **Jupyter MCP**, is **deferred**: it needs a Python ML runtime
the deliberately-minimal native-Windows machine lacks, and there is no model to
train. Jupyter MCP is a reserved registry slot, installed by a separate task
when a concrete model is scoped.
- promptfoo runs manually / CI only — never in a hook (paid LLM calls).
- A11 tools are non-UI → the `ml-ai-tooling` off-phase category.
## Consequences
- Positive: A11 populated; AI features have a build+test+observe toolchain.
- Risk: the Data Scientist skill is third-party — mitigated by vendoring.
- Deferred: no Python runtime until a model is scoped — accepted, this is the
decision.
## Enforcement
None — A11 tools are advisory; verified by use and code review.
```
- [ ] **Step 6: Lint + commit**
```bash
npx markdownlint-cli2 "docs/ml/**/*.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/ml/**/*.md"
```
If A6 landed, also lint `docs/adr/ADR-005-*.md`. Add flagged valid terms (`promptfoo`, `promptfooconfig`, `scikit`, `XGBoost`, `Jupyter`, `Лидерра`, etc.) to `cspell-words.txt`. Then:
```bash
git add docs/ml/ cspell-words.txt
git add docs/adr/ADR-005-*.md # only if Step 5 ran
git commit -m "feat(a11): bootstrap docs/ml — README + promptfoo example + ADR-005"
```
---
## Task 5: Smoke-test the A11 toolset
**Files:** none modified
- [ ] **Step 1: Smoke-test promptfoo config validity (no paid call — ML1)**
```bash
npx promptfoo validate -c docs/ml/promptfoo-example/promptfooconfig.yaml
```
Expected: the config parses as valid. (`validate` checks schema only — no LLM call, no cost.) If `validate` is not a subcommand in the installed promptfoo version, instead run `npx promptfoo eval -c docs/ml/promptfoo-example/promptfooconfig.yaml --no-cache` **only if** `ANTHROPIC_API_KEY` is set and the user approves the small cost; otherwise record "config syntax reviewed manually, eval not run (no key / cost)".
- [ ] **Step 2: Smoke-test the Data Scientist skill**
Invoke the `data-scientist` skill with a trivial ML question (e.g. "which algorithm and evaluation metric fit predicting lead conversion from CRM features?"). Expected: the skill loads, `SKILL.md` routes the intent, and it returns structured classical-ML guidance (algorithm choice, metrics, validation). Functional smoke — no file output required.
- [ ] **Step 3: Smoke-test the claude-api skill reuse**
Confirm the `claude-api` skill is invocable (it appears in the available-skills list). No deep test — A11 only documents it as reuse. Record availability.
- [ ] **Step 4: Confirm the hook chain is intact (ML6)**
Submit a trivial prompt; the economy marker still appears, the Stop verifier still runs, ruflo + CLAUDE.md-warn hooks fire. No plugin/skill leaked a `hooks` entry. No repo files changed in Task 5 → no commit.
---
## Task 6: Normative registry sync (ML / NUM1)
**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 + the live counter (NUM1)**
Read for exact insertion points and the **current** counter: `docs/Tooling_v8_3.md` Прил. Н §0 + the last `§4.x` subsection; `docs/Plugin_stack_rules_v1.md` R10.1; `docs/Pravila_raboty_Claude_v1_1.md` §13.2. Using the Task 1 Step 5 result, decide the A11 numbers:
- `#N` promptfoo, `#N+1` Data Scientist skill, `#N+2` Jupyter MCP (registered **pending**) — sequential from `counter + 1`, after any A6/D3/C9 entries.
- If Task 1 Step 5 found claude-api / context7 **unregistered**, also assign them numbers (reuse positions). Record the full number assignment.
- [ ] **Step 2: Add the Tooling Прил. Н ml-ai-tooling subsection(s)**
Edit `docs/Tooling_v8_3.md`: add a subsection for each new number, category **ml-ai-tooling** (off-phase). Per tool:
- **promptfoo** — npm `promptfoo` MIT (OpenAI-owned), root `package.json` devDependency, `npx promptfoo`; runs manually/CI only — never in a hook (ML1); red-team boundary vs D3 ToB/Semgrep (ML2); no CC hooks.
- **Data Scientist skill** — vendored standalone skill in `.claude/skills/data-scientist/` (source repo from Task 1 Step 4), knowledge-only; bus-factor mitigated by vendoring (ML7).
- **Jupyter MCP**`datalayer/jupyter-mcp-server`, **pending — NOT installed**; deferred severable task gated on a Python ML environment (ML9); the Sentry #34 "pending" precedent.
- claude-api / context7 (if unregistered) — reuse positions.
Add the ml-ai-tooling category as the **seventh** off-phase subcategory (after UI-pool, infrastructure, debug-runtime, architecture-tooling, audit-security — confirm the exact count against the live file). Bump §0 counter; bump the Прил. Н version header.
- [ ] **Step 3: Add PSR_v1 R10.1 rows**
Edit `docs/Plugin_stack_rules_v1.md`: add a row per new tool to R10.1, category **ml-ai-tooling** (off-phase) — explicitly *outside* the UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline (same treatment as `claude-md-management`, architecture-tooling, audit-security). 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 **ml-ai-tooling** category note, alongside the existing infrastructure / debug-runtime / 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 bump, §1 priority-chain row 2b count bump, new §3.3 ml-ai-tooling row(s). The plugin also writes the `docs/CHANGELOG_claude_md.md` entry and bumps §0 cross-ref versions (Tooling / PSR_v1 / Pravila). **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(a11): register ml-ai-tooling category — promptfoo/Data Scientist skill/Jupyter MCP (NUM1)"
```
---
## Task 7: Reflect A11 on the map — close the section
**Files:** Modify `docs/automation-graph.html`
- [ ] **Step 1: Read the structures to replicate**
In `docs/automation-graph.html` read, as templates: a vendored-skill node (`mermaid_skill`), a plugin-skill node, and an MCP node (`mcp_sentry`) across `NODES`, `NODE_DETAILS`/`nd(...)`, `NODE_SECTION`, and the "Паспорт узла" date fields. Record the current node/edge counts from the header and the group-count comments.
- [ ] **Step 2: Add the 3 A11 nodes**
Add to `NODES`, replicating the template shapes:
- `claude_api` — label `claude-api\n(skill)`, skills group.
- `promptfoo` — label `promptfoo`, an appropriate tooling group (match how `bin/` CLIs / lefthook tools are grouped — e.g. the group used for gitleaks/lychee-class tools, or a plugins/skills group if no CLI group exists; record the choice).
- `data_scientist` — label `Data Scientist\n(skill)`, skills group.
Add matching `nd(...)` / `NODE_DETAILS` entries (Russian, per the file's convention), Паспорт `since: '2026-05-17'`:
- `claude_api` — "Скил сборки AI-фич на Anthropic SDK (prompt-кэш). Reuse — раздел A11 опирается также на context7 MCP (доки) и Sentry MCP (LLM-наблюдаемость)."
- `promptfoo` — "npm-CLI eval LLM-промптов: ассерты, регрессия, red-team. Запуск вручную/CI — не в хуках (платные вызовы)."
- `data_scientist` — "Vendored-скил: классический ML-воркфлоу — выбор алгоритма, feature engineering, оценка модели."
- [ ] **Step 3: Map the 3 nodes to section A11**
In `NODE_SECTION` add (a new comment block, after the A6 block):
```js
// A11 ml-ai-tooling 17.05.2026 — раздел «ML / AI-разработка» наполнен
claude_api: 'A11', promptfoo: 'A11', data_scientist: 'A11',
```
`A11 «ML / AI-разработка»` goes from 0 → 3 nodes — the section is no longer empty. (No `jupyter_mcp` node — Jupyter MCP is deferred, ML9.)
- [ ] **Step 4: Update header metrics + group-count comments**
Bump the node count in the map header/legend by 3. Bump the edge count if Step 2's node details add governing edges (match how the A6 nodes were wired — e.g. an edge to the governing normative node; replicate that pattern, else node-only). Update the `NODE_SECTION` group-count comments (skills, and whatever group `promptfoo` joined).
- [ ] **Step 5: Smoke-test the map**
```bash
npx stylelint docs/automation-graph.html
```
Open `docs/automation-graph.html` (Playwright MCP or a local `http.server` — quirk 90: `file://` rejected). Expected: 0 JS console errors; the 3 new nodes render; clicking section `A11` highlights all three.
- [ ] **Step 6: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): A11 nodes — closes section «ML / AI-разработка»"
```
---
## Task 8: Final regression & branch finish
**Files:** none modified
- [ ] **Step 1: Rebase onto latest origin/main (sequencing)**
```bash
git fetch origin
git rebase origin/main
```
Expected: a clean rebase (A6/D3/C9 already landed; A11 touched the same shared files — the map, 4 normative docs, the Tooling counter — so resolve any conflict by **re-reading the live file and re-applying the A11 delta**, never blindly). If conflicts are non-trivial → stop, report. Re-run the live-counter read (Task 6 Step 1) if the Tooling counter moved.
- [ ] **Step 2: Full pre-commit chain**
```bash
npx lefthook run pre-commit
```
Expected: all jobs green — A11 adds **no** lefthook job (job count unchanged vs the Task 1 baseline).
- [ ] **Step 3: Confirm app code untouched — run the suites**
A11 changes no `app/` code → suites must match the Task 1 Step 3 baseline:
```bash
cd app && php artisan test --parallel
cd .. && 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 4: Confirm the economy/ruflo hook chain is intact**
Economy marker still appears; the Stop verifier still runs; no plugin/skill leaked a `hooks` entry into either `settings.json`. Compare to the Task 1 Step 2 snapshot.
- [ ] **Step 5: Pre-push checks**
```bash
./bin/gitleaks.exe detect --source . --no-banner --redact
./bin/lychee.exe --config .lychee.toml "docs/**/*.md" "*.md"
```
Expected: gitleaks 0 leaks (the promptfoo example holds no key — ML1); lychee 0 broken (new `docs/ml/**/*.md` + `docs/adr/ADR-005-*.md` are scanned — fix or `.lychee.toml`-exclude any link, e.g. the `promptfoo.dev` docs link).
- [ ] **Step 6: 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/a11-ml-ai-tooling:main`.
---
## Deferred Task (NOT in this plan — future, severable)
**Jupyter MCP install — gated on a trigger.** When a concrete ML model is scoped
(a real lead-scoring / lead-quality model with a dataset), run a separate task:
1. Decide the Python ML environment (native Windows venv vs a YC instance) — an
explicit runtime weighing, like the Docker / pg_partman decisions.
2. `alpha-substrate-spike-first` — spike Jupyter MCP (it is experimental, Notebook
6.x only) before integrating.
3. Install `datalayer/jupyter-mcp-server` as the 8th `.mcp.json` server, version-pinned.
4. Flip the Tooling registry entry from **pending** to **active**; add a `jupyter_mcp`
node to `docs/automation-graph.html``NODE_SECTION` A11 (now 4 nodes).
5. Re-run the full regression + the conflict re-audit.
Until then, A11 is fully covered by the 5 installed/reuse positions.
---
## Self-Review
**1. Spec coverage (the six-position toolset, Approach А).** Reuse layer — claude-api / context7 / Sentry documented in `docs/ml/README.md` (Task 4 Step 1), registration audited (Task 1 Step 5) and synced (Task 6). promptfoo — installed (Task 2), seed example (Task 4 Steps 2-3), smoked (Task 5 Step 1). Data Scientist skill — repo resolved (Task 1 Step 4), vendored (Task 3), smoked (Task 5 Step 2). Jupyter MCP — registered pending (Task 6 Step 2), deferred task documented ("Deferred Task"). Section closure: normative (Task 6), map (Task 7), regression/finish (Task 8). Conflict audit: ML1→T2.4+T4.1/3, ML2→T4.1+T6.2, ML3→T3.4, ML4→T4.1+T7.2-3, ML5→T6.2-4, ML6→T2.5+T3.5, ML7→T3.3+T6.2, ML8→T1.5+T6.1-2, ML9→T6.2+T7.3, NUM1→T1.6+T6.1+T8.1. No gaps.
**2. Placeholder scan.** `#N`/`#N+1`/`#N+2` (Task 6), `<RESOLVED_REPO_URL>`/`<RESOLVED_SKILL_PATH>` (Task 3), and the promptfoo node's map group (Task 7 Step 2) are **runtime-resolved by design** — the live Tooling counter, the Data Scientist skill source repo, and the live map group shapes are not knowable before Task 1 Step 4-6 / reading the 2400-line map, and each carries concrete resolution criteria (the A6/C9/D3 pattern). All file contents shown in full — `docs/ml/README.md`, both promptfoo-example files, ADR-005, the seed `promptfooconfig.yaml`. No "TBD" / "handle edge cases".
**3. Consistency.** Branch `feat/a11-ml-ai-tooling` consistent T1↔T8. Node ids `claude_api` / `promptfoo` / `data_scientist` consistent T7 Steps 2-3 + the `docs/ml/README.md` table. Category name **ml-ai-tooling** consistent T6 Steps 2-4 + ADR-005. Paths consistent: `.claude/skills/data-scientist/`, `docs/ml/`, `docs/ml/promptfoo-example/`. promptfoo install mode (root `package.json` devDep) consistent T2↔file-structure↔Tooling-entry. Jupyter MCP flagged **deferred/pending** uniformly (Tool Identity, ML9, Task 6 Step 2, Task 7 Step 3, "Deferred Task"). No lefthook job added — consistent with the minimal-conflict goal (ML1).
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-17-a11-ml-ai-tooling-integration.md`. Two execution options:
1. **Subagent-Driven** — fresh subagent per task, two-stage review. *Caveat:* Task 2 (`npx promptfoo`), Task 3 Step 5 + Task 5 Steps 2-3 (`/reload-plugins`, skill invocations) 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** — install/config/docs-heavy with many interactive main-session steps (the A6/C9/D3 pattern).
**Sequencing reminder:** A11 rebases onto `origin/main` after A6 / D3 / C9 land (Task 8 Step 1). One open item before execution: execution method — **1** (Subagent-Driven) or **2** (Inline, recommended here).
@@ -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,588 @@
# C9 Project-Management 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:** Populate the empty `C9 «Управление проектами»` map section with a conflict-minimal project-management toolset — reuse the existing GitHub MCP (Projects v2) + Superpowers planning skills, vendor **CCPM** (PRD→epic→issue→code traceability), and conditionally install the Anthropic **product-management** plugin — so C9 becomes a working playbook.
**Architecture:** C9 is an **empty** functional section — `NODE_SECTION` in `docs/automation-graph.html` tags zero nodes `C9`. Variant А (chosen 2026-05-17): a **reuse core** (GitHub MCP `projects` toolset + `issue`/`sub_issue` tools, Superpowers `writing-plans`/`executing-plans`/`subagent-driven-development`, the `q-item-add` skill + `Открытые_вопросы` registry, ruflo task layer) plus **one verified-clean install** — CCPM, vendored as a standalone skill into `.claude/skills/ccpm/` (no plugin, no marketplace, no hooks — the A6 mermaid pattern). The Anthropic **product-management** plugin is a **conditional** add (Task 5, severable) — installed only if Task 1 confirms Claude Code installability. The three conflict-heavy candidates (`task-tracker-plugin` 🔴 hook collision, `claude-kanban` 🔴 lefthook + CLAUDE.md collision, `pm-skills` sprawl) are **dropped**. All C9 tools are non-UI → **project-management** category, outside the PSR_v1 UI-pool. C9 artifacts live in `docs/projects/`; CCPM's PRD/epic store lives under `.claude/prds/` and `.claude/epics/` (CCPM's hardcoded convention).
**Tech Stack:** CCPM (`automazeio/ccpm`, standalone skill, MIT); the official GitHub MCP Server (`github/github-mcp-server`, already installed — `projects` toolset enabled here); Anthropic **product-management** plugin (conditional); adr-kit v0.13.1 (reused from the A6 plan, if landed); project normative docs; `docs/automation-graph.html` (vis.js).
**Sequencing (locked 2026-05-17):** the A6 plan (`2026-05-17-a6-architecture-tooling-integration.md`) and the D3 plan (`2026-05-17-d3-audit-risk-tooling-integration.md`) run to completion and push **first**, in that order; C9 Task 1 then forks from the updated `origin/main`. Rationale: all three epics touch shared files (the map, 4 normative docs, the Tooling counter). C9's seed ADR (Task 4) reuses A6's adr-kit + `docs/adr/ADR-000`. Run strictly sequentially, never interleaved. C9 numbers are runtime-resolved (NUM1) — never hard-coded before reading the live counter.
---
## Tool Identity (verified 2026-05-17 via WebFetch/WebSearch)
| # | Tool | Install mode | Source / License | Hooks? |
|---|---|---|---|---|
| 1 | **CCPM** (Claude Code PM) — `skill/ccpm/`: `SKILL.md` + `references/` (`plan.md`, `structure.md`, `sync.md`, `execute.md`, `track.md`, `conventions.md`) + `references/scripts/` (14 bash scripts: `status.sh`, `standup.sh`, `epic-list.sh`, `search.sh`, …). PRD→epic→GitHub-issue→code, 5 phases. | Standalone skill — vendored copy into `.claude/skills/ccpm/` (no plugin, no marketplace) | GitHub `automazeio/ccpm`, **MIT**; a preserved `v1` branch holds the original system | None disclosed (skills-only; "activates when the agent detects PM intent") — **verify on install** |
| 2 | **product-management** (Anthropic) — `/write-spec`, `/roadmap-update`, `/stakeholder-update`, `/synthesize-research`, `/competitive-brief`, `/metrics-review` | Marketplace plugin (Anthropic-verified) — **conditional** (Task 5) | Anthropic; marketplace name "Product Management" | Unknown — **verify on install** (expected none — skills/commands plugin) |
| — | **GitHub MCP `projects` toolset** (`projects_get` / `projects_list` / `projects_write`) + `issue_read`/`issue_write`/`sub_issue_write` | **Reuse** — already-installed official server; enable the `projects` toolset | `github/github-mcp-server`, official | n/a (MCP server, no CC lifecycle hooks) |
**Verification status:** CCPM — repo, MIT, skill structure, no-`install.sh` (manual symlink/vendor) confirmed via WebFetch. product-management — Anthropic publisher + command list confirmed; **Claude Code (vs Cowork) installability NOT confirmed** — Task 1 Step 5 resolves it. GitHub MCP `projects` toolset — confirmed to exist in the official server; **NOT currently exposed in this project** (the `mcp__github__*` tool set lacks `projects_*`) → Task 2 enables it.
**Dropped (with reason — no tasks):**
- **`task-tracker-plugin`** (`victor-software-house`) — 🔴 registers 5 lifecycle hooks incl. its own `Stop` + `SessionStart`, colliding with the economy/skill-discipline/ruflo hook stack. Compaction context-loss is already mitigated by the ruflo recall hook + memory files.
- **`claude-kanban`** (`alessiocol`) — 🔴 registers git pre-commit hooks (collides with lefthook) and writes its own `CLAUDE.md` (violates CLAUDE.md §5 п.10); 5 commits / no releases (very high bus-factor). Sprint-board need is met by GitHub Projects v2.
- **`pm-skills`** (`phuryn`) — 100+ skills = sprawl; a material library, not a решатель.
- **Separate GitHub Projects v2 MCP** (Arclio / kunwarVivek) — redundant with the official server's `projects` toolset (§5 п.6).
- **`feature-dev`** (Anthropic official) — 7-phase feature workflow + code-explorer/architect/reviewer agents overlap Superpowers (brainstorming→writing-plans→subagent-driven-development) (§5 п.6).
---
## Design Decisions & Conflict Audit
Pattern follows the K1K8 / AK1CC1 audits used for claude-mem, the A6 plan, and the D3 plan. Verified against the CCPM repo, the product-management plugin page, project `.claude/settings.json`, `~/.claude/settings.json`, `.mcp.json`, `lefthook.yml`, `cspell.json`, `.markdownlintignore`, and the A6/D3 plans.
| # | Tool | Sev | Conflict | Resolution (locked) |
|---|---|---|---|---|
| CP1 | CCPM | 🟡 | Vendored `.claude/skills/ccpm/**/*.md` (third-party English skill files) gets caught by the cspell + markdownlint pre-commit jobs. | Add `.claude/skills/ccpm/**` to `cspell.json` `ignorePaths` and `.markdownlintignore` (Task 3). The project's own skills (`q-item-add`, `rls-check`, `regression`, `mermaid`) stay linted. Same as A6 MK1. |
| CP2 | CCPM | 🟡 | CCPM "activates automatically when the agent detects PM intent" — could collide with Superpowers `brainstorming`/`writing-plans` auto-activation and the §12.2 task-map. | Boundary in `docs/projects/README.md` (Task 4): **CCPM** = the PRD→epic→GitHub-issue→code *traceability* layer (explicit `/pm` flow); **Superpowers `writing-plans`** = authoring execution plan-files in `docs/superpowers/plans/`. CCPM's auto-intent is advisory; the §12.2 map is **unchanged** — CCPM is not added to it. |
| CP3 | CCPM | 🟡 | CCPM uses GitHub Issues as source-of-truth; the project currently barely uses issues (FF-push pattern, `git push origin <branch>:main`). | This is the **deliberate architecture decision of Variant А** — adopt GitHub-issue-backed PM. It is an architectural choice, not a technical clash. Documented as an ADR (Task 4 Step 4, adr-kit from A6). Issues created via the reused GitHub MCP `issue_write` / `sub_issue_write`. |
| CP4 | CCPM | 🟢 | Lifecycle-hook collision with the 6 economy + skill-discipline + 2 ruflo hooks. | Expected none — CCPM is skills-only. Re-verify Task 1 Step 4 + Task 3 Step 4. If CCPM ships a `hooks` block → **stop** and re-audit. |
| CP5 | CCPM | 🟡 | CCPM's scripts hard-code `.claude/prds/` and `.claude/epics/` as the PRD/epic store — `.claude/` is config, not docs; the project keeps docs under `docs/`. | Accept CCPM's hard-coded store (do not patch its 14 scripts — YAGNI). `.claude/prds/` + `.claude/epics/` are committed project content and **stay linted** (NOT covered by the CP1 skill-only lint-ignore). Documented in `docs/projects/README.md`. |
| CP6 | CCPM | 🟢 | 14 bash scripts (`status.sh`, `standup.sh`, …) on native Windows. | None — the project already runs bash (`lefthook`, `gitleaks`, `bin/*.exe`) via Git Bash / the Bash tool. Smoke-tested in Task 6 Step 2. |
| PG1 | product-mgmt | 🟡 | The product-management plugin's page says "Available in Claude Cowork" — Claude **Code** installability is unconfirmed. Installing an uninstallable plugin wastes the task. | Task 1 Step 5 fact-checks the `/plugin marketplace add` + `/plugin install` strings / a backing GitHub marketplace repo. Task 5 installs **only if** Code-installable; otherwise **defer** with a documented fallback (reuse layer authors specs via `writing-plans`; roadmap via the registry + map WISHLIST). Same default-defer shape as D3 #5. |
| PG2 | product-mgmt | 🟢 | Could the plugin register CC lifecycle hooks? | Verify on install (Task 5 Step 2). Skills/commands plugins register none; if it does → stop and re-audit. |
| PG3 | product-mgmt | 🟡 | `/write-spec` (PRD) overlaps the CCPM PRD phase (§5 п.6). | Boundary (Task 7 Tooling entry): CCPM PRD = the *engineering* epic→issue→code traceability artifact; product-management `/write-spec` = the *product-strategy* PRD (problem→spec, roadmap-aligned). Different altitude. Moot if PG1 defers product-management. |
| GH1 | GitHub MCP | 🟢 | Enabling the `projects` toolset = a config + permissions change on an already-installed official server. | Task 2: enable `projects` in the github server's toolset config in `.mcp.json`; add `mcp__github__projects_get/list/write` to `~/.claude/settings.json` `permissions.allow`. No new install. |
| GH2 | GitHub MCP | 🟡 | A separate GitHub Projects v2 MCP (Arclio etc.) would duplicate the official `projects` toolset (§5 п.6). | **Dropped** — not installed. Documented here, no task. |
| REU1 | reuse layer | 🟢 | Re-tagging existing nodes (`sk_wplans`, `mcp_gh`, `sk_qitem`) to C9 would *empty* their current sections — `NODE_SECTION` is 1-node→1-section. | Reuse nodes **stay** in their sections; C9 gets its **own** new nodes (CCPM, product-management). The reuse coverage is documented in `docs/projects/README.md` and noted in the new nodes' `NODE_DETAILS` (Task 8). Same as A6/D3 (they added new nodes, did not re-tag). |
| CC1 | all | 🟡 | Bus-factor — CCPM is a community project (`automazeio`, pre-`v1`-tagged); product-management is Anthropic (low risk). | CCPM is **vendored** → immune to upstream loss (like A6's mermaid). product-management stays a marketplace plugin (cache-pinned). Noted in the Tooling entry. No alpha-substrate spike needed — neither has a known-broken core (unlike ruflo K7). |
| NUM1 | normative sync | 🟡 | The A6 plan claims Tooling slots #36-#38; the D3 plan claims the next ≥2 after A6. C9 must not collide. | Task 1 Step 6 + Task 7 Step 1 read the **live** `docs/Tooling_v8_3.md` Прил. Н §0 counter and assign C9's number(s) sequentially after whatever is current. Never hard-code a number before reading. |
**Severable scope.** Core C9 = Tasks 1-4 + 6-9 (reuse enablement + CCPM vendored + normative + map + finish) — already populates the section. **Task 5** (product-management) is severable + conditional (PG1). Unlike A6, C9 adds **no lefthook job** (no commit-pipeline gate) — fewer conflicts by design. **Selected 2026-05-17 (Variant А): core + conditional Task 5.**
---
## File Structure
| File | Created / Modified | Responsibility |
|---|---|---|
| `.claude/skills/ccpm/` | Create (vendored) | The CCPM skill — `SKILL.md` + `references/` + `references/scripts/` |
| `.claude/prds/.gitkeep` | Create | CCPM PRD store (CCPM's hard-coded convention — CP5) |
| `.claude/epics/.gitkeep` | Create | CCPM epic/task store (CCPM's hard-coded convention — CP5) |
| `docs/projects/` | Create dir | C9 home — the project-management playbook & convention |
| `docs/projects/README.md` | Create | The PM convention: GitHub-Projects-v2-as-sprint-board; CCPM vs `writing-plans` vs registry vs ADR boundaries; reuse-layer map |
| `docs/adr/ADR-004-project-management-tooling.md` | Create (conditional — adr-kit present, A6 landed) | Seed ADR documenting the C9 tooling + GitHub-issue-backed-PM decision (CP3) |
| `.mcp.json` | Modify | Enable the `projects` toolset on the `github` server (GH1) |
| `~/.claude/settings.json` | Modify | `permissions.allow` += `mcp__github__projects_*`; `enabledPlugins` += product-management (conditional); `extraKnownMarketplaces` += 1 (conditional) |
| `cspell.json` | Modify | `ignorePaths` += `.claude/skills/ccpm/**` (CP1) |
| `.markdownlintignore` | Modify | += `.claude/skills/ccpm/` (CP1) |
| `cspell-words.txt` | Modify (conditional) | New PM vocabulary |
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new project-management subsection(s) + §0 counter bump |
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — new project-management rows |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — project-management category note |
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, new §3.3 project-management rows |
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
| `docs/automation-graph.html` | Modify | New C9 nodes → `NODE_SECTION` C9; header metrics |
---
## Task 1: Pre-flight — baseline, branch, snapshot, fact-check
**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 rev-parse --short HEAD
git fetch origin && git checkout -b feat/c9-project-management-tooling origin/main
```
Expected: record `origin/main` HEAD SHA as the regression baseline; new branch `feat/c9-project-management-tooling` created off it. (Push pattern at the end: `git push origin feat/c9-project-management-tooling:main`.)
- [ ] **Step 2: Snapshot the hook chain**
Read `.claude/settings.json`, `.claude/settings.local.json` (if present), and `~/.claude/settings.json`. Record every hook on `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `PostCompact`, `Stop`. This is the CP4/PG2 baseline — Tasks 3 and 5 compare against it.
Expected (`~/.claude/settings.json`): SessionStart economy-self-check; PreToolUse skill-marker/skill-check/economy-state-guard; UserPromptSubmit economy-mode; PostCompact economy-postcompact; Stop economy-verifier (Sonnet). Project `.claude/settings.json`: ruflo-recall + ruflo-queen (UserPromptSubmit), CLAUDE.md-warn (PreToolUse), markdownlint-fix + schema-CHANGELOG-reminder (PostToolUse).
- [ ] **Step 3: Baseline regression**
```
/regression quick
```
Expected: GREEN. Record the current Pest / Vitest counts from the last green run (memory `project_state.md`). C9 touches no `app/` code → the final run (Task 9) must match.
- [ ] **Step 4: Fact-check CCPM**
Open `https://github.com/automazeio/ccpm` and confirm assumptions still hold:
- Skill structure: `skill/ccpm/SKILL.md` + `skill/ccpm/references/` + `skill/ccpm/references/scripts/` (~14 bash scripts).
- MIT license.
- No `settings.json` `hooks` block / no Claude Code lifecycle hook registration.
- Integration is manual (symlink / copy) — no `install.sh` required.
If CCPM now registers CC lifecycle hooks → **stop**, re-audit CP4 before continuing.
- [ ] **Step 5: Fact-check product-management Claude Code installability (PG1)**
Determine whether the Anthropic product-management plugin is installable in Claude **Code** (not only Claude Cowork):
- WebFetch `https://claude.com/plugins/product-management` for an explicit `/plugin marketplace add …` + `/plugin install …` instruction.
- WebSearch for a backing GitHub marketplace repo (e.g. in `anthropics/claude-plugins-official` `marketplace.json`, or a dedicated repo).
Record the verdict: **product-management Code-installable? yes/no** + the exact marketplace + install strings if yes. This drives Task 5 (conditional) and Task 8 (whether a `product_mgmt` node is added).
- [ ] **Step 6: Check A6/D3 landed + read the live Tooling counter (NUM1)**
```bash
git log --oneline origin/main | grep -iE "adr-kit|architecture-patterns|mermaid|trail of bits|security guidance" | head
ls ~/.claude/plugins/cache/ | grep -iE "adr-kit|architecture"
```
Read `docs/Tooling_v8_3.md` Прил. Н §0 — record the **live** tool counter. Record: **A6 landed?** yes/no (drives Task 4's conditional ADR-004) and the counter value (drives Task 7 numbering).
No repo files changed → no commit.
---
## Task 2: Enable the GitHub MCP `projects` toolset (reuse — GH1)
**Files:** Modify `.mcp.json`; Modify `~/.claude/settings.json` (the `Edit` triggers the `ask` permission — expected)
- [ ] **Step 1: Inspect the github server config**
Read `.mcp.json` — locate the `github` server block. Identify how toolsets are configured (the official server gates toolsets via a `--toolsets` arg or a `GITHUB_TOOLSETS` env var; the hosted server may use a query param). Record the current toolset value.
- [ ] **Step 2: Enable the `projects` toolset**
Edit `.mcp.json` — add `projects` to the github server's enabled toolsets (alongside `issues`, `pull_requests`, `repos`, etc.). If toolsets were previously unset (server default), set an explicit list that **includes the previously-working toolsets plus `projects`** — do not narrow the surface. Example (adjust to the actual config shape):
```json
"github": {
"...": "...",
"env": { "GITHUB_TOOLSETS": "repos,issues,pull_requests,projects" }
}
```
- [ ] **Step 3: Add the `projects` tool permissions**
Edit `~/.claude/settings.json` — append to `permissions.allow`:
```
"mcp__github__projects_get",
"mcp__github__projects_list",
"mcp__github__projects_write"
```
- [ ] **Step 4: Reload and verify**
```
/reload-plugins
```
Confirm `mcp__github__projects_get`, `mcp__github__projects_list`, `mcp__github__projects_write` are now available MCP tools. If they are still absent → the toolset value or the server version is wrong; re-inspect Step 1-2. Confirm no `settings.json` `hooks` block changed.
- [ ] **Step 5: Commit**
```bash
git add .mcp.json
git commit -m "feat(c9): enable GitHub MCP projects toolset for Projects v2 (GH1)"
```
(`~/.claude/settings.json` is outside the repo — not committed.)
---
## Task 3: Vendor the CCPM skill (standalone, vendored — CP1/CP4)
**Files:**
- Create: `.claude/skills/ccpm/` (vendored skill tree)
- Modify: `cspell.json`, `.markdownlintignore`
- [ ] **Step 1: Clone the source to a temp location**
```bash
git clone --depth 1 https://github.com/automazeio/ccpm.git /tmp/ccpm-src
ls -R /tmp/ccpm-src/skill/ccpm/
```
Expected: `SKILL.md` + `references/` (`plan.md`, `structure.md`, `sync.md`, `execute.md`, `track.md`, `conventions.md`) + `references/scripts/` (~14 `.sh` files).
- [ ] **Step 2: Verify CCPM ships no Claude Code hooks (CP4)**
```bash
grep -rIl "hooks" /tmp/ccpm-src --include="*.json" || echo "no hooks json"
ls /tmp/ccpm-src/.claude/ 2>/dev/null || echo "no .claude config dir"
```
Expected: no `settings.json` with a `hooks` block. If CCPM ships hooks → **stop**, re-audit CP4.
- [ ] **Step 3: Vendor the skill into the project**
```bash
mkdir -p ".claude/skills/ccpm"
cp -r /tmp/ccpm-src/skill/ccpm/. ".claude/skills/ccpm/"
ls -R ".claude/skills/ccpm/"
rm -rf /tmp/ccpm-src
```
Expected: `.claude/skills/ccpm/SKILL.md` + `references/**` present. (Vendoring — not a submodule — keeps it on Windows+Cyrillic paths and immune to upstream loss, CC1.)
- [ ] **Step 4: Exclude the vendored skill from the doc-lint chain (CP1)**
Edit `.markdownlintignore` — append:
```
.claude/skills/ccpm/
```
Edit `cspell.json` — add `.claude/skills/ccpm/**` to the `ignorePaths` array. Do **not** ignore `.claude/skills/` wholesale — `q-item-add`, `rls-check`, `regression`, `mermaid` stay linted.
- [ ] **Step 5: Reload and verify the skill is discoverable**
```
/reload-plugins
```
Confirm a `ccpm` skill is now listed among available skills (project `.claude/skills/` is auto-discovered, like the other project skills). Confirm neither `settings.json` `hooks` block changed (CP4).
- [ ] **Step 6: Verify lint exclusion works, then commit**
```bash
git add .claude/skills/ccpm/ cspell.json .markdownlintignore
npx lefthook run pre-commit
```
Expected: cspell + markdownlint jobs do NOT report errors from `.claude/skills/ccpm/**`; all 8 jobs green.
```bash
git commit -m "feat(c9): vendor CCPM skill into .claude/skills + lint-ignore (CP1)"
```
---
## Task 4: CCPM bootstrap + the C9 home (CP3 / CP5)
**Files:**
- Create: `.claude/prds/.gitkeep`, `.claude/epics/.gitkeep`
- Create: `docs/projects/README.md`
- Create (conditional — A6 landed): `docs/adr/ADR-004-project-management-tooling.md`
- Modify (conditional): `cspell-words.txt`
- [ ] **Step 1: Create the CCPM store directories**
```bash
mkdir -p ".claude/prds" ".claude/epics"
echo "# CCPM PRD store — see docs/projects/README.md" > ".claude/prds/.gitkeep"
echo "# CCPM epic/task store — see docs/projects/README.md" > ".claude/epics/.gitkeep"
```
(CCPM hard-codes these paths — CP5. `.gitkeep` keeps the empty dirs tracked.)
- [ ] **Step 2: Create the C9 home + the PM convention**
```bash
mkdir -p "docs/projects"
```
Create `docs/projects/README.md`:
```markdown
# docs/projects — project-management playbook (map section C9)
Home of the `C9 «Управление проектами»` section. Defines how the Лидерра
development project is planned, tracked, and reported.
## Toolset
- **CCPM** (`.claude/skills/ccpm/`) — the PRD→epic→GitHub-issue→code traceability
layer. PRDs live in `.claude/prds/`, epics/tasks in `.claude/epics/<feature>/`.
Invoked via the `/pm` flow / the `ccpm` skill.
- **GitHub MCP** — issues, sub-issues (epic = parent issue, task = sub-issue),
and **Projects v2** boards used as the sprint/iteration board.
- **Superpowers `writing-plans` / `executing-plans` / `subagent-driven-development`**
— authoring and running execution plan-files in `docs/superpowers/plans/`.
- **`q-item-add` skill + `docs/Открытые_вопросы_v8_3.md`** — the open-question
registry (Б-/CTO-/Ю-/Диз-/DO-/OPEN- codes).
- **product-management plugin** (if installed — see Task 5) — PRD/roadmap/metrics
product-strategy ceremonies.
## Boundaries (which tool for which artifact)
- **Open product/business/legal question**`Открытые_вопросы` registry. Not an
epic, not an ADR — it has no resolution yet.
- **A closed technical/architecture decision**`docs/adr/` (adr-kit, A6).
- **A feature to build, with engineering traceability** → a CCPM PRD → epic →
GitHub issues.
- **The step-by-step execution plan for that epic** → a `docs/superpowers/plans/`
file (Superpowers `writing-plans`). CCPM tracks *what & status*; the plan
file tracks *how, task-by-task*.
- **Product-strategy spec / roadmap** → product-management `/write-spec` /
`/roadmap-update` (if installed); else a registry/WISHLIST entry.
## Workflow
1. Idea → CCPM PRD (`.claude/prds/`).
2. PRD → epic + GitHub issues (CCPM `sync`, via GitHub MCP `issue_write` /
`sub_issue_write`).
3. Epic → a `docs/superpowers/plans/` plan-file → execution.
4. Status → CCPM `status.sh` / `standup.sh`; the GitHub Projects v2 board is the
sprint view.
```
Adjust the bullet list to whatever Task 5 resolves for product-management.
- [ ] **Step 3: Decide on the conditional ADR (uses Task 1 Step 6 result)**
If **A6 landed** (`docs/adr/ADR-000-adr-process.md` exists, adr-kit present) → do Step 4. If A6 has **not** landed → skip Step 4, and add a line to `docs/projects/README.md` noting "the C9 tooling decision will be recorded as an ADR once adr-kit (A6) lands". Record the branch taken.
- [ ] **Step 4: Write ADR-004 (conditional)**
Create `docs/adr/ADR-004-project-management-tooling.md`:
```markdown
# ADR-004: Project-management tooling (C9)
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
The `C9 «Управление проектами»` map section had zero tooling. Planning was done
ad-hoc via `docs/superpowers/plans/` files, the `Открытые_вопросы` registry, and
sprint notes in memory. GitHub Issues were barely used (FF-push workflow).
## Decision
- Project management adopts a **GitHub-issue-backed** model: CCPM
(vendored skill) drives PRD→epic→issue→code with traceability; epics are
parent issues, tasks are sub-issues.
- The **GitHub Projects v2** board (official GitHub MCP `projects` toolset) is
the sprint/iteration board.
- Execution plans stay as `docs/superpowers/plans/` files (Superpowers).
- The `Открытые_вопросы` registry remains the home of *open* questions; an ADR
records *closed* decisions; CCPM tracks *features in flight*.
- product-management (Anthropic) is added only if Claude-Code-installable.
## Consequences
- Positive: C9 populated; planning traceable; sprints visible on a board.
- Risk: CCPM is third-party (bus-factor) — mitigated by vendoring.
- Risk: a workflow shift toward GitHub Issues — accepted, this is the decision.
## Enforcement
None — C9 tools are advisory; verified by use and code review.
```
- [ ] **Step 5: Lint + commit**
```bash
npx markdownlint-cli2 "docs/projects/*.md" "docs/adr/ADR-004-*.md"
npx cspell --no-progress --no-summary --no-gitignore "docs/projects/*.md" "docs/adr/ADR-004-*.md"
```
Add flagged valid terms (`CCPM`, `Лидерра`, `WISHLIST`, etc.) to `cspell-words.txt`. Then:
```bash
git add docs/projects/ .claude/prds/ .claude/epics/ cspell-words.txt
git add docs/adr/ADR-004-*.md # only if Step 4 ran
git commit -m "feat(c9): bootstrap docs/projects + CCPM store + ADR-004"
```
---
## Task 5: product-management plugin — conditional install (PG1 / PG2) — *severable*
**Files:** Modify `~/.claude/settings.json`. No repo file changed.
> Run this task **only if Task 1 Step 5 confirmed Claude Code installability**. Otherwise skip — append to `docs/projects/README.md` a line: "product-management deferred — not Claude-Code-installable as of 2026-05-17; PRD/roadmap covered by `writing-plans` + the registry/WISHLIST" — and treat Task 5 as deferred for Tasks 7-8.
- [ ] **Step 1: Install the plugin**
Run the exact strings confirmed in Task 1 Step 5, e.g.:
```
/plugin marketplace add <anthropic-marketplace>
/plugin install product-management@<anthropic-marketplace>
/reload-plugins
```
- [ ] **Step 2: Verify the plugin + NO lifecycle hooks (PG2)**
Read `~/.claude/settings.json``enabledPlugins` contains `product-management…: true`. Read the `hooks` block of `~/.claude/settings.json` AND project `.claude/settings.json` — both **unchanged** vs the Task 1 Step 2 snapshot. If product-management injected a `hooks` entry → stop and re-audit PG2.
- [ ] **Step 3: Smoke-test a command**
Invoke `/write-spec` (or `/roadmap-update`) on a trivial sample. Expected: it runs and produces a structured PRD/roadmap artifact. Functional smoke — no file output required.
- [ ] **Step 4: Confirm the economy/ruflo chain intact**
Submit a trivial prompt; the economy marker still appears, no hook errors. No repo files changed in Task 5 → no commit.
---
## Task 6: Smoke-test CCPM + close out
**Files:** none modified
- [ ] **Step 1: Smoke-test the CCPM skill**
Invoke the `ccpm` skill with a trivial PM intent (e.g. "draft a PRD for a sample feature"). Expected: the skill loads, `SKILL.md` routes intent, a PRD-shaped artifact is produced under `.claude/prds/` (or proposed). Do **not** sync to real GitHub issues — functional smoke only.
- [ ] **Step 2: Smoke-test a CCPM bash script on Windows (CP6)**
```bash
bash .claude/skills/ccpm/references/scripts/status.sh
```
Expected: the script runs under Git Bash / the Bash tool and prints a status summary (empty store is fine). If it fails on a Windows path / CRLF issue → record it; the script set is advisory, not commit-critical — note in `docs/projects/README.md` and continue.
- [ ] **Step 3: Confirm the hook chain is intact**
Economy marker still appears; the Stop verifier still runs; ruflo + CLAUDE.md-warn hooks fire. No plugin/skill leaked a `hooks` entry. No commit (no files changed).
---
## Task 7: Normative registry sync (CP / PG / CC / NUM1)
**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 + the live counter (NUM1)**
Read for exact insertion points and the **current** counter: `docs/Tooling_v8_3.md` Прил. Н §0 + the last `§4.x` subsection; `docs/Plugin_stack_rules_v1.md` R10.1; `docs/Pravila_raboty_Claude_v1_1.md` §13.2. Assign C9's number(s) sequentially from `counter + 1`**after** any A6/D3 entries already present. Record `#N` (CCPM) and, if Task 5 ran, `#N+1` (product-management).
- [ ] **Step 2: Add the Tooling Прил. Н project-management subsection(s)**
Edit `docs/Tooling_v8_3.md`: add a subsection for `#N CCPM` (and `#N+1 product-management` if installed), category **project-management** (off-phase). Per tool record: CCPM (GitHub `automazeio/ccpm` MIT, **vendored** standalone skill in `.claude/skills/ccpm/`, no hooks; GitHub-issue-backed traceability — CP3; PRD/epic store in `.claude/prds`/`.claude/epics` — CP5; boundary vs `writing-plans` — CP2); product-management (Anthropic-verified marketplace plugin; boundary vs CCPM PRD — PG3) — or, if deferred, a one-line "deferred — not Claude-Code-installable" note instead. Also note the GitHub MCP `projects` toolset reuse (no new slot — GitHub MCP is already #3). Add the bus-factor note (CC1). Bump §0 counter and the Прил. Н version header.
- [ ] **Step 3: Add PSR_v1 R10.1 rows**
Edit `docs/Plugin_stack_rules_v1.md`: add a CCPM row (and product-management row if installed) to R10.1, category **project-management** (off-phase) — explicitly *outside* the UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline (same treatment as `claude-md-management`, the A6 architecture-tooling, and the D3 audit-security categories). 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 **project-management** category note covering CCPM (+ product-management if installed), alongside the existing infrastructure / debug-runtime / 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 bump, §1 priority-chain row 2b count bump, new §3.3 project-management row(s) for `#N` (+`#N+1`). The plugin also writes the `docs/CHANGELOG_claude_md.md` entry and bumps §0 cross-ref versions (Tooling / PSR_v1 / Pravila). **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(c9): register CCPM (+product-management) project-management category (NUM1)"
```
---
## Task 8: Reflect C9 on the map — close the section
**Files:** Modify `docs/automation-graph.html`
- [ ] **Step 1: Read the structures to replicate**
In `docs/automation-graph.html` read, as templates: an existing vendored-skill node (`mermaid_skill` if A6 landed, else a project-skill node `sk_rls`) and a plugin node (`claude_md_mgmt`) across `NODES`, `NODE_DETAILS`/`nd(...)`, `NODE_SECTION`, and the "Паспорт узла" date fields. Record the current node/edge counts from the header.
- [ ] **Step 2: Add the C9 nodes**
Add to `NODES`, replicating the template shapes:
- `ccpm` — label `CCPM\n(skill)`, skills group.
- `product_mgmt` — label `product-\nmanagement`, plugins group. *(omit if Task 5 was deferred)*
Add matching `nd(...)` / `NODE_DETAILS` entries (Russian, per the file's convention), Паспорт `since: '2026-05-17'`:
- `ccpm` — "Vendored-скил: PRD→эпик→GitHub-issue→код с трассируемостью. Раздел C9 опирается также на reuse — GitHub MCP (issues/Projects v2) + Superpowers writing-plans + q-item-add."
- `product_mgmt` — "Плагин Anthropic: PRD/роадмап/метрики (product-strategy церемонии)."
- [ ] **Step 3: Map the new nodes to section C9**
In `NODE_SECTION` add (omit `product_mgmt` if Task 5 deferred):
```js
ccpm: 'C9', product_mgmt: 'C9',
```
`C9 «Управление проектами»` goes from 0 → 1-2 nodes — the section is no longer empty.
- [ ] **Step 4: Update header metrics + group-count comments**
Bump the node count in the map header/legend by 1-2. Update the `NODE_SECTION` group-count comments (plugins, skills).
- [ ] **Step 5: Smoke-test the map**
```bash
npx stylelint docs/automation-graph.html
```
Open `docs/automation-graph.html` (Playwright MCP or a local `http.server` — quirk 90: `file://` rejected). Expected: 0 JS console errors; the new node(s) render; clicking section `C9` highlights them.
- [ ] **Step 6: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): C9 nodes — closes section «Управление проектами»"
```
---
## Task 9: Final regression & branch finish
**Files:** none modified
- [ ] **Step 1: Full pre-commit chain**
```bash
npx lefthook run pre-commit
```
Expected: all jobs green — C9 adds **no** lefthook job (job count unchanged: 8, or 9 if A6's adr-judge landed).
- [ ] **Step 2: Confirm app code untouched — run the suites**
C9 changes no `app/` code → suites must match the Task 1 Step 3 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; the Stop verifier still runs; no plugin/skill leaked a `hooks` entry into either `settings.json`. Compare to the Task 1 Step 2 snapshot.
- [ ] **Step 4: Pre-push checks**
```bash
./bin/gitleaks.exe detect --source . --no-banner --redact
./bin/lychee.exe --config .lychee.toml "docs/**/*.md" "*.md"
```
Expected: gitleaks 0 leaks; lychee 0 broken (new `docs/projects/*.md` + `docs/adr/ADR-004-*.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/c9-project-management-tooling:main`.
---
## Self-Review
**1. Spec coverage (Variant А — reuse core + CCPM + conditional product-management).** Reuse — GitHub MCP `projects` toolset enabled (Task 2), Superpowers/`q-item-add`/ruflo documented in the convention (Task 4 Step 2). CCPM — vendored (Task 3), bootstrapped (Task 4), smoked (Task 6). product-management — conditional install (Task 5). Section closure: normative (Task 7), map (Task 8), regression/finish (Task 9). Conflict audit: CP1→T3.4, CP2→T4.2, CP3→T4.4 (ADR-004), CP4→T1.4+T3.2, CP5→T4.1+T7.2, CP6→T6.2, PG1→T1.5+T5(header), PG2→T5.2, PG3→T7.2, GH1→T2, GH2→no task (dropped), REU1→T8.2, CC1→T7.2, NUM1→T1.6+T7.1. No gaps.
**2. Placeholder scan.** `#N`/`#N+1` (Task 7), the exact `/plugin install` strings (Task 5), and the `.mcp.json` toolset config shape (Task 2) are **runtime-resolved by design** — the live Tooling counter, the product-management marketplace, and the actual `.mcp.json` shape are not knowable before Task 1 Step 5-6 / Task 2 Step 1, which carry concrete resolution criteria (same pattern as the A6/D3 plans). All file contents (`docs/projects/README.md`, ADR-004, the `.gitkeep` seeds) are shown in full. No "TBD" / "handle edge cases".
**3. Consistency.** Branch `feat/c9-project-management-tooling` consistent T1↔T9. Node ids `ccpm` / `product_mgmt` consistent T8 Steps 2-3. Category name **project-management** consistent T7 Steps 2-4. Paths consistent: `.claude/skills/ccpm/`, `.claude/prds/`, `.claude/epics/`, `docs/projects/`. The conditional `product_mgmt` is flagged uniformly (Task 5 header, Task 7 Step 2, Task 8 Steps 2-3). No lefthook job added — consistent with the "minimal-conflict" goal.
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-17-c9-project-management-tooling-integration.md`. Two execution options:
1. **Subagent-Driven (recommended for the doc-heavy tasks)** — fresh subagent per task, two-stage review. *Caveat:* Tasks 2, 5 (`/plugin …`, `/reload-plugins`), Task 3 Step 5 + Task 6 Step 1 (skill invocations) and Task 7 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** — install/config/docs-heavy with many interactive main-session steps (same as the A6/D3 plans).
**Sequencing reminder:** C9 cannot start until A6 **and** D3 are complete and pushed (locked — see "Sequencing" in the header). One open item before execution: execution method — **1** (Subagent-Driven) or **2** (Inline, recommended here).
@@ -12,6 +12,8 @@
---
> **NB (исправлено 17.05.2026, нормативка v2.5):** #4 Security Guidance в этом плане описан как «warn-only / does not block» (строки ~21, ~251, ~542) — **фактически неверно**: хук делает `sys.exit(2)` → это **блокирующий** PreToolUse-хук (одноразовый speed-bump per «файл+правило» за сессию, retry проходит). Актуальная характеристика — Tooling §4.15 / ADR-003 / CLAUDE.md §3.3 #40. План оставлен как исторический снимок, тело не переписано.
## Tool Identity (verified 2026-05-17 via WebFetch/WebSearch)
| # | Tool | Install mode | Source / License | Hooks? |
@@ -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,378 @@
# Sprint 6 — P3 Polish + Cleanup Tail 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:** Закрыть P3-эпики финального спринта portal-audit (`docs/superpowers/specs/2026-05-15-portal-audit-design.md` §3 Sprint 6) — a11y-доводка, гигиена констант, снятие устаревшего dev-workaround'а.
**Architecture:** 5 независимых XS-эпиков, все — frontend-only (Vue 3.5 SFC + TypeScript), 0 backend / 0 schema / 0 миграций. Каждый эпик правит обособленную группу файлов — пересечений между задачами нет, порядок T1→T5 произвольный, конфликты исключены.
**Tech Stack:** Vue 3.5 + Vuetify 3.12 + TypeScript, Vitest 4 (тесты), `import.meta.env.DEV` (DEV-гейт, статически вырезается Vite в prod-сборке).
**Scope-решение по 3 эпикам Sprint 6, НЕ входящим в этот план:**
- **F5** (`new_device_login` через session-fingerprint) — XL, требует инфраструктуры сессий-фингерпринтов; вне MVP, остаётся в реестре.
- **G8** (ImpersonationDialog two-person approval, CTO-15) — требует Б-1 (Yandex 360 SSO); блокирован внешним блокером.
- **I2** (dev-indices.json — gitignore-решение) — **отложен вместе с I1**. I1 (снос DevIndexBadge/DevIndexOverlay) заказчик отложил в Sprint 5D до заморозки UI; `dev-indices.json` — генерируемый манифест этой же временной фичи. Решать его git-судьбу до сноса фичи — churn на артефакте, который всё равно удалится. Отложен синхронно с I1.
**Эпики в этом плане (5):** A9, B6, F4, G9, I5.
---
## File Structure
| Файл | Задача | Ответственность |
|---|---|---|
| `app/resources/js/views/auth/LoginView.vue` | T1 (A9) | eye-toggle через `#append-inner` slot с accessible-name |
| `app/resources/js/views/auth/RegisterView.vue` | T1 (A9) | то же |
| `app/resources/js/views/auth/ResetPasswordView.vue` | T1 (A9) | то же (только первое поле пароля; поле «Повторите» eye-иконки не имеет) |
| `app/resources/js/layouts/AdminLayout.vue` | T2 (B6) | DEV-only баннер о застабленном auth-gate |
| `app/resources/js/constants/polling.ts` | T3 (F4) | **создаётся** — именованные интервалы polling |
| `app/resources/js/composables/usePolling.ts` | T3 (F4) | дефолт интервала из константы |
| `app/resources/js/layouts/AppLayout.vue` | T3 (F4) | call-site → константы |
| `app/resources/js/components/admin/ImpersonationBanner.vue` | T3 (F4) | call-site → константа |
| `app/resources/js/views/ReportsView.vue` | T3 (F4) | call-site → константа |
| `app/resources/js/views/admin/AdminSystemView.vue` | T4 (G9) | aria-label на edit-кнопки |
| `app/resources/js/views/ProjectsView.vue` | T5 (I5) | удаление устаревшего clearable-workaround'а из `<style>` |
| spec-файлы в `app/tests/Frontend/` | T1/T2/T4 | failing-тесты + сверка существующих |
---
## Task 1: A9 — accessible-name на eye-icon переключателях пароля
**Контекст:** В `LoginView`/`RegisterView`/`ResetPasswordView` поле пароля переключает видимость через Vuetify-проп `:append-inner-icon` + `@click:append-inner`. Иконка-переключатель кликабельна, но не имеет accessible-name и не доступна с клавиатуры → screen-reader пользователь не знает, что это кнопка. Фикс — заменить проп на слот `#append-inner` с `<v-icon>` в роли кнопки: `role="button"` + `tabindex` + `:aria-label` + keyboard-обработчики.
**Files:**
- Modify: `app/resources/js/views/auth/LoginView.vue:81-93`
- Modify: `app/resources/js/views/auth/RegisterView.vue:97-109`
- Modify: `app/resources/js/views/auth/ResetPasswordView.vue:107-119`
- Test: `app/tests/Frontend/LoginView.spec.ts`, `RegisterView.spec.ts`, `ResetPasswordView.spec.ts`
- [ ] **Step 1: Сверить существующие spec-файлы**
Прочитать 3 spec-файла. Найти тесты, взаимодействующие с переключателем (grep `append-inner`, `showPassword`, `eye`). Если тест триггерит `click:append-inner` — после рефактора это событие не возникнет (теперь клик по `<v-icon>` в слоте), такие тесты переписать на клик по иконке с aria-label. Запомнить mount-setup (плагины Vuetify/Pinia/router-стабы) для нового теста.
- [ ] **Step 2: Написать failing-тест (для каждой из 3 вью)**
В каждый spec добавить тест (mount-setup взять из существующих тестов файла):
```ts
it('переключатель видимости пароля имеет accessible-name и работает', async () => {
const wrapper = mount(LoginView, { /* global: из существующего setup файла */ });
const toggle = wrapper.find('[aria-label="Показать пароль"]');
expect(toggle.exists()).toBe(true);
expect(toggle.attributes('role')).toBe('button');
await toggle.trigger('click');
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
});
```
Для `RegisterView`/`ResetPasswordView` — аналогично, заменив компонент.
- [ ] **Step 3: Прогнать тесты — убедиться, что падают**
Run: `cd app && npm run test:vue -- LoginView RegisterView ResetPasswordView`
Expected: 3 новых теста FAIL (`aria-label="Показать пароль"` не найден — сейчас проп `append-inner-icon`).
- [ ] **Step 4: Реализовать слот в LoginView.vue**
Заменить блок `app/resources/js/views/auth/LoginView.vue:81-93` (поле пароля):
```vue
<v-text-field
v-model="password"
label="Пароль"
:type="showPassword ? 'text' : 'password'"
autocomplete="current-password"
placeholder="Минимум 8 символов"
variant="outlined"
density="comfortable"
required
:error-messages="errors.password"
>
<template #append-inner>
<v-icon
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
role="button"
tabindex="0"
@click="showPassword = !showPassword"
@keydown.enter.prevent="showPassword = !showPassword"
@keydown.space.prevent="showPassword = !showPassword"
/>
</template>
</v-text-field>
```
- [ ] **Step 5: Реализовать слот в RegisterView.vue**
Заменить блок `app/resources/js/views/auth/RegisterView.vue:97-109` тем же паттерном (поле пароля, `autocomplete="new-password"` — сохранить, `placeholder="Минимум 8 символов"` — сохранить). Снять строки `:append-inner-icon="..."` и `@click:append-inner="..."`, добавить `#append-inner`-слот как в Step 4.
- [ ] **Step 6: Реализовать слот в ResetPasswordView.vue**
Заменить блок `app/resources/js/views/auth/ResetPasswordView.vue:107-119` (первое поле — «Новый пароль») тем же паттерном. **Поле «Повторите пароль» (`:122-130`) не трогать** — у него нет eye-иконки, оно наследует `showPassword`.
- [ ] **Step 7: Прогнать тесты — убедиться, что зелёные**
Run: `cd app && npm run test:vue -- LoginView RegisterView ResetPasswordView`
Expected: PASS — новые 3 теста + все ранее существовавшие в этих файлах.
- [ ] **Step 8: Commit**
```bash
git add app/resources/js/views/auth/LoginView.vue app/resources/js/views/auth/RegisterView.vue app/resources/js/views/auth/ResetPasswordView.vue app/tests/Frontend/LoginView.spec.ts app/tests/Frontend/RegisterView.spec.ts app/tests/Frontend/ResetPasswordView.spec.ts
git commit -m "fix(a11y): accessible eye-toggle на полях пароля — Sprint 6 A9"
```
---
## Task 2: B6 — DEV-only баннер о застабленном auth-gate админки
**Контекст:** Sprint 3F (J2) поставил middleware `EnsureSaasAdmin` на `/api/admin/*` как стаб: в dev пропускает все запросы, в prod отдаёт 503. Комментарий в шапке `AdminLayout.vue:9-12` фиксирует, что полноценный auth-guard (`super_admin` role + 2FA через Yandex 360 SSO) ждёт Б-1. B6 — сделать этот auth-gap видимым в dev-UI баннером. Гейт — `import.meta.env.DEV` (Vite статически вырежет баннер в prod-сборке, паттерн I4 из Sprint 5D).
**Files:**
- Modify: `app/resources/js/layouts/AdminLayout.vue` (script + template)
- Test: `app/tests/Frontend/AdminLayout.spec.ts`
- [ ] **Step 1: Сверить AdminLayout.spec.ts**
Прочитать spec — запомнить mount-setup. Проверить наличие `vi.unstubAllEnvs()` в `afterEach` (если нет — добавить в Step 2, иначе stub `DEV` протечёт в другие тесты).
- [ ] **Step 2: Написать failing-тест**
В `AdminLayout.spec.ts` добавить (mount-setup — из существующих тестов):
```ts
it('B6: показывает DEV-баннер auth-gap в dev-режиме', () => {
const wrapper = mount(AdminLayout, { /* global: из существующего setup */ });
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(true);
});
it('B6: скрывает DEV-баннер в production-режиме', () => {
vi.stubEnv('DEV', false);
const wrapper = mount(AdminLayout, { /* global: из существующего setup */ });
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(false);
});
```
Убедиться, что в файле есть `afterEach(() => { vi.unstubAllEnvs(); });` (добавить, если отсутствует).
- [ ] **Step 3: Прогнать тест — убедиться, что падает**
Run: `cd app && npm run test:vue -- AdminLayout`
Expected: тест `показывает DEV-баннер` FAIL (`data-testid="dev-auth-gap-banner"` не найден).
- [ ] **Step 4: Добавить DEV-флаг в script**
В `app/resources/js/layouts/AdminLayout.vue` после `const auth = useAuthStore();` (`:39`) добавить:
```ts
/** DEV-режим: показываем баннер о застабленном auth-gate админки (B6). */
const isDevEnv = import.meta.env.DEV;
```
- [ ] **Step 5: Добавить баннер в template**
В `<v-main class="admin-main">` (`:133`) — перед `<ImpersonationBanner />` вставить:
```vue
<v-alert
v-if="isDevEnv"
type="warning"
variant="tonal"
density="compact"
class="ma-4"
data-testid="dev-auth-gap-banner"
>
DEV-режим: доступ к админке открыт без SSO-проверки — middleware
<code>EnsureSaasAdmin</code> в dev пропускает все запросы. В production
требуется вход через Yandex 360 + роль <code>super_admin</code> (Б-1);
неавторизованные запросы получают 503.
</v-alert>
```
- [ ] **Step 6: Прогнать тест — убедиться, что зелёные**
Run: `cd app && npm run test:vue -- AdminLayout`
Expected: PASS — оба новых теста + ранее существовавшие.
- [ ] **Step 7: Commit**
```bash
git add app/resources/js/layouts/AdminLayout.vue app/tests/Frontend/AdminLayout.spec.ts
git commit -m "feat(admin): DEV-only баннер о застабленном auth-gate — Sprint 6 B6"
```
---
## Task 3: F4 — вынести polling-интервалы в `constants/polling.ts`
**Контекст:** Дефолтный интервал `30_000` зашит в `usePolling.ts`, а call-site'ы `AppLayout`/`ImpersonationBanner`/`ReportsView` дублируют литералы `30_000`/`60_000`. F4 — собрать «магические» числа в один модуль. Чистый рефактор: поведение не меняется, защитная сетка — существующие тесты.
**Files:**
- Create: `app/resources/js/constants/polling.ts`
- Modify: `app/resources/js/composables/usePolling.ts:18,25`
- Modify: `app/resources/js/layouts/AppLayout.vue:17,60,61`
- Modify: `app/resources/js/components/admin/ImpersonationBanner.vue:16,40`
- Modify: `app/resources/js/views/ReportsView.vue:14,62`
- [ ] **Step 1: Создать `constants/polling.ts`**
```ts
/**
* Интервалы polling-обновления view-данных — единый источник «магических»
* чисел для usePolling. До приезда SSE/WebSocket в production это покрывает
* «real-time»-паттерн (см. composables/usePolling.ts).
*/
/** Базовый интервал авто-обновления (сделки, биллинг, инциденты, тенанты, отчёты). */
export const POLLING_INTERVAL_MS = 30_000;
/** Интервал для менее срочных счётчиков (напоминания в сайдбаре). */
export const POLLING_REMINDERS_INTERVAL_MS = 60_000;
```
- [ ] **Step 2: Подключить константу в usePolling.ts**
В `app/resources/js/composables/usePolling.ts`:
- Первой строкой добавить импорт: `import { POLLING_INTERVAL_MS } from '../constants/polling';` (после `import { onBeforeUnmount, onMounted } from 'vue';`).
- Строка `:18` doc-комментарий: `/** Период polling в миллисекундах. По умолчанию 30_000. */``/** Период polling в миллисекундах. По умолчанию POLLING_INTERVAL_MS (30 с). */`
- Строка `:25`: `const intervalMs = options.intervalMs ?? 30_000;``const intervalMs = options.intervalMs ?? POLLING_INTERVAL_MS;`
- [ ] **Step 3: Обновить call-site'ы**
`AppLayout.vue` — добавить к импортам (`:17`): `import { POLLING_INTERVAL_MS, POLLING_REMINDERS_INTERVAL_MS } from '../constants/polling';`
- `:60` `usePolling(loadNotifications, { intervalMs: 30_000, enabled: true });``{ intervalMs: POLLING_INTERVAL_MS, enabled: true }`
- `:61` `usePolling(loadReminderCounts, { intervalMs: 60_000, enabled: true });``{ intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true }`
`ImpersonationBanner.vue` — добавить импорт `import { POLLING_INTERVAL_MS } from '../../constants/polling';`
- `:40` `usePolling(load, { intervalMs: 30_000 });``{ intervalMs: POLLING_INTERVAL_MS }`
`ReportsView.vue` — добавить импорт `import { POLLING_INTERVAL_MS } from '../constants/polling';`
- `:62` `usePolling(loadJobs, { intervalMs: 30_000 });``{ intervalMs: POLLING_INTERVAL_MS }`
Call-site'ы на дефолте (`DealsView`/`KanbanView`/`AdminBillingView`/`AdminIncidentsView`/`AdminTenantsView`) — **не трогать**, они уже получают значение через дефолт `usePolling`.
- [ ] **Step 4: Type-check + тесты (рефактор — поведение без изменений)**
Run: `cd app && npm run type-check && npm run test:vue -- usePolling AppLayout ImpersonationBanner ReportsView`
Expected: vue-tsc 0 ошибок; все тесты PASS без изменений (интервалы численно те же — `30_000`/`60_000`).
- [ ] **Step 5: Commit**
```bash
git add app/resources/js/constants/polling.ts app/resources/js/composables/usePolling.ts app/resources/js/layouts/AppLayout.vue app/resources/js/components/admin/ImpersonationBanner.vue app/resources/js/views/ReportsView.vue
git commit -m "refactor(polling): вынести интервалы в constants/polling.ts — Sprint 6 F4"
```
---
## Task 4: G9 — aria-label на edit-кнопки AdminSystemView
**Контекст:** В списке `system_settings` каждая строка имеет кнопку «Изменить» (`AdminSystemView.vue:166-175`). У всех кнопок одинаковый видимый текст «Изменить» — screen-reader пользователь, проходя список, слышит «Изменить, Изменить, Изменить» без контекста, какая настройка. Фикс — `:aria-label` с ключом настройки.
**Files:**
- Modify: `app/resources/js/views/admin/AdminSystemView.vue:166-175`
- Test: `app/tests/Frontend/AdminSystemView.spec.ts`
- [ ] **Step 1: Написать failing-тест**
В `AdminSystemView.spec.ts` добавить (mount-setup — из существующих тестов файла; компонент стартует с mock-данными до `onMounted`-загрузки, либо использовать `defineExpose`d `settingsState`):
```ts
it('G9: edit-кнопки имеют aria-label с ключом настройки', () => {
const wrapper = mount(AdminSystemView, { /* global: из существующего setup */ });
const editBtns = wrapper.findAll('[data-testid^="edit-"]');
expect(editBtns.length).toBeGreaterThan(0);
for (const btn of editBtns) {
const label = btn.attributes('aria-label') ?? '';
expect(label).toMatch(/^Изменить настройку .+/);
}
});
```
- [ ] **Step 2: Прогнать тест — убедиться, что падает**
Run: `cd app && npm run test:vue -- AdminSystemView`
Expected: FAIL — `aria-label` отсутствует на кнопках.
- [ ] **Step 3: Добавить aria-label**
В `app/resources/js/views/admin/AdminSystemView.vue` в `<v-btn>` (`:166-175`) добавить строку `:aria-label` между `prepend-icon` и `:data-testid`:
```vue
<v-btn
variant="text"
size="small"
density="comfortable"
prepend-icon="mdi-pencil"
:aria-label="`Изменить настройку ${setting.key}`"
:data-testid="`edit-${setting.key}-btn`"
@click="openEdit(setting)"
>
Изменить
</v-btn>
```
- [ ] **Step 4: Прогнать тест — убедиться, что зелёный**
Run: `cd app && npm run test:vue -- AdminSystemView`
Expected: PASS — новый тест + ранее существовавшие.
- [ ] **Step 5: Commit**
```bash
git add app/resources/js/views/admin/AdminSystemView.vue app/tests/Frontend/AdminSystemView.spec.ts
git commit -m "fix(a11y): aria-label с ключом на edit-кнопках AdminSystem — Sprint 6 G9"
```
---
## Task 5: I5 — снять устаревший clearable-workaround из ProjectsView
**Контекст:** `ProjectsView.vue:170-196` содержит CSS-workaround: у `clearable` `v-text-field` иконка `mdi-close-circle` делалась прозрачной, а вместо неё `::after`-псевдоэлементом рисовался Unicode-глиф `✕` — потому что MDI-шрифт не был подключён (Диз-4). CTO-19 (миграция на Lucide) закрыта: `app/resources/js/plugins/vuetify.ts:164` маппит `'mdi-close-circle': XCircle` — clearable-иконка теперь рендерится нативным Lucide-SVG. Workaround мёртв → удалить.
**Files:**
- Modify: `app/resources/js/views/ProjectsView.vue` (удаление CSS-блока `:170-196`)
- [ ] **Step 1: Проверить премису (фальсифицировать перед удалением)**
Подтвердить, что `app/resources/js/plugins/vuetify.ts` содержит `'mdi-close-circle': XCircle` в Lucide IconSet-маппинге и `XCircle` импортирован из `lucide-vue-next`. Если маппинга нет — задача **BLOCKED**, эскалировать (workaround снимать нельзя без замены).
- [ ] **Step 2: Удалить CSS-блок workaround'а**
В `app/resources/js/views/ProjectsView.vue` удалить строки `:170-196` целиком — комментарий-заголовок `/* Workaround: MDI-шрифт... */` и 4 CSS-правила: `.projects-view :deep(.v-field__clearable)`, `.projects-view :deep(.v-field__clearable .v-icon)`, `.projects-view :deep(.v-field--dirty .v-field__clearable)::after`, `.projects-view :deep(.v-field--dirty .v-field__clearable:hover)::after`. Соседние блоки (`.projects-grid` выше, `.toolbar-check` ниже) не трогать.
- [ ] **Step 3: Type-check + сборка + существующие тесты**
Run: `cd app && npm run type-check && npm run test:vue -- ProjectsView`
Expected: vue-tsc 0; `ProjectsView.spec.ts` PASS без изменений (правка чисто CSS, JS-поведение не затронуто).
- [ ] **Step 4: Визуальный smoke (Playwright)**
Запустить dev-сервер, открыть `/projects`, ввести текст в поле поиска проектов (`clearable`). Подтвердить: иконка очистки (Lucide `XCircle`) **видима** справа в поле и клик по ней очищает значение. Сделать скриншот. Если иконка не рендерится — premise опровергнута, `git revert` Step 2 и эскалировать.
- [ ] **Step 5: Commit**
```bash
git add app/resources/js/views/ProjectsView.vue
git commit -m "chore(cleanup): снять устаревший MDI clearable-workaround (CTO-19 tail) — Sprint 6 I5"
```
---
## Финальная верификация (после всех 5 задач)
- [ ] **Полная регрессия Vitest**`cd app && npm run test:vue -- --maxWorkers=2` (full-suite без `--maxWorkers=2` OOM'ит в worktree — квирк 98). Ожидаемо: 0 fail; число passed ≥ baseline + новые тесты (T1 ×3, T2 ×2, T4 ×1).
- [ ] **vue-tsc**`cd app && npm run type-check` → 0 ошибок.
- [ ] **ESLint**`cd app && npm run lint:vue` → известная pre-existing ошибка `tests/Frontend/ImportView.spec.ts:4` (Sprint 4 долг, вне scope Sprint 6); 0 новых.
- [ ] **Vite build**`cd app && npm run build` → OK (подтверждает, что DEV-гейт B6 валиден для prod-сборки).
- [ ] **Pest** опционально — Sprint 6 не трогает PHP-файлы (0 backend-изменений), backend-регрессия структурно невозможна; полный прогон Pest — belt, не обязателен.
- [ ] **Финальный holistic code-review** всего диффа Sprint 6.
- [ ] **Pre-push:** gitleaks-full-history + lychee (lefthook не в PATH worktree — прогонять вручную).
## Self-Review (выполнено при написании плана)
- **Spec coverage:** Sprint 6 §3 спека = 8 эпиков. A9/B6/F4/G9/I5 — в плане (5 задач). F5/G8 — внешне блокированы (инфра/Б-1). I2 — отложен с I1 (решение заказчика Sprint 5D). Покрытие полное и обоснованное.
- **Placeholder-скан:** весь production-код приведён дословно (file:line); тест-код — с конкретными ассертами, mount-setup берётся из существующих spec-файлов (они прочитаны на Step 1 каждой задачи).
- **Type consistency:** `POLLING_INTERVAL_MS` / `POLLING_REMINDERS_INTERVAL_MS` — единые имена в T3 (создание + 4 call-site). `data-testid="dev-auth-gap-banner"` — единое имя в T2 (template + 2 теста). `isDevEnv` — единое имя в T2.
@@ -0,0 +1,167 @@
# A11 ML / AI Tooling Integration — Design
**Date:** 2026-05-17
**Topic:** Populate the empty `A11 «ML / AI-разработка»` map section.
**Deciders:** Дмитрий
**Method:** `superpowers:brainstorming` (economy 5%). Approach А approved 2026-05-17.
---
## Goal
Populate the empty `A11 «ML / AI-разработка»` section of `docs/automation-graph.html`
with a conflict-minimal ML/AI toolset — so A11 becomes a working playbook, the same
way A6 «Архитектура систем» was closed with adr-kit/mermaid/architecture-patterns and
C9 «Управление проектами» with the CCPM stack.
## Context
- **A11 is empty.** `SECTIONS` in `docs/automation-graph.html:1836` defines
`A11 = «ML / AI-разработка»` (bucket A «Технические и продуктовые»); `NODE_SECTION`
tags **zero** nodes to A11.
- **The map is a tooling map** — «карта dev-автоматики» (plugins / skills / MCP /
hooks Claude uses). A11 must therefore be populated with **ML/AI *development
tools*** — not with product features.
- **The project has no ML/AI code.** Лидерра is a Laravel 13 + Vue 3 multi-tenant
CRM. The only scoring artifact, `calc_lead_score` (`db/schema.sql`), is a
deterministic SQL function — not a model. No dataset, no feature store, no ML/AI
item in `Открытые_вопросы` or the plan backlog.
- **The machine is runtime-minimal by policy.** Native Windows, no Docker (no
nested virt on the OpenStack VPS), pg_partman replaced with a cron command. Every
new runtime passes an explicit weighing. Python is **not** in the stack (PHP +
Node only).
## Scope decision
A11 covers **both** subcategories — classical ML **and** LLM integration (user
choice 2026-05-17). The **Python runtime is deferred**: the executable classical-ML
part (Jupyter MCP) is a severable/conditional task, not a now-install. Rationale:
no ML model to train, no dataset, a new runtime on a deliberately-minimal machine,
and the Jupyter MCP is experimental (Notebook 6.x only). The classical-ML
*knowledge* layer (Data Scientist skill, zero runtime) **is** installed now, so the
subcategory is not empty — only its executor is gated.
This mirrors the project idiom: C9 made product-management a conditional task; A6
made the lefthook-enforcement task severable.
---
## Architecture — the 6-position toolset (Approach А)
Two subcategories. Three positions are **reuse** (already installed — A11 documents
the coverage, REU1), two are **new + light**, one is **new + deferred**.
| # | Tool | Subcategory | Install mode | Status |
|---|---|---|---|---|
| 1 | **claude-api skill** | LLM integration | already available (plugin skill) | reuse |
| 2 | **context7 MCP** | both (library docs) | already installed (MCP, map node `context7` in E7) | reuse |
| 3 | **Sentry MCP** (Tooling #34) | LLM observability | already installed (MCP, map node `mcp_sentry` in A7) | reuse — AI/LLM monitoring activates on the Sentry deployment (blocked on Б-1) |
| 4 | **promptfoo** | LLM integration | new — Node CLI, project `package.json` devDependency, invoked `npx promptfoo`; MIT (OpenAI-owned, OSS) | new, light |
| 5 | **Data Scientist skill** | classical ML | new — vendored standalone skill into `.claude/skills/data-scientist/` (no plugin, no marketplace, no hooks — the A6 mermaid pattern) | new, light |
| 6 | **Jupyter MCP** (`datalayer/jupyter-mcp-server`) | classical ML | new — MCP server, Python-gated | **severable / conditional** — installed only when a concrete ML model is scoped |
**Reuse layer (positions 1-3).** claude-api skill = building AI features on the
Anthropic SDK (lead qualification, call-summary, email drafts) with prompt caching.
context7 MCP = up-to-date docs for AI/ML libraries and SDKs. Sentry MCP = debugging
AI features in production via Sentry's AI/LLM monitoring (read-only, pending the
Sentry instance — Б-1).
**New layer (positions 4-5).** promptfoo = a test suite for LLM prompts/agents:
assertions, regression catching, LLM-graded eval, red-teaming. Node-based — **needs
no new runtime**. Data Scientist skill = a knowledge-only skill: business
objective → ML task, algorithm selection (Linear Regression … XGBoost), feature
engineering, experiment-tracking guidance, A/B analysis.
**Deferred (position 6).** Jupyter MCP = executable notebooks for real model
training. Gated on a Python ML environment + a concrete ML model to train.
A11 is **fully covered** by positions 1-5 from day one; position 6 is a reserved,
registered-but-not-installed slot.
---
## Conflict audit
Pattern follows the K1K8 / AK1CC1 / CP1NUM1 audits used for claude-mem and the
A6 / C9 / D3 plans. Full audit with locked resolutions is produced in the
implementation plan; the known conflicts:
| # | Tool | Sev | Conflict | Resolution direction |
|---|---|---|---|---|
| ML1 | promptfoo | 🟡 | A real eval run needs an Anthropic API key + costs money (paid LLM calls). | Key via env (PowerShell User scope, like Sentry) — never committed (gitleaks). promptfoo runs **manually / CI-only** — never in a hook, never in pre-commit, never auto. No economy-chain impact. |
| ML2 | promptfoo | 🟢 | promptfoo's red-team module overlaps the D3 audit-security tools. | None — promptfoo red-team tests *LLM prompts* for jailbreak/injection; Trail of Bits / Semgrep (D3 #39, #25) are SAST of *code*. Different objects. Boundary documented. |
| ML3 | Data Scientist skill | 🟡 | Vendored `.claude/skills/data-scientist/**/*.md` (third-party English files) is caught by the cspell + markdownlint pre-commit jobs. | Add `.claude/skills/data-scientist/**` to `cspell.json` `ignorePaths` and `.markdownlintignore` (the A6 MK1 pattern). The project's own skills stay linted. |
| ML4 | reuse layer | 🟢 | Re-tagging the existing `context7` (E7) / `mcp_sentry` (A7) nodes to A11 would empty their current sections — `NODE_SECTION` is 1-node→1-section. | Reuse nodes **stay** in their sections; A11 gets its **own** new nodes. The reuse coverage is documented in `docs/ml/README.md`. Same as A6/D3/C9 REU1. |
| ML5 | all | 🟢 | A11 is non-UI tooling. | New off-phase category **ml-ai-tooling**, outside the PSR_v1 UI-pool (no R6.0/R6.1 stack-filter, no R14 pipeline) — same treatment as architecture-tooling (A6) and audit-security (D3). |
| ML6 | Jupyter MCP | 🟡 | Experimental (Notebook 6.x only), would be the 8th `.mcp.json` server, on a Python-less machine. | **Deferred** — severable/conditional task. When triggered: `alpha-substrate-spike-first` — a spike before integration. Registered in the registry/map as a reserved slot now; `.mcp.json` is untouched in the core scope. |
| ML7 | all | 🟡 | Bus-factor — Data Scientist skill is community; Jupyter MCP is community + experimental. | Data Scientist skill is **vendored** → immune to upstream loss (the A6 mermaid pattern). promptfoo is MIT, OpenAI-owned, stable. Jupyter MCP — version-pinned at install time (deferred). Noted in the Tooling entry. |
| ML8 | claude-api skill | 🟡 | claude-api is an available skill but may not be a formalized Tooling-registry position → using it unregistered is a PSR_v1 R0.2/R10 gap. | The plan's pre-flight checks whether claude-api (and its backing plugin) is already in `~/.claude/settings.json` `enabledPlugins` + the Tooling registry; if absent, register it as part of the A11 sync. |
| ML9 | promptfoo | 🟢 | promptfoo install mode — global npm vs project devDep vs vendored binary. | Project `package.json` **devDependency** — version-pinned via `package-lock.json`, no global state, invoked `npx promptfoo`. (Not a `bin/*.exe` — promptfoo is an npm package, not a single binary.) |
---
## File structure
| File | Created / Modified | Responsibility |
|---|---|---|
| `docs/ml/` | Create dir | A11 home — the ML/AI playbook |
| `docs/ml/README.md` | Create | The ML/AI convention: tool boundaries (claude-api skill = *build* AI features / promptfoo = *test* prompts / Data Scientist skill = classical-ML *workflow* / Jupyter MCP = *execute* — when scoped); the reuse-layer map |
| `docs/ml/promptfoo-example/promptfooconfig.yaml` | Create | One seed eval config — a worked example (e.g. a lead-qualification prompt with assertions) |
| `.claude/skills/data-scientist/` | Create (vendored) | The Data Scientist skill — `SKILL.md` + `references/` |
| `docs/adr/ADR-005-ml-ai-tooling.md` | Create (conditional — adr-kit/A6 landed) | Seed ADR documenting the A11 tooling decision + the Python-defer |
| `package.json` | Modify | `promptfoo` devDependency |
| `cspell.json` | Modify | `ignorePaths` += `.claude/skills/data-scientist/**` (ML3) |
| `.markdownlintignore` | Modify | += `.claude/skills/data-scientist/` (ML3) |
| `cspell-words.txt` | Modify (conditional) | New ML/AI vocabulary |
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new ml-ai-tooling subsection(s) + §0 counter bump |
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — new ml-ai-tooling rows |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — ml-ai-tooling category note |
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, new §3.3 ml-ai-tooling rows |
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
| `docs/automation-graph.html` | Modify | 3 new A11 nodes (`claude_api`, `promptfoo`, `data_scientist`) → `NODE_SECTION` A11; header metrics |
| `.mcp.json` | **NOT modified in core scope** | Jupyter MCP deferred — `.mcp.json` touched only by the conditional task |
**Map nodes.** A11 gets **3 new nodes**`claude_api`, `promptfoo`, `data_scientist`.
The reuse nodes `context7` (E7) and `mcp_sentry` (A7) stay put (ML4). A `jupyter_mcp`
node is added only when the conditional task runs (same as C9's conditional
`product_mgmt` node).
---
## Severable scope
**Core A11** = the reuse layer + promptfoo + Data Scientist skill + normative sync +
map closure. This already populates and closes the section.
**Conditional / severable** = the Jupyter MCP install (position 6) — a separate task,
gated on (a) a Python ML environment decision and (b) a concrete ML model to build.
Registered now as a reserved slot; installed later.
A11 adds **no lefthook job** and **no `.mcp.json` change** in the core scope — fewer
conflicts by design (the C9 shape).
---
## Out of scope
- Building actual AI features into the CRM product (AI lead qualification, call
summaries) — that is product work, tracked separately; A11 only provides the
*tooling*.
- Training an actual ML model / standing up a Python environment — the Jupyter MCP
conditional task, triggered later.
- Any LLM-eval gate in the commit pipeline (ML1 — promptfoo stays manual/CI-only).
## Open items resolved by the implementation plan
- **NUM** — A11's Tooling-registry numbers are runtime-resolved: the plan reads the
**live** `docs/Tooling_v8_3.md` Прил. Н §0 counter (after A6 #36-38, D3 #39-40, and
whatever C9 added) and assigns A11's numbers sequentially.
- **claude-api registration** (ML8) — the plan's pre-flight checks whether claude-api
is already a formalized Tooling position; registers it if not.
- **ADR-005 conditional** — written only if adr-kit (A6) has landed in `origin/main`.
- **Sequencing** — A11 forks off `origin/main` after the A6 / D3 / C9 epics land
(they touch the same shared files: the map, 4 normative docs, the Tooling counter).
The branch created for this spec is a scratch branch; execution re-forks.
- **Exact tool identity** (promptfoo repo/version, Data Scientist skill source repo,
Jupyter MCP repo) — verified via WebFetch/WebSearch in the plan's Tool Identity
section (the A6/C9/D3 pattern).
@@ -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).
+20 -2
View File
@@ -31,10 +31,11 @@ pre-commit:
# 2. markdownlint — стиль Markdown с авто-fix
- name: markdownlint
glob: "*.md"
# Вендоренный сторонний скил .claude/skills/mermaid/ — не линтуем (его .md
# Вендоренные сторонние скилы .claude/skills/{mermaid,ccpm}/ — не линтуем (их .md
# не наши; markdownlint-cli2 игнорирует .markdownlintignore при явных путях).
exclude:
- ".claude/skills/mermaid/**"
- ".claude/skills/ccpm/**"
run: npx markdownlint-cli2 --fix {staged_files}
stage_fixed: true
fail_text: |
@@ -46,9 +47,10 @@ pre-commit:
# (useGitignore:true) игнорирует worktree-коммиты под gitignored .claude/worktrees/.
- name: cspell
glob: "*.md"
# Вендоренный сторонний скил .claude/skills/mermaid/ — не проверяем орфографию.
# Вендоренные сторонние скилы .claude/skills/{mermaid,ccpm}/ — не проверяем орфографию.
exclude:
- ".claude/skills/mermaid/**"
- ".claude/skills/ccpm/**"
run: npx cspell --no-progress --no-summary --no-gitignore {staged_files}
fail_text: |
cspell нашёл слова, отсутствующие в словаре.
@@ -130,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