Compare commits

...

16 Commits

Author SHA1 Message Date
Дмитрий f29b1b7e50 docs(5d): план Sprint 5D — cleanup mock fallback (I3/I4) 2026-05-17 07:13:51 +03:00
Дмитрий 0d2c64aa8c test(deals): T1 fixup — DealsListIntegration/KanbanRedesign под I3 (убран MOCK_DEALS-fallback) 2026-05-17 07:13:51 +03:00
Дмитрий 256acf8781 fix(admin): I4 — devPlainCode-баннер за import.meta.env.DEV 2026-05-17 07:13:51 +03:00
Дмитрий a0b1cfdcae fix(admin): I3 — убрать mock fallback в System/Tenants 2026-05-17 07:13:50 +03:00
Дмитрий 2b04bbd4f8 fix(admin): I3 — убрать mockAdmin fallback в Billing/Incidents 2026-05-17 07:13:50 +03:00
Дмитрий 888b7563cd fix(deals): I3 — убрать mock-fallback в NewDealDialog/DealDetailDrawer 2026-05-17 07:13:50 +03:00
Дмитрий 3a58090db9 test(deals): T1 review-fixup — I3-тесты через onMounted-путь 2026-05-17 07:13:50 +03:00
Дмитрий 23579dd9be fix(deals): I3 — убрать MOCK_DEALS fallback в DealsView/KanbanView 2026-05-17 07:13:50 +03:00
Дмитрий 7c12b7419c feat(map): D3 nodes — closes section «Аудит и управление рисками» 2026-05-17 06:15:30 +03:00
Дмитрий f05bb4dde2 docs(audit): CLAUDE.md v2.4 — register #39-40 audit-security (D3) 2026-05-17 06:15:30 +03:00
Дмитрий 703f101c11 docs(audit): register Trail of Bits + Security Guidance #39-40 (D3 audit-security) 2026-05-17 06:15:30 +03:00
Дмитрий 30eec9fb7d feat(audit): distill 14-phase portal audit into audit-portal skill (D3) 2026-05-17 06:15:29 +03:00
Дмитрий 83a831c46d docs(audit): toolchain attack-surface procedure + audit/ home (D3 #5) 2026-05-17 06:15:29 +03:00
Дмитрий b72780c54e feat(adr): ADR-003 — D3 audit & risk-management tooling decision 2026-05-17 06:15:29 +03:00
Дмитрий 8c9a91be1c feat(audit): customize /security-review with project FP-filter (D3 #2) 2026-05-17 06:15:29 +03:00
Дмитрий f892c94feb docs(plan): D3 audit & risk-management tooling integration plan 2026-05-17 06:15:29 +03:00
38 changed files with 2044 additions and 192 deletions
+239
View File
@@ -0,0 +1,239 @@
---
allowed-tools: Bash(git diff:*), Bash(git status:*), Bash(git log:*), Bash(git show:*), Bash(git remote show:*), Read, Glob, Grep, LS, Task
description: Complete a security review of the pending changes on the current branch
---
You are a senior security engineer conducting a focused security review of the changes on this branch.
GIT STATUS:
```
!`git status`
```
FILES MODIFIED:
```
!`git diff --name-only origin/HEAD...`
```
COMMITS:
```
!`git log --no-decorate origin/HEAD...`
```
DIFF CONTENT:
```
!`git diff --merge-base origin/HEAD`
```
Review the complete diff above. This contains all code changes in the PR.
OBJECTIVE:
Perform a security-focused code review to identify HIGH-CONFIDENCE security vulnerabilities that could have real exploitation potential. This is not a general code review - focus ONLY on security implications newly added by this PR. Do not comment on existing security concerns.
CRITICAL INSTRUCTIONS:
1. MINIMIZE FALSE POSITIVES: Only flag issues where you're >80% confident of actual exploitability
2. AVOID NOISE: Skip theoretical issues, style concerns, or low-impact findings
3. FOCUS ON IMPACT: Prioritize vulnerabilities that could lead to unauthorized access, data breaches, or system compromise
4. EXCLUSIONS: Do NOT report the following issue types:
- Denial of Service (DOS) vulnerabilities, even if they allow service disruption
- Secrets or sensitive data stored on disk (these are handled by other processes)
- Rate limiting or resource exhaustion issues
SECURITY CATEGORIES TO EXAMINE:
**Input Validation Vulnerabilities:**
- SQL injection via unsanitized user input
- Command injection in system calls or subprocesses
- XXE injection in XML parsing
- Template injection in templating engines
- NoSQL injection in database queries
- Path traversal in file operations
**Authentication & Authorization Issues:**
- Authentication bypass logic
- Privilege escalation paths
- Session management flaws
- JWT token vulnerabilities
- Authorization logic bypasses
**Crypto & Secrets Management:**
- Hardcoded API keys, passwords, or tokens
- Weak cryptographic algorithms or implementations
- Improper key storage or management
- Cryptographic randomness issues
- Certificate validation bypasses
**Injection & Code Execution:**
- Remote code execution via deseralization
- Pickle injection in Python
- YAML deserialization vulnerabilities
- Eval injection in dynamic code execution
- XSS vulnerabilities in web applications (reflected, stored, DOM-based)
**Data Exposure:**
- Sensitive data logging or storage
- PII handling violations
- API endpoint data leakage
- Debug information exposure
Additional notes:
- Even if something is only exploitable from the local network, it can still be a HIGH severity issue
ANALYSIS METHODOLOGY:
Phase 1 - Repository Context Research (Use file search tools):
- Identify existing security frameworks and libraries in use
- Look for established secure coding patterns in the codebase
- Examine existing sanitization and validation patterns
- Understand the project's security model and threat model
Phase 2 - Comparative Analysis:
- Compare new code changes against existing security patterns
- Identify deviations from established secure practices
- Look for inconsistent security implementations
- Flag code that introduces new attack surfaces
Phase 3 - Vulnerability Assessment:
- Examine each modified file for security implications
- Trace data flow from user inputs to sensitive operations
- Look for privilege boundaries being crossed unsafely
- Identify injection points and unsafe deserialization
REQUIRED OUTPUT FORMAT:
You MUST output your findings in markdown. The markdown output should contain the file, line number, severity, category (e.g. `sql_injection` or `xss`), description, exploit scenario, and fix recommendation.
For example:
# Vuln 1: XSS: `foo.py:42`
- Severity: High
- Description: User input from `username` parameter is directly interpolated into HTML without escaping, allowing reflected XSS attacks
- Exploit Scenario: Attacker crafts URL like `/bar?q=<script>alert(document.cookie)</script>` to execute JavaScript in victim's browser, enabling session hijacking or data theft
- Recommendation: Use Flask's escape() function or Jinja2 templates with auto-escaping enabled for all user inputs rendered in HTML
SEVERITY GUIDELINES:
- **HIGH**: Directly exploitable vulnerabilities leading to RCE, data breach, or authentication bypass
- **MEDIUM**: Vulnerabilities requiring specific conditions but with significant impact
- **LOW**: Defense-in-depth issues or lower-impact vulnerabilities
CONFIDENCE SCORING:
- 0.9-1.0: Certain exploit path identified, tested if possible
- 0.8-0.9: Clear vulnerability pattern with known exploitation methods
- 0.7-0.8: Suspicious pattern requiring specific conditions to exploit
- Below 0.7: Don't report (too speculative)
FINAL REMINDER:
Focus on HIGH and MEDIUM findings only. Better to miss some theoretical issues than flood the report with false positives. Each finding should be something a security engineer would confidently raise in a PR review.
FALSE POSITIVE FILTERING:
> You do not need to run commands to reproduce the vulnerability, just read the code to determine if it is a real vulnerability. Do not use the bash tool or write to any files.
>
> HARD EXCLUSIONS - Automatically exclude findings matching these patterns:
>
> 1. Denial of Service (DOS) vulnerabilities or resource exhaustion attacks.
> 2. Secrets or credentials stored on disk if they are otherwise secured.
> 3. Rate limiting concerns or service overload scenarios.
> 4. Memory consumption or CPU exhaustion issues.
> 5. Lack of input validation on non-security-critical fields without proven security impact.
> 6. Input sanitization concerns for GitHub Action workflows unless they are clearly triggerable via untrusted input.
> 7. A lack of hardening measures. Code is not expected to implement all security best practices, only flag concrete vulnerabilities.
> 8. Race conditions or timing attacks that are theoretical rather than practical issues. Only report a race condition if it is concretely problematic.
> 9. Vulnerabilities related to outdated third-party libraries. These are managed separately and should not be reported here.
> 10. Memory safety issues such as buffer overflows or use-after-free-vulnerabilities are impossible in rust. Do not report memory safety issues in rust or any other memory safe languages.
> 11. Files that are only unit tests or only used as part of running tests.
> 12. Log spoofing concerns. Outputting un-sanitized user input to logs is not a vulnerability.
> 13. SSRF vulnerabilities that only control the path. SSRF is only a concern if it can control the host or protocol.
> 14. Including user-controlled content in AI system prompts is not a vulnerability.
> 15. Regex injection. Injecting untrusted content into a regex is not a vulnerability.
> 16. Regex DOS concerns.
> 17. Insecure documentation. Do not report any findings in documentation files such as markdown files.
> 18. A lack of audit logs is not a vulnerability.
>
> PRECEDENTS -
>
> 1. Logging high value secrets in plaintext is a vulnerability. Logging URLs is assumed to be safe.
> 2. UUIDs can be assumed to be unguessable and do not need to be validated.
> 3. Environment variables and CLI flags are trusted values. Attackers are generally not able to modify them in a secure environment. Any attack that relies on controlling an environment variable is invalid.
> 4. Resource management issues such as memory or file descriptor leaks are not valid.
> 5. Subtle or low impact web vulnerabilities such as tabnabbing, XS-Leaks, prototype pollution, and open redirects should not be reported unless they are extremely high confidence.
> 6. React and Angular are generally secure against XSS. These frameworks do not need to sanitize or escape user input unless it is using dangerouslySetInnerHTML, bypassSecurityTrustHtml, or similar methods. Do not report XSS vulnerabilities in React or Angular components or tsx files unless they are using unsafe methods.
> 7. Most vulnerabilities in github action workflows are not exploitable in practice. Before validating a github action workflow vulnerability ensure it is concrete and has a very specific attack path.
> 8. A lack of permission checking or authentication in client-side JS/TS code is not a vulnerability. Client-side code is not trusted and does not need to implement these checks, they are handled on the server-side. The same applies to all flows that send untrusted data to the backend, the backend is responsible for validating and sanitizing all inputs.
> 9. Only include MEDIUM findings if they are obvious and concrete issues.
> 10. Most vulnerabilities in ipython notebooks (*.ipynb files) are not exploitable in practice. Before validating a notebook vulnerability ensure it is concrete and has a very specific attack path where untrusted input can trigger the vulnerability.
> 11. Logging non-PII data is not a vulnerability even if the data may be sensitive. Only report logging vulnerabilities if they expose sensitive information such as secrets, passwords, or personally identifiable information (PII).
> 12. Command injection vulnerabilities in shell scripts are generally not exploitable in practice since shell scripts generally do not run with untrusted user input. Only report command injection vulnerabilities in shell scripts if they are concrete and have a very specific attack path for untrusted input.
>
> SIGNAL QUALITY CRITERIA - For remaining findings, assess:
>
> 1. Is there a concrete, exploitable vulnerability with a clear attack path?
> 2. Does this represent a real security risk vs theoretical best practice?
> 3. Are there specific code locations and reproduction steps?
> 4. Would this finding be actionable for a security team?
>
> For each finding, assign a confidence score from 1-10:
>
> - 1-3: Low confidence, likely false positive or noise
> - 4-6: Medium confidence, needs investigation
> - 7-10: High confidence, likely true vulnerability
PROJECT FALSE-POSITIVE GUIDANCE (Лидерра):
> This section is project-specific (Лидерра CRM — Laravel 13 + Vue 3 multi-tenant SaaS).
> Apply it alongside the HARD EXCLUSIONS and PRECEDENTS above when filtering findings.
>
> EXPECTED — treat as NOT a finding:
>
> 1. Missing application-layer tenant checks where the table has PostgreSQL Row-Level
> Security. Tenant isolation is enforced at the DB layer (`SET LOCAL
> app.current_tenant_id` via the `SetTenantContext` middleware; 5 DB roles; 39 RLS
> policies — see `docs/adr/ADR-002-multitenancy-postgres-rls.md`). DO still flag
> queued jobs or code running as the `crm_supplier_worker` role (which is BYPASSRLS)
> that read/write tenant-scoped tables WITHOUT an explicit `where('tenant_id', ...)`.
> 2. The `tools/*.mjs` economy / ruflo hook scripts using `child_process.spawnSync`
> or `process.env`. These are intentional local CLI hooks, not user-facing or
> network-reachable code paths.
> 3. Hardcoded-secret findings already covered by gitleaks (pre-commit + pre-push).
> Do NOT re-report unless a NEW hardcoded credential is introduced by this diff.
> 4. Test factories / seeders (`*Factory.php`, `*Seeder.php`) using `Faker` or
> predictable values — test-only, per HARD EXCLUSION 11.
>
> PRIORITISE for this project:
>
> 1. HMAC / signature verification gaps on inbound webhooks (supplier lead intake).
> 2. Signed-URL generation and validation (report file downloads, e.g. the reports
> `/api/reports/jobs/{id}/file` endpoint).
> 3. `auth:sanctum` + tenant middleware coverage on `/api/*` routes — a missing guard
> is a cross-tenant data-leak vector (cf. the J1 / CTO-18 fix).
> 4. Personal-data (ПДн) handling under 152-ФЗ — exposure of subject data in
> responses, logs, or exports.
> 5. Mass-assignment on Eloquent models (`$fillable` / `$guarded` gaps) reachable
> from a request.
START ANALYSIS:
Begin your analysis now. Do this in 3 steps:
1. Use a sub-task to identify vulnerabilities. Use the repository exploration tools to understand the codebase context, then analyze the PR changes for security implications. In the prompt for this sub-task, include all of the above.
2. Then for each vulnerability identified by the above sub-task, create a new sub-task to filter out false-positives. Launch these sub-tasks as parallel sub-tasks. In the prompt for these sub-tasks, include everything in the "FALSE POSITIVE FILTERING" instructions (including the "PROJECT FALSE-POSITIVE GUIDANCE (Лидерра)" block).
3. Filter out any vulnerabilities where the sub-task reported a confidence less than 8.
Your final reply must contain the markdown report and nothing else.
+69
View File
@@ -0,0 +1,69 @@
---
name: audit-portal
description: Запускать при полном аудите портала Лидерры — периодической сквозной проверке качества и безопасности (статанализ, тесты, схема БД, security, UI-smoke, a11y, coverage, bundle, pre-prod). Триггеры — «провести аудит портала», «полный аудит», «portal audit», подготовка к pre-prod или релизу.
---
# Audit Portal — 14-фазный аудит портала
## Когда использовать
Периодический сквозной аудит всего портала Лидерры. Прецеденты — аудиты #1
(2026-05-12), #2 (2026-05-13), #3 (2026-05-14). НЕ для точечной проверки одного
файла или фичи — для этого прямой инструмент (`/regression`, `/security-review`,
Pest).
## 14 фаз
Фазы последовательны; фаза 2 — 4 параллельных субагента. Каждая фаза пишет
находки в `docs/superpowers/audits/<дата>-portal-full-audit-findings.md`, секция
`## Phase N`. BLOCKED-пункты — в `<дата>-portal-full-audit-blocked.md`.
| # | Фаза | Инструмент |
|---|---|---|
| 1 | Pre-flight — ветка/HEAD, delta-коммиты, `composer`/`npm install`, skeleton-файлы аудита | git, composer, npm |
| 2 | Статанализ — ×4 параллельных субагента | A backend: pint+stan+composer audit · B frontend: eslint+vue-tsc+prettier+knip · C docs: markdownlint+cspell+lychee · D SQL: squawk+pgFormatter |
| 3 | Тестовые своды | Pest --parallel + sequential, Vitest, Histoire build, Vite build |
| 4 | Целостность схемы — root tables, RLS-политики (инвариант 39), 5 user-функций поимённо, orphan-FK, header drift | Laravel Boost MCP (`database-query`) |
| 5 | Security — перечислить CI-workflows ПЕРВЫМ, gitleaks delta + полная история + no-git | gitleaks, `ls .github/workflows/`, `/security-review` + Trail of Bits плагины |
| 6 | UI-smoke — обход 24 маршрутов: рендер, 0 JS-ошибок, иконки | Playwright MCP |
| 7 | Кросс-док целостность — версии нормативки, schema-маркер, `routes/web.php`, `.mcp.json` | Read, Grep, Select-String |
| 8 | A11y — Pa11y на 4 guest-URL + axe-core на auth-views | Pa11y, axe-core через Playwright |
| 9 | Coverage — Vitest --coverage, сверка с baseline | `@vitest/coverage-v8` |
| 10 | Bundle — Vite build + анализ чанков vs baseline | `parse-bundle-analyze.mjs` |
| 11 | Pre-prod + TODO-sweep — schedule, RUNBOOK, `.env.example` diff, Sentry SDK, TODO/FIXME | `artisan schedule:list`, `composer show`, Select-String |
| 12 | Категоризация + fix-loop — rollup P0P3; P0/P1 чинятся через TDD (failing test → fix → `test:parallel`) | Pest, Vitest, git |
| 13 | Финальная регрессия | Pest --parallel, Vitest, Vite build, gitleaks, lychee |
| 14 | Report + memory + push | Write, `git push` (pre-push: gitleaks-full-history + lychee) |
Нумерация — Audit #3 (самый свежий). Audit #2 использовал Phase 0–14 с иным
порядком a11y / coverage / bundle; при расхождении — версия выше.
## Рубрика серьёзности
- **P0** — блокирует production / data corruption / security incident.
- **P1** — нарушение функциональности / failing test / type error / a11y violation.
- **P2** — warning / style / dead code / stale doc.
- **P3** — cosmetic / nice-to-have.
Fix-eligibility: `[FIX-NOW]` — P0/P1, ≤30 мин, atomic-коммит на находку;
`[FIX-DEFER]` — P2/P3, только запись в findings, без кода; `[BLOCKED]` — нужно
явное «закрываем» от заказчика → `blocked.md` (категории Q.HARD / Q.PRODUCT /
Q.DEFER / Q.INFO).
## Методология
- Каждая фаза завершается `git commit` находок. После каждых 3 коммитов —
self-review §8 (метрики схемы, версии нормативки).
- Регрессия в фазе 12/13 → `systematic-debugging` (≥3 гипотезы) → rollback или
forward-fix → перепрогон фазы.
- Hard-stop'ы decision-tree: не менять `db/schema.sql`, не закрывать
Б-/CTO-/Ю-/Диз-/DO-/OPEN- без явного «закрываем», не ставить пакеты, не
править корневой `CLAUDE.md` напрямую, не делать force-push.
- BLOCKED-находка, требующая решения владельца → в реестр `Открытые_вопросы`
через скил `q-item-add`.
## Не использовать когда
- Нужна одна проверка (тест / lint / security одного диффа) — прямой инструмент
или `/regression quick`.
- Точечный security-review диффа ветки — `/security-review` напрямую.
+2 -1
View File
@@ -185,5 +185,6 @@ ruflo-mcp-stderr.log
.claude/agents/templates/
.claude/agents/testing/
.claude/agents/v3/
.claude/commands/
.claude/commands/*
!.claude/commands/security-review.md
.claude/helpers/
+13 -7
View File
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
* 3-step state-machine:
* 1. 'reason' — textarea для основания (≥30 chars) → POST /api/admin/impersonation/init.
* 2. 'verify' — показ email клиента + ввод 6-значного кода → /api/admin/impersonation/verify.
* На dev показывается _dev_plain_code (на prod исчезнет после MailService).
* На dev показывается _dev_plain_code (за import.meta.env.DEV; на prod баннер не рендерится).
* 3. 'active' — chip «Сессия активна», кнопка «Завершить» → /api/admin/impersonation/end.
*
* NB: на MVP saas-admin auth не реализован, requested_by передаётся параметром
@@ -49,6 +49,10 @@ const expiresAt = ref<string | null>(null);
const devPlainCode = ref<string | null>(null);
const usedAtIso = ref<string | null>(null);
// I4: явный frontend DEV-gate. import.meta.env.DEV статически заменяется Vite —
// в prod-сборке = false, баннер с плейн-кодом tree-shake'ится.
const isDevEnv = import.meta.env.DEV;
const reasonLength = computed(() => reason.value.trim().length);
const reasonRemaining = computed(() => Math.max(0, 30 - reasonLength.value));
const reasonValid = computed(() => reasonLength.value >= 30);
@@ -216,7 +220,7 @@ function close() {
data-testid="code-input"
/>
<v-alert
v-if="devPlainCode"
v-if="isDevEnv && devPlainCode"
type="success"
variant="tonal"
density="compact"
@@ -20,7 +20,7 @@
*/
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import type { MockDeal } from '../../composables/mockDeals';
import { type DealEvent, MOCK_EVENTS } from '../../composables/mockDealEvents';
import { type DealEvent } from '../../composables/mockDealEvents';
import { mapApiDealEvent } from '../../composables/dealsApiMapper';
import * as dealsApi from '../../api/deals';
import * as remindersApi from '../../api/reminders';
@@ -56,8 +56,8 @@ function formatCost(cost: number): string {
}
// Activity timeline: при наличии tenant_id делаем GET /api/deals/{id} и
// показываем реальные events. На fail / без tenant_id — fallback на MOCK_EVENTS.
const events = ref<DealEvent[]>([...MOCK_EVENTS]);
// показываем реальные events. На fail / без tenant_id — events пуст + eventsFetchError.
const events = ref<DealEvent[]>([]);
const eventsLoading = ref(false);
const eventsFetchError = ref(false);
@@ -116,7 +116,7 @@ function formatReminderTime(iso: string | null): string {
async function loadEvents() {
if (!props.deal || !props.tenantId) {
events.value = [...MOCK_EVENTS];
events.value = [];
commentDraft.value = '';
return;
}
@@ -128,7 +128,7 @@ async function loadEvents() {
commentDraft.value = res.deal.comment ?? '';
} catch {
eventsFetchError.value = true;
events.value = [...MOCK_EVENTS];
events.value = [];
commentDraft.value = '';
} finally {
eventsLoading.value = false;
@@ -14,15 +14,16 @@ import * as dealsApi from '../../api/deals';
import { extractErrorMessage } from '../../api/client';
import { ref, watch } from 'vue';
import { LEAD_STATUSES } from '../../composables/leadStatuses';
import { MOCK_MANAGERS, MOCK_PROJECTS, type MockDeal, type MockManager } from '../../composables/mockDeals';
import { type MockDeal, type MockManager } from '../../composables/mockDeals';
/**
* Управление source для проектов и менеджеров. Если tenantId передан, загружаем
* с backend через GET /api/projects, /api/managers. На fail (network)
* fallback на MOCK_PROJECTS/MOCK_MANAGERS (UI всё равно работоспособен).
* Списки проектов и менеджеров грузятся с backend через GET /api/projects,
* /api/managers при открытии диалога (если передан tenantId). На fail
* списки пустые + degradation-alert (lookupsFailed), создание блокируется
* до повторной успешной загрузки.
*/
const projectOptions = ref<string[]>([...MOCK_PROJECTS]);
const managerOptions = ref<MockManager[]>([...MOCK_MANAGERS]);
const projectOptions = ref<string[]>([]);
const managerOptions = ref<MockManager[]>([]);
// Map name → backend-id, нужен только когда manager_id отправляется на backend.
const managerIdByName = ref<Map<string, number>>(new Map());
@@ -77,7 +78,7 @@ const errors = ref<Record<string, string>>({});
const submitError = ref<string | null>(null);
const busy = ref(false);
// Audit C6: loadLookups упал → показываем degradation-alert (списки = mock).
// Audit C6: loadLookups упал → показываем degradation-alert (списки пусты).
const lookupsFailed = ref(false);
// Регенерируем ID на каждое создание для local-mode. На API — backend SERIAL.
@@ -175,7 +176,7 @@ async function submit() {
}
}
defineExpose({ lookupsFailed });
defineExpose({ lookupsFailed, projectOptions, managerOptions });
function close() {
dialogOpen.value = false;
@@ -205,8 +206,7 @@ function close() {
class="mb-3"
data-testid="lookups-error-alert"
>
Не удалось загрузить списки проектов и менеджеров показаны примерные значения. Проверьте выбор
перед сохранением.
Не удалось загрузить списки проектов и менеджеров попробуйте позже.
</v-alert>
<v-row dense>
<v-col cols="12" md="6">
+7 -8
View File
@@ -3,7 +3,7 @@
* Список сделок — центральный экран CRM. Используется менеджерами ежедневно.
*
* Источник дизайна: liderra_v8_handoff/concepts/v8_deals.html.
* MVP: page-head + chiprow со срезами + поиск + v-data-table с mock'ами.
* MVP: page-head + chiprow со срезами + поиск + v-data-table (данные из API).
*
* Не входит в этот коммит (отдельные TODO):
* - Drawer с деталями сделки при клике на строку (правая панель v-navigation-drawer right).
@@ -15,7 +15,7 @@
*/
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { DEALS_TABS, MOCK_DEALS, type MockDeal } from '../composables/mockDeals';
import { DEALS_TABS, type MockDeal } from '../composables/mockDeals';
import { mapApiDeal } from '../composables/dealsApiMapper';
import { usePolling } from '../composables/usePolling';
// Sprint 2 Phase B / O-perf-06: lazy-imports для тяжёлых компонентов, гейтящихся
@@ -107,10 +107,9 @@ const selected = ref<number[]>([]);
const filterProjects = ref<string[]>([]);
const filterManagers = ref<string[]>([]);
// Локальная reactive-копия. При наличии auth.user.tenant_id — fetch через
// API (см. onMounted ниже); на network/500 — fallback на MOCK_DEALS чтобы UI
// оставался работоспособным (полезно для dev и Vitest jsdom-среды).
const dealsState = reactive<MockDeal[]>(MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
// Локальная reactive-копия. Наполняется через API (loadDeals/onMounted);
// до загрузки и при ошибке пуст.
const dealsState = reactive<MockDeal[]>([]);
const loading = ref(false);
const fetchError = ref(false);
@@ -130,7 +129,7 @@ async function loadDeals() {
const mapped = deals.map((d) => mapApiDeal(d));
dealsState.splice(0, dealsState.length, ...mapped);
} catch {
fetchError.value = true; // оставляем MOCK_DEALS как fallback
fetchError.value = true; // state остаётся пустым — показываем error-alert
} finally {
loading.value = false;
}
@@ -720,7 +719,7 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai
class="mt-3"
data-testid="fetch-error-alert"
>
Backend недоступен показаны mock-данные.
Не удалось загрузить сделки. Попробуйте обновить.
</v-alert>
<!-- Task 15: wrapper с .ld-hover-lift + .ld-stagger-row для quiet-luxury motion
+6 -7
View File
@@ -20,7 +20,7 @@
*/
import { computed, onMounted, reactive, ref } from 'vue';
import { LEAD_STATUSES } from '../composables/leadStatuses';
import { MOCK_DEALS, type MockDeal } from '../composables/mockDeals';
import { type MockDeal } from '../composables/mockDeals';
import { mapApiDeal } from '../composables/dealsApiMapper';
import { usePolling } from '../composables/usePolling';
import * as dealsApi from '../api/deals';
@@ -44,11 +44,10 @@ interface DraggableChangeEvent {
}
// Reactive Record<slug, MockDeal[]> отдельный массив для каждой колонки
// (vuedraggable v-model требует независимые arrays). Deep-clone объектов
// сделок чтобы не мутировать MOCK_DEALS const при DnD.
// (vuedraggable v-model требует независимые arrays).
const dealsByStatus = reactive<Record<string, MockDeal[]>>(
LEAD_STATUSES.reduce<Record<string, MockDeal[]>>((acc, s) => {
acc[s.slug] = MOCK_DEALS.filter((d) => d.statusSlug === s.slug).map((d) => ({ ...d }));
acc[s.slug] = [];
return acc;
}, {}),
);
@@ -108,7 +107,7 @@ function onOpenDeal(id: number) {
}
}
const totalDeals = ref(MOCK_DEALS.length);
const totalDeals = ref(0);
const fetchError = ref(false);
const newDealOpen = ref(false);
@@ -139,7 +138,7 @@ async function loadDeals() {
}
totalDeals.value = total;
} catch {
fetchError.value = true; // оставляем MOCK_DEALS как fallback
fetchError.value = true; // state остаётся пустым показываем error-alert
}
}
@@ -205,7 +204,7 @@ defineExpose({
class="mt-3"
data-testid="fetch-error-alert"
>
Backend недоступен показаны mock-данные.
Не удалось загрузить сделки. Попробуйте обновить.
</v-alert>
<div class="kanban-board mt-4" tabindex="0" role="region" aria-label="Канбан-доска воронки продаж">
@@ -5,10 +5,8 @@
* Сводный биллинг по всем тенантам: выручка, MRR, retention, refunds.
* Источник данных: aggregate balance_transactions / invoices / tariff_subscriptions.
*
* MVP только display-вьюха с mock-данными. Backend `/api/admin/billing/*`
* подключается отдельным коммитом.
* Данные грузятся с backend GET /api/admin/billing.
*/
import { ADMIN_BILLING_SUMMARY as MOCK_SUMMARY, ADMIN_BILLING_TENANTS } from '../../composables/mockAdmin';
import { computed, onMounted, reactive, ref } from 'vue';
import { usePolling } from '../../composables/usePolling';
import * as adminApi from '../../api/admin';
@@ -17,11 +15,6 @@ import { extractErrorMessage } from '../../api/client';
const search = ref('');
/**
* Reactive-копия initial = MOCK для UI без backend'а; replace на API на mount.
* View работает в обоих режимах: row может быть из mock (узкие enum-types)
* или из API (открытые string-типы).
*/
type BillingRow = {
id: number;
name: string;
@@ -34,25 +27,13 @@ type BillingRow = {
status: string;
};
const rowsState = reactive<BillingRow[]>(
ADMIN_BILLING_TENANTS.map((r) => ({
id: r.id,
name: r.name,
inn: r.inn,
tariff: r.tariff,
balance_rub: r.balance_rub,
monthly_topups_rub: r.monthly_topups_rub,
monthly_charges_rub: r.monthly_charges_rub,
mrr_rub: r.mrr_rub,
status: r.status,
})),
);
const rowsState = reactive<BillingRow[]>([]);
const summary = reactive({
total_mrr_rub: MOCK_SUMMARY.total_mrr_rub,
monthly_revenue_rub: MOCK_SUMMARY.monthly_revenue_rub,
overdue_count: MOCK_SUMMARY.overdue_count,
refunds_count_30d: MOCK_SUMMARY.refunds_count_30d,
total_mrr_rub: 0,
monthly_revenue_rub: 0,
overdue_count: 0,
refunds_count_30d: 0,
});
const loading = ref(false);
@@ -255,7 +236,7 @@ function tariffLabel(t: string): string {
class="mb-4"
data-testid="fetch-error-alert"
>
Backend недоступен показаны mock-данные.
Не удалось загрузить биллинг. Попробуйте обновить.
</v-alert>
<!-- Stats row -->
@@ -6,10 +6,8 @@
* Категории: PDN-breach, service_outage, security, billing, data_loss.
* При PDN-breach обязательное уведомление РКН за 24 ч (152-ФЗ).
*
* MVP display + фильтр по статусу/severity. Backend `/api/admin/incidents`
* подключается отдельным коммитом.
* Display + фильтр по статусу/severity. Данные с backend GET /api/admin/incidents.
*/
import { ADMIN_INCIDENTS } from '../../composables/mockAdmin';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { usePolling } from '../../composables/usePolling';
@@ -73,32 +71,12 @@ function categoryLabel(c: string): string {
return categoryMap[c] ?? c;
}
// Reactive initial = MOCK; replace на API на mount.
const rowsState = reactive<IncidentRow[]>(
ADMIN_INCIDENTS.map((r) => ({
id: r.id,
incident_id: r.incident_id,
title: r.title,
severity: r.severity,
category: r.category,
status: r.status,
detected_at: r.detected_at,
affected_tenants: r.affected_tenants,
rkn_notified: r.rkn_notified,
rkn_deadline_at: r.rkn_deadline_at,
})),
);
// Reactive наполняется через loadIncidents (API).
const rowsState = reactive<IncidentRow[]>([]);
const stats = reactive({ open: 0, investigating: 0, rkn_pending: 0 });
const loading = ref(false);
const fetchError = ref(false);
// Initial stats из mock (UI consistency без backend'а).
stats.open = rowsState.filter((r) => r.status === 'open').length;
stats.investigating = rowsState.filter((r) => r.status === 'investigating').length;
stats.rkn_pending = rowsState.filter(
(r) => (r.category === 'pdn_breach' || r.category === 'data_breach') && !r.rkn_notified,
).length;
async function loadIncidents() {
loading.value = true;
fetchError.value = false;
@@ -176,7 +154,7 @@ function formatDate(iso: string): string {
class="mb-4"
data-testid="fetch-error-alert"
>
Backend недоступен показаны mock-данные.
Не удалось загрузить инциденты. Попробуйте обновить.
</v-alert>
<v-row class="mb-4" data-testid="incidents-stats">
@@ -5,10 +5,8 @@
* Глобальные настройки SaaS-уровня (system_settings по schema v8.7 §10):
* лимиты квот, тарифные планы, фичефлаги, fallback supplier_id.
*
* MVP display + read-only edit-режим. Backend `/api/admin/system-settings`
* + edit-flow подключаются отдельным коммитом.
* Display + edit-режим. Данные с backend GET /api/admin/system-settings.
*/
import { ADMIN_SYSTEM_SETTINGS } from '../../composables/mockAdmin';
import type { AdminSystemSetting } from '../../composables/mockAdmin';
import * as adminApi from '../../api/admin';
import type { SystemSetting as ApiSystemSetting } from '../../api/admin';
@@ -21,13 +19,10 @@ const loading = ref(false);
const fetchError = ref<string | null>(null);
/**
* Settings-state. Инициируется mock-данными (fallback если backend недоступен),
* на mount replace через `adminApi.listSystemSettings()`.
*
* Type-narrowing: AdminSystemSetting (mock) vs ApiSystemSetting различаются
* только origin (mock vs БД), shape совместим оба `{key, value, type, ...}`.
* Settings-state. Наполняется на mount через `adminApi.listSystemSettings()`.
* До загрузки и при ошибке пустой; ошибка показывается через fetchError-banner.
*/
const settingsState = reactive<AdminSystemSetting[]>([...ADMIN_SYSTEM_SETTINGS]);
const settingsState = reactive<AdminSystemSetting[]>([]);
async function loadSettings() {
loading.value = true;
@@ -37,8 +32,8 @@ async function loadSettings() {
// Replace всё содержимое сохранив reactive-ref.
settingsState.splice(0, settingsState.length, ...(fromApi as unknown as AdminSystemSetting[]));
} catch (err) {
// На fail оставляем mock (не очищаем UI). Показываем error-banner.
fetchError.value = extractErrorMessage(err, 'Не удалось загрузить настройки с сервера. Показаны mock-данные.');
// На fail settingsState пустой, показываем error-banner.
fetchError.value = extractErrorMessage(err, 'Не удалось загрузить настройки с сервера. Попробуйте обновить.');
} finally {
loading.value = false;
}
@@ -15,7 +15,7 @@
*/
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { MOCK_STATS, MOCK_TENANTS, type AdminTenant, type TenantStatus } from '../../composables/mockTenants';
import { type AdminTenant, type TenantStatus } from '../../composables/mockTenants';
import { mapApiAdminTenant } from '../../composables/adminTenantsMapper';
import { usePolling } from '../../composables/usePolling';
import * as adminApi from '../../api/admin';
@@ -29,8 +29,8 @@ import TenantsTable from '../../components/admin/tenants/TenantsTable.vue';
const router = useRouter();
const tenantsState = reactive<AdminTenant[]>(MOCK_TENANTS.map((t) => ({ ...t })));
const stats = reactive({ ...MOCK_STATS });
const tenantsState = reactive<AdminTenant[]>([]);
const stats = reactive({ total: 0, active: 0, trial: 0, overdue: 0, monthlyRevenueRub: 0 });
const loading = ref(false);
const fetchError = ref(false);
@@ -123,7 +123,7 @@ const filteredTenants = computed<AdminTenant[]>(() => {
class="mt-3"
data-testid="fetch-error-alert"
>
Backend недоступен показаны mock-данные.
Не удалось загрузить тенантов. Попробуйте обновить.
</v-alert>
<TenantsFilters
+43 -3
View File
@@ -1,8 +1,46 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import AdminBillingView from '../../resources/js/views/admin/AdminBillingView.vue';
import { ADMIN_BILLING_TENANTS, ADMIN_BILLING_SUMMARY } from '../../resources/js/composables/mockAdmin';
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
return {
...orig,
listAdminBilling: vi.fn(),
};
});
const adminApi = await import('../../resources/js/api/admin');
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(adminApi.listAdminBilling).mockResolvedValue({
tenants: ADMIN_BILLING_TENANTS.map((r) => ({
id: r.id,
subdomain: `tenant${r.id}`,
organization_name: r.name,
contact_email: `t${r.id}@test.io`,
status: r.status === 'overdue' ? 'active' : r.status,
balance_rub: String(r.balance_rub),
tariff_id: 1,
tariff_name: { start: 'Старт', basic: 'Базовый', pro: 'Команда', enterprise: 'Enterprise' }[r.tariff] ?? r.tariff,
mrr_rub: String(r.mrr_rub),
monthly_topups_rub: String(r.monthly_topups_rub),
monthly_charges_rub: String(r.monthly_charges_rub),
last_payment_at: r.last_payment_at,
chargeback_unrecovered_rub: r.status === 'overdue' ? '1.00' : '0.00',
})),
summary: {
total_mrr_rub: String(ADMIN_BILLING_SUMMARY.total_mrr_rub),
monthly_revenue_rub: String(ADMIN_BILLING_SUMMARY.monthly_revenue_rub),
overdue_count: ADMIN_BILLING_SUMMARY.overdue_count,
refunds_count_30d: ADMIN_BILLING_SUMMARY.refunds_count_30d,
},
});
});
const mountView = async () => {
const router = createRouter({
@@ -11,9 +49,11 @@ const mountView = async () => {
});
await router.push('/admin/billing');
await router.isReady();
return mount(AdminBillingView, {
const wrapper = mount(AdminBillingView, {
global: { plugins: [createVuetify(), router] },
});
await flushPromises();
return wrapper;
};
describe('AdminBillingView.vue', () => {
@@ -93,7 +93,7 @@ describe('AdminBillingView ↔ GET /api/admin/billing integration', () => {
expect(vm.summary.refunds_count_30d).toBe(3);
});
it('reject → fetchError=true + alert виден + MOCK fallback остаётся', async () => {
it('reject → fetchError=true + alert виден + rowsState пустой', async () => {
vi.mocked(adminApi.listAdminBilling).mockRejectedValueOnce(new Error('500'));
const wrapper = mountView();
@@ -101,7 +101,7 @@ describe('AdminBillingView ↔ GET /api/admin/billing integration', () => {
const vm = wrapper.vm as unknown as { fetchError: boolean; rowsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.rowsState.length).toBeGreaterThan(0);
expect(vm.rowsState.length).toBe(0);
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
+50 -4
View File
@@ -1,8 +1,52 @@
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import AdminIncidentsView from '../../resources/js/views/admin/AdminIncidentsView.vue';
import { ADMIN_INCIDENTS } from '../../resources/js/composables/mockAdmin';
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
return {
...orig,
listAdminIncidents: vi.fn(),
};
});
const adminApi = await import('../../resources/js/api/admin');
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(adminApi.listAdminIncidents).mockResolvedValue({
incidents: ADMIN_INCIDENTS.map((r) => ({
id: r.id,
incident_id: r.incident_id,
type: r.category as string,
severity: r.severity,
summary: r.title,
started_at: r.detected_at,
detected_at: r.detected_at,
resolved_at: null,
status: (r.status === 'closed' ? 'resolved' : r.status) as 'open' | 'investigating' | 'resolved',
affected_tenants_count: r.affected_tenants,
affected_users_count: null,
rkn_notified: r.rkn_notified,
rkn_notified_at: null,
rkn_deadline_at: r.rkn_deadline_at,
})),
total: ADMIN_INCIDENTS.length,
limit: 100,
offset: 0,
summary: {
open: ADMIN_INCIDENTS.filter((r) => r.status === 'open').length,
investigating: ADMIN_INCIDENTS.filter((r) => r.status === 'investigating').length,
rkn_pending: ADMIN_INCIDENTS.filter(
(r) => ['pdn_breach', 'data_breach'].includes(r.category) && !r.rkn_notified,
).length,
total_unresolved: ADMIN_INCIDENTS.filter((r) => r.status !== 'resolved' && r.status !== 'closed').length,
},
});
});
const mountView = async () => {
const router = createRouter({
@@ -14,7 +58,9 @@ const mountView = async () => {
});
await router.push('/admin/incidents');
await router.isReady();
return { wrapper: mount(AdminIncidentsView, { global: { plugins: [createVuetify(), router] } }), router };
const wrapper = mount(AdminIncidentsView, { global: { plugins: [createVuetify(), router] } });
await flushPromises();
return { wrapper, router };
};
describe('AdminIncidentsView.vue', () => {
@@ -57,7 +103,7 @@ describe('AdminIncidentsView.vue', () => {
it('клик по строке инцидента вызывает router.push на admin-incident-detail', async () => {
const { wrapper, router } = await mountView();
const pushSpy = vi.spyOn(router, 'push');
// get first row — mock data has id from ADMIN_INCIDENTS[0]
// get first row — populated via API mock
const vm = wrapper.vm as unknown as { rowsState: Array<{ id: number }> };
const firstId = vm.rowsState[0].id;
const row = wrapper.find(`[data-testid="incident-row-${firstId}"]`);
@@ -97,7 +97,7 @@ describe('AdminIncidentsView ↔ GET /api/admin/incidents integration', () => {
expect(vm.stats.rkn_pending).toBe(1);
});
it('reject → fetchError=true + alert виден + MOCK fallback', async () => {
it('reject → fetchError=true + alert виден + rowsState пустой', async () => {
vi.mocked(adminApi.listAdminIncidents).mockRejectedValueOnce(new Error('500'));
const wrapper = mountView();
@@ -105,7 +105,7 @@ describe('AdminIncidentsView ↔ GET /api/admin/incidents integration', () => {
const vm = wrapper.vm as unknown as { fetchError: boolean; rowsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.rowsState.length).toBeGreaterThan(0);
expect(vm.rowsState.length).toBe(0);
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
+6 -4
View File
@@ -26,12 +26,14 @@ const mountView = async () => {
});
await router.push('/admin/system');
await router.isReady();
return mount(AdminSystemView, {
const wrapper = mount(AdminSystemView, {
global: {
plugins: [createVuetify(), router],
stubs: { SystemSettingEditDialog: true },
},
});
await flushPromises();
return wrapper;
};
describe('AdminSystemView.vue', () => {
@@ -120,15 +122,15 @@ describe('AdminSystemView.vue', () => {
expect(adminApi.listSystemSettings).toHaveBeenCalledTimes(1);
});
it('при сетевой ошибке показывает warning-banner + сохраняет mock-данные', async () => {
it('при сетевой ошибке показывает warning-banner + settingsState пустой', async () => {
vi.mocked(adminApi.listSystemSettings).mockRejectedValueOnce(new Error('Network down'));
const wrapper = await mountView();
await flushPromises();
const banner = wrapper.find('[data-testid="fetch-error-alert"]');
expect(banner.exists()).toBe(true);
// Mock-настройки остались (fallback)
// Пустой при ошибке — без mock-fallback
const rows = wrapper.findAll('[data-testid="setting-row"]');
expect(rows.length).toBe(7);
expect(rows.length).toBe(0);
});
it('onSettingUpdated обновляет value и updated_at в settingsState', async () => {
+91 -34
View File
@@ -1,12 +1,33 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import AdminTenantsView from '../../resources/js/views/admin/AdminTenantsView.vue';
import { MOCK_STATS, MOCK_TENANTS } from '../../resources/js/composables/mockTenants';
import { MOCK_STATS, MOCK_TENANTS, type AdminTenant } from '../../resources/js/composables/mockTenants';
// Мокаем api/admin: listAdminTenants возвращает пустой ответ —
// smoke-тесты затем seed'ят tenantsState/stats напрямую через vm (defineExpose).
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
return {
...orig,
listAdminTenants: vi.fn().mockResolvedValue({
tenants: [],
total: 0,
limit: 100,
offset: 0,
stats: { total: 0, active: 0, trial: 0, overdue: 0 },
}),
};
});
beforeEach(() => {
vi.clearAllMocks();
});
describe('AdminTenantsView.vue', () => {
const factory = () => {
/** Монтирует view, ждёт mount-цикл, затем seed'ит state фикстурами. */
const factory = async () => {
// useRouter() в AdminTenantsView требует router-context в тестах.
const router = createRouter({
history: createMemoryHistory(),
@@ -15,22 +36,34 @@ describe('AdminTenantsView.vue', () => {
{ path: '/admin/tenants/:code', name: 'admin-tenant-detail', component: { template: '<div />' } },
],
});
return mount(AdminTenantsView, {
await router.push('/admin/tenants');
await router.isReady();
const wrapper = mount(AdminTenantsView, {
global: {
plugins: [createVuetify(), router],
// ImpersonationDialog stubим — внутри использует api/admin axios.
stubs: { ImpersonationDialog: true },
},
});
await flushPromises();
// Seed state напрямую через defineExpose — имитирует успешную загрузку с теми же фикстурами.
const vm = wrapper.vm as unknown as {
tenantsState: AdminTenant[];
stats: typeof MOCK_STATS;
};
vm.tenantsState.splice(0, vm.tenantsState.length, ...MOCK_TENANTS.map((t) => ({ ...t })));
Object.assign(vm.stats, MOCK_STATS);
await wrapper.vm.$nextTick();
return wrapper;
};
it('монтируется и содержит заголовок «Тенанты»', () => {
const wrapper = factory();
it('монтируется и содержит заголовок «Тенанты»', async () => {
const wrapper = await factory();
expect(wrapper.find('h1').text()).toBe('Тенанты');
});
it('показывает 5 stats: всего/активны/trial/просрочка/выручка', () => {
const wrapper = factory();
it('показывает 5 stats: всего/активны/trial/просрочка/выручка', async () => {
const wrapper = await factory();
const text = wrapper.text();
expect(text).toContain(`${MOCK_STATS.total}`); // 142
expect(text).toContain('всего');
@@ -45,22 +78,22 @@ describe('AdminTenantsView.vue', () => {
expect(text).toMatch(/1\s+248\s+600\s*₽/);
});
it('таблица содержит 7 колонок (Тенант/Статус/Тариф/Баланс/Желаем×факт/MRR/Активность)', () => {
const wrapper = factory();
it('таблица содержит 7 колонок (Тенант/Статус/Тариф/Баланс/Желаем×факт/MRR/Активность)', async () => {
const wrapper = await factory();
const headers = wrapper.findAll('thead th').map((h) => h.text());
['Тенант', 'Статус', 'Тариф', 'Баланс', 'Желаем×факт', 'MRR', 'Активность'].forEach((label) => {
expect(headers.some((h) => h.includes(label))).toBe(true);
});
});
it('рендерит все 7 mock-tenants', () => {
const wrapper = factory();
it('рендерит все 7 mock-tenants', async () => {
const wrapper = await factory();
const rows = wrapper.findAll('tbody tr');
expect(rows.length).toBe(MOCK_TENANTS.length);
});
it('первая строка — Окна Москва ООО + ИНН + Активен + Команда', () => {
const wrapper = factory();
it('первая строка — Окна Москва ООО + ИНН + Активен + Команда', async () => {
const wrapper = await factory();
const text = wrapper.text();
expect(text).toContain('Окна Москва ООО');
expect(text).toContain('ИНН 7724444444');
@@ -68,37 +101,37 @@ describe('AdminTenantsView.vue', () => {
expect(text).toContain('Команда');
});
it('overdue-тенант (Двери Премиум) показывает «Просрочка 3 дня» + отрицательный баланс', () => {
const wrapper = factory();
it('overdue-тенант (Двери Премиум) показывает «Просрочка 3 дня» + отрицательный баланс', async () => {
const wrapper = await factory();
const text = wrapper.text();
expect(text).toContain('Двери Премиум');
expect(text).toContain('Просрочка 3 дня');
expect(text).toMatch(/1\s+200/); // -1200 без 0 ₽
});
it('trial-тенант (Ремонт под ключ) показывает «Trial · 4 дня» + MRR=—', () => {
const wrapper = factory();
it('trial-тенант (Ремонт под ключ) показывает «Trial · 4 дня» + MRR=—', async () => {
const wrapper = await factory();
const text = wrapper.text();
expect(text).toContain('Ремонт под ключ');
expect(text).toContain('Trial · 4 дня');
});
it('suspended-тенант (Оконные системы РФ) показывает «Приостановлен»', () => {
const wrapper = factory();
it('suspended-тенант (Оконные системы РФ) показывает «Приостановлен»', async () => {
const wrapper = await factory();
const text = wrapper.text();
expect(text).toContain('Оконные системы РФ');
expect(text).toContain('Приостановлен');
});
it('содержит search-input с placeholder «ИНН, юр. лицо, email админа…»', () => {
const wrapper = factory();
it('содержит search-input с placeholder «ИНН, юр. лицо, email админа…»', async () => {
const wrapper = await factory();
const input = wrapper.find('input[type="text"]');
expect(input.exists()).toBe(true);
expect(input.attributes('placeholder')).toContain('ИНН');
});
it('фильтр по search оставляет только matching-tenants', async () => {
const wrapper = factory();
const wrapper = await factory();
const input = wrapper.find('input[type="text"]');
await input.setValue('Натяжные');
await wrapper.vm.$nextTick();
@@ -107,15 +140,15 @@ describe('AdminTenantsView.vue', () => {
expect(rows[0].text()).toContain('Натяжные потолки СПб');
});
it('содержит Экспорт-кнопку и фильтры Статус/Тариф', () => {
const wrapper = factory();
it('содержит Экспорт-кнопку и фильтры Статус/Тариф', async () => {
const wrapper = await factory();
expect(wrapper.text()).toContain('Экспорт');
expect(wrapper.find('[data-testid="filter-statuses"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="filter-tariffs"]').exists()).toBe(true);
});
it('фильтр по статусу «overdue» оставляет только просроченных', async () => {
const wrapper = factory();
const wrapper = await factory();
const vm = wrapper.vm as unknown as { filterStatuses: string[] };
vm.filterStatuses = ['overdue'];
await wrapper.vm.$nextTick();
@@ -125,7 +158,7 @@ describe('AdminTenantsView.vue', () => {
});
it('фильтр по тарифу «Pro» оставляет 1 row', async () => {
const wrapper = factory();
const wrapper = await factory();
const vm = wrapper.vm as unknown as { filterTariffs: string[] };
vm.filterTariffs = ['Pro'];
await wrapper.vm.$nextTick();
@@ -135,7 +168,7 @@ describe('AdminTenantsView.vue', () => {
});
it('clearFilters сбрасывает оба фильтра + кнопка «Сбросить» появляется только когда фильтры активны', async () => {
const wrapper = factory();
const wrapper = await factory();
const vm = wrapper.vm as unknown as {
filterStatuses: string[];
filterTariffs: string[];
@@ -152,8 +185,8 @@ describe('AdminTenantsView.vue', () => {
expect(vm.filterTariffs).toEqual([]);
});
it('каждая строка имеет impersonate-кнопку (mdi-account-switch) с уникальным data-testid', () => {
const wrapper = factory();
it('каждая строка имеет impersonate-кнопку (mdi-account-switch) с уникальным data-testid', async () => {
const wrapper = await factory();
// Все 7 mock-tenants должны иметь кнопку
MOCK_TENANTS.forEach((t) => {
const btn = wrapper.find(`[data-testid="impersonate-btn-${t.id}"]`);
@@ -161,8 +194,8 @@ describe('AdminTenantsView.vue', () => {
});
});
it('impersonate-кнопка disabled для suspended-тенанта (Оконные системы РФ id=105)', () => {
const wrapper = factory();
it('impersonate-кнопка disabled для suspended-тенанта (Оконные системы РФ id=105)', async () => {
const wrapper = await factory();
const suspendedBtn = wrapper.find('[data-testid="impersonate-btn-105"]');
expect(suspendedBtn.exists()).toBe(true);
// v-btn disabled-state — атрибут disabled на DOM-элементе
@@ -170,7 +203,7 @@ describe('AdminTenantsView.vue', () => {
});
it('click на impersonate-кнопке открывает ImpersonationDialog с правильным tenant', async () => {
const wrapper = factory();
const wrapper = await factory();
// До click — диалог закрыт (modelValue=false)
const dialogStub = wrapper.findComponent({ name: 'ImpersonationDialog' });
expect(dialogStub.exists()).toBe(true);
@@ -186,4 +219,28 @@ describe('AdminTenantsView.vue', () => {
expect(dialogStub.props('tenant')).toMatchObject({ id: 42, name: 'Окна Москва ООО' });
expect(dialogStub.props('requestedBy')).toBe(1);
});
it('API reject → tenantsState пустой + fetch-error-alert виден', async () => {
const adminApi = await import('../../resources/js/api/admin');
vi.mocked(adminApi.listAdminTenants).mockRejectedValueOnce(new Error('Network error'));
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/admin/tenants', name: 'admin-tenants', component: AdminTenantsView },
{ path: '/admin/tenants/:code', name: 'admin-tenant-detail', component: { template: '<div />' } },
],
});
await router.push('/admin/tenants');
await router.isReady();
const wrapper = mount(AdminTenantsView, {
global: { plugins: [createVuetify(), router], stubs: { ImpersonationDialog: true } },
});
await flushPromises();
const vm = wrapper.vm as unknown as { fetchError: boolean; tenantsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.tenantsState.length).toBe(0);
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
});
@@ -103,7 +103,7 @@ describe('AdminTenantsView ↔ GET /api/admin/tenants integration', () => {
expect(vm.stats.trial).toBe(1);
});
it('reject → fetchError=true + alert виден + MOCK_TENANTS остаётся', async () => {
it('reject → fetchError=true + alert виден + tenantsState пустой', async () => {
vi.mocked(adminApi.listAdminTenants).mockRejectedValueOnce(new Error('500'));
const wrapper = await mountView();
@@ -111,7 +111,7 @@ describe('AdminTenantsView ↔ GET /api/admin/tenants integration', () => {
const vm = wrapper.vm as unknown as { fetchError: boolean; tenantsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.tenantsState.length).toBeGreaterThan(0); // mock-fallback
expect(vm.tenantsState.length).toBe(0); // пустой при ошибке, не mock-fallback
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
+2 -3
View File
@@ -4,7 +4,6 @@ import { createVuetify } from 'vuetify';
import { createPinia, setActivePinia } from 'pinia';
import DealDetailDrawer from '../../resources/js/components/deals/DealDetailDrawer.vue';
import { MOCK_DEALS } from '../../resources/js/composables/mockDeals';
import { MOCK_EVENTS } from '../../resources/js/composables/mockDealEvents';
beforeEach(() => {
setActivePinia(createPinia());
@@ -77,10 +76,10 @@ describe('DealDetailDrawer.vue', () => {
expect(text).toMatch(/1\s+850\s*₽/); // sampleDeal.cost = 1850
});
it('рендерит timeline с MOCK_EVENTS (6 событий)', () => {
it('рендерит timeline без событий (без tenantId events пуст — I3)', () => {
const wrapper = factory({ open: true, deal: sampleDeal });
const items = wrapper.findAll('.timeline-item');
expect(items).toHaveLength(MOCK_EVENTS.length);
expect(items).toHaveLength(0);
});
it('emit-ит update:open=false при close-кнопке', async () => {
@@ -4,7 +4,6 @@ import { createVuetify } from 'vuetify';
import { createPinia, setActivePinia } from 'pinia';
import DealDetailDrawer from '../../resources/js/components/deals/DealDetailDrawer.vue';
import { MOCK_DEALS } from '../../resources/js/composables/mockDeals';
import { MOCK_EVENTS } from '../../resources/js/composables/mockDealEvents';
import type { GetDealResponse, ApiDealEvent } from '../../resources/js/api/deals';
vi.mock('../../resources/js/api/deals', async (importOriginal) => {
@@ -49,13 +48,13 @@ function makeApiEvent(overrides: Partial<ApiDealEvent> = {}): ApiDealEvent {
}
describe('DealDetailDrawer ↔ GET /api/deals/{id} integration', () => {
it('БЕЗ tenantId — getDeal не вызывается, показываются MOCK_EVENTS', async () => {
it('БЕЗ tenantId — getDeal не вызывается, events пуст (I3)', async () => {
const wrapper = factory({ open: true });
await flushPromises();
expect(dealsApi.getDeal).not.toHaveBeenCalled();
const items = wrapper.findAll('.timeline-item');
expect(items).toHaveLength(MOCK_EVENTS.length);
expect(items).toHaveLength(0);
});
it('С tenantId — getDeal вызывается, events заменяются на API', async () => {
@@ -97,7 +96,7 @@ describe('DealDetailDrawer ↔ GET /api/deals/{id} integration', () => {
expect(wrapper.text()).toContain('new → paid');
});
it('getDeal reject → eventsFetchError=true, alert виден, MOCK_EVENTS как fallback', async () => {
it('getDeal reject → eventsFetchError=true, alert виден, events пуст (I3)', async () => {
vi.mocked(dealsApi.getDeal).mockRejectedValueOnce(new Error('500'));
const wrapper = factory({ open: true, tenantId: 1 });
@@ -106,9 +105,9 @@ describe('DealDetailDrawer ↔ GET /api/deals/{id} integration', () => {
const vm = wrapper.vm as unknown as { eventsFetchError: boolean };
expect(vm.eventsFetchError).toBe(true);
expect(wrapper.find('[data-testid="events-fetch-error-alert"]').exists()).toBe(true);
// Fallback на MOCK_EVENTS.
// I3: нет mock-fallback — events пуст.
const items = wrapper.findAll('.timeline-item');
expect(items).toHaveLength(MOCK_EVENTS.length);
expect(items).toHaveLength(0);
});
it('open=false → getDeal не вызывается', async () => {
@@ -7,6 +7,7 @@ import DealsView from '../../resources/js/views/DealsView.vue';
import KanbanView from '../../resources/js/views/KanbanView.vue';
import { useAuthStore } from '../../resources/js/stores/auth';
import type { ApiDeal } from '../../resources/js/api/deals';
import { MOCK_DEALS } from '../../resources/js/composables/mockDeals';
vi.mock('../../resources/js/api/deals', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/deals')>();
@@ -86,14 +87,13 @@ const mountKanbanView = () =>
});
describe('DealsView ↔ GET /api/deals integration', () => {
it('БЕЗ auth.user.tenant_id — listDeals не вызывается, fallback на MOCK_DEALS', async () => {
it('БЕЗ auth.user.tenant_id — listDeals не вызывается, dealsState пустой', async () => {
setupAuth(null);
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).not.toHaveBeenCalled();
// MOCK_DEALS содержит 12 элементов — fallback виден.
const vm = wrapper.vm as unknown as { dealsState: { id: number }[] };
expect(vm.dealsState.length).toBeGreaterThan(0);
expect(vm.dealsState.length).toBe(0);
});
it('С auth.user.tenant_id — listDeals вызывается с tenantId + replace dealsState', async () => {
@@ -120,7 +120,7 @@ describe('DealsView ↔ GET /api/deals integration', () => {
expect(vm.dealsState.find((d) => d.id === 200)?.name).toBe('Из API #1');
});
it('listDeals reject → fetchError=true, alert виден, MOCK_DEALS остаётся как fallback', async () => {
it('listDeals reject → fetchError=true, alert виден, dealsState пустой', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockRejectedValueOnce(new Error('network'));
@@ -129,7 +129,7 @@ describe('DealsView ↔ GET /api/deals integration', () => {
const vm = wrapper.vm as unknown as { fetchError: boolean; dealsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.dealsState.length).toBeGreaterThan(0);
expect(vm.dealsState.length).toBe(0);
// Alert виден.
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
@@ -280,6 +280,9 @@ describe('DealsView ↔ GET /api/deals integration', () => {
applyBulkStatus: (slug: string) => Promise<void>;
dealsState: { id: number; statusSlug: string }[];
};
// Засеваем dealsState mock-фикстурой (после I3 init пустой)
vm.dealsState.push(...MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
await flushPromises();
vm.selected = [1];
await flushPromises();
await vm.applyBulkStatus('paid');
@@ -441,6 +444,9 @@ describe('DealsView ↔ GET /api/deals integration', () => {
applyBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
};
// Засеваем dealsState mock-фикстурой (после I3 init пустой)
vm.dealsState.push(...MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
await flushPromises();
const before = vm.dealsState.length;
vm.selected = [1, 2];
await flushPromises();
@@ -540,6 +546,9 @@ describe('DealsView ↔ GET /api/deals integration', () => {
dealsState: { id: number }[];
lastDeletedSnapshot: { id: number }[];
};
// Засеваем dealsState mock-фикстурой (после I3 init пустой)
vm.dealsState.push(...MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
await flushPromises();
const sample = vm.dealsState[0];
vm.selected = [sample.id];
@@ -651,7 +660,7 @@ describe('KanbanView ↔ GET /api/deals integration', () => {
expect(vm.fetchError).toBe(false);
});
it('listDeals reject → fetchError=true, MOCK_DEALS остаётся в колонках', async () => {
it('listDeals reject → fetchError=true, колонки пусты', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockRejectedValueOnce(new Error('500'));
@@ -663,9 +672,8 @@ describe('KanbanView ↔ GET /api/deals integration', () => {
fetchError: boolean;
};
expect(vm.fetchError).toBe(true);
// Хотя бы одна колонка с mock-сделками заполнена (изначальный state).
const filledColumns = Object.values(vm.dealsByStatus).filter((arr) => arr.length > 0);
expect(filledColumns.length).toBeGreaterThan(0);
expect(filledColumns.length).toBe(0);
});
it('reload-btn вызывает listDeals второй раз', async () => {
+76 -4
View File
@@ -4,13 +4,19 @@ import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia';
import DealsView from '../../resources/js/views/DealsView.vue';
import { MOCK_DEALS } from '../../resources/js/composables/mockDeals';
import { MOCK_DEALS, type MockDeal } from '../../resources/js/composables/mockDeals';
import * as dealsApi from '../../resources/js/api/deals';
import { useAuthStore } from '../../resources/js/stores/auth';
import type { AuthUser } from '../../resources/js/api/auth';
// Smoke-тесты DealsView с mock-данными.
/** Засевает dealsState фикстурой MOCK_DEALS (имитирует успешный API-ответ). */
function seedDealsState(wrapper: ReturnType<typeof mount>) {
const vm = wrapper.vm as unknown as { dealsState: MockDeal[] };
vm.dealsState.push(...MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
}
const mountDeals = async () => {
setActivePinia(createPinia());
const router = createRouter({
@@ -25,12 +31,16 @@ const mountDeals = async () => {
// layout-injection от v-app. В Vitest vite-plugin-vuetify auto-import не
// работает, layout-context недоступен. Stub'им сам Drawer (тестируется
// отдельно в DealDetailDrawer.spec.ts).
return mount(DealsView, {
const wrapper = mount(DealsView, {
global: {
plugins: [createVuetify(), router],
stubs: { DealDetailDrawer: true, NewDealDialog: true },
},
});
await flushPromises();
seedDealsState(wrapper);
await flushPromises();
return wrapper;
};
/** Audit C8/F3: монтирует DealsView по произвольному пути (с query-параметрами). */
@@ -42,12 +52,16 @@ const mountDealsViewAt = async (path: string) => {
});
await router.push(path);
await router.isReady();
return mount(DealsView, {
const wrapper = mount(DealsView, {
global: {
plugins: [createVuetify(), router],
stubs: { DealDetailDrawer: true, NewDealDialog: true },
},
});
await flushPromises();
seedDealsState(wrapper);
await flushPromises();
return wrapper;
};
describe('DealsView.vue', () => {
@@ -295,7 +309,36 @@ describe('DealsView.vue', () => {
// Audit C8/F3: deep-link /deals?openId=
it('route.query.openId открывает drawer соответствующей сделки', async () => {
const openId = MOCK_DEALS[0].id;
const wrapper = await mountDealsViewAt(`/deals?openId=${openId}`);
// Мокаем API чтобы loadDeals заполнил state до вызова openDealFromQuery в onMounted.
vi.spyOn(dealsApi, 'listDeals').mockResolvedValue({
deals: MOCK_DEALS.map((d) => ({
id: d.id,
name: d.name,
phone: d.phone,
status: d.statusSlug,
project_name: d.project,
manager_name: d.manager.name,
cost: d.cost,
created_at: new Date(Date.now() - d.receivedMinutesAgo * 60000).toISOString(),
deleted_at: null,
})),
total: MOCK_DEALS.length,
} as never);
setActivePinia(createPinia());
const auth = useAuthStore();
auth.user = { id: 1, tenant_id: 42, email: 'test@test.com' } as AuthUser;
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/deals', component: DealsView }],
});
await router.push(`/deals?openId=${openId}`);
await router.isReady();
const wrapper = mount(DealsView, {
global: {
plugins: [createVuetify(), router],
stubs: { DealDetailDrawer: true, NewDealDialog: true },
},
});
await flushPromises();
const vm = wrapper.vm as unknown as { drawerOpen: boolean; selectedDeal: { id: number } | null };
expect(vm.drawerOpen).toBe(true);
@@ -372,4 +415,33 @@ test('C3: exportAllFiltered на пустом списке показывает
expect(vm.exportToastText).toBe('Список пуст — нечего экспортировать.');
});
// I3 regression: API reject → dealsState пустой + fetchError=true (нет mock-fallback)
// Faithful-паттерн: auth + mock ДО mount, onMounted сам вызывает loadDeals.
test('I3: loadDeals reject оставляет dealsState пустым и выставляет fetchError', async () => {
vi.spyOn(dealsApi, 'listDeals').mockRejectedValue(new Error('500'));
setActivePinia(createPinia());
const auth = useAuthStore();
auth.user = { id: 1, tenant_id: 42, email: 'test@test.com' } as AuthUser;
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/deals', component: DealsView }],
});
await router.push('/deals');
await router.isReady();
const wrapper = mount(DealsView, {
global: {
plugins: [createVuetify(), router],
stubs: { DealDetailDrawer: true, NewDealDialog: true },
},
});
await flushPromises();
const vm = wrapper.vm as unknown as {
dealsState: MockDeal[];
fetchError: boolean;
};
expect(vm.dealsState.length).toBe(0);
expect(vm.fetchError).toBe(true);
});
afterEach(() => vi.restoreAllMocks());
+27 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
@@ -52,6 +52,10 @@ describe('ImpersonationDialog.vue', () => {
vi.clearAllMocks();
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('не рендерит content когда modelValue=false', () => {
const wrapper = factory({ modelValue: false, tenant: sampleTenant });
expect(wrapper.find('.dialog-stub').exists()).toBe(false);
@@ -198,4 +202,26 @@ describe('ImpersonationDialog.vue', () => {
expect(events).toBeDefined();
expect(events?.[0]).toEqual([false]);
});
it('I4 — dev-code-banner НЕ рендерится когда import.meta.env.DEV=false (prod)', async () => {
vi.stubEnv('DEV', false);
vi.mocked(adminApi.impersonationInit).mockResolvedValue({
token_id: 42,
expires_at: '2026-05-09T12:00:00Z',
sent_to_email: 'admin@okna-moscow.ru',
_dev_plain_code: '123456',
});
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
await wrapper.find('[data-testid="reason-input"] textarea').setValue(
'Тикет SUP-12453: клиент сообщил, что в карточке сделки не сохраняется коммент.',
);
await wrapper.find('[data-testid="submit-init-btn"]').trigger('click');
await flushPromises();
// step verify достигнут — devPlainCode='123456' есть, но DEV=false → баннер не рендерится
expect(wrapper.text()).toContain('Код отправлен на email клиента');
expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(false);
});
});
@@ -4,6 +4,7 @@ import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
import { createMemoryHistory, createRouter } from 'vue-router';
import KanbanView from '../../resources/js/views/KanbanView.vue';
import { MOCK_DEALS } from '../../resources/js/composables/mockDeals';
// KanbanView содержит DealDetailDrawer (VNavigationDrawer) — требует
// injected layout от v-app, недоступной в Vitest. Stub'им как в KanbanView.spec.ts.
@@ -28,6 +29,12 @@ describe('KanbanView — redesigned', () => {
it('card containers have ld-hover-lift class', async () => {
const w = setup();
await flushPromises();
// Засеваем dealsByStatus (после I3 init пустой — карточек нет)
const vm = w.vm as unknown as { dealsByStatus: Record<string, unknown[]> };
for (const d of MOCK_DEALS) {
(vm.dealsByStatus[d.statusSlug] ??= []).push({ ...d, manager: { ...d.manager } });
}
await flushPromises();
expect(w.html()).toMatch(/ld-hover-lift/);
});
+38 -1
View File
@@ -1,11 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createPinia, setActivePinia } from 'pinia';
import KanbanView from '../../resources/js/views/KanbanView.vue';
import { useAuthStore } from '../../resources/js/stores/auth';
import * as dealsApi from '../../resources/js/api/deals';
import { LEAD_STATUSES } from '../../resources/js/composables/leadStatuses';
import { MOCK_DEALS, type MockDeal } from '../../resources/js/composables/mockDeals';
describe('KanbanView.vue', () => {
// KanbanView содержит DealDetailDrawer (v-navigation-drawer), который требует
@@ -97,6 +98,15 @@ describe('KanbanView.vue', () => {
it('обновляет statusSlug сделки при drop в новую колонку (event.added)', async () => {
const wrapper = factory();
// Засеваем dealsByStatus фикстурой MOCK_DEALS (init теперь пустой).
const vm = wrapper.vm as unknown as { dealsByStatus: Record<string, MockDeal[]> };
for (const deal of MOCK_DEALS) {
if (vm.dealsByStatus[deal.statusSlug]) {
vm.dealsByStatus[deal.statusSlug].push({ ...deal });
}
}
await wrapper.vm.$nextTick();
const cols = wrapper.findAllComponents({ name: 'KanbanColumn' });
// Берём сделку из первой колонки (new) и эмулируем «added» в paid-колонке.
const newCol = cols[0]; // new — sortOrder=1
@@ -114,6 +124,33 @@ describe('KanbanView.vue', () => {
});
});
// I3 regression: API reject → dealsByStatus пустые + fetchError=true (нет mock-fallback)
// Faithful-паттерн: auth + mock ДО mount, onMounted сам вызывает loadDeals.
describe('KanbanView I3 regression', () => {
it('loadDeals reject оставляет dealsByStatus пустыми и выставляет fetchError', async () => {
vi.spyOn(dealsApi, 'listDeals').mockRejectedValue(new Error('500'));
setActivePinia(createPinia());
const auth = useAuthStore();
auth.user = { id: 1, tenant_id: 42, email: 'test@test.com' } as never;
const wrapper = mount(KanbanView, {
global: {
plugins: [createVuetify()],
stubs: { DealDetailDrawer: true, NewDealDialog: true },
},
});
await flushPromises();
const vm = wrapper.vm as unknown as {
dealsByStatus: Record<string, MockDeal[]>;
fetchError: boolean;
};
expect(vm.fetchError).toBe(true);
// Все колонки пусты — нет mock-fallback
const allDeals = Object.values(vm.dealsByStatus).flat();
expect(allDeals.length).toBe(0);
});
});
describe('KanbanView DnD persist (Sprint 1 C4)', () => {
beforeEach(() => {
vi.clearAllMocks();
+11
View File
@@ -288,6 +288,17 @@ describe('NewDealDialog.vue', () => {
expect(closeEmits === undefined || !closeEmits.some((e) => e[0] === false)).toBe(true);
});
it('I3: без tenantId — projectOptions и managerOptions пусты (нет mock-fallback)', async () => {
const wrapper = factory({ modelValue: true }); // нет tenantId
await flushPromises();
const vm = wrapper.vm as unknown as {
projectOptions: string[];
managerOptions: unknown[];
};
expect(vm.projectOptions).toHaveLength(0);
expect(vm.managerOptions).toHaveLength(0);
});
it('C6: при провале loadLookups показывает degradation-alert', async () => {
vi.spyOn(dealsApi, 'listProjects').mockRejectedValue(new Error('network'));
vi.spyOn(dealsApi, 'listManagers').mockRejectedValue(new Error('network'));
+9
View File
@@ -1321,3 +1321,12 @@ mmdc
inertiajs
Sev
вендоренный
# D3 audit-risk tooling integration (Прил. Н #39-40)
unvetted
mcpmarket
behaviour
triada
trailofbits
hackathon
субсет
+7 -1
View File
@@ -1,8 +1,10 @@
# Plugin Stack Rules — Superpowers + Frontend Design (v3.3)
# Plugin Stack Rules — Superpowers + Frontend Design (v3.4)
**Дата:** 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.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.
**v3.2** — реколлаж R0: sub-policy → top-of-stack gate (ruflo не entry-point по факту рантайма: 0 задач, рой idle). R0 title восстановлен, уровень −1 убран из R0.1 таблицы, R0.2 абзац перед gate-диаграммой возвращён к stack-gate формулировке. Связано: Pravila v1.16, CLAUDE.md v2.2, Tooling v2.2.
@@ -396,6 +398,8 @@ Stack — **головной**. Все плагины вне stack'а — **ин
| **claude-md-management** *(skills `claude-md-improver` + `revise-claude-md`)* | `anthropics/claude-plugins-official` | инфраструктурный плагин для CLAUDE.md edits | **обязательно** при любом изменении CLAUDE.md (выполнение CLAUDE.md §5 п.10). Не альтернатива stack'у, а инструмент внутри stack-фазы «реализация». Категория: **инфраструктурная** (вне UI-пула Pravila §13) |
| **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 |
**Блок 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.
@@ -753,6 +757,8 @@ Pipeline активируется при одновременном выполн
## История версий
- **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`.
- **v3.2 (2026-05-16)** — реколлаж R0: sub-policy → top-of-stack gate (ruflo не entry-point по факту рантайма: 0 задач, рой idle). **Изменено:** R0 title → «Stack-gate: paired-stack delegation pattern»; R0.1 таблица — удалена строка уровня −1 (ruflo entry-point), строка уровня 3 (PSR_v1) → «— (PSR_v1 — сам stack-документ, вопрос неприменим)»; R0.1 преамбула — убраны формулировки sub-policy-под-ruflo, stack снова головной над уровнями 4–6; R0.2 абзац перед диаграммой — возвращён к stack-gate формулировке; шапка cross-refs: CLAUDE.md v2.0+ → v2.2+, Pravila v1.15+ → v1.16+, Tooling v2.0+ → v2.2+. ASCII-диаграмма (STACK GATE) и R0.5 не тронуты. **R0.6 п.11 удалён** (ruflo autonomous-routing hard-stop — висячая ссылка на ruflo как маршрутизатор задач; противоречит реколлажу: ruflo не entry-point, рой idle, 0 задач). Связано: Pravila v1.16 / CLAUDE.md v2.2 / Tooling v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`.
+6 -2
View File
@@ -1,10 +1,12 @@
# Правила работы Claude в проекте «Лидерра»
**Версия:** v1.17 (17.05.2026)
**Версия:** v1.18 (17.05.2026)
**Дата:** 17.05.2026
**Назначение:** настройки проекта (Project instructions) — Claude читает этот файл в каждом чате и следует правилам ниже.
**Статус документа:** ✅ утверждён. Содержимое скопировано в поле "Project instructions" Claude.ai. Файл хранится в архиве как служебный документ.
**Что изменилось в 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`.
**Краткое резюме v1.16:** реколлаж ruflo к фактическому рантайму: §12 sub-policy → hard-rule (title + абзацы), §12.4 первый буллет → «§9 не применяется», §0 priority note убран ruflo уровень −1 (цепочка начинается с §12 explicit hard-rule), §14.6 cross-ref убран «ruflo — уровень −1» → «ruflo как инструмент (хук + MCP), не уровень иерархии», §13.9/§13.10 PSR_v1 cross-refs «v3.0+, R0 → sub-policy» → «v3.2+, R0 — top-of-stack gate». Связано: CLAUDE.md v2.2 / PSR_v1 v3.2 / Tooling v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`.
@@ -690,10 +692,12 @@ 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`).
**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 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 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`.
### 13.3. Скоуп
| Тип задачи | Кто отвечает |
+28 -5
View File
@@ -1,10 +1,10 @@
# Приложение Н — Tooling, скиллы и плагины Claude (v8.3)
**Дата:** 17.05.2026
**Версия:** 2.3 (A6 architecture-tooling — формализованы 3 инструмента раздела A6 карты «Архитектура систем»: **#36 adr-kit** (ADR-решения + `adr-judge` gate), **#37 mermaid-skill** (C4-диаграммы), **#38 architecture-patterns** (паттерны) — новые §4.11–4.13, новая пятая off-phase подкатегория «architecture-tooling»; §0 счётчик 35→38 формализованных позиций (55→58 total), §0 table row off-phase +5→+8. Связано: PSR_v1 v3.3, Pravila v1.17, CLAUDE.md v2.3; план `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`. **v2.2 наследие:** §4.10 реколлаж — ruflo переописан из «entry-point иерархии» в «advisory/automation-подсистему» (декларация приведена к рантайму: рой idle, 0 задач); заголовок §4.10 + «Архитектурная роль» переписаны; §0 table row + «Категории off-phase tools» + «Назначение» обновлены; §13 +v2.2 entry. Связано: Pravila v1.16, PSR_v1 v3.2, CLAUDE.md v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`. **v2.1 наследие:** §4.10 +абзац «Queen trigger»: триггер queen/королева → безусловный route через ruflo Queen (`hive-mind spawn --claude`), explicit hard-rule Pravila §14, enforcement-хук `tools/ruflo-queen-hook.mjs`. Связано: spec/plan `docs/superpowers/{specs,plans}/2026-05-15-ruflo-queen-trigger-and-delegation*`, Pravila v1.15, CLAUDE.md v2.1, PSR_v1 v3.1. **v2.0 наследие:** Ruflo big-bang — major bump: добавлен **orchestration layer (ruflo)** как четвёртая off-phase подкатегория. §0 +ruflo orchestration row: 35 формализованных позиций + 20 ruflo plugins = 55 total; новая §4.10 «Orchestration layer (ruflo)». Связано: spec/plan 2026-05-15, Pravila v1.14, PSR_v1 v3.0, CLAUDE.md v2.0.)
**Версия:** 2.4 (D3 audit-security — формализованы #39 Trail of Bits Skills (субсет 8 audit-плагинов, marketplace `trailofbits`, CC-BY-SA-4.0) + #40 Security Guidance (Anthropic warn-only PreToolUse-хук) как новая 6-я off-phase подкатегория «audit-security» — §4.14/§4.15; §0 счётчик 38→40 (58→60 total); off-phase row +8→+10. Связано: PSR_v1 v3.4, Pravila v1.18, CLAUDE.md v2.4; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. **v2.3 наследие:** A6 architecture-tooling — формализованы 3 инструмента раздела A6 карты «Архитектура систем»: **#36 adr-kit** (ADR-решения + `adr-judge` gate), **#37 mermaid-skill** (C4-диаграммы), **#38 architecture-patterns** (паттерны) — новые §4.11–4.13, новая пятая off-phase подкатегория «architecture-tooling»; §0 счётчик 35→38 формализованных позиций (55→58 total), §0 table row off-phase +5→+8. Связано: PSR_v1 v3.3, Pravila v1.17, CLAUDE.md v2.3; план `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`. **v2.2 наследие:** §4.10 реколлаж — ruflo переописан из «entry-point иерархии» в «advisory/automation-подсистему» (декларация приведена к рантайму: рой idle, 0 задач); заголовок §4.10 + «Архитектурная роль» переписаны; §0 table row + «Категории off-phase tools» + «Назначение» обновлены; §13 +v2.2 entry. Связано: Pravila v1.16, PSR_v1 v3.2, CLAUDE.md v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`. **v2.1 наследие:** §4.10 +абзац «Queen trigger»: триггер queen/королева → безусловный route через ruflo Queen (`hive-mind spawn --claude`), explicit hard-rule Pravila §14, enforcement-хук `tools/ruflo-queen-hook.mjs`. Связано: spec/plan `docs/superpowers/{specs,plans}/2026-05-15-ruflo-queen-trigger-and-delegation*`, Pravila v1.15, CLAUDE.md v2.1, PSR_v1 v3.1. **v2.0 наследие:** Ruflo big-bang — major bump: добавлен **orchestration layer (ruflo)** как четвёртая off-phase подкатегория. §0 +ruflo orchestration row: 35 формализованных позиций + 20 ruflo plugins = 55 total; новая §4.10 «Orchestration layer (ruflo)». Связано: spec/plan 2026-05-15, Pravila v1.14, PSR_v1 v3.0, CLAUDE.md v2.0.)
**Предыдущая версия:** 1.17 (13.05.2026 day +1 — формализация retrospective двух off-phase MCP debug-инструментов установленных на feat/claude-automation `6f7e7d7` + `bd4ec48` после merge PR #3 в main `cc5f63b`: §0 счётчик off-phase 3 → 5, итого 33 → 35; §4.8 новый — #34 Sentry MCP; §4.9 новый — #35 Redis MCP. Категория debug-runtime, отдельная от UI-пула.)
**Адресат:** Claude + разработчики проекта Лидерра
**Назначение:** единый источник истины по 38 формализованным позициям тулчейна + 20 ruflo orchestration plugins = 58 total (29 «активных» номеров фаз + 8 off-phase инструментов-резерв в категориях UI-пул, инфраструктура, debug-runtime, architecture-tooling — UPM, 21st, claude-md-management, Sentry MCP, Redis MCP, adr-kit, mermaid-skill, architecture-patterns; +1 заменённый PG MCP исторически; +ruflo advisory/automation-подсистема — 20 plugins, см. §4.10), скиллам Claude Code, MCP-серверам и плагинам, используемым в проекте. Зафиксирован выбор, объяснено, что заменяет что, и в какой фазе вводится каждый инструмент.
**Назначение:** единый источник истины по 40 формализованным позициям тулчейна + 20 ruflo orchestration plugins = 60 total (29 «активных» номеров фаз + 10 off-phase инструментов-резерв в категориях UI-пул, инфраструктура, debug-runtime, architecture-tooling, audit-security — UPM, 21st, claude-md-management, Sentry MCP, Redis MCP, adr-kit, mermaid-skill, architecture-patterns, Trail of Bits Skills, Security Guidance; +1 заменённый PG MCP исторически; +ruflo advisory/automation-подсистема — 20 plugins, см. §4.10), скиллам Claude Code, MCP-серверам и плагинам, используемым в проекте. Зафиксирован выбор, объяснено, что заменяет что, и в какой фазе вводится каждый инструмент.
> **Связано:**
>
@@ -81,10 +81,10 @@
| **1 — старт Laravel** | `composer create-project laravel/laravel` | **17** | +9 новых, −1 заменённый (PostgreSQL MCP → Laravel Boost) |
| **2 — старт frontend** | первый коммит в `resources/js/` (Vue 3 + Vuetify 3) | **24** | +7 (включая #30 Frontend Design plugin, добавлен post-MVP в v1.10) |
| **3 — pre-production** | ~спринт 12, перед публичным релизом | **29** | +5 |
| **off-phase tools** | по факту включения в `~/.claude/settings.json` / `~/.claude.json` / `.mcp.json` / `.claude/skills/` | **+8** | #31 UPM (UI-резерв), #32 21st Magic MCP (UI-генератор), #33 claude-md-management (инфраструктура CLAUDE.md edits), #34 Sentry MCP (debug self-hosted Sentry в Yandex Cloud), #35 Redis MCP (debug Memurai/Redis runtime), #36 adr-kit (ADR-решения, architecture-tooling), #37 mermaid-skill (C4-диаграммы), #38 architecture-patterns (паттерны) |
| **off-phase tools** | по факту включения в `~/.claude/settings.json` / `~/.claude.json` / `.mcp.json` / `.claude/skills/` | **+10** | #31 UPM (UI-резерв), #32 21st Magic MCP (UI-генератор), #33 claude-md-management (инфраструктура CLAUDE.md edits), #34 Sentry MCP (debug self-hosted Sentry в Yandex Cloud), #35 Redis MCP (debug Memurai/Redis runtime), #36 adr-kit (ADR-решения, architecture-tooling), #37 mermaid-skill (C4-диаграммы), #38 architecture-patterns (паттерны), #39 Trail of Bits Skills (8 audit-плагинов, audit-security), #40 Security Guidance (inline security warn-hook) |
| **ruflo advisory/automation-подсистема** (off-phase, post-MVP 2026-05-15) | `npx ruflo@latest init` + `.mcp.json` ruflo entry | **+20 plugins** | `ruflo` v3.7.0-alpha.38+ + 20 plugins (`@claude-flow/*`, IPFS-registry) — advisory/automation-подсистема; orchestration подкатегория off-phase (см. §4.10) |
**Итого формализованных позиций:** 38 (29 активных по фазам + 8 off-phase + 1 заменённый PG MCP исторически) + 20 ruflo orchestration plugins = **58 total**. Полный перечень — §2–§5 (по фазам) + §4.5/§4.6/§4.7/§4.8/§4.9/§4.11/§4.12/§4.13 (off-phase) + §4.10 (ruflo orchestration). Карта «когда что использовать» — §7. Что НЕ ставим и почему — §9.
**Итого формализованных позиций:** 40 (29 активных по фазам + 10 off-phase + 1 заменённый PG MCP исторически) + 20 ruflo orchestration plugins = **60 total**. Полный перечень — §2–§5 (по фазам) + §4.5/§4.6/§4.7/§4.8/§4.9/§4.11/§4.12/§4.13/§4.14/§4.15 (off-phase) + §4.10 (ruflo orchestration). Карта «когда что использовать» — §7. Что НЕ ставим и почему — §9.
**Ключевой принцип фазирования:** не активируем фазу N+1, пока не закрыт триггер фазы N. Без `composer create-project` Boost не работает; без Vuetify-приложения Histoire бесполезен.
@@ -416,7 +416,27 @@
**Категория:** off-phase, architecture-tooling. Knowledge-only скил, без машинерии. Регулируется PSR_v1 R10.1 Блок 1.
**Категории off-phase tools (v2.3):** пять подкатегорий — UI-пул (#31 UPM + #32 21st), infrastructure (#33 claude-md-management), debug-runtime (#34 Sentry + #35 Redis), orchestration (ruflo §4.10), **architecture-tooling (#36 adr-kit + #37 mermaid-skill + #38 architecture-patterns)**.
### 4.14. Trail of Bits Skills — аудит безопасности (off-phase, audit-security)
**Trail of Bits Skills** (Claude Code marketplace `trailofbits/skills`, имя marketplace `trailofbits`, **CC-BY-SA-4.0**, репутабельный AppSec-вендор). Marketplace из 38 плагинов; в `enabledPlugins` включён курированный субсет **8 плагинов** под раздел D3 «Аудит и управление рисками»: `differential-review`, `audit-context-building`, `supply-chain-risk-auditor`, `insecure-defaults`, `sharp-edges`, `static-analysis`, `variant-analysis`, `agentic-actions-auditor`. Все 8 — skill/agent-плагины, **0 Claude Code lifecycle-хуков** (статически верифицировано по репо — ни у одного нет `hooks/`-папки).
**Роль:** инструмент **#39**, раздел D3 карты «Аудит и управление рисками» — глубокие on-demand аудит-кампании (security-аудит diff, supply-chain риск зависимостей, поиск вариантов уязвимостей по кодбазе).
**Категория:** off-phase, **audit-security** — шестая off-phase подкатегория (отдельная от UI-пула UPM/21st, infrastructure claude-md-management, debug-runtime Sentry/Redis, orchestration ruflo, architecture-tooling adr-kit/mermaid/architecture-patterns). Не UI → **не** проходит R6.0/R6.1/R14 PSR_v1. Регулируется PSR_v1 R10.1 Блок 1.
**Конфликт-аудит интеграции:** TB1 — `static-analysis` пересекается с Semgrep MCP (#25): граница — Semgrep MCP = inline SAST в рутине/CI, ToB = глубокие аудит-кампании on-demand. TB4 — CC-BY-SA-4.0 ShareAlike: остаётся marketplace-плагином (кэш вне репо, не вендорен) → ShareAlike-обязательство не триггерится. `fp-check` исключён из субсета — единственный из 9 рассмотренных с `hooks/`-папкой (lifecycle-хук); цепочка хуков проекта держится минимальной. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
### 4.15. Security Guidance — inline-предупреждения уязвимостей (off-phase, audit-security)
**Security Guidance** (Claude Code plugin, marketplace `anthropics/claude-plugins-official`, plugin `security-guidance@claude-plugins-official`, Anthropic Verified). Один PreToolUse `Write|Edit|MultiEdit`-хук — **warn-only**: печатает session-scoped предупреждение об уязвимом паттерне (8 категорий — command/shell injection, `eval`, XSS, pickle-десериализация и т.д.) перед применением правки, **не блокирует**.
**Роль:** инструмент **#40**, раздел D3 — real-time inline-напоминание об уязвимостях во время редактирования (дополняет on-demand аудит ToB/Semgrep).
**Категория:** off-phase, audit-security. Регулируется PSR_v1 R10.1 Блок 1.
**Конфликт-аудит интеграции:** SG1 — добавляет 5-й PreToolUse-хук поверх 4 существующих (skill-marker / skill-check / economy-state-guard в `~/.claude/settings.json` + CLAUDE.md-warn в проектном `.claude/settings.json`); warn-only (не `decision:block`), economy/ruflo-цепочка хуков не нарушается, +~34 мс/правку latency принято. `/security-review` (Anthropic built-in, customized в `.claude/commands/security-review.md` с проектным FP-фильтром — RLS/ПДн/economy-хуки) — D3 #2, не отдельный нумерованный слот (built-in, не installed tool).
**Категории off-phase tools (v2.4):** шесть подкатегорий — UI-пул (#31 UPM + #32 21st), infrastructure (#33 claude-md-management), debug-runtime (#34 Sentry + #35 Redis), orchestration (ruflo §4.10), **architecture-tooling (#36 adr-kit + #37 mermaid-skill + #38 architecture-patterns)**, **audit-security (#39 Trail of Bits Skills + #40 Security Guidance)**.
---
@@ -706,9 +726,12 @@ Vuetify-тема — `liderraLight` и `liderraDark` — определена в
| **v2.1** | **15.05.2026** | §4.10 +абзац «Queen trigger»: триггер queen/королева → безусловный route через ruflo Queen (`hive-mind spawn --claude`), explicit hard-rule Pravila §14, enforcement-хук `tools/ruflo-queen-hook.mjs`; footer-колонтитул v2.1. Связано: spec/plan `docs/superpowers/{specs,plans}/2026-05-15-ruflo-queen-trigger-and-delegation*`, Pravila v1.15 / CLAUDE.md v2.1 / PSR_v1 v3.1. |
| **v2.2** | **16.05.2026** | **§4.10 реколлаж:** ruflo переописан из «entry-point иерархии» в «advisory/automation-подсистему» (декларация приведена к рантайму: рой idle, 0 задач / 0 раундов консенсуса; Claude-сессии работают напрямую). Заголовок §4.10 изменён («Orchestration layer (ruflo) — entry-point иерархии» → «ruflo — advisory/automation-подсистема»); «Архитектурная роль» переписана; §0 table row обновлён; «Категории off-phase tools» обновлены; «Назначение» обновлено; шапка v2.1 → v2.2, дата 16.05.2026. Связано: Pravila v1.16, PSR_v1 v3.2, CLAUDE.md v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`. |
| **v2.3** | **17.05.2026** | **A6 architecture-tooling:** формализованы 3 инструмента раздела A6 карты «Архитектура систем» как новая пятая off-phase подкатегория «architecture-tooling» — **§4.11 #36 adr-kit** (ADR-решения, `adr-judge` lefthook job 9), **§4.12 #37 mermaid-skill** (C4-диаграммы, вендорен в `.claude/skills/mermaid/`), **§4.13 #38 architecture-patterns** (паттерны). §0 счётчик 35→38 формализованных позиций (55→58 total); §0 table row off-phase `+5``+8`; «Назначение» обновлено. Конфликт-аудит интеграции — AK1 (git-хук adr-kit не ставится, `adr-judge` через lefthook), AK2 (`init` не пишет CLAUDE.md), AK6 (`adr-judge` без `--llm` → 0 стоимости), MK1 (lefthook exclude вендоренного скила). Связано: PSR_v1 v3.2→v3.3 (R10.1 +3 строки), Pravila v1.16→v1.17 (§13.2 +architecture-tooling абзац), CLAUDE.md v2.2→v2.3 (§3.3 +#36-38). Через manual Edit (Tooling/PSR_v1/Pravila) + `/claude-md-management:claude-md-improver` (CLAUDE.md per §5 п.10). План `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`. |
| **v2.4** | 17.05.2026 | **D3 audit-security:** формализованы #39 Trail of Bits Skills (субсет 8 audit-плагинов, marketplace `trailofbits`, CC-BY-SA-4.0) + #40 Security Guidance (Anthropic warn-only PreToolUse-хук) как новая 6-я off-phase подкатегория «audit-security» — §4.14/§4.15. §0 счётчик 38→40 (58→60 total); off-phase row +8→+10. Конфликт-аудит — TB1 (граница с Semgrep MCP), TB4 (CC-BY-SA не триггерится — не вендорено), SG1 (5-й PreToolUse warn-хук). Связано: PSR_v1 v3.3→v3.4 (R10.1 Блок 1 +2 строки), Pravila v1.17→v1.18 (§13.2 +audit-security абзац), CLAUDE.md v2.3→v2.4 (§3.3 +#39-40). План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. |
---
*Прил. Н v2.4 от 17.05.2026 — D3 audit-security: формализованы #39 Trail of Bits Skills + #40 Security Guidance (§4.14/§4.15), новая 6-я off-phase подкатегория. 40 формализованных позиций (29 по фазам + 10 off-phase + 1 PG MCP) + 20 ruflo = 60 total. Связано: PSR_v1 v3.4, Pravila v1.18, CLAUDE.md v2.4.*
*Прил. Н v2.3 от 17.05.2026 — A6 architecture-tooling: формализованы #36 adr-kit + #37 mermaid-skill + #38 architecture-patterns (§4.114.13), новая пятая off-phase подкатегория. 38 формализованных позиций (29 по фазам + 8 off-phase + 1 PG MCP) + 20 ruflo = 58 total. Связано: PSR_v1 v3.3, Pravila v1.17, CLAUDE.md v2.3.*
*Прил. Н v2.2 от 16.05.2026 — §4.10 реколлаж: ruflo переописан из «entry-point иерархии» в «advisory/automation-подсистему» (декларация приведена к рантайму). Связано: Pravila v1.16, PSR_v1 v3.2, CLAUDE.md v2.2.*
+91
View File
@@ -0,0 +1,91 @@
# ADR-003 Adopt the D3 audit and risk-management toolset
## Status
Accepted, 2026-05-17.
## Context
The `D3 «Аудит и управление рисками»` section of the automation map
(`docs/automation-graph.html`) had no tooling — `NODE_SECTION` tagged zero
nodes `D3`. Security audits of the portal (#1, #2, #3) were run ad-hoc with no
named toolset, and there was no standing store for closed decisions and their
residual risks.
This ADR records the toolset chosen to populate the section. It is the audit
counterpart of ADR-000, which adopted the ADR process itself.
## Decision
The D3 audit and risk-management toolset is:
- **`/security-review`** — the Anthropic command, customized at
`.claude/commands/security-review.md` with a project false-positive filter
(RLS, ПДн, economy hooks).
- **Trail of Bits Skills** — eight plugins from the `trailofbits` marketplace
(`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.
- **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
unresolved ones. D3 adds no separate risk-register tool.
- **Manual toolchain attack-surface procedure** — in `docs/audit/`, run on
plugin or MCP-server changes; community auto-auditors are deferred
(unverified provenance).
- **`audit-portal`** — a project skill encoding the repeated 14-phase
portal-audit method.
## Alternatives Considered
- **Install a dedicated risk-register tool.** Rejected: an ADR `## Consequences`
block plus the Открытые_вопросы registry already cover closed-decision risk
and open risk respectively; a third store would violate the "one tool per
task" rule (`CLAUDE.md` §5 п.6) and blur the boundaries fixed by ADR-000.
- **Enable all 38 Trail of Bits marketplace plugins.** Rejected: most target
blockchain, Android, C/C++, or macOS contexts irrelevant to a Laravel + Vue
codebase; the eight-plugin subset matches the project's actual audit surface.
`fp-check` was additionally dropped — it ships a lifecycle hook, and the
project keeps its hook chain minimal.
- **Install a community toolchain attack-surface auditor.** Deferred: the
candidate plugins have unverified provenance, and installing an unvetted
plugin to perform risk management would itself be a risk-management failure.
A manual procedure is used until a vetted tool is found.
## Consequences
**Positive:**
- The D3 map section is populated; portal audits have a named, repeatable
toolset instead of ad-hoc invocation.
- Closed decisions and their residual risks are version-controlled in
`docs/adr/`; the boundary with the open-questions registry is fixed by
ADR-000.
**Negative:**
- Trail of Bits and Security Guidance are third-party plugins — a bus-factor
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.
- The toolchain attack surface still depends on a manual procedure until a
vetted auto-auditor exists.
## Related Decisions
- ADR-000 — the ADR process and the `docs/adr/` to registry boundary this
record relies on.
- ADR-002 — tenant isolation via Row-Level Security; its rule drives the
`/security-review` project false-positive filter.
## References
- `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`
the D3 integration plan.
- `.claude/commands/security-review.md` — the customized security-review
command.
- `docs/audit/` — the audit procedures and the toolchain attack-surface check.
- `docs/Открытые_вопросы_v8_3.md` — the open-questions and open-risk registry.
+23
View File
@@ -0,0 +1,23 @@
# docs/audit — audit procedures and artifacts
This directory is the home of the `D3 «Аудит и управление рисками»` section of
the automation map (`docs/automation-graph.html`). It holds repeatable audit
procedures and their artifacts.
## Toolset
- `/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.
- `audit-portal` — the project skill encoding the 14-phase portal audit.
## Boundaries
- Closed decisions and their residual risks → `docs/adr/` (see ADR-003).
- Open product, business, and legal risks → `docs/Открытые_вопросы_v8_3.md`.
## Procedures
- `toolchain-attack-surface.md` — manual audit of the Claude Code plugin and
MCP-server attack surface.
+61
View File
@@ -0,0 +1,61 @@
# Toolchain attack-surface audit (manual procedure)
Part of the `D3 «Аудит и управление рисками»` section. Run this procedure
quarterly, and after any new Claude Code plugin or MCP server is added.
Motivation: the post-ruflo toolchain is large — about 20 ruflo plugins, ~210
MCP tools, and seven MCP servers in `.mcp.json` — and 2026 disclosures (npm
`postinstall` MCP-URL rewriting; the ClaudeBleed script-injection class) make
the toolchain itself a standing attack surface.
## 1. MCP servers
- Review every server in `.mcp.json``command`, `args`, `env`. Flag any
non-pinned `npx` package and any server reachable over the network.
- Confirm no MCP server URL was rewritten by a dependency `postinstall` script.
## 2. Plugins
- List `enabledPlugins` in `~/.claude/settings.json`. For each: source repo,
license, last commit, and the hooks it contributes.
- Flag any plugin that registers a `PreToolUse` hook with `decision: block`.
## 3. Hooks
- Diff the `hooks` blocks of `.claude/settings.json` and
`~/.claude/settings.json` against the last audited snapshot. Investigate any
unexplained change.
## 4. Permissions
- Review `permissions.allow` and `permissions.deny` — no broadened wildcard and
no new unscoped `Bash(*)` beyond what is already recorded.
## 5. Secrets
- Run `gitleaks` over the full history; confirm no token sits in a gitignored
cache file.
## Outcome
Record findings as P0P3 items in `docs/Открытые_вопросы_v8_3.md` (via the
`q-item-add` skill), or as an ADR in `docs/adr/` if a tooling decision results.
## Community auto-auditors — evaluated, deferred (2026-05-17)
The D3 integration evaluated two community plugins that would automate this
procedure. Both were deferred:
- **Claude Code Canary** (`geoffrey-young/anthropic-hackathon-2026`) — a
one-off hackathon entry (9 commits, 2 stars); the author explicitly
disclaims production use. It registers three broad lifecycle hooks
(SessionStart, PreToolUse, PostToolUse) and its design relies on the same
stderr-injection class it defends against. Rejected — unfit for a global
config and a heavy collision with the project hook chain.
- **Plugin Security Auditor** (an mcpmarket aggregator listing) — source
repository, author, and license could not be verified. Installing an
unverifiable plugin to perform security auditing is itself a risk-management
failure. Deferred until a vetted source is found.
Until a vetted auto-auditor exists, this manual procedure is the D3 control for
toolchain attack-surface risk.
+62 -3
View File
@@ -233,7 +233,7 @@ const NODES = [
{ id: 'psr_v1', label: 'PSR_v1 v3.2', group: 'rules', size: 32, ring: 1, ...pos(1, 150) },
{ id: 'tooling', label: 'Tooling v2.2', group: 'rules', size: 30, ring: 1, ...pos(1, 270) },
// ── ПЛАГИНЫ (11) ── второе кольцо ──────────────
// ── ПЛАГИНЫ (13) ── второе кольцо ──────────────
{ id: 'superpowers', label: 'Superpowers v5.1', group: 'plugins', size: 30, ring: 2, ...pos(2, 45) },
{ id: 'fd_plugin', label: 'Frontend Design', group: 'plugins', size: 26, ring: 2, ...pos(2, 135) },
{ id: 'upm', label: 'UI UX Pro Max', group: 'plugins', size: 22, ring: 2, ...pos(2, 165) },
@@ -246,6 +246,9 @@ const NODES = [
// A6 architecture-tooling (17.05.2026) — 2 плагина раздела «Архитектура систем»
{ 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) },
// 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) },
// ── СКИЛЫ SUPERPOWERS (14) — N sector (090) ────
{ id: 'sk_brainstorm', label: 'brainstorming', group: 'skills_sp', size: 18, ring: 3, ...pos(3, 5) },
@@ -263,12 +266,15 @@ const NODES = [
{ id: 'sk_wskills', label: 'writing-skills', group: 'skills_sp', size: 16, ring: 3, ...pos(3, 85) },
{ id: 'sk_elements', label: 'elements-of-style', group: 'skills_sp', size: 16, ring: 3, ...pos(3, 92) },
// ── СКИЛЫ ПРОЕКТА (4) — W sector (RLS/arch) ────
// ── СКИЛЫ ПРОЕКТА (6) — W sector (RLS/arch/audit) ────
{ id: 'sk_rls', label: 'rls-check', group: 'skills_proj', size: 20, ring: 3, ...pos(3, 305) },
{ id: 'sk_qitem', label: 'q-item-add', group: 'skills_proj', size: 20, ring: 3, ...pos(3, 220) },
{ id: 'sk_regression', label: 'regression', group: 'skills_proj', size: 20, ring: 3, ...pos(3, 260) },
// A6 architecture-tooling (17.05.2026) — вендоренный скил диаграмм
{ id: 'mermaid_skill', label: 'mermaid (skill)', group: 'skills_proj', size: 18, ring: 3, ...pos(3, 280) },
// 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) },
// ── ХУКИ (12) — S+infra + E (economy/skill) ───
{ id: 'hk_session', label: 'SessionStart:\ncontext-inject', group: 'hooks', size: 24, ring: 4, ...pos(4, 100) },
@@ -501,6 +507,16 @@ const EDGES = [
E('psr_v1', 'arch_patterns', 'R10.1 блок 1:\narchitecture-tooling'),
E('tooling', 'mermaid_skill', '§4.12: реестр\n(вендоренный скил)'),
// ── D3 AUDIT-SECURITY 17.05.2026 — связи новых узлов ──
E('psr_v1', 'tob_skills', 'R10.1 блок 1:\naudit-security'),
E('psr_v1', 'sec_guidance', 'R10.1 блок 1:\naudit-security'),
E('tooling', 'tob_skills', '§4.14 #39 — реестр'),
E('tooling', 'sec_guidance', '§4.15 #40 — реестр'),
E('sk_audit_portal', 'sk_security_review', 'оркеструет\nкак фазу аудита'),
E('sk_audit_portal', 'tob_skills', 'оркеструет\nглубокие кампании'),
E('sk_audit_portal', 'sk_regression', 'использует\nна фазе тестов'),
CONFLICT('tob_skills', 'mcp_semgrep', 'TB1: граница разграничена — Semgrep = inline SAST, Trail of Bits = глубокие on-demand аудит-кампании. Параллельное использование разрешено при разных сценариях.', 'GREEN'),
// ══════════════════════════════════════════════════
// КОНФЛИКТЫ — 3-color classification (iter2 §4)
// 🔴 не закрыт правилом / ⚫ возник на практике / 🟢 закрыт правилом
@@ -705,6 +721,41 @@ const NODE_DETAILS = {
[{ name: 'docs/architecture/', cond: 'C4-диаграммы → c4-context.md' }]
),
// ── D3 AUDIT-SECURITY (17.05.2026) ───────────────
tob_skills: nd(
'Marketplace-плагин Trail of Bits (`trailofbits/skills`) — курированный субсет 8 audit-плагинов: `differential-review`, `audit-context-building`, `supply-chain-risk-auditor`, `insecure-defaults`, `sharp-edges`, `static-analysis`, `variant-analysis`, `agentic-actions-auditor`. Глубокие on-demand аудит-кампании. Раздел D3. Tooling #39, off-phase audit-security.',
'При глубоком аудите безопасности: diff-ревью, supply-chain риски зависимостей, поиск вариантов уязвимостей, статический анализ (SARIF). Глубокие кампании — не inline SAST.',
'Правило PSR_v1 R10.1 блок 1 (audit-security, off-phase). Граница TB1: Semgrep MCP (#25) = inline SAST, ToB = глубокие on-demand кампании. CC-BY-SA-4.0 — не вендорено, лицензионный триггер TB4 не применяется. Не UI → вне R6.0/R6.1/R14. Tooling §4.14, CLAUDE.md §3.3 #39.',
[{ name: 'PSR_v1', cond: 'R10.1 блок 1: audit-security' }, { name: 'Tooling', cond: '§4.14 #39 — реестр' }],
[],
[{ name: 'MCP: semgrep', cond: 'TB1: граница — Semgrep inline SAST, ToB глубокие кампании' }],
[{ 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.',
[{ 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 ручной' }]
),
sk_security_review: nd(
'Кастомизированная Anthropic-команда `/security-review` — копия в `.claude/commands/security-review.md` с проектным FP-фильтром (RLS / ПДн / economy-хуки). Раздел D3. D3 #2.',
'При ручном security-review кода или PR — запуск `/security-review` с проектным контекстом для фильтрации ложных срабатываний.',
'Проектный FP-фильтр: RLS-политики / ПДн-поля / economy-хуки — не считаются уязвимостями. Раздел D3.',
[{ name: 'Tooling', cond: 'D3 #2 — проектная кастомизация' }],
[],
[{ name: 'sec_guidance', cond: 'оба D3 audit-security; sec_guidance — inline warn, security-review — ручной аудит' }, { name: 'audit-portal', cond: 'sk_audit_portal оркеструет security-review как фазу аудита' }]
),
sk_audit_portal: nd(
'Проектный скил — 14-фазный портальный аудит (дистилляция аудитов #1/#2/#3). Покрывает: PHP/Vue статический анализ, RLS, a11y, coverage, зависимости, secrets. Раздел D3.',
'При проведении полного аудита портала — запуск 14 фаз последовательно с документированием находок P0/P1/P2/P3.',
'Раздел D3. Оркеструет несколько инструментов: sk_security_review, Trail of Bits Skills, regression-скил. Находки фиксируются в docs/superpowers/audits/.',
[{ name: 'Tooling', cond: 'D3 — проектный аудит-скил' }],
[{ name: 'скил security-review', cond: 'оркеструет как security-фазу аудита' }, { name: 'Trail of Bits Skills', cond: 'оркеструет для глубоких кампаний' }, { name: 'скил regression', cond: 'использует на фазе тестов' }],
[{ name: 'скил security-review', cond: 'пара в D3 audit-security' }]
),
// ── СКИЛЫ SUPERPOWERS ────────────────────────────
sk_brainstorm: nd(
'Продумывает задачу вместе с заказчиком, формулирует варианты A/B/C и согласует дизайн до написания кода.',
@@ -1782,6 +1833,12 @@ 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: '—' },
// ── 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: 'скил' },
};
// Явные парные дубли (Фича 3) — попадают в кнопку «⧉ Дубли».
@@ -1864,7 +1921,7 @@ const SECTIONS = [
{ id: 'E7', bucket: 'E', label: 'Исследования' },
{ id: 'E8', bucket: 'E', label: 'Самообучение Claude' },
];
// Узел -> раздел. Покрывает все 106 узлов карты.
// Узел -> раздел. Покрывает все 110 узлов карты.
const NODE_SECTION = {
// правила (4)
pravila: 'E1', claude_md: 'E1', psr_v1: 'E1', tooling: 'E1',
@@ -1908,6 +1965,8 @@ const NODE_SECTION = {
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',
// D3 audit-security 17.05.2026 — раздел «Аудит и управление рисками» наполнен
tob_skills: 'D3', sec_guidance: 'D3', sk_security_review: 'D3', sk_audit_portal: 'D3',
};
// Производные индексы для рендера панели и Паспорта.
const SECTION_BY_ID = new Map(SECTIONS.map(s => [s.id, s]));
@@ -0,0 +1,578 @@
# D3 Audit & Risk-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:** Integrate the D3 top-5 (variant «а») — install #1 Trail of Bits Skills, formalize #2 `/security-review`, install #4 Anthropic Security Guidance, reuse #3 adr-kit (from the A6 plan) for the decision/risk register, spike #5 toolchain-auditor — and distill the project's 14-phase portal-audit method into a project skill, so the `D3 «Аудит и управление рисками»` map section becomes a populated, working playbook.
**Architecture:** Audit & risk-management is currently an **empty** functional section — `NODE_SECTION` in `docs/automation-graph.html` tags zero nodes `D3`. Two third-party tools are added: **Trail of Bits Skills** (marketplace plugin — 16 audit/vuln/supply-chain skills) and **Anthropic Security Guidance** (marketplace plugin — a warn-only PreToolUse hook). `/security-review` (Anthropic built-in) is customized via a project copy in `.claude/commands/`. The decision/risk register is **not** a new tool — it reuses **adr-kit** introduced by the A6 plan (`2026-05-17-a6-architecture-tooling-integration.md`); D3 only adds the risk-register convention. #5 (toolchain attack-surface auditor) is a **provenance spike** — community plugins are unvetted, so the default outcome is a documented manual procedure, not an install. Finally the project's own repeated 14-phase audit (#1/#2/#3) is distilled into an `audit-portal` project skill. All tools are non-UI → **audit-security** category, outside the PSR_v1 UI-pool. D3 artifacts live in `docs/audit/`.
**Tech Stack:** Trail of Bits Skills (marketplace `trailofbits/skills`, CC-BY-SA-4.0); `anthropics/claude-code-security-review` (Anthropic, the `/security-review` command); Anthropic Security Guidance (Anthropic-verified marketplace plugin, PreToolUse hook); adr-kit v0.13.1 (reused — see A6 plan); project normative docs; `docs/automation-graph.html` (vis.js); `skill-creator` (for the `audit-portal` skill).
**Sequencing (locked 2026-05-17):** the A6 plan (`2026-05-17-a6-architecture-tooling-integration.md`) runs to completion and pushes **first**; D3 Task 1 then forks from the updated `origin/main`. Rationale: D3 Task 5 reuses A6's adr-kit (AD1) and builds on its `docs/adr/ADR-000`; A6-first means D3 completes in one pass with no self-skip and no tool-number rework (NUM1). The two epics touch shared files (the map, 4 normative docs) → run strictly sequentially, never interleaved.
---
## Tool Identity (verified 2026-05-17 via WebFetch/WebSearch)
| # | Tool | Install mode | Source / License | Hooks? |
|---|---|---|---|---|
| 1 | **Trail of Bits Skills** (16 skills: `differential-review`, `audit-context-building`, `supply-chain-risk-auditor`, `insecure-defaults`, `sharp-edges`, `fp-check`, `static-analysis`, `c-review`, `variant-analysis`, `semgrep-rule-creator`, …) | Marketplace plugin — `/plugin marketplace add trailofbits/skills` | GitHub `trailofbits/skills`, **CC-BY-SA-4.0**, 113 commits, reputable AppSec vendor | Verify on install — expected none (skills only) |
| 2 | **`/security-review`** | Built-in Claude Code command; customized via a project copy in `.claude/commands/security-review.md` | GitHub `anthropics/claude-code-security-review`, Anthropic | None (slash command) |
| 4 | **Security Guidance** | Marketplace plugin (Anthropic) | `claude.com/plugins/security-guidance`, **Anthropic Verified**, ~153k installs | **YES** — one PreToolUse `Write\|Edit\|MultiEdit` hook, **warn-only** (8 vuln categories, session-scoped, does not block) |
| 3 | **adr-kit** | **NOT installed by D3** — reused from the A6 plan | (see A6 plan) | (see A6 plan) |
| 5 | **toolchain attack-surface auditor** | **NOT installed by default** — Task 6 is a provenance spike | community (`geoffrey-young/anthropic-hackathon-2026` "Claude Code Canary"; "Plugin Security Auditor" on mcpmarket) — **provenance unverified** | TBD by spike |
**Verification status:** #1 and #4 — confirmed via WebFetch (skill list, license, install command / publisher, hook behaviour). #2 — present in this environment as the `/security-review` command; repo confirmed via search. #3 — defined by the A6 plan. #5**search-level only, provenance NOT verified** (this is by design — Task 6).
---
## Design Decisions & Conflict Audit
Pattern follows the K1K8 / AK1CC1 audits used for claude-mem and the A6 plan. Verified against the three sources, project `.claude/settings.json`, `.mcp.json`, and the A6 plan.
| # | Tool | Sev | Conflict | Resolution (locked) |
|---|---|---|---|---|
| TB1 | ToB Skills | 🟡 | `static-analysis` / `semgrep-rule-creator` overlap the existing **Semgrep MCP** (#25, section A8) → CLAUDE.md §5 п.6 "не два инструмента на одну задачу". | Boundary, documented in the Tooling entry (Task 8): **Semgrep MCP** = inline SAST during routine dev / CI; **ToB Skills** = deep on-demand *audit campaigns*. Different cadence, not the same task. |
| TB2 | ToB Skills | 🟡 | `differential-review` / `c-review` overlap `superpowers:requesting-code-review` (§12) and `/security-review`. | Different depth/scope: `/security-review` = quick diff scan; `requesting-code-review` = general review; ToB `differential-review` = security-focused *deep* audit of a change-set. §12.2 task-map **unchanged** — ToB skills are not added to it. |
| TB3 | ToB Skills | 🟢 | Could the plugin register CC lifecycle hooks? | Verify on install (Task 3 Step 4). Skills-only plugins register none; if it does → stop and re-audit. |
| TB4 | ToB Skills | 🟢 | CC-BY-SA-4.0 (ShareAlike) — could obligate the project. | None — ToB stays a **marketplace plugin** (cache outside the repo, not vendored, not modified). ShareAlike triggers only on redistribution/derivative works. Do **not** vendor ToB skill files into the repo. Noted in the Tooling entry. |
| SG1 | Security Guidance | 🟡 | Adds a PreToolUse `Write\|Edit\|MultiEdit` hook → coexists with the project `CLAUDE.md`-direct-edit-warn hook (`.claude/settings.json`) and the economy/skill-discipline PreToolUse hooks (in `.claude/settings.local.json` / `~/.claude/settings.json`). | Verified **warn-only** — it prints session-scoped warnings, never blocks (WebFetch). Task 4 Steps 3-4 verify: the SG hook fires AND the economy + ruflo + CLAUDE-warn chain still fire, no `decision:block`. Accept +~34 ms/edit latency (memory: per-hook median). |
| SG2 | Security Guidance | 🟢 | Overlaps ToB `insecure-defaults` / Semgrep. | None — SG = real-time inline warn *during the edit* (8 fixed categories); ToB/Semgrep = on-demand audit. Different timing. Additive. |
| SG3 | Security Guidance | 🟡 | Anthropic-verified marketplace plugin, unregistered = PSR_v1 R0.2/R10 violation on use. | Register in 4 normative homes (Task 8). |
| SR1 | `/security-review` | 🟢 | A customized `.claude/commands/security-review.md` shadows the built-in command. | Intended — this is the documented Anthropic customization path. Note in the Tooling entry that the project copy replaces the built-in. |
| AD1 | ADR / risk register | 🟡 | D3 needs a decision/risk register. Installing a *second* ADR tool collides with **adr-kit** (A6 plan, Tooling #36) → §5 п.6. | D3 **reuses adr-kit** — installs nothing. The risk register = adr-kit `## Consequences` (risks + mitigations) + the `Открытые_вопросы` registry (open risks). Task 5 only adds the convention. |
| AD2 | ADR / risk register | 🟢 | D3 Task 5 depends on the A6 plan having installed adr-kit. | **Locked sequencing** — A6 runs to completion first; D3 Task 1 forks from the updated `origin/main`. Task 5 therefore always finds adr-kit present; there is no self-skip path. |
| TA1 | #5 toolchain auditor | 🟡 | Community plugin-auditors are unvetted. Installing an unverified plugin to *do risk management* is itself a D3 risk-management failure. | Task 6 = provenance spike (`superpowers:systematic-debugging` style). **Default outcome = defer** + a documented manual `docs/audit/toolchain-attack-surface.md` procedure. Install only if provenance clears the bar in the spike. |
| AP1 | `audit-portal` skill | 🟢 | A new project skill duplicating `superpowers` or ToB skills. | None — `audit-portal` encodes the project's *own* 14-phase methodology (run 3× as audits #1/#2/#3), distilled per the E8 triada policy (workflow ≥3× → skill). It *orchestrates* `/security-review` + ToB skills, it does not reimplement them. |
| CC1 | all | 🟡 | Bus-factor. | ToB = reputable firm (low risk); Security Guidance = Anthropic (low risk); #5 = community (high risk → spike, default defer). Noted in the Tooling entry. |
| NUM1 | normative sync | 🟡 | The A6 plan (untracked, not yet executed) claims Tooling slots **#36-#38**, PSR_v1 R10.1 rows, and plugin slots #10/#11. D3 must not collide. | Task 8 Step 1 reads the **live** `docs/Tooling_v8_3.md` §0 counter and assigns D3's entries sequentially after whatever is current (≥#36 if A6 has not landed, ≥#39 if it has). Never hard-code a number before reading. |
**Severable scope.** Minimal D3 = Tasks 1-3 + 8-10 (formalize `/security-review`, install ToB, normative sync, map, finish) — already populates the section. Independently severable: **Task 4** (Security Guidance hook — env change), **Task 5** (ADR risk register — depends on A6), **Task 6** (#5 spike), **Task 7** (`audit-portal` skill). Primary path below = everything. **Selected 2026-05-17: full integration — all 10 tasks, no task severed.**
---
## File Structure
| File | Created / Modified | Responsibility |
|---|---|---|
| `.claude/commands/` | Create dir | Project slash-command overrides |
| `.claude/commands/security-review.md` | Create | Customized `/security-review` — project false-positive filter |
| `docs/audit/` | Create dir | D3 home — audit procedures & artifacts |
| `docs/audit/README.md` | Create | Index — defines `docs/audit/` purpose, links D3 + the toolset |
| `docs/audit/toolchain-attack-surface.md` | Create | #5 deliverable — manual toolchain attack-surface audit procedure |
| `.claude/skills/audit-portal/` | Create | The distilled 14-phase portal-audit project skill (`SKILL.md`) |
| `docs/adr/ADR-003-audit-risk-tooling.md` | Create (conditional — AD2) | Seed risk-ADR documenting the D3 tooling decision; only if adr-kit present |
| `~/.claude/settings.json` | Modify | `enabledPlugins` += Trail of Bits + Security Guidance; `extraKnownMarketplaces` += 1-2 |
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new audit-security subsection(s) + §0 counter bump |
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — new audit-security rows |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — audit-security category note |
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, new §3.3 audit-security rows |
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
| `docs/automation-graph.html` | Modify | New D3 nodes → `NODE_SECTION` D3; header metrics |
| `cspell-words.txt` | Modify (conditional) | New audit/security vocabulary |
---
## Task 1: Pre-flight — baseline, 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/d3-audit-risk-tooling origin/main
```
Expected: record `origin/main` HEAD SHA as the regression baseline; new branch `feat/d3-audit-risk-tooling` created off it. (Push pattern at the end: `git push origin feat/d3-audit-risk-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 `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`. This is the SG1 baseline — Task 4 compares against it.
Expected (project `.claude/settings.json`): 2× UserPromptSubmit (ruflo-recall, ruflo-queen), 1× PreToolUse (CLAUDE.md-warn), 2× PostToolUse (markdownlint-fix, schema-CHANGELOG-reminder). Economy/skill-discipline/Stop-verifier hooks live in the local/global file — record them too.
- [ ] **Step 3: Baseline regression**
```
/regression quick
```
Expected: GREEN. Also record current Pest / Vitest counts from the last green run (memory `project_state.md`: Pest 891/888/3sk/0, Vitest 102f/849/3sk/0) — D3 touches no `app/` code, so the final run must match.
- [ ] **Step 4: Check whether the A6 plan has landed (AD2 / NUM1)**
```bash
git log --oneline origin/main | grep -iE "adr-kit|architecture-patterns|mermaid" | head
ls ~/.claude/plugins/cache/ | grep -i adr-kit
```
Read `docs/Tooling_v8_3.md` Прил. Н §0 — record the **live** tool counter.
Record: **A6 landed?** yes/no — drives Task 5 (conditional) and Task 8 numbering.
- [ ] **Step 5: Fact-check the tools**
Confirm assumptions still hold:
- `https://github.com/trailofbits/skills` — marketplace `trailofbits/skills`, CC-BY-SA-4.0, skills-only (no CC lifecycle hooks).
- `https://claude.com/plugins/security-guidance` — Anthropic-verified; confirm the exact `/plugin marketplace add …` + `/plugin install …` strings (the marketplace org/name).
- `https://github.com/anthropics/claude-code-security-review` — confirm the path of `security-review.md` in the repo.
If ToB Skills or Security Guidance now differs materially (e.g. ToB registers lifecycle hooks) → **stop**, re-audit TB3/SG1.
No repo files changed → no commit.
---
## Task 2: Formalize `/security-review` — customized project copy (#2)
**Files:**
- Create: `.claude/commands/` (dir), `.claude/commands/security-review.md`
- [ ] **Step 1: Create the commands directory**
```bash
mkdir -p ".claude/commands"
```
- [ ] **Step 2: Fetch the upstream command file**
```bash
git clone --depth 1 https://github.com/anthropics/claude-code-security-review.git /tmp/ccsr-src
cp /tmp/ccsr-src/security-review.md ".claude/commands/security-review.md"
rm -rf /tmp/ccsr-src
```
(If `security-review.md` lives in a subfolder — confirmed in Task 1 Step 5 — copy from that path.)
- [ ] **Step 3: Add the project false-positive filter**
Append to `.claude/commands/security-review.md` a `## Project false-positive guidance (Лидерра)` section with these concrete directions:
```markdown
## Project false-positive guidance (Лидерра)
When filtering findings for this repository, treat the following as expected, not findings:
- Missing application-layer tenant checks where the table has PostgreSQL RLS — tenant
isolation is enforced at the DB layer (`SET LOCAL app.current_tenant_id`, 5 roles,
39 policies; see ADR-002 / CLAUDE.md §2). DO still flag queued jobs running as
`crm_supplier_worker` (BYPASSRLS) that lack an explicit `where('tenant_id', …)`.
- `tools/*.mjs` economy/ruflo hook scripts using `child_process.spawnSync` / `process.env`
— these are intentional local CLI hooks, not user-facing code paths.
- Secrets: gitleaks already gates secrets at pre-commit + pre-push; do not re-report
unless a NEW hardcoded credential appears in the diff.
- Test factories / seeders using `Faker` and predictable values — test-only.
DO prioritise: HMAC/webhook verification gaps, signed-URL handling, auth/tenant
middleware on `/api/*` routes, ПДн handling (152-ФЗ), and mass-assignment on Eloquent models.
```
- [ ] **Step 4: Smoke-test the customized command**
Stage the new file, then run the command:
```bash
git add .claude/commands/security-review.md
```
Invoke `/security-review`. Expected: it runs, reads the customized `.claude/commands/security-review.md`, and produces a structured findings report (a docs/command change → expected result "no security-relevant findings"). Functional smoke only.
- [ ] **Step 5: Commit**
```bash
git commit -m "feat(audit): customize /security-review with project FP-filter (D3 #2)"
```
---
## Task 3: Install Trail of Bits Skills (#1)
**Files:** Modify `~/.claude/settings.json` (the `Edit` triggers the `ask` permission — expected). No repo file changed.
- [ ] **Step 1: Add the marketplace and install**
```
/plugin marketplace add trailofbits/skills
/plugin install <plugin-name>@trailofbits-skills
```
(Resolve the exact plugin name from the marketplace listing shown after `add` — likely `skills@trailofbits-skills` or per the repo's `plugin.json`.)
- [ ] **Step 2: Reload**
```
/reload-plugins
```
- [ ] **Step 3: Verify the skills loaded**
```bash
ls ~/.claude/plugins/cache/trailofbits-skills/*/skills/
```
Expected: ~16 skill directories including `differential-review`, `supply-chain-risk-auditor`, `audit-context-building`, `insecure-defaults`, `fp-check`, `static-analysis`. Read `~/.claude/settings.json``enabledPlugins` contains the ToB entry.
- [ ] **Step 4: Verify NO lifecycle hooks were added (TB3)**
Read the `hooks` block of `~/.claude/settings.json` AND project `.claude/settings.json`. Both must be **unchanged** vs the Task 1 Step 2 snapshot. If ToB injected a `hooks` entry → stop and re-audit TB3.
- [ ] **Step 5: Smoke-test a ToB audit skill**
Invoke the `differential-review` skill against the last commit's diff (`git show HEAD`). Expected: the skill activates and returns a security-focused review of the change-set. Functional smoke — no file output, no findings expected on a docs change.
- [ ] **Step 6: Confirm economy/ruflo chain intact**
Submit a trivial prompt; the economy marker still appears, no hook errors. No repo files changed in Task 3 → no commit.
---
## Task 4: Install Security Guidance (#4) — *severable*
**Files:** Modify `~/.claude/settings.json`. No repo file changed.
> Skip this task for the minimal D3 scope (see "Severable scope"). It is the only env-changing step that adds a hook.
- [ ] **Step 1: Install the plugin**
Run the exact strings confirmed in Task 1 Step 5, e.g.:
```
/plugin marketplace add <anthropic-marketplace>
/plugin install security-guidance@<anthropic-marketplace>
/reload-plugins
```
- [ ] **Step 2: Verify the plugin and its hook**
Read `~/.claude/settings.json``enabledPlugins` contains `security-guidance…: true`. Confirm the plugin contributes exactly one PreToolUse `Write|Edit|MultiEdit` hook (via the plugin's own `hooks/`, merged at runtime — it may not appear literally in `settings.json`).
- [ ] **Step 3: Verify warn-only behaviour + chain integrity (SG1)**
Make a trivial edit to a scratch file containing an obvious pattern (e.g. `eval("x")` in a `.js` temp file). Expected:
- The Security Guidance warning prints to stderr.
- The edit **still applies** — the hook does NOT block (`decision:block` absent).
- The project `CLAUDE.md`-warn hook, the economy hooks, and the ruflo hooks **all still fire** — compare to the Task 1 Step 2 snapshot.
If the SG hook blocks an edit, or any pre-existing hook stops firing → **stop**, document, and treat Task 4 as deferred.
- [ ] **Step 4: Clean up the scratch file**
```bash
rm <scratch-file>
```
No repo files changed in Task 4 → no commit.
---
## Task 5: ADR risk-register convention — reuse adr-kit (#3)
**Files:** Create `docs/adr/ADR-003-audit-risk-tooling.md`
> Sequencing is locked — A6 runs first, so adr-kit is present when D3 starts. Step 1 is a confirmation, not a branch. If adr-kit is somehow absent, the locked sequencing was violated → stop and run A6 to completion before continuing.
- [ ] **Step 1: Confirm adr-kit is available**
```bash
ls ~/.claude/plugins/cache/*adr-kit*/
ls docs/adr/
```
Expected: the adr-kit plugin cache exists and `docs/adr/ADR-000-adr-process.md` exists (created by the A6 plan). If absent → the locked sequencing was violated; stop and run A6 to completion first.
- [ ] **Step 2: Write the D3 risk-ADR**
Create `docs/adr/ADR-003-audit-risk-tooling.md`:
```markdown
# ADR-003: Audit & risk-management tooling (D3)
- **Status:** Accepted
- **Date:** 2026-05-17
- **Deciders:** Дмитрий
## Context
The `D3 «Аудит и управление рисками»` map section had zero tooling. Audits #1/#2/#3
were run ad-hoc. There was no standing decision/risk register.
## Decision
- Security/code audit — `/security-review` (customized) + Trail of Bits Skills.
- Inline risk warnings — Anthropic Security Guidance.
- Decision/risk register — **adr-kit ADRs** (this directory). An ADR's
`## Consequences` section records risks + mitigations; the `Открытые_вопросы`
registry holds *open, unresolved* risks. No separate risk-register tool.
- Toolchain attack-surface — manual procedure `docs/audit/toolchain-attack-surface.md`
(community auto-auditors deferred — unverified provenance).
- The 14-phase portal-audit method is encoded as the `audit-portal` project skill.
## Consequences
- Positive: D3 section populated; audits repeatable.
- Risk: ToB / Security Guidance are third-party — bus-factor; mitigated by
marketplace-cache pinning. Mitigation: re-verify on `/plugin` upgrades.
- Risk: +1 PreToolUse hook latency (~34 ms/edit) — accepted.
## Enforcement
None — D3 tools are advisory; verified by audits and code review.
```
- [ ] **Step 3: Lint + commit**
```bash
npx markdownlint-cli2 "docs/adr/ADR-003-audit-risk-tooling.md"
git add docs/adr/ADR-003-audit-risk-tooling.md
git commit -m "feat(adr): ADR-003 — D3 audit & risk-management tooling decision"
```
---
## Task 6: #5 toolchain attack-surface — provenance spike + manual procedure (TA1) — *severable*
**Files:** Create `docs/audit/` (dir), `docs/audit/README.md`, `docs/audit/toolchain-attack-surface.md`
- [ ] **Step 1: Provenance spike on the community auto-auditors**
Use `superpowers:systematic-debugging` discipline — 3 hypotheses, falsify each:
- H1 "Claude Code Canary (`geoffrey-young/anthropic-hackathon-2026`) is install-safe" — check: stars/maintenance, hackathon-only?, does it register hooks / read secrets?
- H2 "`Plugin Security Auditor` (mcpmarket) is install-safe" — check: real GitHub source, license, maintainer.
- H3 "neither clears the bar; a manual procedure is sufficient for D3 now."
Record the verdict. **Default expectation: H3** — defer the install.
- [ ] **Step 2: Create the audit directory + index**
```bash
mkdir -p "docs/audit"
```
Create `docs/audit/README.md`:
```markdown
# docs/audit — audit procedures & artifacts
Home of the `D3 «Аудит и управление рисками»` section. Holds repeatable audit
procedures. Toolset: `/security-review` (customized), Trail of Bits Skills,
Security Guidance, the `audit-portal` skill. Decisions/risks → `docs/adr/`.
Open product/business/legal risks → `docs/Открытые_вопросы_v8_3.md`.
```
- [ ] **Step 3: Write the manual toolchain attack-surface procedure (#5 deliverable)**
Create `docs/audit/toolchain-attack-surface.md` — a checklist covering the post-ruflo attack surface (20 ruflo plugins + ~210 MCP tools + 7 `.mcp.json` servers), motivated by the May-2026 MCP-MITM / ClaudeBleed disclosures:
```markdown
# Toolchain attack-surface audit (manual procedure)
Run quarterly, or after any new plugin / MCP server is added.
## 1. MCP servers
- Review every server in `.mcp.json` — command, args, env. Flag any non-pinned
`npx` package and any server reachable over the network.
- Confirm no MCP URL was rewritten (npm postinstall MITM vector).
## 2. Plugins
- List `enabledPlugins` in `~/.claude/settings.json`. For each: source repo,
license, last commit, hooks contributed.
- Flag plugins that register `PreToolUse` hooks with `decision:block`.
## 3. Hooks
- Diff `.claude/settings.json` + `.claude/settings.local.json` hook blocks vs
the last audited snapshot. Any unexplained change → investigate.
## 4. Permissions
- Review `permissions.allow` / `deny` — no broadened wildcard, no new `Bash(*)`.
## 5. Secrets
- `gitleaks detect` full history; confirm no token in a gitignored cache file.
## Outcome
Record findings as P0-P3 in `docs/Открытые_вопросы_v8_3.md` (via `q-item-add`)
or as an ADR if a tooling decision results.
```
If Step 1 cleared a community auditor, additionally note it here as an optional accelerator.
- [ ] **Step 4: Lint + commit**
```bash
npx markdownlint-cli2 "docs/audit/*.md"
git add docs/audit/
git commit -m "docs(audit): toolchain attack-surface procedure + audit/ home (D3 #5)"
```
---
## Task 7: Distill the 14-phase portal audit into the `audit-portal` skill — *severable*
**Files:** Create `.claude/skills/audit-portal/SKILL.md`
- [ ] **Step 1: Gather the source material**
Read the three prior audit plans/findings as the methodology source:
`docs/superpowers/plans/2026-05-12-portal-full-audit.md`, `2026-05-13-portal-full-audit-2.md`, `2026-05-14-portal-full-audit-3.md` (and their `docs/superpowers/audits/` findings). Extract the stable 14-phase structure and the P0-P3 severity convention.
- [ ] **Step 2: Author the skill via skill-creator**
Invoke `skill-creator`. Create `.claude/skills/audit-portal/SKILL.md` — a project skill (auto-discovered like `rls-check` / `regression`) whose body:
- Lists the 14 audit phases in order, each with its concrete check.
- Maps each phase to the tool that runs it (`/security-review`, ToB `audit-context-building` / `static-analysis` / `supply-chain-risk-auditor`, `regression` skill, Pa11y, `rls-reviewer`).
- States the P0-P3 severity rubric and that findings are filed via `q-item-add`.
- `description:` frontmatter triggers on "audit the portal" / "full audit" / "портальный аудит".
- [ ] **Step 3: Verify the skill is discoverable**
```
/reload-plugins
```
Confirm `audit-portal` appears among available skills. Confirm no `hooks` block changed.
- [ ] **Step 4: Lint + commit**
```bash
npx markdownlint-cli2 ".claude/skills/audit-portal/SKILL.md"
git add .claude/skills/audit-portal/
git commit -m "feat(audit): distill 14-phase portal audit into audit-portal skill (D3)"
```
---
## Task 8: Normative registry sync (TB1/SG3/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 and 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 D3's numbers sequentially from `counter + 1`**after** any A6 entries already present. Record the two assigned numbers as `#N` (Trail of Bits Skills) and `#N+1` (Security Guidance).
- [ ] **Step 2: Add the Tooling Прил. Н audit-security subsection(s)**
Edit `docs/Tooling_v8_3.md`: add a new subsection for `#N Trail of Bits Skills` and `#N+1 Security Guidance`, category **audit-security** (off-phase). Per tool record: ToB Skills (marketplace `trailofbits/skills`, CC-BY-SA-4.0; **not vendored** — TB4; boundary vs Semgrep MCP — TB1); Security Guidance (Anthropic-verified marketplace plugin, one warn-only PreToolUse hook — SG1). Also document `/security-review` (Anthropic built-in, customized at `.claude/commands/security-review.md`) as a sub-bullet of the same subsection — it is a built-in, not an installed tool, so no numbered slot. 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 Trail of Bits Skills + Security Guidance rows to R10.1, category **audit-security** (off-phase) — explicitly *outside* the UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline (same treatment as `claude-md-management` and the A6 architecture-tooling category). 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 audit-security category note covering the two tools + `/security-review`, alongside the existing infrastructure / debug-runtime notes. 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 priority-chain row 2b count bump, new §3.3 audit-security rows for `#N`/`#N+1` + the `/security-review` sub-note. 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(audit): register Trail of Bits + Security Guidance (D3 audit-security)"
```
---
## Task 9: Reflect D3 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 plugin node (`claude_md_mgmt`) and a project-skill node (`sk_rls`) across `NODES`, `NODE_DETAILS`/`nd(...)`, `NODE_SECTION`, and the "Паспорт узла" date fields. Record the current node/edge counts from the header (memory: 103 nodes / 106 edges).
- [ ] **Step 2: Add the D3 nodes**
Add to `NODES`, replicating the template shapes:
- `tob_skills` — label `Trail of Bits\nskills`, plugins group.
- `sec_guidance` — label `Security\nGuidance`, plugins group. *(omit if Task 4 was severed)*
- `sk_security_review` — label `security-review`, skills group (the customized Anthropic command).
- `sk_audit_portal` — label `audit-portal`, project-skills group. *(omit if Task 7 was severed)*
Add matching `nd(...)` / `NODE_DETAILS` entries (Russian, per the file's convention) with Паспорт `since: '2026-05-17'`. Add edges where real: `sk_audit_portal → sk_security_review` / `→ tob_skills` ("оркеструет"); `sec_guidance` as a PreToolUse-hook node if the file models hooks that way.
- [ ] **Step 3: Map the new nodes to section D3**
In `NODE_SECTION` add (omit any severed node):
```js
tob_skills: 'D3', sec_guidance: 'D3', sk_security_review: 'D3', sk_audit_portal: 'D3',
```
`D3 «Аудит и управление рисками»` goes from 0 → up to 4 nodes — the section is no longer empty.
- [ ] **Step 4: Update header metrics + group-count comments**
Bump the node count (and edge count if edges were added) in the map header/legend. 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 local `http.server` — quirk 90: `file://` rejected). Expected: 0 JS console errors; the new nodes render; clicking section `D3` highlights them.
- [ ] **Step 6: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(map): D3 nodes — closes section «Аудит и управление рисками»"
```
---
## Task 10: Final regression & branch finish
**Files:** none modified
- [ ] **Step 1: Full regression**
```
/regression full
```
Expected: GREEN. D3 changed no `app/` code → Pest / Vitest counts must match the Task 1 Step 3 baseline (0 regressions). Record exact counts; write out any failure with file:line.
- [ ] **Step 2: Confirm the hook chain is intact**
Economy marker still appears; Stop verifier still runs; ruflo + `CLAUDE.md`-warn hooks fire. If Task 4 ran, the Security Guidance hook is present and warn-only. Compare to the Task 1 Step 2 snapshot.
- [ ] **Step 3: 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/audit/*.md`, `docs/adr/ADR-003*.md`, `.claude/commands/security-review.md` are scanned — fix or `.lychee.toml`-exclude any link).
- [ ] **Step 4: 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/d3-audit-risk-tooling:main`.
---
## Self-Review
**1. Spec coverage (D3 top-5, variant «а»).** #1 ToB Skills — install (Task 3). #2 `/security-review` — formalize (Task 2). #3 ADR/risk register — reuse adr-kit (Task 5, conditional). #4 Security Guidance — install (Task 4). #5 toolchain auditor — spike + manual procedure (Task 6). Bonus `audit-portal` skill (Task 7). Section closure: normative (Task 8), map (Task 9), regression/finish (Task 10). Conflict audit: TB1→T8.2, TB2→no task (map note), TB3→T3.4, TB4→T8.2, SG1→T4.3, SG2→no task, SG3→T8, SR1→T8.2, AD1→T5, AD2→T5 Step 1 (conditional), TA1→T6, AP1→no task, CC1→T8.2, NUM1→T8.1. No gaps.
**2. Placeholder scan.** `#N`/`#N+1` (Task 8) and the exact `/plugin install` plugin names (Tasks 3-4) are **runtime-resolved by design** — the live Tooling counter and the marketplace listing are not knowable before Task 1 Step 5 / Task 8 Step 1, which carry concrete resolution criteria (matches the A6 plan's "read the current node count" pattern). All file contents (security-review FP-filter, ADR-003, audit READMEs, the toolchain procedure) are shown in full. No "TBD"/"handle edge cases".
**3. Consistency.** Branch `feat/d3-audit-risk-tooling` consistent T1↔T10. Node ids `tob_skills` / `sec_guidance` / `sk_security_review` / `sk_audit_portal` consistent T9 Steps 2-3. Category name **audit-security** consistent T8 Steps 2-4. Severable tasks (4, 5, 6, 7) flagged at the header, in "Severable scope", and inline. `docs/audit/` / `docs/adr/` paths consistent.
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. Two execution options:
1. **Subagent-Driven (recommended for the doc-heavy tasks)** — fresh subagent per task, two-stage review. *Caveat:* Tasks 3, 4, 7 (`/plugin …`, `/reload-plugins`, `skill-creator`), Task 2 Step 4 + Task 3 Step 5 (skill/command invocations) and Task 8 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 plan).
**Decided 2026-05-17:** scope = **full integration** (all 10 tasks); sequencing = **A6 plan first, then D3** (locked — see "Sequencing" in the header).
One open item before execution: execution method — **1** (Subagent-Driven) or **2** (Inline, recommended here). And: D3 cannot start until the A6 epic is complete and pushed — confirm who runs A6.
@@ -0,0 +1,423 @@
# Sprint 5D — Cleanup dev-артефактов (I3 + I4) 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:** Убрать production fake-data fallback (вьюхи рендерят выдуманные сделки/тенантов/инциденты при сбое API) и DEV-gate баннер `_dev_plain_code` в impersonation-диалоге.
**Architecture:** Аудит портала, находки I3 + I4 (под-план Sprint 5D). 8 production-файлов инициализируют reactive-state mock-данными из `composables/mock*.ts` и держат их как fallback при ошибке API — в проде юзер при 500/network видит фейковые данные. Фикс: state инициализируется пустым (`[]` / нули); при ошибке показывается error-alert (уже есть `v-alert v-if="fetchError"` в каждом файле — меняется только текст). Файлы `mock*.ts` **остаются на месте** (типы + UI-константы там реальные; mock-массивы используются только тестами как фикстуры) — решение заказчика «убрать fake-fallback», без переезда файлов. I4: баннер `_dev_plain_code` гейтится за `import.meta.env.DEV`.
**Tech Stack:** Vue 3.5 + Vuetify 3.12 + TypeScript, Vitest 4 (`@vue/test-utils` + jsdom).
**Scope (I1 — отложен):** I1 (DevIndexBadge/DevIndexOverlay removal) решением заказчика отложен до заморозки UI — не в этом плане.
**НЕ в scope (discovered minor):** `DealsView.vue` строка `const newToday = 3; // mock` — инлайн-литерал (не `mock*.ts`-композабл), показывает фейковое «+3 новых лида с утра». Требует реальной backend-метрики — отдельная находка, в 5D не трогается.
---
## File Structure
Production (8 файлов — убрать mock-fallback):
- `app/resources/js/views/DealsView.vue``dealsState` init из `MOCK_DEALS`.
- `app/resources/js/views/KanbanView.vue``dealsByStatus` + `totalDeals` init из `MOCK_DEALS`.
- `app/resources/js/components/deals/NewDealDialog.vue``projectOptions`/`managerOptions` init из `MOCK_PROJECTS`/`MOCK_MANAGERS`.
- `app/resources/js/components/deals/DealDetailDrawer.vue``events` init из `MOCK_EVENTS`, fallback на 2 catch-путях.
- `app/resources/js/views/admin/AdminBillingView.vue``rowsState`/`summary` init из `ADMIN_BILLING_TENANTS`/`ADMIN_BILLING_SUMMARY`.
- `app/resources/js/views/admin/AdminIncidentsView.vue``rowsState` + initial `stats` init из `ADMIN_INCIDENTS`.
- `app/resources/js/views/admin/AdminSystemView.vue``settingsState` init из `ADMIN_SYSTEM_SETTINGS` (тип `AdminSystemSetting` — оставить).
- `app/resources/js/views/admin/AdminTenantsView.vue``tenantsState`/`stats` init из `MOCK_TENANTS`/`MOCK_STATS`.
Production (1 файл — I4):
- `app/resources/js/components/admin/ImpersonationDialog.vue` — баннер `dev-code-banner` за `import.meta.env.DEV`.
Не трогаются: `composables/mock*.ts` (типы/константы реальны, mock-массивы — тест-фикстуры).
Tests (обновить — спеки писались вокруг mock-initial-state):
- `app/tests/Frontend/DealsView.spec.ts`, `DealsViewRedesign.spec.ts`, `KanbanView.spec.ts`
- `app/tests/Frontend/NewDealDialog*.spec.ts`, `DealDetailDrawer*.spec.ts` (имена уточнить через `ls`)
- `app/tests/Frontend/AdminBillingView.spec.ts`, `AdminBillingViewApi.spec.ts`
- `app/tests/Frontend/AdminIncidentsView.spec.ts`, `AdminIncidentsViewApi.spec.ts`
- `app/tests/Frontend/AdminSystemView.spec.ts`, `AdminTenantsView.spec.ts`, `AdminTenantsViewApi.spec.ts`
---
## Общий тест-принцип (для всех задач T1–T4)
Init state → `[]` ломает существующие тесты двух типов. Стратегия:
1. **Тесты, использующие mock как фикстуру** (рендер строк, bulk-actions, фильтры). Mock-композабл импортируется **в spec-файле** как фикстура (это разрешено — тесты могут использовать mock-данные). Два варианта вернуть данные в state — выбрать минимально-инвазивный для конкретного спека:
- **Вариант A (предпочтительно для `*Api.spec.ts` и где API уже `vi.mock`'нут):** мок data-API резолвится фикстурой → success-путь `loadX()` наполняет state, `fetchError=false`.
- **Вариант B (для smoke-спеков без `vi.mock`):** после `mount` + `flushPromises()` засеять exposed-state: `vm.<stateRef>.push(...<MOCK_FIXTURE>.map(clone))`. Для seed может потребоваться расширить `defineExpose` (см. задачи).
2. **Тесты, явно ассертящие fake-fallback как поведение** (напр. `AdminBillingViewApi.spec.ts:96-106``expect(vm.rowsState.length).toBeGreaterThan(0)` после reject, заголовок «MOCK fallback остаётся»). **Инвертировать**: `toBe(0)`, заголовок → «state пустой».
3. **Новый regression-тест на каждый production-файл:** API/loadX отклоняется → state пустой (`length === 0`) **и** error-видим (`fetchError`/alert). См. red-green в шагах задач.
Каждая задача завершается полным зелёным прогоном `npm run test:vue` (0 fail) + `npm run lint:vue` + `npm run type-check`.
---
## Task 1: DealsView + KanbanView — убрать MOCK_DEALS fallback
**Files:**
- Modify: `app/resources/js/views/DealsView.vue`
- Modify: `app/resources/js/views/KanbanView.vue`
- Test: `app/tests/Frontend/DealsView.spec.ts`, `DealsViewRedesign.spec.ts`, `KanbanView.spec.ts`
- [ ] **Step 1: Regression-тест на пустой state при ошибке (DealsView) — должен упасть**
В `DealsView.spec.ts` добавить (рядом с C3-тестами в конце файла):
```ts
test('I3: loadDeals reject → dealsState пустой + fetchError', async () => {
vi.spyOn(dealsApi, 'listDeals').mockRejectedValue(new Error('500'));
const wrapper = await mountDeals();
const auth = useAuthStore();
auth.user = { id: 1, tenant_id: 42, email: 't@t.io' } as AuthUser;
const vm = wrapper.vm as unknown as { loadDeals: () => Promise<void>; dealsState: unknown[]; fetchError: boolean };
await vm.loadDeals();
await flushPromises();
expect(vm.dealsState.length).toBe(0);
expect(vm.fetchError).toBe(true);
});
```
- [ ] **Step 2: Прогнать — убедиться, что падает**
Run: `cd app && npm run test:vue -- DealsView.spec.ts`
Expected: новый тест FAIL (`dealsState.length` = длина `MOCK_DEALS`, не 0).
- [ ] **Step 3: DealsView.vue — init пустой**
Импорт (строка 18) — убрать `MOCK_DEALS`:
```ts
import { DEALS_TABS, type MockDeal } from '../composables/mockDeals';
```
Init `dealsState` (строка 113):
```ts
// Локальная reactive-копия. Наполняется через API (см. loadDeals/onMounted).
// До загрузки и при ошибке — пустой массив; ошибка показывается через fetchError.
const dealsState = reactive<MockDeal[]>([]);
```
Catch в `loadDeals` (строка 133) — комментарий:
```ts
} catch {
fetchError.value = true; // state остаётся пустым — показываем error-alert
}
```
Шаблон, alert `fetch-error-alert` (строки ~714-724) — текст:
```
Не удалось загрузить сделки. Попробуйте обновить.
```
Doc-комментарий вверху файла — строку `MVP: page-head + chiprow со срезами + поиск + v-data-table с mock'ами.` поправить на `... + v-data-table (данные из API).`
- [ ] **Step 4: KanbanView.vue — init пустой**
Импорт (строка 23):
```ts
import { type MockDeal } from '../composables/mockDeals';
```
Init `dealsByStatus` (строки 49-54):
```ts
const dealsByStatus = reactive<Record<string, MockDeal[]>>(
LEAD_STATUSES.reduce<Record<string, MockDeal[]>>((acc, s) => {
acc[s.slug] = [];
return acc;
}, {}),
);
```
`totalDeals` (строка 111):
```ts
const totalDeals = ref(0);
```
Catch в `loadDeals` (строка 142) — комментарий: `fetchError.value = true; // state остаётся пустым — показываем error-alert`
Alert `fetch-error-alert` (строки ~199-209) — текст: `Не удалось загрузить сделки. Попробуйте обновить.`
- [ ] **Step 5: Аналогичный regression-тест для KanbanView**
В `KanbanView.spec.ts` добавить тест: замокать `dealsApi.listDeals` reject, выставить `auth.user` с `tenant_id`, вызвать `vm.loadDeals()`, проверить что все массивы `dealsByStatus` пусты (`Object.values(vm.dealsByStatus).every(c => c.length === 0)`) и `vm.fetchError === true`.
- [ ] **Step 6: Починить существующие тесты (DealsView.spec.ts, DealsViewRedesign.spec.ts, KanbanView.spec.ts)**
Многие тесты используют `MOCK_DEALS` как фикстуру (рендер строк, `applyBulkStatus`, фильтры «Окна Москва»/«Иван П.», `route.query.openId=MOCK_DEALS[0].id`). Применить **Вариант B**: в mount-хелперах (`mountDeals`, `mountDealsViewAt`, аналог в KanbanView) после `mount` засеять state из mock-фикстуры:
```ts
const wrapper = mount(DealsView, { /* ... */ });
await flushPromises();
const vm = wrapper.vm as unknown as { dealsState: MockDeal[] };
vm.dealsState.push(...MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
await flushPromises();
return wrapper;
```
`MOCK_DEALS` уже импортируется в `DealsView.spec.ts`. Для KanbanView — засеять `dealsByStatus` по slug'ам. Тесты на новый-deal/bulk/фильтры/openId после seed работают как раньше. Прогонять после правки каждого спека.
- [ ] **Step 7: Полный прогон + линт**
Run: `cd app && npm run test:vue -- DealsView KanbanView && npm run lint:vue && npm run type-check`
Expected: все DealsView/KanbanView спеки PASS (0 fail), ESLint 0, vue-tsc 0.
- [ ] **Step 8: Commit**
```bash
git add app/resources/js/views/DealsView.vue app/resources/js/views/KanbanView.vue app/tests/Frontend/DealsView.spec.ts app/tests/Frontend/DealsViewRedesign.spec.ts app/tests/Frontend/KanbanView.spec.ts
git commit -m "fix(deals): I3 — убрать MOCK_DEALS fallback в DealsView/KanbanView"
```
---
## Task 2: NewDealDialog + DealDetailDrawer — убрать mock-fallback
**Files:**
- Modify: `app/resources/js/components/deals/NewDealDialog.vue`
- Modify: `app/resources/js/components/deals/DealDetailDrawer.vue`
- Test: spec-файлы NewDealDialog / DealDetailDrawer (уточнить `ls tests/Frontend | grep -E "NewDeal|DealDetailDrawer"`)
- [ ] **Step 1: NewDealDialog.vue — пустые опции**
Импорт (строка 17):
```ts
import { type MockDeal, type MockManager } from '../../composables/mockDeals';
```
`projectOptions`/`managerOptions` (строки 24-25):
```ts
const projectOptions = ref<string[]>([]);
const managerOptions = ref<MockManager[]>([]);
```
Doc-комментарий блока (строки 19-23) — переписать без «fallback на MOCK»:
```ts
/**
* Списки проектов и менеджеров грузятся с backend через GET /api/projects,
* /api/managers при открытии диалога (если передан tenantId). На fail —
* списки пустые + degradation-alter (lookupsFailed), создание блокируется
* до повторной успешной загрузки.
*/
```
Комментарий строки 80 → `// Audit C6: loadLookups упал → показываем degradation-alert (списки пусты).`
Alert `lookups-error-alert` (строки ~207-210) — текст:
```
Не удалось загрузить списки проектов и менеджеров — попробуйте позже.
```
`defineExpose` (строка 178) — добавить `projectOptions`, `managerOptions` для seed в тестах:
```ts
defineExpose({ lookupsFailed, projectOptions, managerOptions });
```
- [ ] **Step 2: DealDetailDrawer.vue — пустой timeline**
Импорт (строка 23):
```ts
import { type DealEvent } from '../../composables/mockDealEvents';
```
`events` init (строка 60):
```ts
const events = ref<DealEvent[]>([]);
```
`loadEvents` — путь без deal/tenantId (строка 119): `events.value = [];`
`loadEvents` — catch (строка 131): `events.value = [];`
Комментарий строки 59 → `// показываем реальные events. На fail / без tenant_id — events пуст + eventsFetchError.`
- [ ] **Step 3: Regression-тесты (red-green)**
NewDealDialog spec: тест — открыть диалог **без** `tenantId`, проверить `vm.projectOptions.length === 0` и `vm.managerOptions.length === 0` (раньше были mock). DealDetailDrawer spec: тест — `loadEvents` без tenantId / при reject `getDeal``vm.events.length === 0`. Сначала прогнать (увидеть fail на старом коде — но код уже правится в Step 1-2, поэтому: написать тест → если spec'и проверяют наличие mock-данных, они упадут до правки; порядок — допустимо написать тест и правку вместе, ключ: после правки тест зелёный и проверяет именно пустоту).
- [ ] **Step 4: Починить существующие тесты**
NewDealDialog: тесты submit-flow выбирают проект/менеджера — засеять `vm.projectOptions`/`vm.managerOptions` после mount (Вариант B), либо замокать `dealsApi.listProjects`/`listManagers` резолвом с фикстурой (Вариант A) при открытии с `tenantId`. DealDetailDrawer: тесты timeline — замокать `dealsApi.getDeal` резолвом с events, либо засеять `vm.events`. Тесты на `events-fetch-error-alert` (Sprint 5B C7) — events уже пуст при ошибке, проверить что alert по-прежнему рендерится.
- [ ] **Step 5: Полный прогон + линт**
Run: `cd app && npm run test:vue -- NewDealDialog DealDetailDrawer && npm run lint:vue && npm run type-check`
Expected: PASS 0 fail, ESLint 0, vue-tsc 0.
- [ ] **Step 6: Commit**
```bash
git add app/resources/js/components/deals/NewDealDialog.vue app/resources/js/components/deals/DealDetailDrawer.vue app/tests/Frontend/
git commit -m "fix(deals): I3 — убрать mock-fallback в NewDealDialog/DealDetailDrawer"
```
---
## Task 3: AdminBillingView + AdminIncidentsView — убрать mockAdmin fallback
**Files:**
- Modify: `app/resources/js/views/admin/AdminBillingView.vue`
- Modify: `app/resources/js/views/admin/AdminIncidentsView.vue`
- Test: `app/tests/Frontend/AdminBillingView.spec.ts`, `AdminBillingViewApi.spec.ts`, `AdminIncidentsView.spec.ts`, `AdminIncidentsViewApi.spec.ts`
- [ ] **Step 1: AdminBillingView.vue — init пустой**
Удалить импорт (строка 11) `import { ADMIN_BILLING_SUMMARY as MOCK_SUMMARY, ADMIN_BILLING_TENANTS } from '../../composables/mockAdmin';` целиком.
`rowsState` (строки 37-49):
```ts
const rowsState = reactive<BillingRow[]>([]);
```
`summary` (строки 51-56):
```ts
const summary = reactive({
total_mrr_rub: 0,
monthly_revenue_rub: 0,
overdue_count: 0,
refunds_count_30d: 0,
});
```
Doc-комментарий вверху (строки 8-9): `MVP — только display-вьюха с mock-данными.``Данные грузятся с backend GET /api/admin/billing.`
Комментарий строки 20-24 (над `BillingRow`) — убрать упоминание «initial = MOCK».
Alert `fetch-error-alert` (строки ~249-259) — текст: `Не удалось загрузить биллинг. Попробуйте обновить.`
- [ ] **Step 2: AdminIncidentsView.vue — init пустой**
Удалить импорт (строка 12) `import { ADMIN_INCIDENTS } from '../../composables/mockAdmin';`.
`rowsState` (строки 77-90):
```ts
const rowsState = reactive<IncidentRow[]>([]);
```
Удалить блок initial-stats из mock (строки 96-100 — `stats.open = rowsState.filter(...)` ×3). `stats` остаётся инициализированным нулями на строке 91.
Комментарий строки 76 (`// Reactive — initial = MOCK; replace на API на mount.`) → `// Reactive — наполняется через loadIncidents (API).`
Комментарий строки 95 (`// Initial stats из mock ...`) — удалить вместе с блоком.
Doc-комментарий (строки 8-9): `MVP — display + фильтр ...``Display + фильтр по статусу/severity. Данные с backend GET /api/admin/incidents.`
Alert `fetch-error-alert` (строки ~170-180) — текст: `Не удалось загрузить инциденты. Попробуйте обновить.`
- [ ] **Step 3: Тесты — инвертировать fake-fallback ассерты + regression**
`AdminBillingViewApi.spec.ts:96-106` — тест `'reject → fetchError=true + alert виден + MOCK fallback остаётся'`:
- заголовок → `'reject → fetchError=true + alert виден + rowsState пустой'`
- `expect(vm.rowsState.length).toBeGreaterThan(0);``expect(vm.rowsState.length).toBe(0);`
Аналогично проверить `AdminIncidentsViewApi.spec.ts` на наличие «MOCK fallback»-ассертов — инвертировать.
Smoke-спеки `AdminBillingView.spec.ts` / `AdminIncidentsView.spec.ts`: если рендерят строки из mock-init — применить Вариант A (замокать `adminApi.listAdminBilling`/`listAdminIncidents` резолвом фикстуры — фикстуру взять из `composables/mockAdmin` импортом в спеке) или Вариант B (seed `vm.rowsState`). Добавить regression-тест где ещё нет: reject → `rowsState.length === 0` + `fetchError`.
- [ ] **Step 4: Полный прогон + линт**
Run: `cd app && npm run test:vue -- AdminBilling AdminIncidents && npm run lint:vue && npm run type-check`
Expected: PASS 0 fail, ESLint 0, vue-tsc 0.
- [ ] **Step 5: Commit**
```bash
git add app/resources/js/views/admin/AdminBillingView.vue app/resources/js/views/admin/AdminIncidentsView.vue app/tests/Frontend/
git commit -m "fix(admin): I3 — убрать mockAdmin fallback в Billing/Incidents"
```
---
## Task 4: AdminSystemView + AdminTenantsView — убрать mock fallback
**Files:**
- Modify: `app/resources/js/views/admin/AdminSystemView.vue`
- Modify: `app/resources/js/views/admin/AdminTenantsView.vue`
- Test: `app/tests/Frontend/AdminSystemView.spec.ts`, `AdminTenantsView.spec.ts`, `AdminTenantsViewApi.spec.ts`
- [ ] **Step 1: AdminSystemView.vue — init пустой**
Удалить импорт mock-данных (строка 11) `import { ADMIN_SYSTEM_SETTINGS } from '../../composables/mockAdmin';`.
**Оставить** импорт типа (строка 12) `import type { AdminSystemSetting } from '../../composables/mockAdmin';` — тип используется.
`settingsState` (строка 30):
```ts
const settingsState = reactive<AdminSystemSetting[]>([]);
```
Комментарий строки 23-29 (над `settingsState`) — убрать «Инициируется mock-данными (fallback...)»:
```ts
/**
* Settings-state. Наполняется на mount через `adminApi.listSystemSettings()`.
* До загрузки и при ошибке — пустой; ошибка показывается через fetchError-banner.
*/
```
Catch в `loadSettings` (строка 41) — текст fallback в `extractErrorMessage`:
```ts
fetchError.value = extractErrorMessage(err, 'Не удалось загрузить настройки с сервера. Попробуйте обновить.');
```
Комментарий строки 39-40 (`// На fail оставляем mock ...`) → `// На fail — settingsState пустой, показываем error-banner.`
Doc-комментарий (строки 8-9): `MVP — display + read-only edit-режим.``Display + edit-режим. Данные с backend GET /api/admin/system-settings.`
- [ ] **Step 2: AdminTenantsView.vue — init пустой**
Импорт (строка 18) — убрать `MOCK_STATS`, `MOCK_TENANTS`:
```ts
import { type AdminTenant, type TenantStatus } from '../../composables/mockTenants';
```
`tenantsState` (строка 32):
```ts
const tenantsState = reactive<AdminTenant[]>([]);
```
`stats` (строка 33) — заменить `{ ...MOCK_STATS }` объектом с теми же ключами в нулях. **Сверить точную форму `MOCK_STATS` в `composables/mockTenants.ts`** (`loadTenants` пишет `total/active/trial/overdue`):
```ts
const stats = reactive({ total: 0, active: 0, trial: 0, overdue: 0 });
```
Alert `fetch-error-alert` (строки ~117-127) — текст: `Не удалось загрузить тенантов. Попробуйте обновить.`
- [ ] **Step 3: Тесты — починить + regression**
`AdminTenantsViewApi.spec.ts` — проверить на «MOCK fallback»-ассерты после reject, инвертировать на `length === 0`.
Smoke-спеки `AdminSystemView.spec.ts` / `AdminTenantsView.spec.ts` — рендер строк из mock-init: Вариант A (мок `adminApi.listSystemSettings`/`listAdminTenants` резолвом фикстуры) или B (seed `vm.settingsState`/`vm.tenantsState`). Regression-тест: reject → state пустой + ошибка видна (`AdminSystemView.fetchError` — это `string|null`, при ошибке непустая строка; `AdminTenantsView.fetchError` — boolean).
- [ ] **Step 4: Полный прогон + линт**
Run: `cd app && npm run test:vue -- AdminSystem AdminTenants && npm run lint:vue && npm run type-check`
Expected: PASS 0 fail, ESLint 0, vue-tsc 0.
- [ ] **Step 5: Commit**
```bash
git add app/resources/js/views/admin/AdminSystemView.vue app/resources/js/views/admin/AdminTenantsView.vue app/tests/Frontend/
git commit -m "fix(admin): I3 — убрать mock fallback в System/Tenants"
```
---
## Task 5: I4 — ImpersonationDialog devPlainCode за DEV-gate
**Files:**
- Modify: `app/resources/js/components/admin/ImpersonationDialog.vue`
- Test: `app/tests/Frontend/ImpersonationDialog*.spec.ts` (уточнить `ls`)
Контекст: баннер `data-testid="dev-code-banner"` (строки ~218-228) показывает `_dev_plain_code` (плейн-код impersonation) при `v-if="devPlainCode"`. Сейчас гейт — только наличие данных (backend на prod не отдаёт `_dev_plain_code`). Аудит I4: добавить явный frontend DEV-gate, чтобы баннер не отрисовывался в prod-сборке даже если бэк случайно отдаст код.
- [ ] **Step 1: Regression-тест (red) — баннер скрыт в prod**
В spec ImpersonationDialog добавить тест: `vi.stubEnv('DEV', false)` **до** mount → пройти flow до step `verify` с непустым `devPlainCode` (замокать `adminApi.impersonationInit` резолвом с `_dev_plain_code: '123456'`) → `expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(false)`. `afterEach(() => vi.unstubAllEnvs())`.
- [ ] **Step 2: Прогон — упадёт**
Run: `cd app && npm run test:vue -- ImpersonationDialog`
Expected: новый тест FAIL (баннер рендерится — гейт только по `devPlainCode`).
- [ ] **Step 3: ImpersonationDialog.vue — DEV-gate**
В `<script setup>` после `const devPlainCode = ref<string | null>(null);` (строка 49) добавить:
```ts
// I4: явный frontend DEV-gate. import.meta.env.DEV статически заменяется Vite —
// в prod-сборке = false, баннер с плейн-кодом tree-shake'ится.
const isDevEnv = import.meta.env.DEV;
```
`defineExpose` отсутствует — не добавлять (тест проверяет через DOM).
Шаблон, баннер (строка 219) — гейт:
```html
<v-alert
v-if="isDevEnv && devPlainCode"
```
Doc-комментарий (строка 8) — уточнить: `На dev показывается _dev_plain_code (за import.meta.env.DEV; на prod — баннер не рендерится).`
- [ ] **Step 4: Прогон — зелёный**
Run: `cd app && npm run test:vue -- ImpersonationDialog`
Expected: новый тест PASS. Существующие тесты (в Vitest `import.meta.env.DEV === true`) — баннер по-прежнему виден при `devPlainCode`, PASS.
- [ ] **Step 5: Линт + type-check**
Run: `cd app && npm run lint:vue && npm run type-check`
Expected: ESLint 0, vue-tsc 0.
- [ ] **Step 6: Commit**
```bash
git add app/resources/js/components/admin/ImpersonationDialog.vue app/tests/Frontend/
git commit -m "fix(admin): I4 — devPlainCode-баннер за import.meta.env.DEV"
```
---
## Self-Review (контроллер, после всех задач)
- **Покрытие спека:** I3 — 8 production-файлов init→пусто + error-текст (✓ T1-T4). I4 — DEV-gate (✓ T5). I1 — отложен (вне scope).
- **Нет fake-data в prod-путях:** grep `MOCK_|ADMIN_` по `resources/js/views` + `resources/js/components/deals` + `ImpersonationDialog` — 0 совпадений в production-импортах (только типы). `mock*.ts` не удалены — типы/константы (`MockDeal`, `DEALS_TABS`, `AdminTenant`...) живы.
- **Тесты:** полный `npm run test:vue` зелёный, 0 fail; новые regression-тесты на каждый файл; инвертированы явные «MOCK fallback» ассерты.
- **Регрессия:** `npm run lint:vue` 0, `npm run type-check` 0, Pest не затронут (только frontend).