DeleteSupplierProjectJob: если после удаления Лидерра-проекта у донора
(supplier_project) не осталось других потребителей (pivot
project_supplier_links) — удаляет его у поставщика и локально;
если потребители есть — НЕ удаляет, диспатчит SyncSupplierProjectsJob.
2 Pest-теста (no-consumers / remaining-consumers) GREEN.
phpstan-baseline: +once() Mockery chain (аналог andThrow baseline).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Узлы rector/php_insights/backend_patterns/nightowl теперь в панелях описания (nd())
и теплокарте использования (NODE_META, uses:0 новые). Дополняет 5d82fdd (NODES/EDGES/
NODE_SECTION в data.js). Browser-smoke: 141 узел, NODE_META+NODE_DETAILS у всех 4, 0 JS-ошибок.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AppLayout брал заголовок топбара только из sidebar navItems → /reminders и
/import (которых нет в боковом меню) показывали fallback «Страница». Добавлен
fallback на route.meta.title перед «Страница». TDD: AppLayout.spec.ts (RED→GREEN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DashboardPageHead показывал захардкоженное «Доброе утро, Иван» любому
пользователю. Теперь имя берётся из auth-store (first_name), а приветствие —
по времени суток (ночь/утро/день/вечер). Fallback «коллега» при отсутствии user.
TDD: DashboardPageHead.spec.ts (RED→GREEN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Реальный портал отдаёт rt-projects-load с name='B1_<id>' / 'B2_<id>' / 'B3_<id>'
и чистым идентификатором в поле 'content'. Старое matching по name === uniqueKey
никогда не совпадало с реальным ответом → idMap пустой → SyncSupplierProjectJob
молча выходил, ничего не записав в БД, а на портале оставались orphan-группы.
Объясняет ранее задокументированное в ЭТАЛОН «проект 5 вылечен вручную —
усыновлены 3 портальные записи». Заказчик обходил тот же баг руками.
Фикс — matching по content с fallback на name, чтобы мок-тесты с упрощённым
форматом (без content) продолжали работать; реалистичная фикстура добавлена
в SupplierPortalClientMultiFlagTest.
Verified:
- Pest supplier suite (SyncSupplierProjectJob/SyncSupplierProjectsJob/multi-flag): 16/16 passed
- E2E live на crm.bp-gr.ru: ProjectService::create + sync → supplier_projects записаны
с ext_id, pivot заполнен, портал имеет 3 группы B1/B2/B3
- Multi-tenant ночной батч с computeOrder проверен на 79991177889 (T1+T2+T3+T4
на одном identifier — формула max(max, ceil(Σ/3)) сходится с фактом)
Закрывает два бага sync поставщика, обнаруженные при live-проверке создания
проекта «мой номер» (call, 79135191264, лимит 15, дни Пн-Пт):
1. SyncSupplierProjectJob хардкодил workdays=[1..7] в 7 местах и в DTO для
portal, и в supplier_projects.current_workdays. Заменено на реальную маску
через приватный workdaysFromMask() (зеркало bitmaskToList ночного батча).
2. forceFill в update-path online mode не включал current_workdays — после
первого create со старыми [1..7] последующий ресинк не подтягивал
реальные дни в локальную БД (на portal летели корректные, в нашей таблице
оставались stale).
3. ProjectService::update() ресинкал только при смене sms_*/signal_identifier/
regions. Добавлены daily_limit_target и delivery_days_mask — поставщик
видит новый лимит и дни сразу, не дожидаясь ночного батча 18:00 МСК.
Тесты:
- SyncSupplierProjectJobTest: +2 specs (real-workdays create-path, update-path
current_workdays refresh).
- ProjectsUpdateTest: «without resync» переписан в name-only, +2 specs
(daily_limit_target и delivery_days_mask change → resync).
- Pest 146/146 (Supplier + Plan5/Projects scope), Pint passed, Larastan 0.
Live-ресинк проекта id=5 «мой номер» в dev DB выполнен — current_workdays
теперь [1,2,3,4,5], HTTP ушёл к crm.bp-gr.ru с теми же днями.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Портал crm.bp-gr.ru возвращает status=Doubles при попытке создать
вторую группу с тем же unique_key. Старый код делал одну B1/B2/B3-группу
на каждый регион проекта — вторая группа молча пропадала.
Теперь оба джоба (SyncSupplierProjectJob + SyncSupplierProjectsJob)
формируют ровно одну группу на идентификатор со всеми регионами:
- regions=[82,83] → tag='РФ', regions=[82,83] в одной группе
- regions=[] → tag='РФ', regions=[] (вся РФ)
- regions=[82] → tag='Москва', regions=[82]
subject_code=null во всех supplier_projects и project_supplier_links.
ProjectService::update() теперь триггерит SyncSupplierProjectJob
при изменении поля regions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Активатор v-menu внутри position:fixed v-app-bar уезжает off-screen под
prefers-reduced-motion:reduce (умолчание Windows Server). Подключён
repositionMenuAfterOpen к обоим меню топбара через @update:model-value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
План 4 Task 4 эпика project-migration-redesign.
- NewProjectDialog: отдельный чекбокс «Вся РФ» (89 субъектов в autocomplete
без sentinel сохранены) + inline v-alert предупреждение + подтверждение.
- Взаимоисключение: выбор субъектов снимает «Вся РФ» и наоборот.
- Гейт submit: блок если ни субъектов, ни подтверждённой «Вся РФ»
(errors.regions = «Выберите регион...»); «Вся РФ» -> regions=[] на API.
- Лейбл autocomplete «Регионы» (убрано «(пусто = вся РФ)»).
- watch immediate:true — инициализация vsyaRf/edit-prefill при mount
(чинит EditProjectDialog submit при модальном открытии).
- Vitest 3/3 новых + 22 passed соседних (NewProject/Edit/ProjectsView) без регрессий.
План 4 Task 2 эпика project-migration-redesign.
- AdminSupplierIntegrationController +projectsIndex (список supplier_projects
+ кто заказывал через pivot project_supplier_links -> projects -> tenants
organization_name + дата последней поставки = max supplier_leads.received_at
+ subject_name из RussianRegions::CODE_TO_NAME, «РФ» при NULL subject_code).
- +projectsDestroy (bulk-delete: deleteProject на портале, затем локально;
pivot снимается CASCADE; сбой строки не прерывает batch -> failures[]).
- Routes: GET /projects, POST /projects/delete в admin-группе.
- Pest 5/5 (26 assertions). phpstan-baseline +9 ignore (Pest TestCall).
Closes the «Observer instrument expansion v2» epic. The retro note is
the source of all #1-#19 references in commit messages; the plan is
the procedural source (with REVISION v1.1 after parallel-session rebase).
Both kept in repo for traceability of the 20-commit epic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync header + §12 changelog summarising the 18-task epic «Observer
instrument expansion v2» implementation. Each subsection (§12.1-§12.9)
references the brain-retro 2026-05-20 #N item and the worktree commit
chain.
Closes Task 20 of docs/superpowers/plans/2026-05-20-observer-instrument-expansion.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes brain-retro 2026-05-20 #18 — episodes without schema_version=2
(legacy v1 era pre-2026-05-19T08:06) are now visible in STATUS.md
metrics. They're already filtered out of factor analysis by analyzer's
v1SkippedCount, but their existence was invisible to humans reading
STATUS — masking the bootstrap-epoch gap.
2 new vitest tests, 326/326 GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes brain-retro 2026-05-20 #17 — one-off Node script for investigating
the Glob p50=12.7s anomaly from initial retro. Parses transcript JSONL,
prints top-N slowest Glob round-trips with pattern + path.
Smoke-tested on session 553717ec (5h+ session): finds 32 Glob calls,
median 12690ms (matches retro finding), top-5 all 'docs/adr/**' at
20265ms — Glob recursive on ADR directory is the apparent culprit.
NOT production code path — never imported by parser/hook/analyzer.
Run on demand: node tools/glob-latency-investigator.mjs <transcript.jsonl>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes brain-retro 2026-05-20 #16 — when the next prompt is 'neutral'
(no correction/approval/new_task markers), interpret as silent success
('no objection') and surface as soft_success. Slightly weaker than
explicit approval — labelled separately so brain-retro can show
breakdown.
4 new vitest tests, 324/324 GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>