Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c217fae43 | |||
| 6230c0fa61 | |||
| 7a537105e3 | |||
| 8a7314d198 | |||
| e41844a13b | |||
| 11baaefe21 | |||
| 97a27fdfbf | |||
| d41471c818 | |||
| 3360e6f023 | |||
| 7d84959c15 | |||
| ded07d3a6b | |||
| 608f4b2231 | |||
| 6a64a98fbf | |||
| f29b1b7e50 | |||
| 0d2c64aa8c | |||
| 256acf8781 | |||
| a0b1cfdcae | |||
| 2b04bbd4f8 | |||
| 888b7563cd | |||
| 3a58090db9 | |||
| 23579dd9be | |||
| 7c12b7419c | |||
| f05bb4dde2 | |||
| 703f101c11 | |||
| 30eec9fb7d | |||
| 83a831c46d | |||
| b72780c54e | |||
| 8c9a91be1c | |||
| f892c94feb |
@@ -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.
|
||||
@@ -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 P0–P3; 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
@@ -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/
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { impersonationActive, type ImpersonationActiveSession } from '../../api/admin';
|
||||
import { usePolling } from '../../composables/usePolling';
|
||||
import { POLLING_INTERVAL_MS } from '../../constants/polling';
|
||||
|
||||
const sessions = ref<ImpersonationActiveSession[]>([]);
|
||||
|
||||
@@ -37,7 +38,7 @@ const label = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(load);
|
||||
usePolling(load, { intervalMs: 30_000 });
|
||||
usePolling(load, { intervalMs: POLLING_INTERVAL_MS });
|
||||
|
||||
defineExpose({ sessions, load });
|
||||
</script>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { POLLING_INTERVAL_MS } from '../constants/polling';
|
||||
|
||||
/**
|
||||
* Polling-composable для авто-обновления view-данных.
|
||||
@@ -15,14 +16,14 @@ import { onBeforeUnmount, onMounted } from 'vue';
|
||||
* Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
|
||||
*/
|
||||
export interface PollingOptions {
|
||||
/** Период polling в миллисекундах. По умолчанию 30_000. */
|
||||
/** Период polling в миллисекундах. По умолчанию POLLING_INTERVAL_MS (30 с). */
|
||||
intervalMs?: number;
|
||||
/** Если false — composable не стартует interval (для disable-флага). */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function usePolling(loader: () => void | Promise<void>, options: PollingOptions = {}): void {
|
||||
const intervalMs = options.intervalMs ?? 30_000;
|
||||
const intervalMs = options.intervalMs ?? POLLING_INTERVAL_MS;
|
||||
const enabled = options.enabled ?? true;
|
||||
|
||||
if (!enabled) return;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Интервалы polling-обновления view-данных — единый источник «магических»
|
||||
* чисел для usePolling. До приезда SSE/WebSocket в production это покрывает
|
||||
* «real-time»-паттерн (см. composables/usePolling.ts).
|
||||
*/
|
||||
|
||||
/** Базовый интервал авто-обновления для большинства view-данных (30 с). */
|
||||
export const POLLING_INTERVAL_MS = 30_000;
|
||||
|
||||
/** Интервал для менее срочных счётчиков (напоминания в сайдбаре). */
|
||||
export const POLLING_REMINDERS_INTERVAL_MS = 60_000;
|
||||
@@ -38,6 +38,9 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
/** DEV-режим: показываем баннер о застабленном auth-gate админки (B6). */
|
||||
const isDevEnv = import.meta.env.DEV;
|
||||
|
||||
const userInitials = computed(() => {
|
||||
const u = auth.user;
|
||||
if (!u) return 'АО';
|
||||
@@ -131,6 +134,19 @@ const currentPageTitle = computed(() => {
|
||||
</v-app-bar>
|
||||
|
||||
<v-main class="admin-main">
|
||||
<v-alert
|
||||
v-if="isDevEnv"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="ma-4"
|
||||
data-testid="dev-auth-gap-banner"
|
||||
>
|
||||
DEV-режим: доступ к админке открыт без SSO-проверки — middleware
|
||||
<code>EnsureSaasAdmin</code> в dev пропускает все запросы. В production
|
||||
требуется вход через Yandex 360 + роль <code>super_admin</code> (Б-1);
|
||||
неавторизованные запросы получают 503.
|
||||
</v-alert>
|
||||
<ImpersonationBanner />
|
||||
<RouterView />
|
||||
</v-main>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useAuthStore } from '../stores/auth';
|
||||
import { useNotificationsStore } from '../stores/notifications';
|
||||
import { useRemindersStore } from '../stores/reminders';
|
||||
import { usePolling } from '../composables/usePolling';
|
||||
import { POLLING_INTERVAL_MS, POLLING_REMINDERS_INTERVAL_MS } from '../constants/polling';
|
||||
import AppSidebar from '../components/layout/AppSidebar.vue';
|
||||
import AppTopbar from '../components/layout/AppTopbar.vue';
|
||||
import DevIndexBadge from '../components/DevIndexBadge.vue';
|
||||
@@ -57,8 +58,8 @@ onMounted(() => {
|
||||
void loadNotifications();
|
||||
void loadReminderCounts();
|
||||
});
|
||||
usePolling(loadNotifications, { intervalMs: 30_000, enabled: true });
|
||||
usePolling(loadReminderCounts, { intervalMs: 60_000, enabled: true });
|
||||
usePolling(loadNotifications, { intervalMs: POLLING_INTERVAL_MS, enabled: true });
|
||||
usePolling(loadReminderCounts, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="Канбан-доска воронки продаж">
|
||||
|
||||
@@ -167,33 +167,6 @@ onUnmounted(() => store.stopPolling());
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
/* Workaround: MDI-шрифт не подключён в проекте (Диз-4),
|
||||
`<i class="mdi-close-circle">` рендерится пустым. Подменяем глиф на Unicode `✕`
|
||||
и показываем только когда поле имеет значение (Vuetify ставит `.v-field--dirty`). */
|
||||
.projects-view :deep(.v-field__clearable) {
|
||||
position: relative;
|
||||
}
|
||||
.projects-view :deep(.v-field__clearable .v-icon) {
|
||||
color: transparent;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.projects-view :deep(.v-field--dirty .v-field__clearable)::after {
|
||||
content: '✕';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(1, 32, 25, 0.55);
|
||||
font-size: 14px;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
pointer-events: none;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
.projects-view :deep(.v-field--dirty .v-field__clearable:hover)::after {
|
||||
color: var(--liderra-noir, #012019);
|
||||
}
|
||||
.toolbar-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { cancelReportJob, createReportJob, deleteReportJob, listReportJobs, retryReportJob } from '../api/reports';
|
||||
import { extractErrorMessage, extractValidationErrors } from '../api/client';
|
||||
import { usePolling } from '../composables/usePolling';
|
||||
import { POLLING_INTERVAL_MS } from '../constants/polling';
|
||||
import { type ReportFormat, type ReportJob, type ReportType } from '../composables/mockReports';
|
||||
import { mapApiReportJob, uiTypeToApi } from '../composables/reportsMapper';
|
||||
import ReportRequestForm from '../components/reports/ReportRequestForm.vue';
|
||||
@@ -59,7 +60,7 @@ onMounted(() => {
|
||||
void loadJobs();
|
||||
});
|
||||
|
||||
usePolling(loadJobs, { intervalMs: 30_000 });
|
||||
usePolling(loadJobs, { intervalMs: POLLING_INTERVAL_MS });
|
||||
|
||||
async function submitForm(): Promise<void> {
|
||||
submitting.value = true;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -168,6 +163,7 @@ defineExpose({ settingsState, editOpen, editSetting, openEdit, onSettingUpdated,
|
||||
size="small"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-pencil"
|
||||
:aria-label="`Изменить настройку ${setting.key}`"
|
||||
:data-testid="`edit-${setting.key}-btn`"
|
||||
@click="openEdit(setting)"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -86,11 +86,22 @@ async function handleSubmit() {
|
||||
placeholder="Минимум 8 символов"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
required
|
||||
:error-messages="errors.password"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
class="password-toggle"
|
||||
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="showPassword = !showPassword"
|
||||
@keydown.enter.prevent="showPassword = !showPassword"
|
||||
@keydown.space.prevent="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<div class="d-flex justify-end mb-2">
|
||||
<RouterLink to="/forgot" class="text-body-2 text-primary"> Забыли пароль? </RouterLink>
|
||||
@@ -141,4 +152,10 @@ async function handleSubmit() {
|
||||
.yandex-sso-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-toggle:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -102,11 +102,22 @@ async function handleSubmit() {
|
||||
placeholder="Минимум 8 символов"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
required
|
||||
:error-messages="errors.password"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
class="password-toggle"
|
||||
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="showPassword = !showPassword"
|
||||
@keydown.enter.prevent="showPassword = !showPassword"
|
||||
@keydown.space.prevent="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<div v-if="password" class="strength-block mb-2">
|
||||
<v-progress-linear
|
||||
@@ -184,4 +195,10 @@ async function handleSubmit() {
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.password-toggle:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -112,11 +112,22 @@ async function handleSubmit() {
|
||||
placeholder="Минимум 10 символов"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
required
|
||||
:error-messages="errors.password"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
class="password-toggle"
|
||||
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="showPassword = !showPassword"
|
||||
@keydown.enter.prevent="showPassword = !showPassword"
|
||||
@keydown.space.prevent="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="passwordConfirmation"
|
||||
@@ -165,4 +176,10 @@ async function handleSubmit() {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.password-toggle:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ const mountAdminLayout = async (path = '/admin/tenants', user: AuthUser | null =
|
||||
};
|
||||
|
||||
describe('AdminLayout.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('монтируется без ошибок', async () => {
|
||||
const { wrapper } = await mountAdminLayout();
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
@@ -214,4 +218,16 @@ describe('AdminLayout.vue', () => {
|
||||
const nav = wrapper.find('[aria-label="Админ навигация"]');
|
||||
expect(nav.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('B6: показывает DEV-баннер auth-gap в dev-режиме', async () => {
|
||||
vi.stubEnv('DEV', true);
|
||||
const { wrapper } = await mountAdminLayout();
|
||||
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('B6: скрывает DEV-баннер в production-режиме', async () => {
|
||||
vi.stubEnv('DEV', false);
|
||||
const { wrapper } = await mountAdminLayout();
|
||||
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -147,4 +149,14 @@ describe('AdminSystemView.vue', () => {
|
||||
expect(row?.value).toBe('7');
|
||||
expect(row?.updated_at).toBe('2026-05-09T11:30:00');
|
||||
});
|
||||
|
||||
it('G9: edit-кнопки имеют aria-label с ключом настройки', async () => {
|
||||
const wrapper = await mountView();
|
||||
const editBtns = wrapper.findAll('[data-testid^="edit-"]');
|
||||
expect(editBtns.length).toBeGreaterThan(0);
|
||||
for (const btn of editBtns) {
|
||||
const label = btn.attributes('aria-label') ?? '';
|
||||
expect(label).toMatch(/^Изменить настройку .+/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -87,4 +87,17 @@ describe('LoginView.vue', () => {
|
||||
expect(ssoBtn).toBeDefined();
|
||||
expect(ssoBtn!.classes()).toContain('v-btn--disabled');
|
||||
});
|
||||
|
||||
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
|
||||
const wrapper = await mountLoginView();
|
||||
const toggle = wrapper.find('[aria-label="Показать пароль"]');
|
||||
expect(toggle.exists()).toBe(true);
|
||||
expect(toggle.attributes('role')).toBe('button');
|
||||
await toggle.trigger('click');
|
||||
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
|
||||
|
||||
// keyboard activation (Enter) — toggle back
|
||||
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
|
||||
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -53,4 +53,17 @@ describe('RegisterView.vue', () => {
|
||||
const links = wrapper.findAll('a').map((a) => a.text());
|
||||
expect(links.some((t) => t.includes('Войдите'))).toBe(true);
|
||||
});
|
||||
|
||||
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
|
||||
const wrapper = await mountRegister();
|
||||
const toggle = wrapper.find('[aria-label="Показать пароль"]');
|
||||
expect(toggle.exists()).toBe(true);
|
||||
expect(toggle.attributes('role')).toBe('button');
|
||||
await toggle.trigger('click');
|
||||
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
|
||||
|
||||
// keyboard activation (Enter) — toggle back
|
||||
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
|
||||
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,4 +112,17 @@ describe('ResetPasswordView.vue', () => {
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.text()).toContain('Пароли не совпадают');
|
||||
});
|
||||
|
||||
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
|
||||
const wrapper = await mountReset();
|
||||
const toggle = wrapper.find('[aria-label="Показать пароль"]');
|
||||
expect(toggle.exists()).toBe(true);
|
||||
expect(toggle.attributes('role')).toBe('button');
|
||||
await toggle.trigger('click');
|
||||
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
|
||||
|
||||
// keyboard activation (Enter) — toggle back
|
||||
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
|
||||
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1321,3 +1321,34 @@ mmdc
|
||||
inertiajs
|
||||
Sev
|
||||
вендоренный
|
||||
|
||||
# D3 audit-risk tooling integration (Прил. Н #39-40)
|
||||
unvetted
|
||||
mcpmarket
|
||||
behaviour
|
||||
triada
|
||||
trailofbits
|
||||
hackathon
|
||||
субсет
|
||||
|
||||
# A11 ML/AI tooling integration — brainstorming spec + plan (2026-05-17)
|
||||
CCPM
|
||||
REU
|
||||
promptfoo
|
||||
promptfooconfig
|
||||
datalayer
|
||||
scikit
|
||||
XGBoost
|
||||
Jupyter
|
||||
pandas
|
||||
alirezarezvani
|
||||
Anthropic
|
||||
RAG
|
||||
venv
|
||||
Helicone
|
||||
Langfuse
|
||||
|
||||
# SG #40 Security Guidance correction (2026-05-17)
|
||||
резолва
|
||||
шим
|
||||
characterisation
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# Plugin Stack Rules — Superpowers + Frontend Design (v3.3)
|
||||
# Plugin Stack Rules — Superpowers + Frontend Design (v3.5)
|
||||
|
||||
**Дата:** 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.5** — фактическая правка R10.1 Блок 1 строки **security-guidance**: это **блокирующий** PreToolUse-хук (`sys.exit(2)`, одноразовый speed-bump per «файл+правило» за сессию, retry проходит), не warn-only. Содержательных изменений R0–R14: 0. Связано: Tooling v2.5, Pravila v1.19, CLAUDE.md v2.5; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
|
||||
|
||||
**v3.4** — D3 audit-security: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **Trail of Bits Skills** (`trailofbits/skills`, субсет 8 плагинов) + **security-guidance** (`anthropics/claude-plugins-official`). Новая категория **audit-security** (Tooling #39-40, раздел D3 карты) — не UI → вне R6.0/R6.1/R14, как debug-runtime/infrastructure/architecture-tooling. Содержательных изменений R0–R9/R11–R14: 0. Связано: Tooling v2.4, Pravila v1.18, CLAUDE.md v2.4.
|
||||
|
||||
**v3.3** — A6 architecture-tooling: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **adr-kit** (`rvdbreemen/adr-kit`) + **architecture-patterns** (`secondsky/claude-skills`); Блок 1 +note про **mermaid-skill** (вендоренный сторонний скил). Новая категория **architecture-tooling** (Tooling #36-38, раздел A6 карты) — не UI → вне R6.0/R6.1/R14, как debug-runtime/infrastructure. Содержательных изменений R0–R9/R11–R14: 0. Связано: Tooling v2.3, Pravila v1.17, CLAUDE.md v2.3.
|
||||
|
||||
**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 +400,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-хук, блокирующий)* | `anthropics/claude-plugins-official` | inline-предупреждения уязвимостей при правке кода — **блокирующий** хук (`sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию, retry проходит), 8 контентных правил + 1 path-правило. Категория: **audit-security** (Tooling #40) | автоматически — PreToolUse-хук на Write/Edit/MultiEdit. Не решатель, не UI → вне R6.0/R6.1/R14 |
|
||||
|
||||
**Блок 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 +759,10 @@ Pipeline активируется при одновременном выполн
|
||||
|
||||
## История версий
|
||||
|
||||
- **v3.5 (2026-05-17)** — фактическая правка R10.1 Блок 1 (security-guidance): хук **блокирующий** (`sys.exit(2)`, одноразовый speed-bump per «файл+правило» за сессию), не warn-only. Содержательных изменений R0–R14: 0. Связано: Tooling v2.5, Pravila v1.19, CLAUDE.md v2.5. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
|
||||
|
||||
- **v3.4 (2026-05-17)** — D3 audit-security: R10.1 Блок 1 (`enabledPlugins`) +2 строки (Trail of Bits Skills #39, security-guidance #40) — новая 6-я off-phase подкатегория audit-security. Связано: Tooling v2.4, Pravila v1.18, CLAUDE.md v2.4. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
|
||||
|
||||
- **v3.3 (2026-05-17)** — A6 architecture-tooling: R10.1 Блок 1 (`enabledPlugins`) +2 строки — **adr-kit** (`rvdbreemen/adr-kit`, 8 skills + агент `adr-generator`; `adr-judge` врезан в lefthook pre-commit job 9 декларативно, без `--llm` → 0 вызовов Claude API) + **architecture-patterns** (`secondsky/claude-skills`, knowledge-only справочник паттернов). Блок 1 +note про **mermaid-skill** (вендоренный сторонний скил `.claude/skills/mermaid/`, генератор C4-диаграмм — пассивная утилита вне типологии 3 блоков). Новая категория **architecture-tooling** (Tooling #36-38, раздел A6 карты «Архитектура систем») — не UI → вне R6.0/R6.1/R14 pipeline, как debug-runtime и infrastructure. Содержательных изменений R0–R9/R11–R14: 0. Связано: Tooling v2.2→v2.3, Pravila v1.16→v1.17, CLAUDE.md v2.2→v2.3. Через manual Edit. План `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`.
|
||||
|
||||
- **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`.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Правила работы Claude в проекте «Лидерра»
|
||||
|
||||
**Версия:** v1.17 (17.05.2026)
|
||||
**Версия:** v1.19 (17.05.2026)
|
||||
**Дата:** 17.05.2026
|
||||
**Назначение:** настройки проекта (Project instructions) — Claude читает этот файл в каждом чате и следует правилам ниже.
|
||||
**Статус документа:** ✅ утверждён. Содержимое скопировано в поле "Project instructions" Claude.ai. Файл хранится в архиве как служебный документ.
|
||||
|
||||
**Что изменилось в v1.19 относительно v1.18:** §13.2 абзац «Off-phase audit-security» — фактическая правка характеристики #40 Security Guidance: это **блокирующий** PreToolUse-хук (`sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию), не warn-only. §13.2 PSR_v1 cross-ref v3.4+ → v3.5+. Связано: Tooling v2.5 / PSR_v1 v3.5 / CLAUDE.md v2.5; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
|
||||
|
||||
**Что изменилось в v1.18 относительно v1.17:** §13.2 +абзац «Off-phase audit-security» — формализованы 2 инструмента раздела D3 карты «Аудит и управление рисками» (#39 Trail of Bits Skills, #40 Security Guidance) как шестая off-phase категория; §13.2 PSR_v1 cross-ref v3.3+ → v3.4+. Связано: Tooling v2.4 / PSR_v1 v3.4 / CLAUDE.md v2.4; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
|
||||
|
||||
**Что изменилось в v1.17 относительно v1.16:** §13.2 +абзац «Off-phase architecture-tooling» — формализованы 3 инструмента раздела A6 карты «Архитектура систем» (#36 adr-kit, #37 mermaid-skill, #38 architecture-patterns) как пятая off-phase категория; §13.2 PSR_v1 cross-ref v3.2+ → v3.3+. Связано: Tooling v2.3 / PSR_v1 v3.3 / CLAUDE.md v2.3; план `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`.
|
||||
|
||||
**Краткое резюме 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`.
|
||||
@@ -558,6 +562,8 @@ P0 = блокер старта спринта или регуляторного
|
||||
| **v1.15** | **15.05.2026** | Новый §14 «Ruflo Queen routing — hard rule»: триггер queen/королева → безусловный route через ruflo Queen (`hive-mind spawn --claude`), enforcement-хук `tools/ruflo-queen-hook.mjs`. §13.6 tier-таблица +строка §14 (explicit hard-rule). §0 priority chain +§14 +note. §14.3 — проактивное предложение ruflo-spawn на нетривиальных задачах. Связано: spec/plan 2026-05-15-ruflo-queen-trigger-and-delegation, CLAUDE.md v2.1, PSR_v1 v3.1, Tooling v2.1. Через `superpowers:brainstorming` → `writing-plans` → `subagent-driven-development`. |
|
||||
| **v1.16** | **16.05.2026** | Реколлаж ruflo — приведение декларации к фактическому рантайму: §12 Superpowers переведён из sub-policy обратно в explicit hard-rule; §0 priority note и §14.6 cross-ref — убраны упоминания ruflo как «уровня −1»; §11.5/§13.2/§13.9/§13.10 cross-refs на PSR_v1 v3.2. Связано: CLAUDE.md v2.2 / PSR_v1 v3.2 / Tooling v2.2; spec `docs/superpowers/specs/2026-05-16-ruflo-hierarchy-factual-recollage-design.md`. |
|
||||
| **v1.17** | **17.05.2026** | A6 architecture-tooling: §13.2 +абзац «Off-phase architecture-tooling» — формализованы 3 инструмента раздела A6 карты «Архитектура систем» (#36 adr-kit, #37 mermaid-skill, #38 architecture-patterns) как пятая off-phase категория, отдельная от UI-пула / infrastructure / debug-runtime / orchestration; не UI → вне R6.0/R6.1/R14. §13.2 PSR_v1 cross-ref v3.2+ → v3.3+. Связано: Tooling v2.2→v2.3 (§4.11-4.13 + §0 счётчик 35→38), PSR_v1 v3.2→v3.3 (R10.1 Блок 1 +2 строки + note), CLAUDE.md v2.2→v2.3 (§3.3 +#36-38). Через manual Edit (Pravila/PSR_v1/Tooling) + `/claude-md-management:claude-md-improver` (CLAUDE.md per §5 п.10). План `docs/superpowers/plans/2026-05-17-a6-architecture-tooling-integration.md`. Архитектурных изменений в §§1–12 + §§13.1, 13.3–14: 0. |
|
||||
| **v1.18** | **17.05.2026** | D3 audit-security: §13.2 +абзац «Off-phase audit-security» — формализованы 2 инструмента раздела D3 карты «Аудит и управление рисками» (#39 Trail of Bits Skills, #40 Security Guidance) как шестая off-phase категория; §13.2 PSR_v1 cross-ref v3.3+ → v3.4+. Связано: Tooling v2.4 / PSR_v1 v3.4 / CLAUDE.md v2.4. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. |
|
||||
| **v1.19** | **17.05.2026** | Фактическая правка §13.2 абзаца «Off-phase audit-security»: #40 Security Guidance — **блокирующий** PreToolUse-хук (`sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию), не warn-only; §13.2 PSR_v1 cross-ref v3.4+ → v3.5+. Связано: Tooling v2.5 / PSR_v1 v3.5 / CLAUDE.md v2.5. План `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. |
|
||||
|
||||
---
|
||||
|
||||
@@ -690,10 +696,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.5+**, 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-хук — inline-предупреждения уязвимостей, `sys.exit 2`, одноразовый speed-bump per «файл+правило» за сессию). Дополнительно `/security-review` (Anthropic built-in, customized в `.claude/commands/security-review.md` с проектным FP-фильтром RLS/ПДн/economy-хуки). **Категория отдельная** от UI-пула, infrastructure, debug-runtime, orchestration и architecture-tooling — не UI, **не trigger'ит R6.0/R6.1 stack-фильтры и не входит в R14 pipeline**. Регулируется PSR_v1 R10.1 Блок 1. Установлены 17.05.2026 на ветке `feat/d3-audit-risk-tooling`; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`.
|
||||
|
||||
### 13.3. Скоуп
|
||||
|
||||
| Тип задачи | Кто отвечает |
|
||||
|
||||
+31
-5
@@ -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.5 (фактическая правка #40 Security Guidance — это **блокирующий** PreToolUse-хук (`sys.exit(2)`), не warn-only: при первом за сессию срабатывании уязвимого паттерна блокирует правку (одноразовый speed-bump, retry проходит). §4.15 переписан, SG1 уточнён, +SG2 — Windows-починка python3-резолва (`python3.exe`-шим). Счётчики не меняются — 40 позиций. Связано: PSR_v1 v3.5, Pravila v1.19, CLAUDE.md v2.5; план `docs/superpowers/plans/2026-05-17-d3-audit-risk-tooling-integration.md`. **v2.4 наследие:** D3 audit-security — формализованы #39 Trail of Bits Skills (субсет 8 audit-плагинов, marketplace `trailofbits`, CC-BY-SA-4.0) + #40 Security Guidance (Anthropic 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): при **первом** за сессию срабатывании уязвимого паттерна в данном файле печатает предупреждение в stderr и завершается `sys.exit(2)` → **блокирует правку**. Пара «файл+правило» пишется в session state-файл `~/.claude/security_warnings_state_<id>.json` — повторная попытка той же правки проходит; это **одноразовый speed-bump**, не безусловный блок. 9 правил: 8 контентных (command/shell injection `child_process.exec`/`os.system`, `eval(`, `new Function`, XSS через `innerHTML`/`document.write`/`dangerouslySetInnerHTML`, `pickle`-десериализация) + 1 path-правило на `.github/workflows/*.yml`. Отключается env `ENABLE_SECURITY_REMINDER=0`.
|
||||
|
||||
**Роль:** инструмент **#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`); хук **блокирующий** (`sys.exit(2)`), но одноразовый per «файл+правило» за сессию — economy/ruflo-цепочка не нарушается (PreToolUse-хуки независимы, SG блокирует только на свой уязвимый паттерн), +~34 мс/правку latency. **SG2 (Windows-починка, 17.05.2026):** bundled `hooks.json` жёстко зовёт интерпретатор `python3`, которого в PATH этой машины нет (есть `python` 3.14.4) → без починки хук не спаунился, SG был инертен. Решено: `python3.exe` (копия `python.exe`) добавлен в каталог установки Python в PATH — кэш плагина не трогается, починка переживает обновления плагина. Verified end-to-end 17.05.2026 (manual smoke → `sys.exit 2` + in-session Write-блокировка). `/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,15 @@ 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 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-хук). Связано: 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.5** | 17.05.2026 | **Фактическая правка #40 Security Guidance:** §4.15 переписан — это **блокирующий** PreToolUse-хук (`sys.exit(2)` на первой за сессию правке с уязвимым паттерном, retry проходит — одноразовый speed-bump), не warn-only; SG1 уточнён; +SG2 — Windows-починка python3-резолва (`python3.exe`-шим, кэш плагина не трогается). Verified end-to-end (manual smoke + in-session Write-блок). Счётчики без изменений — 40 позиций. Связано: PSR_v1 v3.5, Pravila v1.19, CLAUDE.md v2.5. |
|
||||
|
||||
---
|
||||
|
||||
*Прил. Н v2.5 от 17.05.2026 — фактическая правка #40 Security Guidance: блокирующий PreToolUse-хук (`sys.exit(2)`, не warn-only) + SG2 Windows-починка python3-резолва. Связано: PSR_v1 v3.5, Pravila v1.19, CLAUDE.md v2.5.*
|
||||
|
||||
*Прил. Н 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.11–4.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.*
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# ADR-003 Adopt the D3 audit and risk-management toolset
|
||||
|
||||
## Status
|
||||
|
||||
Accepted, 2026-05-17. Amended 2026-05-17 — corrected the Security Guidance
|
||||
characterisation (a blocking `PreToolUse` hook, not warn-only) and recorded the
|
||||
`python3.exe` shim needed on the Windows dev host.
|
||||
|
||||
## Context
|
||||
|
||||
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 `PreToolUse` hook plugin, for inline
|
||||
vulnerability reminders while editing. The hook is **blocking** (`sys.exit(2)`):
|
||||
the first edit per session whose content matches a vulnerable pattern in a
|
||||
given file is blocked once — a one-time speed-bump, the retry passes.
|
||||
- **adr-kit** — reused, not re-installed. The decision and risk register is the
|
||||
set of ADRs in `docs/adr/`: each ADR's `## Consequences` records the residual
|
||||
risks of a decision, and the `docs/Открытые_вопросы` registry holds the
|
||||
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. The hook is **blocking** (`sys.exit(2)`),
|
||||
not warn-only; the block is a one-time per-file-and-rule speed-bump, so the
|
||||
cost is bounded. On this Windows host the bundled `hooks.json` hardcodes the
|
||||
`python3` interpreter, which is absent — fixed by a `python3.exe` shim in the
|
||||
Python install directory on PATH (the plugin cache is not modified).
|
||||
- The toolchain attack surface still depends on a manual procedure until a
|
||||
vetted auto-auditor exists.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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 inline-vulnerability hook (blocking
|
||||
`PreToolUse`, a one-time per-file-and-rule speed-bump).
|
||||
- `audit-portal` — the project skill encoding the 14-phase portal audit.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- 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.
|
||||
@@ -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 P0–P3 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.
|
||||
@@ -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 (0–90) ────
|
||||
{ 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) — один блокирующий PreToolUse-хук, inline-предупреждения уязвимостей при правке кода (8 контентных правил + 1 path-правило). При первом за сессию совпадении уязвимого паттерна в файле — sys.exit(2), блокирует правку (одноразовый speed-bump, retry проходит). Раздел D3. Tooling #40.',
|
||||
'Активен автоматически при каждом Write/Edit/MultiEdit — при уязвимом паттерне печатает предупреждение и блокирует первую такую правку файла за сессию; повторная попытка проходит.',
|
||||
'Правило PSR_v1 R10.1 блок 1 (audit-security, off-phase). SG1: 5-й PreToolUse-хук, блокирующий (sys.exit 2), одноразовый per «файл+правило» за сессию — economy/ruflo-цепочка не нарушается, +~34 мс/правку. SG2: Windows-починка — bundled hooks.json зовёт python3 (нет в PATH), решено python3.exe-шимом в каталоге Python. Не UI → вне R6.0/R6.1/R14. Tooling §4.15, CLAUDE.md §3.3 #40.',
|
||||
[{ name: 'PSR_v1', cond: 'R10.1 блок 1: audit-security' }, { name: 'Tooling', cond: '§4.15 #40 — реестр' }],
|
||||
[],
|
||||
[{ name: 'скил security-review', cond: 'оба — D3 audit-security; sec_guidance inline, sk_security_review ручной' }]
|
||||
),
|
||||
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,672 @@
|
||||
# A11 ML / AI Tooling Integration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Populate the empty `A11 «ML / AI-разработка»` map section with a conflict-minimal ML/AI toolset — document a **reuse core** (claude-api skill + context7 MCP + Sentry MCP), install **two new light tools** (promptfoo, a vendored Data Scientist skill), and register **Jupyter MCP as a deferred reserved slot** — so A11 becomes a working playbook.
|
||||
|
||||
**Architecture:** A11 is an **empty** functional section — `NODE_SECTION` in `docs/automation-graph.html` tags zero nodes `A11`. Approach А (chosen 2026-05-17): a **reuse layer** (claude-api skill, context7 MCP, Sentry MCP — all already installed; A11 documents the coverage, never re-tags their nodes) plus **two new installs** — promptfoo as a root `package.json` devDependency, and a Data Scientist skill vendored as a standalone skill into `.claude/skills/data-scientist/` (no plugin, no marketplace, no hooks — the A6 mermaid pattern). **Jupyter MCP** (executable notebooks) is **deferred** — registered now as a reserved slot, installed later by a separate severable task gated on a Python ML environment + a concrete model to train. All A11 tools are non-UI → a new **ml-ai-tooling** off-phase category, outside the PSR_v1 UI-pool. A11 artifacts live in `docs/ml/`.
|
||||
|
||||
**Tech Stack:** promptfoo (`promptfoo/promptfoo`, npm package `promptfoo`, MIT); a Data Scientist skill (vendored standalone skill, MIT/permissive — exact repo resolved in Task 1); Jupyter MCP (`datalayer/jupyter-mcp-server` — deferred, not installed); the already-installed claude-api skill / context7 MCP / Sentry MCP (reuse); project normative docs; `docs/automation-graph.html` (vis.js).
|
||||
|
||||
**Sequencing (2026-05-17):** the A6 / D3 / C9 epics land and push **first** (they touch the same shared files: the map, 4 normative docs, the Tooling counter). A11 then rebases onto the updated `origin/main` (Task 8 Step 1). The working branch `feat/a11-ml-ai-tooling` already exists and holds the brainstorming spec commit (`ae423be`). A11's Tooling numbers are runtime-resolved (NUM1) — never hard-coded before reading the live counter. Push pattern: `git push origin feat/a11-ml-ai-tooling:main`.
|
||||
|
||||
---
|
||||
|
||||
## Tool Identity (verified 2026-05-17 via WebSearch)
|
||||
|
||||
| # | Tool | Install mode | Source / License | Hooks? |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **promptfoo** — CLI test-suite for LLM prompts/agents/RAG: declarative `promptfooconfig.yaml`, assertions (`equals`/`contains`/`llm-rubric`/cost/latency), model comparison, red-teaming. Invoked `npx promptfoo`. | npm **devDependency** in the root `package.json` (`npm i -D promptfoo`) — version-pinned via `package-lock.json` (ML9) | GitHub `promptfoo/promptfoo`, **MIT** (OpenAI-owned since 2026, remains OSS) | None — npm CLI, no Claude Code lifecycle hooks |
|
||||
| 2 | **Data Scientist skill** — knowledge-only skill: business objective → ML task, algorithm selection (Linear Regression … XGBoost), feature engineering, experiment-tracking + A/B-analysis guidance. | Standalone skill — **vendored** copy into `.claude/skills/data-scientist/` (no plugin, no marketplace) | Exact repo **resolved Task 1 Step 4** — candidates: `secondsky/claude-skills` (the A6 architecture-patterns marketplace), `alirezarezvani/claude-skills` (263+ skills), or a dedicated data-science skill repo. MIT/permissive required. | None disclosed (skills-only) — **verify on vendor** |
|
||||
| — | **Jupyter MCP** (`datalayer/jupyter-mcp-server`) — executable notebooks: insert/run cells, read outputs, plots. | **NOT installed** — deferred reserved slot (see "Deferred Task"). | GitHub `datalayer/jupyter-mcp-server` | n/a — not installed |
|
||||
| — | **claude-api skill** / **context7 MCP** / **Sentry MCP** | **Reuse** — already installed | claude-api: plugin skill; context7: MCP (`mcp__plugin_context7_context7__*`); Sentry: Tooling #34 | n/a |
|
||||
|
||||
**Verification status:** promptfoo — repo, MIT, npm package name, config-file convention confirmed via WebSearch. Data Scientist skill — the *category* (151+ data-science skills in the marketplace) and the "Data Scientist" skill *concept* confirmed; the **exact source repo is NOT pinned** → Task 1 Step 4 resolves it with concrete criteria. Jupyter MCP — `datalayer/jupyter-mcp-server` confirmed to exist; **not installed by this plan**.
|
||||
|
||||
**Deferred (with reason — no task in this plan):**
|
||||
|
||||
- **Jupyter MCP** — would be the 8th `.mcp.json` server; experimental (Notebook 6.x only); requires a Python ML environment that the native-Windows machine deliberately lacks; and there is no model to train. Registered in the Tooling registry as a **pending** slot (Task 6); installed later by the severable task described in "Deferred Task" below.
|
||||
|
||||
**Dropped (with reason — no task, no slot):**
|
||||
|
||||
- **A dedicated LLM-observability tool** (Langfuse / Helicone) — redundant with Sentry's AI/LLM monitoring on the already-installed Sentry MCP (#34); CLAUDE.md §5 п.6 (no two tools for one job).
|
||||
- **A standalone Claude-API plugin** — the claude-api skill is already available; installing a second is a §5 п.6 duplication.
|
||||
- **An `ml-developer`-style agent plugin** — the agent layer is already crowded; promptfoo + the Data Scientist skill + claude-api cover the workflow without a new agent.
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions & Conflict Audit
|
||||
|
||||
Pattern follows the K1–K8 / AK1–CC1 / CP1–NUM1 audits used for claude-mem and the A6 / C9 / D3 plans. Verified against the promptfoo repo, project `.claude/settings.json`, `~/.claude/settings.json`, `.mcp.json`, `lefthook.yml`, `cspell.json`, `.markdownlintignore`, root `package.json`, and the A6 plan.
|
||||
|
||||
| # | Tool | Sev | Conflict | Resolution (locked) |
|
||||
|---|---|---|---|---|
|
||||
| ML1 | promptfoo | 🟡 | A real eval run needs an Anthropic API key and makes **paid** LLM calls. Putting it in a hook / pre-commit would cost money on every commit and could break the economy chain. | promptfoo runs **manually / CI-only** — never in a hook, never a lefthook job, never auto. No `lefthook.yml` change. The API key lives in an env var (`ANTHROPIC_API_KEY`, PowerShell User scope — the Sentry `SENTRY_AUTH_TOKEN` pattern), never committed. Documented in `docs/ml/README.md` (Task 4) + the Tooling entry (Task 6). |
|
||||
| ML2 | promptfoo | 🟢 | promptfoo's red-team module overlaps the D3 audit-security tools. | None — promptfoo red-team tests *LLM prompts* for jailbreak/injection; D3 Trail of Bits (#39) + Semgrep (#25) are SAST of *code*. Different objects. Boundary stated in `docs/ml/README.md` (Task 4) + the Tooling entry (Task 6). |
|
||||
| ML3 | Data Scientist skill | 🟡 | Vendored `.claude/skills/data-scientist/**/*.md` (third-party English files) is caught by the cspell + markdownlint pre-commit jobs. | Add `.claude/skills/data-scientist/**` to `cspell.json` `ignorePaths` and `.claude/skills/data-scientist/` to `.markdownlintignore` (Task 3 Step 4) — the A6 MK1 pattern. The project's own skills (`q-item-add`, `rls-check`, `regression`, `mermaid`) stay linted. |
|
||||
| ML4 | reuse layer | 🟢 | Re-tagging the existing `context7` (E7) / `mcp_sentry` (A7) map nodes to A11 would empty their current sections — `NODE_SECTION` is 1-node→1-section. | Reuse nodes **stay** in their sections. A11 gets its **own** new nodes (`claude_api`, `promptfoo`, `data_scientist`). The reuse coverage is documented in `docs/ml/README.md` (Task 4) and noted in the new nodes' details (Task 7). Same as A6/D3/C9 REU1. |
|
||||
| ML5 | all | 🟢 | A11 is non-UI tooling. | New off-phase category **ml-ai-tooling**, outside the PSR_v1 UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline — same treatment as `claude-md-management` (infrastructure), the A6 architecture-tooling, and the D3 audit-security categories. Registered in PSR_v1 R10.1 + Pravila §13.2 (Task 6). |
|
||||
| ML6 | promptfoo | 🟢 | 8 PreToolUse/UserPromptSubmit/Stop economy + skill-discipline + ruflo hooks — does promptfoo touch them? | None — promptfoo is an npm CLI invoked on demand; it registers zero Claude Code lifecycle hooks. Re-verified Task 2 Step 5. |
|
||||
| ML7 | all | 🟡 | Bus-factor — the Data Scientist skill is a community project; Jupyter MCP is community + experimental. | The Data Scientist skill is **vendored** → immune to upstream loss (the A6 mermaid pattern). promptfoo is MIT, OpenAI-owned, stable. Jupyter MCP is deferred and will be version-pinned at install. Noted in the Tooling entry (Task 6). No alpha-substrate spike needed for the core scope — neither promptfoo nor the vendored skill has a known-broken core (unlike ruflo K7). |
|
||||
| ML8 | reuse layer | 🟡 | claude-api skill / context7 MCP are used but may not be **formalized** Tooling-registry positions → using them unregistered is a PSR_v1 R0.2/R10 gap. | Task 1 Step 5 audits whether claude-api and context7 are already in `~/.claude/settings.json` `enabledPlugins` + the Tooling registry. Task 6 registers whatever is missing (as reuse positions). Sentry MCP is already Tooling #34. |
|
||||
| ML9 | Jupyter MCP | 🟡 | Registering a tool that is not installed could mislead a future reader into thinking it is available. | The Tooling entry + `docs/ml/README.md` + the map mark Jupyter MCP explicitly **«pending — не установлен, severable-задача»** (the Sentry #34 "pending Sentry deployment" precedent). No `jupyter_mcp` map node is added until the deferred task runs. |
|
||||
| NUM1 | normative sync | 🟡 | The A6 plan claimed Tooling #36-#38, D3 claimed #39-#40, C9 claims the next after that. A11 must not collide. | Task 1 Step 6 + Task 6 Step 1 read the **live** `docs/Tooling_v8_3.md` Прил. Н §0 counter and assign A11's numbers sequentially after whatever is current. Never hard-code a number before reading the live counter. |
|
||||
|
||||
**Severable scope.** Core A11 = Tasks 1-8 (reuse documentation + promptfoo + Data Scientist skill + normative + map + finish) — already populates and closes the section. There is **no severable task inside this plan** (unlike A6's Task 5): the only deferred piece, Jupyter MCP, is a *future* task outside this plan (see "Deferred Task"). A11 adds **no lefthook job** and **no `.mcp.json` change** — fewer conflicts by design (the C9 shape).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Created / Modified | Responsibility |
|
||||
|---|---|---|
|
||||
| `docs/ml/` | Create dir | A11 home — the ML/AI playbook |
|
||||
| `docs/ml/README.md` | Create | The ML/AI convention: tool boundaries (claude-api skill = *build* / promptfoo = *test* prompts / Data Scientist skill = classical-ML *workflow* / Jupyter MCP = *execute* — deferred); the reuse-layer map; the promptfoo API-key + manual-run note (ML1) |
|
||||
| `docs/ml/promptfoo-example/promptfooconfig.yaml` | Create | One seed eval config — a worked lead-qualification prompt example |
|
||||
| `docs/ml/promptfoo-example/README.md` | Create | How to run the example (`npx promptfoo eval`), the API-key requirement, the "never in CI/hooks" rule |
|
||||
| `.claude/skills/data-scientist/` | Create (vendored) | The Data Scientist skill — `SKILL.md` + `references/` |
|
||||
| `docs/adr/ADR-005-ml-ai-tooling.md` | Create (conditional — adr-kit/A6 landed) | Seed ADR documenting the A11 tooling decision + the Python/Jupyter defer |
|
||||
| `package.json` (repo root) | Modify | `promptfoo` devDependency |
|
||||
| `package-lock.json` (repo root) | Modify | promptfoo dependency tree (written by `npm i`) |
|
||||
| `cspell.json` | Modify | `ignorePaths` += `.claude/skills/data-scientist/**` (ML3) |
|
||||
| `.markdownlintignore` | Modify | += `.claude/skills/data-scientist/` (ML3) |
|
||||
| `cspell-words.txt` | Modify (conditional) | New ML/AI vocabulary |
|
||||
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new ml-ai-tooling subsection(s) + §0 counter bump |
|
||||
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — new ml-ai-tooling rows |
|
||||
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — ml-ai-tooling category note |
|
||||
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, new §3.3 ml-ai-tooling row(s) |
|
||||
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
|
||||
| `docs/automation-graph.html` | Modify | 3 new A11 nodes (`claude_api`, `promptfoo`, `data_scientist`) → `NODE_SECTION` A11; header metrics |
|
||||
| `.mcp.json` | **NOT modified** | Jupyter MCP deferred — `.mcp.json` is untouched by this plan |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Pre-flight — baseline, branch, snapshot, fact-check
|
||||
|
||||
**Files:** none modified (read-only)
|
||||
|
||||
- [ ] **Step 1: Confirm tree state and branch**
|
||||
|
||||
```bash
|
||||
cd "c:/моя/проекты/портал crm/Документация"
|
||||
git status --short
|
||||
git rev-parse --short HEAD
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Expected: branch `feat/a11-ml-ai-tooling`, HEAD at the spec commit `ae423be` (or later). Record the HEAD SHA as the regression baseline. (If a different branch — `git checkout feat/a11-ml-ai-tooling`.)
|
||||
|
||||
- [ ] **Step 2: Snapshot the hook chain**
|
||||
|
||||
Read `.claude/settings.json`, `.claude/settings.local.json` (if present), and `~/.claude/settings.json`. Record every hook on `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `PostCompact`, `Stop`. This is the ML6 baseline — Task 2 compares against it.
|
||||
Expected (`~/.claude/settings.json`): SessionStart economy-self-check; PreToolUse skill-marker/skill-check/economy-state-guard/CLAUDE.md-warn/security-guidance; UserPromptSubmit economy-mode; PostCompact economy-postcompact; Stop economy-verifier. Project `.claude/settings.json`: ruflo-recall + ruflo-queen (UserPromptSubmit), markdownlint-fix + schema-CHANGELOG-reminder (PostToolUse).
|
||||
|
||||
- [ ] **Step 3: Baseline regression**
|
||||
|
||||
```
|
||||
/regression quick
|
||||
```
|
||||
|
||||
Expected: GREEN. Record the current Pest / Vitest counts from the last green run (memory `project_state.md`). A11 touches no `app/` code → the final run (Task 8) must match.
|
||||
|
||||
- [ ] **Step 4: Fact-check promptfoo + resolve the Data Scientist skill repo**
|
||||
|
||||
promptfoo — open `https://github.com/promptfoo/promptfoo` and confirm: npm package `promptfoo`, MIT license, `promptfooconfig.yaml` config convention, the Anthropic provider id form (`anthropic:messages:<model>`), no Claude Code lifecycle hooks.
|
||||
|
||||
Data Scientist skill — resolve the exact source. WebFetch the candidates and pick **one** repo that satisfies ALL of:
|
||||
|
||||
- ships a real standalone **skill** (`SKILL.md` + optional `references/`), not a plugin requiring marketplace machinery;
|
||||
- MIT or other permissive license;
|
||||
- covers classical-ML workflow (algorithm selection, feature engineering, evaluation, experiment tracking);
|
||||
- no `hooks` block / no Claude Code lifecycle hooks.
|
||||
|
||||
Candidates, in priority order: (a) `secondsky/claude-skills` (already a project marketplace from A6 — check for a `data-science`/`data-scientist` skill dir), (b) `alirezarezvani/claude-skills`, (c) a dedicated data-science skill repo found via WebSearch. Record the chosen repo URL + license + the in-repo path to the skill directory. If a candidate registers CC lifecycle hooks → reject it, try the next.
|
||||
|
||||
If **no** candidate qualifies → **stop**, report to the user; A11 falls back to a project-authored minimal `data-scientist` skill (out of this plan's scope).
|
||||
|
||||
- [ ] **Step 5: Audit reuse-tool registration (ML8)**
|
||||
|
||||
Check whether the reuse tools are already formalized:
|
||||
|
||||
- `~/.claude/settings.json` `enabledPlugins` — is the claude-api skill's backing plugin listed? Record yes/no + the plugin id.
|
||||
- `.mcp.json` / `~/.claude/settings.json` — is context7 configured? (It is — `mcp__plugin_context7_context7__*` tools exist.) Record the server/plugin id.
|
||||
- `docs/Tooling_v8_3.md` Прил. Н — grep for `claude-api`, `context7`. Record which (if any) already have a Tooling number.
|
||||
|
||||
This drives Task 6: register whatever reuse tool is missing a Tooling position.
|
||||
|
||||
- [ ] **Step 6: Check A6 landed + read the live Tooling counter (NUM1)**
|
||||
|
||||
```bash
|
||||
git log --oneline | grep -iE "adr-kit|architecture-patterns|mermaid|ADR-00" | head
|
||||
ls docs/adr/ 2>/dev/null
|
||||
```
|
||||
|
||||
Read `docs/Tooling_v8_3.md` Прил. Н §0 — record the **live** tool counter. Record: **A6 landed?** yes/no (drives Task 4's conditional ADR-005 — `docs/adr/ADR-000-adr-process.md` exists ⇒ yes) and the counter value (drives Task 6 numbering). (Note: the Task-0 pre-commit run already showed `adr-judge` as lefthook job 9 + "1 ADR with Enforcement" — A6 has very likely landed; confirm here.)
|
||||
|
||||
No repo files changed → no commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Install promptfoo (npm devDependency — ML1/ML9)
|
||||
|
||||
**Files:** Modify `package.json`, `package-lock.json` (repo root)
|
||||
|
||||
- [ ] **Step 1: Inspect the root `package.json`**
|
||||
|
||||
Read the repo-root `package.json`. Confirm it is the doc-tooling project (scripts `lint:md`, `spell`, `links`, `a11y`; devDependencies `markdownlint-cli2`, `cspell`, etc.). Record the current `devDependencies` shape.
|
||||
|
||||
- [ ] **Step 2: Install promptfoo as a devDependency**
|
||||
|
||||
```bash
|
||||
cd "c:/моя/проекты/портал crm/Документация"
|
||||
npm install --save-dev promptfoo
|
||||
```
|
||||
|
||||
Expected: `promptfoo` appears in `package.json` `devDependencies` with a pinned `^`-range; `package-lock.json` updated. If `npm install` warns about peer deps, record the warning — do NOT use `--force`/`--legacy-peer-deps` unless a hard failure requires it (record the reason if so).
|
||||
|
||||
- [ ] **Step 3: Verify the CLI runs**
|
||||
|
||||
```bash
|
||||
npx promptfoo --version
|
||||
```
|
||||
|
||||
Expected: prints a version number (a real run with no API key still reports `--version`). No paid LLM call is made here.
|
||||
|
||||
- [ ] **Step 4: Add a convenience npm script (optional but recommended)**
|
||||
|
||||
Edit `package.json` `scripts` — add:
|
||||
|
||||
```json
|
||||
"eval:llm": "promptfoo eval -c docs/ml/promptfoo-example/promptfooconfig.yaml"
|
||||
```
|
||||
|
||||
This documents the manual invocation. It is **not** wired into any other script, hook, or CI step (ML1).
|
||||
|
||||
- [ ] **Step 5: Verify NO lifecycle hooks were added (ML6)**
|
||||
|
||||
Read the `hooks` block of `~/.claude/settings.json` AND project `.claude/settings.json`. Both must be **unchanged** vs the Task 1 Step 2 snapshot — npm installs do not touch Claude Code settings, but confirm. If anything changed → stop and re-audit.
|
||||
|
||||
- [ ] **Step 6: Lint + commit**
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json
|
||||
npx lefthook run pre-commit
|
||||
```
|
||||
|
||||
Expected: all jobs green (`package.json`/`package-lock.json` are JSON — not linted by markdownlint/cspell; gitleaks scans them — promptfoo's tree has no secrets).
|
||||
|
||||
```bash
|
||||
git commit -m "feat(a11): add promptfoo as devDependency for LLM prompt eval (ML1)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Vendor the Data Scientist skill (standalone, vendored — ML3/ML7)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `.claude/skills/data-scientist/` (vendored skill tree)
|
||||
- Modify: `cspell.json`, `.markdownlintignore`
|
||||
|
||||
- [ ] **Step 1: Clone the resolved source to a temp location**
|
||||
|
||||
Using the repo URL + in-repo skill path resolved in Task 1 Step 4:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 <RESOLVED_REPO_URL> /tmp/ds-skill-src
|
||||
ls -R /tmp/ds-skill-src/<RESOLVED_SKILL_PATH>/
|
||||
```
|
||||
|
||||
Expected: a `SKILL.md` + optional `references/` directory.
|
||||
|
||||
- [ ] **Step 2: Verify the skill ships no Claude Code hooks (ML6/ML7)**
|
||||
|
||||
```bash
|
||||
grep -rIl "hooks" /tmp/ds-skill-src --include="*.json" || echo "no hooks json"
|
||||
```
|
||||
|
||||
Expected: no `settings.json` with a `hooks` block. If the skill ships hooks → **stop**, re-audit (pick another candidate from Task 1 Step 4).
|
||||
|
||||
- [ ] **Step 3: Vendor the skill into the project**
|
||||
|
||||
```bash
|
||||
mkdir -p ".claude/skills/data-scientist"
|
||||
cp -r /tmp/ds-skill-src/<RESOLVED_SKILL_PATH>/. ".claude/skills/data-scientist/"
|
||||
ls -R ".claude/skills/data-scientist/"
|
||||
rm -rf /tmp/ds-skill-src
|
||||
```
|
||||
|
||||
Expected: `.claude/skills/data-scientist/SKILL.md` (+ `references/**` if present). The `SKILL.md` frontmatter `name:` should be `data-scientist` (or a clear ML name) — if the upstream `name:` clashes with an existing skill, edit it to `data-scientist`. (Vendoring — not a submodule — keeps it on Windows+Cyrillic paths and immune to upstream loss, ML7.)
|
||||
|
||||
- [ ] **Step 4: Exclude the vendored skill from the doc-lint chain (ML3)**
|
||||
|
||||
Edit `.markdownlintignore` — append:
|
||||
|
||||
```
|
||||
.claude/skills/data-scientist/
|
||||
```
|
||||
|
||||
Edit `cspell.json` — add `.claude/skills/data-scientist/**` to the `ignorePaths` array. Do **not** ignore `.claude/skills/` wholesale — `q-item-add`, `rls-check`, `regression`, `mermaid` stay linted.
|
||||
|
||||
- [ ] **Step 5: Reload and verify the skill is discoverable**
|
||||
|
||||
```
|
||||
/reload-plugins
|
||||
```
|
||||
|
||||
Confirm a `data-scientist` skill is now listed among available skills (project `.claude/skills/` is auto-discovered, like the other project skills). Confirm neither `settings.json` `hooks` block changed (ML6).
|
||||
|
||||
- [ ] **Step 6: Verify lint exclusion works, then commit**
|
||||
|
||||
```bash
|
||||
git add .claude/skills/data-scientist/ cspell.json .markdownlintignore
|
||||
npx lefthook run pre-commit
|
||||
```
|
||||
|
||||
Expected: cspell + markdownlint do NOT report errors from `.claude/skills/data-scientist/**`; all jobs green.
|
||||
|
||||
```bash
|
||||
git commit -m "feat(a11): vendor Data Scientist skill into .claude/skills + lint-ignore (ML3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Bootstrap the A11 home — `docs/ml/` + seed promptfoo example + ADR-005
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `docs/ml/README.md`, `docs/ml/promptfoo-example/promptfooconfig.yaml`, `docs/ml/promptfoo-example/README.md`
|
||||
- Create (conditional — A6 landed): `docs/adr/ADR-005-ml-ai-tooling.md`
|
||||
- Modify (conditional): `cspell-words.txt`
|
||||
|
||||
- [ ] **Step 1: Create the A11 home + the ML/AI convention**
|
||||
|
||||
```bash
|
||||
mkdir -p "docs/ml/promptfoo-example"
|
||||
```
|
||||
|
||||
Create `docs/ml/README.md`:
|
||||
|
||||
```markdown
|
||||
# docs/ml — ML / AI playbook (map section A11)
|
||||
|
||||
Home of the `A11 «ML / AI-разработка»` section. Defines the tooling Лидерра uses
|
||||
to build and test ML/AI capability. The portal currently ships no ML/AI code —
|
||||
this section is the toolset, ready for when AI features are scoped.
|
||||
|
||||
## Toolset
|
||||
|
||||
| Tool | Role | Status |
|
||||
|---|---|---|
|
||||
| **claude-api skill** | Build AI features on the Anthropic SDK (lead qualification, call summaries, email drafts) with prompt caching. | reuse — already available |
|
||||
| **context7 MCP** | Up-to-date docs for AI/ML libraries and SDKs. | reuse — already installed |
|
||||
| **Sentry MCP** | Debug AI features in production via Sentry AI/LLM monitoring (read-only). | reuse — Tooling #34, pending the Sentry deployment (Б-1) |
|
||||
| **promptfoo** | Test suite for LLM prompts/agents: assertions, regression, LLM-graded eval, red-team. | installed — `npx promptfoo` |
|
||||
| **Data Scientist skill** | Classical-ML workflow: business objective → ML task, algorithm selection, feature engineering, evaluation. | installed — vendored skill |
|
||||
| **Jupyter MCP** | Executable notebooks for real model training. | **deferred** — see below |
|
||||
|
||||
## Boundaries (which tool for which job)
|
||||
|
||||
- **Building an AI feature** (a prompt-backed endpoint) → the **claude-api skill**.
|
||||
- **Testing / regression-checking an LLM prompt** → **promptfoo** (`docs/ml/promptfoo-example/`).
|
||||
- **A classical-ML modelling question** (which algorithm, how to evaluate) → the
|
||||
**Data Scientist skill**.
|
||||
- **Executing a notebook / training a model** → **Jupyter MCP** — *deferred*.
|
||||
- promptfoo's **red-team** tests *prompts*; the D3 Trail of Bits / Semgrep tools do
|
||||
SAST of *code*. Different objects — not a duplication.
|
||||
|
||||
## promptfoo — running an eval
|
||||
|
||||
promptfoo makes **paid** Anthropic API calls. It runs **manually or in CI only** —
|
||||
never in a git hook, never in pre-commit, never automatically.
|
||||
|
||||
- API key: `ANTHROPIC_API_KEY` env var (PowerShell User scope — the Sentry
|
||||
`SENTRY_AUTH_TOKEN` pattern). Never commit a key.
|
||||
- Run the seed example: `npm run eval:llm` (or
|
||||
`npx promptfoo eval -c docs/ml/promptfoo-example/promptfooconfig.yaml`).
|
||||
|
||||
## Jupyter MCP — why deferred
|
||||
|
||||
Jupyter MCP executes notebooks; it needs a Python ML environment (pandas /
|
||||
scikit-learn / Jupyter). The machine is native Windows, deliberately runtime-minimal
|
||||
(no Docker), and there is no model to train yet. Jupyter MCP is a **reserved slot**:
|
||||
registered in the Tooling registry as *pending*, installed by a separate severable
|
||||
task when a concrete ML model is scoped. See the A11 plan's "Deferred Task".
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the seed promptfoo example config**
|
||||
|
||||
Create `docs/ml/promptfoo-example/promptfooconfig.yaml`:
|
||||
|
||||
```yaml
|
||||
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
|
||||
# Seed example — A11. Lead-qualification prompt eval.
|
||||
# Run manually: npm run eval:llm (needs ANTHROPIC_API_KEY — never in CI/hooks)
|
||||
description: "Лидерра — lead-qualification prompt eval (example)"
|
||||
|
||||
prompts:
|
||||
- |
|
||||
Классифицируй обращение лида как HOT, WARM или COLD.
|
||||
Ответь РОВНО одним словом — HOT, WARM или COLD.
|
||||
|
||||
Обращение: {{message}}
|
||||
|
||||
providers:
|
||||
- id: anthropic:messages:claude-haiku-4-5-20251001
|
||||
|
||||
tests:
|
||||
- vars:
|
||||
message: "Нужно срочно, бюджет согласован, готовы подписать договор сегодня."
|
||||
assert:
|
||||
- type: equals
|
||||
value: HOT
|
||||
- vars:
|
||||
message: "Интересно, расскажите подробнее про условия и сроки."
|
||||
assert:
|
||||
- type: equals
|
||||
value: WARM
|
||||
- vars:
|
||||
message: "Просто смотрю что есть на рынке, ничего конкретного."
|
||||
assert:
|
||||
- type: equals
|
||||
value: COLD
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create the example README**
|
||||
|
||||
Create `docs/ml/promptfoo-example/README.md`:
|
||||
|
||||
```markdown
|
||||
# promptfoo example — lead-qualification eval
|
||||
|
||||
A worked promptfoo eval: a HOT/WARM/COLD lead-classification prompt with three
|
||||
assertion cases. Demonstrates the A11 prompt-testing workflow.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# from the repo root; ANTHROPIC_API_KEY must be set (PowerShell User scope)
|
||||
npm run eval:llm
|
||||
```
|
||||
|
||||
This makes **paid** Anthropic API calls. Run it manually or in CI only — never
|
||||
in a git hook or pre-commit (A11 rule ML1). See `docs/ml/README.md`.
|
||||
|
||||
## Adapt
|
||||
|
||||
Copy `promptfooconfig.yaml` next to a real prompt when an AI feature is built.
|
||||
Swap the model, add `tests`, use richer assertions (`contains`, `llm-rubric`,
|
||||
cost/latency thresholds). Full reference: <https://promptfoo.dev/docs/>.
|
||||
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Decide on the conditional ADR (uses Task 1 Step 6 result)**
|
||||
|
||||
If **A6 landed** (`docs/adr/ADR-000-adr-process.md` exists, adr-kit present) → do Step 5. If A6 has **not** landed → skip Step 5, and add a line to `docs/ml/README.md` noting "the A11 tooling decision will be recorded as an ADR once adr-kit (A6) lands". Record the branch taken.
|
||||
|
||||
- [ ] **Step 5: Write ADR-005 (conditional)**
|
||||
|
||||
Create `docs/adr/ADR-005-ml-ai-tooling.md`:
|
||||
```markdown
|
||||
# ADR-005: ML / AI tooling (A11)
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-05-17
|
||||
- **Deciders:** Дмитрий
|
||||
|
||||
## Context
|
||||
The `A11 «ML / AI-разработка»` map section had zero tooling. Лидерра ships no
|
||||
ML/AI code; `calc_lead_score` is a deterministic SQL function. A toolset is needed
|
||||
for the day AI features (LLM-backed) or a scoring model are scoped.
|
||||
|
||||
## Decision
|
||||
A11 adopts a six-position toolset in two subcategories:
|
||||
- **LLM integration** — the claude-api skill (build), promptfoo (test prompts),
|
||||
Sentry MCP (observe). All reuse or light.
|
||||
- **Classical ML** — a vendored Data Scientist skill (workflow knowledge). The
|
||||
executable part, **Jupyter MCP**, is **deferred**: it needs a Python ML runtime
|
||||
the deliberately-minimal native-Windows machine lacks, and there is no model to
|
||||
train. Jupyter MCP is a reserved registry slot, installed by a separate task
|
||||
when a concrete model is scoped.
|
||||
- promptfoo runs manually / CI only — never in a hook (paid LLM calls).
|
||||
- A11 tools are non-UI → the `ml-ai-tooling` off-phase category.
|
||||
|
||||
## Consequences
|
||||
- Positive: A11 populated; AI features have a build+test+observe toolchain.
|
||||
- Risk: the Data Scientist skill is third-party — mitigated by vendoring.
|
||||
- Deferred: no Python runtime until a model is scoped — accepted, this is the
|
||||
decision.
|
||||
|
||||
## Enforcement
|
||||
None — A11 tools are advisory; verified by use and code review.
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Lint + commit**
|
||||
|
||||
```bash
|
||||
npx markdownlint-cli2 "docs/ml/**/*.md"
|
||||
npx cspell --no-progress --no-summary --no-gitignore "docs/ml/**/*.md"
|
||||
```
|
||||
|
||||
If A6 landed, also lint `docs/adr/ADR-005-*.md`. Add flagged valid terms (`promptfoo`, `promptfooconfig`, `scikit`, `XGBoost`, `Jupyter`, `Лидерра`, etc.) to `cspell-words.txt`. Then:
|
||||
|
||||
```bash
|
||||
git add docs/ml/ cspell-words.txt
|
||||
git add docs/adr/ADR-005-*.md # only if Step 5 ran
|
||||
git commit -m "feat(a11): bootstrap docs/ml — README + promptfoo example + ADR-005"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Smoke-test the A11 toolset
|
||||
|
||||
**Files:** none modified
|
||||
|
||||
- [ ] **Step 1: Smoke-test promptfoo config validity (no paid call — ML1)**
|
||||
|
||||
```bash
|
||||
npx promptfoo validate -c docs/ml/promptfoo-example/promptfooconfig.yaml
|
||||
```
|
||||
|
||||
Expected: the config parses as valid. (`validate` checks schema only — no LLM call, no cost.) If `validate` is not a subcommand in the installed promptfoo version, instead run `npx promptfoo eval -c docs/ml/promptfoo-example/promptfooconfig.yaml --no-cache` **only if** `ANTHROPIC_API_KEY` is set and the user approves the small cost; otherwise record "config syntax reviewed manually, eval not run (no key / cost)".
|
||||
|
||||
- [ ] **Step 2: Smoke-test the Data Scientist skill**
|
||||
|
||||
Invoke the `data-scientist` skill with a trivial ML question (e.g. "which algorithm and evaluation metric fit predicting lead conversion from CRM features?"). Expected: the skill loads, `SKILL.md` routes the intent, and it returns structured classical-ML guidance (algorithm choice, metrics, validation). Functional smoke — no file output required.
|
||||
|
||||
- [ ] **Step 3: Smoke-test the claude-api skill reuse**
|
||||
|
||||
Confirm the `claude-api` skill is invocable (it appears in the available-skills list). No deep test — A11 only documents it as reuse. Record availability.
|
||||
|
||||
- [ ] **Step 4: Confirm the hook chain is intact (ML6)**
|
||||
|
||||
Submit a trivial prompt; the economy marker still appears, the Stop verifier still runs, ruflo + CLAUDE.md-warn hooks fire. No plugin/skill leaked a `hooks` entry. No repo files changed in Task 5 → no commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Normative registry sync (ML / NUM1)
|
||||
|
||||
**Files:** Modify `docs/Tooling_v8_3.md`, `docs/Plugin_stack_rules_v1.md`, `docs/Pravila_raboty_Claude_v1_1.md`, `CLAUDE.md`, `docs/CHANGELOG_claude_md.md`
|
||||
|
||||
- [ ] **Step 1: Read the registry homes + the live counter (NUM1)**
|
||||
|
||||
Read for exact insertion points and the **current** counter: `docs/Tooling_v8_3.md` Прил. Н §0 + the last `§4.x` subsection; `docs/Plugin_stack_rules_v1.md` R10.1; `docs/Pravila_raboty_Claude_v1_1.md` §13.2. Using the Task 1 Step 5 result, decide the A11 numbers:
|
||||
|
||||
- `#N` promptfoo, `#N+1` Data Scientist skill, `#N+2` Jupyter MCP (registered **pending**) — sequential from `counter + 1`, after any A6/D3/C9 entries.
|
||||
- If Task 1 Step 5 found claude-api / context7 **unregistered**, also assign them numbers (reuse positions). Record the full number assignment.
|
||||
|
||||
- [ ] **Step 2: Add the Tooling Прил. Н ml-ai-tooling subsection(s)**
|
||||
|
||||
Edit `docs/Tooling_v8_3.md`: add a subsection for each new number, category **ml-ai-tooling** (off-phase). Per tool:
|
||||
|
||||
- **promptfoo** — npm `promptfoo` MIT (OpenAI-owned), root `package.json` devDependency, `npx promptfoo`; runs manually/CI only — never in a hook (ML1); red-team boundary vs D3 ToB/Semgrep (ML2); no CC hooks.
|
||||
- **Data Scientist skill** — vendored standalone skill in `.claude/skills/data-scientist/` (source repo from Task 1 Step 4), knowledge-only; bus-factor mitigated by vendoring (ML7).
|
||||
- **Jupyter MCP** — `datalayer/jupyter-mcp-server`, **pending — NOT installed**; deferred severable task gated on a Python ML environment (ML9); the Sentry #34 "pending" precedent.
|
||||
- claude-api / context7 (if unregistered) — reuse positions.
|
||||
|
||||
Add the ml-ai-tooling category as the **seventh** off-phase subcategory (after UI-pool, infrastructure, debug-runtime, architecture-tooling, audit-security — confirm the exact count against the live file). Bump §0 counter; bump the Прил. Н version header.
|
||||
|
||||
- [ ] **Step 3: Add PSR_v1 R10.1 rows**
|
||||
|
||||
Edit `docs/Plugin_stack_rules_v1.md`: add a row per new tool to R10.1, category **ml-ai-tooling** (off-phase) — explicitly *outside* the UI-pool → no R6.0/R6.1 stack-filter, no R14 pipeline (same treatment as `claude-md-management`, architecture-tooling, audit-security). Bump the PSR_v1 version header.
|
||||
|
||||
- [ ] **Step 4: Add the Pravila §13.2 note**
|
||||
|
||||
Edit `docs/Pravila_raboty_Claude_v1_1.md` §13.2: add a one-line **ml-ai-tooling** category note, alongside the existing infrastructure / debug-runtime / architecture-tooling / audit-security notes. Re-read Pravila §0/§13 first to keep section numbering consistent. Bump the Pravila version header.
|
||||
|
||||
- [ ] **Step 5: Update CLAUDE.md via the governed channel**
|
||||
|
||||
Invoke `/claude-md-management:claude-md-improver`. Apply: §3 title count bump, §1 priority-chain row 2b count bump, new §3.3 ml-ai-tooling row(s). The plugin also writes the `docs/CHANGELOG_claude_md.md` entry and bumps §0 cross-ref versions (Tooling / PSR_v1 / Pravila). **Do not** edit `CLAUDE.md` directly (§5 п.10).
|
||||
|
||||
- [ ] **Step 6: Lint + commit**
|
||||
|
||||
```bash
|
||||
npx markdownlint-cli2 "docs/Tooling_v8_3.md" "docs/Plugin_stack_rules_v1.md" "docs/Pravila_raboty_Claude_v1_1.md" "docs/CHANGELOG_claude_md.md"
|
||||
npx cspell --no-progress --no-summary --no-gitignore "docs/Tooling_v8_3.md" "docs/Plugin_stack_rules_v1.md" "docs/Pravila_raboty_Claude_v1_1.md"
|
||||
git add docs/Tooling_v8_3.md docs/Plugin_stack_rules_v1.md docs/Pravila_raboty_Claude_v1_1.md CLAUDE.md docs/CHANGELOG_claude_md.md cspell-words.txt
|
||||
git commit -m "docs(a11): register ml-ai-tooling category — promptfoo/Data Scientist skill/Jupyter MCP (NUM1)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Reflect A11 on the map — close the section
|
||||
|
||||
**Files:** Modify `docs/automation-graph.html`
|
||||
|
||||
- [ ] **Step 1: Read the structures to replicate**
|
||||
|
||||
In `docs/automation-graph.html` read, as templates: a vendored-skill node (`mermaid_skill`), a plugin-skill node, and an MCP node (`mcp_sentry`) across `NODES`, `NODE_DETAILS`/`nd(...)`, `NODE_SECTION`, and the "Паспорт узла" date fields. Record the current node/edge counts from the header and the group-count comments.
|
||||
|
||||
- [ ] **Step 2: Add the 3 A11 nodes**
|
||||
|
||||
Add to `NODES`, replicating the template shapes:
|
||||
|
||||
- `claude_api` — label `claude-api\n(skill)`, skills group.
|
||||
- `promptfoo` — label `promptfoo`, an appropriate tooling group (match how `bin/` CLIs / lefthook tools are grouped — e.g. the group used for gitleaks/lychee-class tools, or a plugins/skills group if no CLI group exists; record the choice).
|
||||
- `data_scientist` — label `Data Scientist\n(skill)`, skills group.
|
||||
|
||||
Add matching `nd(...)` / `NODE_DETAILS` entries (Russian, per the file's convention), Паспорт `since: '2026-05-17'`:
|
||||
|
||||
- `claude_api` — "Скил сборки AI-фич на Anthropic SDK (prompt-кэш). Reuse — раздел A11 опирается также на context7 MCP (доки) и Sentry MCP (LLM-наблюдаемость)."
|
||||
- `promptfoo` — "npm-CLI eval LLM-промптов: ассерты, регрессия, red-team. Запуск вручную/CI — не в хуках (платные вызовы)."
|
||||
- `data_scientist` — "Vendored-скил: классический ML-воркфлоу — выбор алгоритма, feature engineering, оценка модели."
|
||||
|
||||
- [ ] **Step 3: Map the 3 nodes to section A11**
|
||||
|
||||
In `NODE_SECTION` add (a new comment block, after the A6 block):
|
||||
|
||||
```js
|
||||
// A11 ml-ai-tooling 17.05.2026 — раздел «ML / AI-разработка» наполнен
|
||||
claude_api: 'A11', promptfoo: 'A11', data_scientist: 'A11',
|
||||
```
|
||||
|
||||
`A11 «ML / AI-разработка»` goes from 0 → 3 nodes — the section is no longer empty. (No `jupyter_mcp` node — Jupyter MCP is deferred, ML9.)
|
||||
|
||||
- [ ] **Step 4: Update header metrics + group-count comments**
|
||||
|
||||
Bump the node count in the map header/legend by 3. Bump the edge count if Step 2's node details add governing edges (match how the A6 nodes were wired — e.g. an edge to the governing normative node; replicate that pattern, else node-only). Update the `NODE_SECTION` group-count comments (skills, and whatever group `promptfoo` joined).
|
||||
|
||||
- [ ] **Step 5: Smoke-test the map**
|
||||
|
||||
```bash
|
||||
npx stylelint docs/automation-graph.html
|
||||
```
|
||||
|
||||
Open `docs/automation-graph.html` (Playwright MCP or a local `http.server` — quirk 90: `file://` rejected). Expected: 0 JS console errors; the 3 new nodes render; clicking section `A11` highlights all three.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/automation-graph.html
|
||||
git commit -m "feat(map): A11 nodes — closes section «ML / AI-разработка»"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Final regression & branch finish
|
||||
|
||||
**Files:** none modified
|
||||
|
||||
- [ ] **Step 1: Rebase onto latest origin/main (sequencing)**
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
Expected: a clean rebase (A6/D3/C9 already landed; A11 touched the same shared files — the map, 4 normative docs, the Tooling counter — so resolve any conflict by **re-reading the live file and re-applying the A11 delta**, never blindly). If conflicts are non-trivial → stop, report. Re-run the live-counter read (Task 6 Step 1) if the Tooling counter moved.
|
||||
|
||||
- [ ] **Step 2: Full pre-commit chain**
|
||||
|
||||
```bash
|
||||
npx lefthook run pre-commit
|
||||
```
|
||||
|
||||
Expected: all jobs green — A11 adds **no** lefthook job (job count unchanged vs the Task 1 baseline).
|
||||
|
||||
- [ ] **Step 3: Confirm app code untouched — run the suites**
|
||||
|
||||
A11 changes no `app/` code → suites must match the Task 1 Step 3 baseline:
|
||||
|
||||
```bash
|
||||
cd app && php artisan test --parallel
|
||||
cd .. && npm run test:vue
|
||||
```
|
||||
|
||||
Expected: Pest and Vitest counts unchanged vs the Task 1 baseline (0 regressions). Record exact counts; write out any failure with file:line.
|
||||
|
||||
- [ ] **Step 4: Confirm the economy/ruflo hook chain is intact**
|
||||
|
||||
Economy marker still appears; the Stop verifier still runs; no plugin/skill leaked a `hooks` entry into either `settings.json`. Compare to the Task 1 Step 2 snapshot.
|
||||
|
||||
- [ ] **Step 5: Pre-push checks**
|
||||
|
||||
```bash
|
||||
./bin/gitleaks.exe detect --source . --no-banner --redact
|
||||
./bin/lychee.exe --config .lychee.toml "docs/**/*.md" "*.md"
|
||||
```
|
||||
|
||||
Expected: gitleaks 0 leaks (the promptfoo example holds no key — ML1); lychee 0 broken (new `docs/ml/**/*.md` + `docs/adr/ADR-005-*.md` are scanned — fix or `.lychee.toml`-exclude any link, e.g. the `promptfoo.dev` docs link).
|
||||
|
||||
- [ ] **Step 6: Finish the branch**
|
||||
|
||||
Invoke `superpowers:finishing-a-development-branch` — present the standard options. Do **not** push without an explicit user choice. Push pattern: `git push origin feat/a11-ml-ai-tooling:main`.
|
||||
|
||||
---
|
||||
|
||||
## Deferred Task (NOT in this plan — future, severable)
|
||||
|
||||
**Jupyter MCP install — gated on a trigger.** When a concrete ML model is scoped
|
||||
(a real lead-scoring / lead-quality model with a dataset), run a separate task:
|
||||
|
||||
1. Decide the Python ML environment (native Windows venv vs a YC instance) — an
|
||||
explicit runtime weighing, like the Docker / pg_partman decisions.
|
||||
2. `alpha-substrate-spike-first` — spike Jupyter MCP (it is experimental, Notebook
|
||||
6.x only) before integrating.
|
||||
3. Install `datalayer/jupyter-mcp-server` as the 8th `.mcp.json` server, version-pinned.
|
||||
4. Flip the Tooling registry entry from **pending** to **active**; add a `jupyter_mcp`
|
||||
node to `docs/automation-graph.html` → `NODE_SECTION` A11 (now 4 nodes).
|
||||
5. Re-run the full regression + the conflict re-audit.
|
||||
|
||||
Until then, A11 is fully covered by the 5 installed/reuse positions.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**1. Spec coverage (the six-position toolset, Approach А).** Reuse layer — claude-api / context7 / Sentry documented in `docs/ml/README.md` (Task 4 Step 1), registration audited (Task 1 Step 5) and synced (Task 6). promptfoo — installed (Task 2), seed example (Task 4 Steps 2-3), smoked (Task 5 Step 1). Data Scientist skill — repo resolved (Task 1 Step 4), vendored (Task 3), smoked (Task 5 Step 2). Jupyter MCP — registered pending (Task 6 Step 2), deferred task documented ("Deferred Task"). Section closure: normative (Task 6), map (Task 7), regression/finish (Task 8). Conflict audit: ML1→T2.4+T4.1/3, ML2→T4.1+T6.2, ML3→T3.4, ML4→T4.1+T7.2-3, ML5→T6.2-4, ML6→T2.5+T3.5, ML7→T3.3+T6.2, ML8→T1.5+T6.1-2, ML9→T6.2+T7.3, NUM1→T1.6+T6.1+T8.1. No gaps.
|
||||
|
||||
**2. Placeholder scan.** `#N`/`#N+1`/`#N+2` (Task 6), `<RESOLVED_REPO_URL>`/`<RESOLVED_SKILL_PATH>` (Task 3), and the promptfoo node's map group (Task 7 Step 2) are **runtime-resolved by design** — the live Tooling counter, the Data Scientist skill source repo, and the live map group shapes are not knowable before Task 1 Step 4-6 / reading the 2400-line map, and each carries concrete resolution criteria (the A6/C9/D3 pattern). All file contents shown in full — `docs/ml/README.md`, both promptfoo-example files, ADR-005, the seed `promptfooconfig.yaml`. No "TBD" / "handle edge cases".
|
||||
|
||||
**3. Consistency.** Branch `feat/a11-ml-ai-tooling` consistent T1↔T8. Node ids `claude_api` / `promptfoo` / `data_scientist` consistent T7 Steps 2-3 + the `docs/ml/README.md` table. Category name **ml-ai-tooling** consistent T6 Steps 2-4 + ADR-005. Paths consistent: `.claude/skills/data-scientist/`, `docs/ml/`, `docs/ml/promptfoo-example/`. promptfoo install mode (root `package.json` devDep) consistent T2↔file-structure↔Tooling-entry. Jupyter MCP flagged **deferred/pending** uniformly (Tool Identity, ML9, Task 6 Step 2, Task 7 Step 3, "Deferred Task"). No lefthook job added — consistent with the minimal-conflict goal (ML1).
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Plan complete and saved to `docs/superpowers/plans/2026-05-17-a11-ml-ai-tooling-integration.md`. Two execution options:
|
||||
|
||||
1. **Subagent-Driven** — fresh subagent per task, two-stage review. *Caveat:* Task 2 (`npx promptfoo`), Task 3 Step 5 + Task 5 Steps 2-3 (`/reload-plugins`, skill invocations) and Task 6 Step 5 (`/claude-md-management`) are main-session-bound — those steps stay with the controller.
|
||||
2. **Inline Execution** — `superpowers:executing-plans`, batch with checkpoints. **Recommended here** — install/config/docs-heavy with many interactive main-session steps (the A6/C9/D3 pattern).
|
||||
|
||||
**Sequencing reminder:** A11 rebases onto `origin/main` after A6 / D3 / C9 land (Task 8 Step 1). One open item before execution: execution method — **1** (Subagent-Driven) or **2** (Inline, recommended here).
|
||||
@@ -0,0 +1,580 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
> **NB (исправлено 17.05.2026, нормативка v2.5):** #4 Security Guidance в этом плане описан как «warn-only / does not block» (строки ~21, ~251, ~542) — **фактически неверно**: хук делает `sys.exit(2)` → это **блокирующий** PreToolUse-хук (одноразовый speed-bump per «файл+правило» за сессию, retry проходит). Актуальная характеристика — Tooling §4.15 / ADR-003 / CLAUDE.md §3.3 #40. План оставлен как исторический снимок, тело не переписано.
|
||||
|
||||
## Tool Identity (verified 2026-05-17 via WebFetch/WebSearch)
|
||||
|
||||
| # | Tool | Install mode | Source / License | Hooks? |
|
||||
|---|---|---|---|---|
|
||||
| 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 K1–K8 / AK1–CC1 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).
|
||||
@@ -0,0 +1,378 @@
|
||||
# Sprint 6 — P3 Polish + Cleanup Tail Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Закрыть P3-эпики финального спринта portal-audit (`docs/superpowers/specs/2026-05-15-portal-audit-design.md` §3 Sprint 6) — a11y-доводка, гигиена констант, снятие устаревшего dev-workaround'а.
|
||||
|
||||
**Architecture:** 5 независимых XS-эпиков, все — frontend-only (Vue 3.5 SFC + TypeScript), 0 backend / 0 schema / 0 миграций. Каждый эпик правит обособленную группу файлов — пересечений между задачами нет, порядок T1→T5 произвольный, конфликты исключены.
|
||||
|
||||
**Tech Stack:** Vue 3.5 + Vuetify 3.12 + TypeScript, Vitest 4 (тесты), `import.meta.env.DEV` (DEV-гейт, статически вырезается Vite в prod-сборке).
|
||||
|
||||
**Scope-решение по 3 эпикам Sprint 6, НЕ входящим в этот план:**
|
||||
|
||||
- **F5** (`new_device_login` через session-fingerprint) — XL, требует инфраструктуры сессий-фингерпринтов; вне MVP, остаётся в реестре.
|
||||
- **G8** (ImpersonationDialog two-person approval, CTO-15) — требует Б-1 (Yandex 360 SSO); блокирован внешним блокером.
|
||||
- **I2** (dev-indices.json — gitignore-решение) — **отложен вместе с I1**. I1 (снос DevIndexBadge/DevIndexOverlay) заказчик отложил в Sprint 5D до заморозки UI; `dev-indices.json` — генерируемый манифест этой же временной фичи. Решать его git-судьбу до сноса фичи — churn на артефакте, который всё равно удалится. Отложен синхронно с I1.
|
||||
|
||||
**Эпики в этом плане (5):** A9, B6, F4, G9, I5.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Файл | Задача | Ответственность |
|
||||
|---|---|---|
|
||||
| `app/resources/js/views/auth/LoginView.vue` | T1 (A9) | eye-toggle через `#append-inner` slot с accessible-name |
|
||||
| `app/resources/js/views/auth/RegisterView.vue` | T1 (A9) | то же |
|
||||
| `app/resources/js/views/auth/ResetPasswordView.vue` | T1 (A9) | то же (только первое поле пароля; поле «Повторите» eye-иконки не имеет) |
|
||||
| `app/resources/js/layouts/AdminLayout.vue` | T2 (B6) | DEV-only баннер о застабленном auth-gate |
|
||||
| `app/resources/js/constants/polling.ts` | T3 (F4) | **создаётся** — именованные интервалы polling |
|
||||
| `app/resources/js/composables/usePolling.ts` | T3 (F4) | дефолт интервала из константы |
|
||||
| `app/resources/js/layouts/AppLayout.vue` | T3 (F4) | call-site → константы |
|
||||
| `app/resources/js/components/admin/ImpersonationBanner.vue` | T3 (F4) | call-site → константа |
|
||||
| `app/resources/js/views/ReportsView.vue` | T3 (F4) | call-site → константа |
|
||||
| `app/resources/js/views/admin/AdminSystemView.vue` | T4 (G9) | aria-label на edit-кнопки |
|
||||
| `app/resources/js/views/ProjectsView.vue` | T5 (I5) | удаление устаревшего clearable-workaround'а из `<style>` |
|
||||
| spec-файлы в `app/tests/Frontend/` | T1/T2/T4 | failing-тесты + сверка существующих |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: A9 — accessible-name на eye-icon переключателях пароля
|
||||
|
||||
**Контекст:** В `LoginView`/`RegisterView`/`ResetPasswordView` поле пароля переключает видимость через Vuetify-проп `:append-inner-icon` + `@click:append-inner`. Иконка-переключатель кликабельна, но не имеет accessible-name и не доступна с клавиатуры → screen-reader пользователь не знает, что это кнопка. Фикс — заменить проп на слот `#append-inner` с `<v-icon>` в роли кнопки: `role="button"` + `tabindex` + `:aria-label` + keyboard-обработчики.
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/resources/js/views/auth/LoginView.vue:81-93`
|
||||
- Modify: `app/resources/js/views/auth/RegisterView.vue:97-109`
|
||||
- Modify: `app/resources/js/views/auth/ResetPasswordView.vue:107-119`
|
||||
- Test: `app/tests/Frontend/LoginView.spec.ts`, `RegisterView.spec.ts`, `ResetPasswordView.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Сверить существующие spec-файлы**
|
||||
|
||||
Прочитать 3 spec-файла. Найти тесты, взаимодействующие с переключателем (grep `append-inner`, `showPassword`, `eye`). Если тест триггерит `click:append-inner` — после рефактора это событие не возникнет (теперь клик по `<v-icon>` в слоте), такие тесты переписать на клик по иконке с aria-label. Запомнить mount-setup (плагины Vuetify/Pinia/router-стабы) для нового теста.
|
||||
|
||||
- [ ] **Step 2: Написать failing-тест (для каждой из 3 вью)**
|
||||
|
||||
В каждый spec добавить тест (mount-setup взять из существующих тестов файла):
|
||||
|
||||
```ts
|
||||
it('переключатель видимости пароля имеет accessible-name и работает', async () => {
|
||||
const wrapper = mount(LoginView, { /* global: из существующего setup файла */ });
|
||||
const toggle = wrapper.find('[aria-label="Показать пароль"]');
|
||||
expect(toggle.exists()).toBe(true);
|
||||
expect(toggle.attributes('role')).toBe('button');
|
||||
await toggle.trigger('click');
|
||||
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
Для `RegisterView`/`ResetPasswordView` — аналогично, заменив компонент.
|
||||
|
||||
- [ ] **Step 3: Прогнать тесты — убедиться, что падают**
|
||||
|
||||
Run: `cd app && npm run test:vue -- LoginView RegisterView ResetPasswordView`
|
||||
Expected: 3 новых теста FAIL (`aria-label="Показать пароль"` не найден — сейчас проп `append-inner-icon`).
|
||||
|
||||
- [ ] **Step 4: Реализовать слот в LoginView.vue**
|
||||
|
||||
Заменить блок `app/resources/js/views/auth/LoginView.vue:81-93` (поле пароля):
|
||||
|
||||
```vue
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Пароль"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
autocomplete="current-password"
|
||||
placeholder="Минимум 8 символов"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
required
|
||||
:error-messages="errors.password"
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="showPassword = !showPassword"
|
||||
@keydown.enter.prevent="showPassword = !showPassword"
|
||||
@keydown.space.prevent="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Реализовать слот в RegisterView.vue**
|
||||
|
||||
Заменить блок `app/resources/js/views/auth/RegisterView.vue:97-109` тем же паттерном (поле пароля, `autocomplete="new-password"` — сохранить, `placeholder="Минимум 8 символов"` — сохранить). Снять строки `:append-inner-icon="..."` и `@click:append-inner="..."`, добавить `#append-inner`-слот как в Step 4.
|
||||
|
||||
- [ ] **Step 6: Реализовать слот в ResetPasswordView.vue**
|
||||
|
||||
Заменить блок `app/resources/js/views/auth/ResetPasswordView.vue:107-119` (первое поле — «Новый пароль») тем же паттерном. **Поле «Повторите пароль» (`:122-130`) не трогать** — у него нет eye-иконки, оно наследует `showPassword`.
|
||||
|
||||
- [ ] **Step 7: Прогнать тесты — убедиться, что зелёные**
|
||||
|
||||
Run: `cd app && npm run test:vue -- LoginView RegisterView ResetPasswordView`
|
||||
Expected: PASS — новые 3 теста + все ранее существовавшие в этих файлах.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add app/resources/js/views/auth/LoginView.vue app/resources/js/views/auth/RegisterView.vue app/resources/js/views/auth/ResetPasswordView.vue app/tests/Frontend/LoginView.spec.ts app/tests/Frontend/RegisterView.spec.ts app/tests/Frontend/ResetPasswordView.spec.ts
|
||||
git commit -m "fix(a11y): accessible eye-toggle на полях пароля — Sprint 6 A9"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: B6 — DEV-only баннер о застабленном auth-gate админки
|
||||
|
||||
**Контекст:** Sprint 3F (J2) поставил middleware `EnsureSaasAdmin` на `/api/admin/*` как стаб: в dev пропускает все запросы, в prod отдаёт 503. Комментарий в шапке `AdminLayout.vue:9-12` фиксирует, что полноценный auth-guard (`super_admin` role + 2FA через Yandex 360 SSO) ждёт Б-1. B6 — сделать этот auth-gap видимым в dev-UI баннером. Гейт — `import.meta.env.DEV` (Vite статически вырежет баннер в prod-сборке, паттерн I4 из Sprint 5D).
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/resources/js/layouts/AdminLayout.vue` (script + template)
|
||||
- Test: `app/tests/Frontend/AdminLayout.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Сверить AdminLayout.spec.ts**
|
||||
|
||||
Прочитать spec — запомнить mount-setup. Проверить наличие `vi.unstubAllEnvs()` в `afterEach` (если нет — добавить в Step 2, иначе stub `DEV` протечёт в другие тесты).
|
||||
|
||||
- [ ] **Step 2: Написать failing-тест**
|
||||
|
||||
В `AdminLayout.spec.ts` добавить (mount-setup — из существующих тестов):
|
||||
|
||||
```ts
|
||||
it('B6: показывает DEV-баннер auth-gap в dev-режиме', () => {
|
||||
const wrapper = mount(AdminLayout, { /* global: из существующего setup */ });
|
||||
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('B6: скрывает DEV-баннер в production-режиме', () => {
|
||||
vi.stubEnv('DEV', false);
|
||||
const wrapper = mount(AdminLayout, { /* global: из существующего setup */ });
|
||||
expect(wrapper.find('[data-testid="dev-auth-gap-banner"]').exists()).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
Убедиться, что в файле есть `afterEach(() => { vi.unstubAllEnvs(); });` (добавить, если отсутствует).
|
||||
|
||||
- [ ] **Step 3: Прогнать тест — убедиться, что падает**
|
||||
|
||||
Run: `cd app && npm run test:vue -- AdminLayout`
|
||||
Expected: тест `показывает DEV-баннер` FAIL (`data-testid="dev-auth-gap-banner"` не найден).
|
||||
|
||||
- [ ] **Step 4: Добавить DEV-флаг в script**
|
||||
|
||||
В `app/resources/js/layouts/AdminLayout.vue` после `const auth = useAuthStore();` (`:39`) добавить:
|
||||
|
||||
```ts
|
||||
|
||||
/** DEV-режим: показываем баннер о застабленном auth-gate админки (B6). */
|
||||
const isDevEnv = import.meta.env.DEV;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Добавить баннер в template**
|
||||
|
||||
В `<v-main class="admin-main">` (`:133`) — перед `<ImpersonationBanner />` вставить:
|
||||
|
||||
```vue
|
||||
<v-alert
|
||||
v-if="isDevEnv"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="ma-4"
|
||||
data-testid="dev-auth-gap-banner"
|
||||
>
|
||||
DEV-режим: доступ к админке открыт без SSO-проверки — middleware
|
||||
<code>EnsureSaasAdmin</code> в dev пропускает все запросы. В production
|
||||
требуется вход через Yandex 360 + роль <code>super_admin</code> (Б-1);
|
||||
неавторизованные запросы получают 503.
|
||||
</v-alert>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Прогнать тест — убедиться, что зелёные**
|
||||
|
||||
Run: `cd app && npm run test:vue -- AdminLayout`
|
||||
Expected: PASS — оба новых теста + ранее существовавшие.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add app/resources/js/layouts/AdminLayout.vue app/tests/Frontend/AdminLayout.spec.ts
|
||||
git commit -m "feat(admin): DEV-only баннер о застабленном auth-gate — Sprint 6 B6"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: F4 — вынести polling-интервалы в `constants/polling.ts`
|
||||
|
||||
**Контекст:** Дефолтный интервал `30_000` зашит в `usePolling.ts`, а call-site'ы `AppLayout`/`ImpersonationBanner`/`ReportsView` дублируют литералы `30_000`/`60_000`. F4 — собрать «магические» числа в один модуль. Чистый рефактор: поведение не меняется, защитная сетка — существующие тесты.
|
||||
|
||||
**Files:**
|
||||
- Create: `app/resources/js/constants/polling.ts`
|
||||
- Modify: `app/resources/js/composables/usePolling.ts:18,25`
|
||||
- Modify: `app/resources/js/layouts/AppLayout.vue:17,60,61`
|
||||
- Modify: `app/resources/js/components/admin/ImpersonationBanner.vue:16,40`
|
||||
- Modify: `app/resources/js/views/ReportsView.vue:14,62`
|
||||
|
||||
- [ ] **Step 1: Создать `constants/polling.ts`**
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Интервалы polling-обновления view-данных — единый источник «магических»
|
||||
* чисел для usePolling. До приезда SSE/WebSocket в production это покрывает
|
||||
* «real-time»-паттерн (см. composables/usePolling.ts).
|
||||
*/
|
||||
|
||||
/** Базовый интервал авто-обновления (сделки, биллинг, инциденты, тенанты, отчёты). */
|
||||
export const POLLING_INTERVAL_MS = 30_000;
|
||||
|
||||
/** Интервал для менее срочных счётчиков (напоминания в сайдбаре). */
|
||||
export const POLLING_REMINDERS_INTERVAL_MS = 60_000;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Подключить константу в usePolling.ts**
|
||||
|
||||
В `app/resources/js/composables/usePolling.ts`:
|
||||
- Первой строкой добавить импорт: `import { POLLING_INTERVAL_MS } from '../constants/polling';` (после `import { onBeforeUnmount, onMounted } from 'vue';`).
|
||||
- Строка `:18` doc-комментарий: `/** Период polling в миллисекундах. По умолчанию 30_000. */` → `/** Период polling в миллисекундах. По умолчанию POLLING_INTERVAL_MS (30 с). */`
|
||||
- Строка `:25`: `const intervalMs = options.intervalMs ?? 30_000;` → `const intervalMs = options.intervalMs ?? POLLING_INTERVAL_MS;`
|
||||
|
||||
- [ ] **Step 3: Обновить call-site'ы**
|
||||
|
||||
`AppLayout.vue` — добавить к импортам (`:17`): `import { POLLING_INTERVAL_MS, POLLING_REMINDERS_INTERVAL_MS } from '../constants/polling';`
|
||||
- `:60` `usePolling(loadNotifications, { intervalMs: 30_000, enabled: true });` → `{ intervalMs: POLLING_INTERVAL_MS, enabled: true }`
|
||||
- `:61` `usePolling(loadReminderCounts, { intervalMs: 60_000, enabled: true });` → `{ intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true }`
|
||||
|
||||
`ImpersonationBanner.vue` — добавить импорт `import { POLLING_INTERVAL_MS } from '../../constants/polling';`
|
||||
- `:40` `usePolling(load, { intervalMs: 30_000 });` → `{ intervalMs: POLLING_INTERVAL_MS }`
|
||||
|
||||
`ReportsView.vue` — добавить импорт `import { POLLING_INTERVAL_MS } from '../constants/polling';`
|
||||
- `:62` `usePolling(loadJobs, { intervalMs: 30_000 });` → `{ intervalMs: POLLING_INTERVAL_MS }`
|
||||
|
||||
Call-site'ы на дефолте (`DealsView`/`KanbanView`/`AdminBillingView`/`AdminIncidentsView`/`AdminTenantsView`) — **не трогать**, они уже получают значение через дефолт `usePolling`.
|
||||
|
||||
- [ ] **Step 4: Type-check + тесты (рефактор — поведение без изменений)**
|
||||
|
||||
Run: `cd app && npm run type-check && npm run test:vue -- usePolling AppLayout ImpersonationBanner ReportsView`
|
||||
Expected: vue-tsc 0 ошибок; все тесты PASS без изменений (интервалы численно те же — `30_000`/`60_000`).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/resources/js/constants/polling.ts app/resources/js/composables/usePolling.ts app/resources/js/layouts/AppLayout.vue app/resources/js/components/admin/ImpersonationBanner.vue app/resources/js/views/ReportsView.vue
|
||||
git commit -m "refactor(polling): вынести интервалы в constants/polling.ts — Sprint 6 F4"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: G9 — aria-label на edit-кнопки AdminSystemView
|
||||
|
||||
**Контекст:** В списке `system_settings` каждая строка имеет кнопку «Изменить» (`AdminSystemView.vue:166-175`). У всех кнопок одинаковый видимый текст «Изменить» — screen-reader пользователь, проходя список, слышит «Изменить, Изменить, Изменить» без контекста, какая настройка. Фикс — `:aria-label` с ключом настройки.
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/resources/js/views/admin/AdminSystemView.vue:166-175`
|
||||
- Test: `app/tests/Frontend/AdminSystemView.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Написать failing-тест**
|
||||
|
||||
В `AdminSystemView.spec.ts` добавить (mount-setup — из существующих тестов файла; компонент стартует с mock-данными до `onMounted`-загрузки, либо использовать `defineExpose`d `settingsState`):
|
||||
|
||||
```ts
|
||||
it('G9: edit-кнопки имеют aria-label с ключом настройки', () => {
|
||||
const wrapper = mount(AdminSystemView, { /* global: из существующего setup */ });
|
||||
const editBtns = wrapper.findAll('[data-testid^="edit-"]');
|
||||
expect(editBtns.length).toBeGreaterThan(0);
|
||||
for (const btn of editBtns) {
|
||||
const label = btn.attributes('aria-label') ?? '';
|
||||
expect(label).toMatch(/^Изменить настройку .+/);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Прогнать тест — убедиться, что падает**
|
||||
|
||||
Run: `cd app && npm run test:vue -- AdminSystemView`
|
||||
Expected: FAIL — `aria-label` отсутствует на кнопках.
|
||||
|
||||
- [ ] **Step 3: Добавить aria-label**
|
||||
|
||||
В `app/resources/js/views/admin/AdminSystemView.vue` в `<v-btn>` (`:166-175`) добавить строку `:aria-label` между `prepend-icon` и `:data-testid`:
|
||||
|
||||
```vue
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-pencil"
|
||||
:aria-label="`Изменить настройку ${setting.key}`"
|
||||
:data-testid="`edit-${setting.key}-btn`"
|
||||
@click="openEdit(setting)"
|
||||
>
|
||||
Изменить
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Прогнать тест — убедиться, что зелёный**
|
||||
|
||||
Run: `cd app && npm run test:vue -- AdminSystemView`
|
||||
Expected: PASS — новый тест + ранее существовавшие.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/resources/js/views/admin/AdminSystemView.vue app/tests/Frontend/AdminSystemView.spec.ts
|
||||
git commit -m "fix(a11y): aria-label с ключом на edit-кнопках AdminSystem — Sprint 6 G9"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: I5 — снять устаревший clearable-workaround из ProjectsView
|
||||
|
||||
**Контекст:** `ProjectsView.vue:170-196` содержит CSS-workaround: у `clearable` `v-text-field` иконка `mdi-close-circle` делалась прозрачной, а вместо неё `::after`-псевдоэлементом рисовался Unicode-глиф `✕` — потому что MDI-шрифт не был подключён (Диз-4). CTO-19 (миграция на Lucide) закрыта: `app/resources/js/plugins/vuetify.ts:164` маппит `'mdi-close-circle': XCircle` — clearable-иконка теперь рендерится нативным Lucide-SVG. Workaround мёртв → удалить.
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/resources/js/views/ProjectsView.vue` (удаление CSS-блока `:170-196`)
|
||||
|
||||
- [ ] **Step 1: Проверить премису (фальсифицировать перед удалением)**
|
||||
|
||||
Подтвердить, что `app/resources/js/plugins/vuetify.ts` содержит `'mdi-close-circle': XCircle` в Lucide IconSet-маппинге и `XCircle` импортирован из `lucide-vue-next`. Если маппинга нет — задача **BLOCKED**, эскалировать (workaround снимать нельзя без замены).
|
||||
|
||||
- [ ] **Step 2: Удалить CSS-блок workaround'а**
|
||||
|
||||
В `app/resources/js/views/ProjectsView.vue` удалить строки `:170-196` целиком — комментарий-заголовок `/* Workaround: MDI-шрифт... */` и 4 CSS-правила: `.projects-view :deep(.v-field__clearable)`, `.projects-view :deep(.v-field__clearable .v-icon)`, `.projects-view :deep(.v-field--dirty .v-field__clearable)::after`, `.projects-view :deep(.v-field--dirty .v-field__clearable:hover)::after`. Соседние блоки (`.projects-grid` выше, `.toolbar-check` ниже) не трогать.
|
||||
|
||||
- [ ] **Step 3: Type-check + сборка + существующие тесты**
|
||||
|
||||
Run: `cd app && npm run type-check && npm run test:vue -- ProjectsView`
|
||||
Expected: vue-tsc 0; `ProjectsView.spec.ts` PASS без изменений (правка чисто CSS, JS-поведение не затронуто).
|
||||
|
||||
- [ ] **Step 4: Визуальный smoke (Playwright)**
|
||||
|
||||
Запустить dev-сервер, открыть `/projects`, ввести текст в поле поиска проектов (`clearable`). Подтвердить: иконка очистки (Lucide `XCircle`) **видима** справа в поле и клик по ней очищает значение. Сделать скриншот. Если иконка не рендерится — premise опровергнута, `git revert` Step 2 и эскалировать.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/resources/js/views/ProjectsView.vue
|
||||
git commit -m "chore(cleanup): снять устаревший MDI clearable-workaround (CTO-19 tail) — Sprint 6 I5"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Финальная верификация (после всех 5 задач)
|
||||
|
||||
- [ ] **Полная регрессия Vitest** — `cd app && npm run test:vue -- --maxWorkers=2` (full-suite без `--maxWorkers=2` OOM'ит в worktree — квирк 98). Ожидаемо: 0 fail; число passed ≥ baseline + новые тесты (T1 ×3, T2 ×2, T4 ×1).
|
||||
- [ ] **vue-tsc** — `cd app && npm run type-check` → 0 ошибок.
|
||||
- [ ] **ESLint** — `cd app && npm run lint:vue` → известная pre-existing ошибка `tests/Frontend/ImportView.spec.ts:4` (Sprint 4 долг, вне scope Sprint 6); 0 новых.
|
||||
- [ ] **Vite build** — `cd app && npm run build` → OK (подтверждает, что DEV-гейт B6 валиден для prod-сборки).
|
||||
- [ ] **Pest** опционально — Sprint 6 не трогает PHP-файлы (0 backend-изменений), backend-регрессия структурно невозможна; полный прогон Pest — belt, не обязателен.
|
||||
- [ ] **Финальный holistic code-review** всего диффа Sprint 6.
|
||||
- [ ] **Pre-push:** gitleaks-full-history + lychee (lefthook не в PATH worktree — прогонять вручную).
|
||||
|
||||
## Self-Review (выполнено при написании плана)
|
||||
|
||||
- **Spec coverage:** Sprint 6 §3 спека = 8 эпиков. A9/B6/F4/G9/I5 — в плане (5 задач). F5/G8 — внешне блокированы (инфра/Б-1). I2 — отложен с I1 (решение заказчика Sprint 5D). Покрытие полное и обоснованное.
|
||||
- **Placeholder-скан:** весь production-код приведён дословно (file:line); тест-код — с конкретными ассертами, mount-setup берётся из существующих spec-файлов (они прочитаны на Step 1 каждой задачи).
|
||||
- **Type consistency:** `POLLING_INTERVAL_MS` / `POLLING_REMINDERS_INTERVAL_MS` — единые имена в T3 (создание + 4 call-site). `data-testid="dev-auth-gap-banner"` — единое имя в T2 (template + 2 теста). `isDevEnv` — единое имя в T2.
|
||||
@@ -0,0 +1,167 @@
|
||||
# A11 ML / AI Tooling Integration — Design
|
||||
|
||||
**Date:** 2026-05-17
|
||||
**Topic:** Populate the empty `A11 «ML / AI-разработка»` map section.
|
||||
**Deciders:** Дмитрий
|
||||
**Method:** `superpowers:brainstorming` (economy 5%). Approach А approved 2026-05-17.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Populate the empty `A11 «ML / AI-разработка»` section of `docs/automation-graph.html`
|
||||
with a conflict-minimal ML/AI toolset — so A11 becomes a working playbook, the same
|
||||
way A6 «Архитектура систем» was closed with adr-kit/mermaid/architecture-patterns and
|
||||
C9 «Управление проектами» with the CCPM stack.
|
||||
|
||||
## Context
|
||||
|
||||
- **A11 is empty.** `SECTIONS` in `docs/automation-graph.html:1836` defines
|
||||
`A11 = «ML / AI-разработка»` (bucket A «Технические и продуктовые»); `NODE_SECTION`
|
||||
tags **zero** nodes to A11.
|
||||
- **The map is a tooling map** — «карта dev-автоматики» (plugins / skills / MCP /
|
||||
hooks Claude uses). A11 must therefore be populated with **ML/AI *development
|
||||
tools*** — not with product features.
|
||||
- **The project has no ML/AI code.** Лидерра is a Laravel 13 + Vue 3 multi-tenant
|
||||
CRM. The only scoring artifact, `calc_lead_score` (`db/schema.sql`), is a
|
||||
deterministic SQL function — not a model. No dataset, no feature store, no ML/AI
|
||||
item in `Открытые_вопросы` or the plan backlog.
|
||||
- **The machine is runtime-minimal by policy.** Native Windows, no Docker (no
|
||||
nested virt on the OpenStack VPS), pg_partman replaced with a cron command. Every
|
||||
new runtime passes an explicit weighing. Python is **not** in the stack (PHP +
|
||||
Node only).
|
||||
|
||||
## Scope decision
|
||||
|
||||
A11 covers **both** subcategories — classical ML **and** LLM integration (user
|
||||
choice 2026-05-17). The **Python runtime is deferred**: the executable classical-ML
|
||||
part (Jupyter MCP) is a severable/conditional task, not a now-install. Rationale:
|
||||
no ML model to train, no dataset, a new runtime on a deliberately-minimal machine,
|
||||
and the Jupyter MCP is experimental (Notebook 6.x only). The classical-ML
|
||||
*knowledge* layer (Data Scientist skill, zero runtime) **is** installed now, so the
|
||||
subcategory is not empty — only its executor is gated.
|
||||
|
||||
This mirrors the project idiom: C9 made product-management a conditional task; A6
|
||||
made the lefthook-enforcement task severable.
|
||||
|
||||
---
|
||||
|
||||
## Architecture — the 6-position toolset (Approach А)
|
||||
|
||||
Two subcategories. Three positions are **reuse** (already installed — A11 documents
|
||||
the coverage, REU1), two are **new + light**, one is **new + deferred**.
|
||||
|
||||
| # | Tool | Subcategory | Install mode | Status |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **claude-api skill** | LLM integration | already available (plugin skill) | reuse |
|
||||
| 2 | **context7 MCP** | both (library docs) | already installed (MCP, map node `context7` in E7) | reuse |
|
||||
| 3 | **Sentry MCP** (Tooling #34) | LLM observability | already installed (MCP, map node `mcp_sentry` in A7) | reuse — AI/LLM monitoring activates on the Sentry deployment (blocked on Б-1) |
|
||||
| 4 | **promptfoo** | LLM integration | new — Node CLI, project `package.json` devDependency, invoked `npx promptfoo`; MIT (OpenAI-owned, OSS) | new, light |
|
||||
| 5 | **Data Scientist skill** | classical ML | new — vendored standalone skill into `.claude/skills/data-scientist/` (no plugin, no marketplace, no hooks — the A6 mermaid pattern) | new, light |
|
||||
| 6 | **Jupyter MCP** (`datalayer/jupyter-mcp-server`) | classical ML | new — MCP server, Python-gated | **severable / conditional** — installed only when a concrete ML model is scoped |
|
||||
|
||||
**Reuse layer (positions 1-3).** claude-api skill = building AI features on the
|
||||
Anthropic SDK (lead qualification, call-summary, email drafts) with prompt caching.
|
||||
context7 MCP = up-to-date docs for AI/ML libraries and SDKs. Sentry MCP = debugging
|
||||
AI features in production via Sentry's AI/LLM monitoring (read-only, pending the
|
||||
Sentry instance — Б-1).
|
||||
|
||||
**New layer (positions 4-5).** promptfoo = a test suite for LLM prompts/agents:
|
||||
assertions, regression catching, LLM-graded eval, red-teaming. Node-based — **needs
|
||||
no new runtime**. Data Scientist skill = a knowledge-only skill: business
|
||||
objective → ML task, algorithm selection (Linear Regression … XGBoost), feature
|
||||
engineering, experiment-tracking guidance, A/B analysis.
|
||||
|
||||
**Deferred (position 6).** Jupyter MCP = executable notebooks for real model
|
||||
training. Gated on a Python ML environment + a concrete ML model to train.
|
||||
|
||||
A11 is **fully covered** by positions 1-5 from day one; position 6 is a reserved,
|
||||
registered-but-not-installed slot.
|
||||
|
||||
---
|
||||
|
||||
## Conflict audit
|
||||
|
||||
Pattern follows the K1–K8 / AK1–CC1 / CP1–NUM1 audits used for claude-mem and the
|
||||
A6 / C9 / D3 plans. Full audit with locked resolutions is produced in the
|
||||
implementation plan; the known conflicts:
|
||||
|
||||
| # | Tool | Sev | Conflict | Resolution direction |
|
||||
|---|---|---|---|---|
|
||||
| ML1 | promptfoo | 🟡 | A real eval run needs an Anthropic API key + costs money (paid LLM calls). | Key via env (PowerShell User scope, like Sentry) — never committed (gitleaks). promptfoo runs **manually / CI-only** — never in a hook, never in pre-commit, never auto. No economy-chain impact. |
|
||||
| ML2 | promptfoo | 🟢 | promptfoo's red-team module overlaps the D3 audit-security tools. | None — promptfoo red-team tests *LLM prompts* for jailbreak/injection; Trail of Bits / Semgrep (D3 #39, #25) are SAST of *code*. Different objects. Boundary documented. |
|
||||
| ML3 | Data Scientist skill | 🟡 | Vendored `.claude/skills/data-scientist/**/*.md` (third-party English files) is caught by the cspell + markdownlint pre-commit jobs. | Add `.claude/skills/data-scientist/**` to `cspell.json` `ignorePaths` and `.markdownlintignore` (the A6 MK1 pattern). The project's own skills stay linted. |
|
||||
| ML4 | reuse layer | 🟢 | Re-tagging the existing `context7` (E7) / `mcp_sentry` (A7) nodes to A11 would empty their current sections — `NODE_SECTION` is 1-node→1-section. | Reuse nodes **stay** in their sections; A11 gets its **own** new nodes. The reuse coverage is documented in `docs/ml/README.md`. Same as A6/D3/C9 REU1. |
|
||||
| ML5 | all | 🟢 | A11 is non-UI tooling. | New off-phase category **ml-ai-tooling**, outside the PSR_v1 UI-pool (no R6.0/R6.1 stack-filter, no R14 pipeline) — same treatment as architecture-tooling (A6) and audit-security (D3). |
|
||||
| ML6 | Jupyter MCP | 🟡 | Experimental (Notebook 6.x only), would be the 8th `.mcp.json` server, on a Python-less machine. | **Deferred** — severable/conditional task. When triggered: `alpha-substrate-spike-first` — a spike before integration. Registered in the registry/map as a reserved slot now; `.mcp.json` is untouched in the core scope. |
|
||||
| ML7 | all | 🟡 | Bus-factor — Data Scientist skill is community; Jupyter MCP is community + experimental. | Data Scientist skill is **vendored** → immune to upstream loss (the A6 mermaid pattern). promptfoo is MIT, OpenAI-owned, stable. Jupyter MCP — version-pinned at install time (deferred). Noted in the Tooling entry. |
|
||||
| ML8 | claude-api skill | 🟡 | claude-api is an available skill but may not be a formalized Tooling-registry position → using it unregistered is a PSR_v1 R0.2/R10 gap. | The plan's pre-flight checks whether claude-api (and its backing plugin) is already in `~/.claude/settings.json` `enabledPlugins` + the Tooling registry; if absent, register it as part of the A11 sync. |
|
||||
| ML9 | promptfoo | 🟢 | promptfoo install mode — global npm vs project devDep vs vendored binary. | Project `package.json` **devDependency** — version-pinned via `package-lock.json`, no global state, invoked `npx promptfoo`. (Not a `bin/*.exe` — promptfoo is an npm package, not a single binary.) |
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Created / Modified | Responsibility |
|
||||
|---|---|---|
|
||||
| `docs/ml/` | Create dir | A11 home — the ML/AI playbook |
|
||||
| `docs/ml/README.md` | Create | The ML/AI convention: tool boundaries (claude-api skill = *build* AI features / promptfoo = *test* prompts / Data Scientist skill = classical-ML *workflow* / Jupyter MCP = *execute* — when scoped); the reuse-layer map |
|
||||
| `docs/ml/promptfoo-example/promptfooconfig.yaml` | Create | One seed eval config — a worked example (e.g. a lead-qualification prompt with assertions) |
|
||||
| `.claude/skills/data-scientist/` | Create (vendored) | The Data Scientist skill — `SKILL.md` + `references/` |
|
||||
| `docs/adr/ADR-005-ml-ai-tooling.md` | Create (conditional — adr-kit/A6 landed) | Seed ADR documenting the A11 tooling decision + the Python-defer |
|
||||
| `package.json` | Modify | `promptfoo` devDependency |
|
||||
| `cspell.json` | Modify | `ignorePaths` += `.claude/skills/data-scientist/**` (ML3) |
|
||||
| `.markdownlintignore` | Modify | += `.claude/skills/data-scientist/` (ML3) |
|
||||
| `cspell-words.txt` | Modify (conditional) | New ML/AI vocabulary |
|
||||
| `docs/Tooling_v8_3.md` | Modify | Прил. Н — new ml-ai-tooling subsection(s) + §0 counter bump |
|
||||
| `docs/Plugin_stack_rules_v1.md` | Modify | R10.1 — new ml-ai-tooling rows |
|
||||
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §13.2 — ml-ai-tooling category note |
|
||||
| `CLAUDE.md` | Modify (**via claude-md-management only**) | §3 title count, §1 row 2b count, new §3.3 ml-ai-tooling rows |
|
||||
| `docs/CHANGELOG_claude_md.md` | Modify | CLAUDE.md version-bump entry |
|
||||
| `docs/automation-graph.html` | Modify | 3 new A11 nodes (`claude_api`, `promptfoo`, `data_scientist`) → `NODE_SECTION` A11; header metrics |
|
||||
| `.mcp.json` | **NOT modified in core scope** | Jupyter MCP deferred — `.mcp.json` touched only by the conditional task |
|
||||
|
||||
**Map nodes.** A11 gets **3 new nodes** — `claude_api`, `promptfoo`, `data_scientist`.
|
||||
The reuse nodes `context7` (E7) and `mcp_sentry` (A7) stay put (ML4). A `jupyter_mcp`
|
||||
node is added only when the conditional task runs (same as C9's conditional
|
||||
`product_mgmt` node).
|
||||
|
||||
---
|
||||
|
||||
## Severable scope
|
||||
|
||||
**Core A11** = the reuse layer + promptfoo + Data Scientist skill + normative sync +
|
||||
map closure. This already populates and closes the section.
|
||||
|
||||
**Conditional / severable** = the Jupyter MCP install (position 6) — a separate task,
|
||||
gated on (a) a Python ML environment decision and (b) a concrete ML model to build.
|
||||
Registered now as a reserved slot; installed later.
|
||||
|
||||
A11 adds **no lefthook job** and **no `.mcp.json` change** in the core scope — fewer
|
||||
conflicts by design (the C9 shape).
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Building actual AI features into the CRM product (AI lead qualification, call
|
||||
summaries) — that is product work, tracked separately; A11 only provides the
|
||||
*tooling*.
|
||||
- Training an actual ML model / standing up a Python environment — the Jupyter MCP
|
||||
conditional task, triggered later.
|
||||
- Any LLM-eval gate in the commit pipeline (ML1 — promptfoo stays manual/CI-only).
|
||||
|
||||
## Open items resolved by the implementation plan
|
||||
|
||||
- **NUM** — A11's Tooling-registry numbers are runtime-resolved: the plan reads the
|
||||
**live** `docs/Tooling_v8_3.md` Прил. Н §0 counter (after A6 #36-38, D3 #39-40, and
|
||||
whatever C9 added) and assigns A11's numbers sequentially.
|
||||
- **claude-api registration** (ML8) — the plan's pre-flight checks whether claude-api
|
||||
is already a formalized Tooling position; registers it if not.
|
||||
- **ADR-005 conditional** — written only if adr-kit (A6) has landed in `origin/main`.
|
||||
- **Sequencing** — A11 forks off `origin/main` after the A6 / D3 / C9 epics land
|
||||
(they touch the same shared files: the map, 4 normative docs, the Tooling counter).
|
||||
The branch created for this spec is a scratch branch; execution re-forks.
|
||||
- **Exact tool identity** (promptfoo repo/version, Data Scientist skill source repo,
|
||||
Jupyter MCP repo) — verified via WebFetch/WebSearch in the plan's Tool Identity
|
||||
section (the A6/C9/D3 pattern).
|
||||
Reference in New Issue
Block a user