fix: возврат чужой работы, которую откатило моё сведение
Перед слиянием с main я откатил 4 файла к своей старой версии, чтобы
сдвинуть с места застрявшее слияние — и слияние закрепило этот откат.
Пострадала починка от 29.07: чтение связок поставщика из pivot (без неё
5 из 9 работающих проектов показывали жёлтое «Готовим к запуску» при
живом заказе) и два набора тестов вебхука/сверки CSV.
Файлы возвращены к состоянию main 7361d1ae3. На боевой откат НЕ уезжал —
выкат был точечный, только 6 файлов воронки продаж; проверено на живом
сервере: починка там на месте.
This commit is contained in:
@@ -38,7 +38,7 @@ class ProjectController extends Controller
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Project::query()
|
||||
->with(['supplierB1', 'supplierB2', 'supplierB3']) // eager-load to avoid N+1 in aggregation helpers
|
||||
->with(['supplierProjects', 'supplierB1', 'supplierB2', 'supplierB3']) // eager-load to avoid N+1 in aggregation helpers
|
||||
->withCount('supplierProjects') // ProjectResource::source_locked — анти-N+1 (hasLinks без per-row запроса)
|
||||
->where('tenant_id', $request->user()->tenant_id);
|
||||
|
||||
@@ -185,7 +185,7 @@ class ProjectController extends Controller
|
||||
/** GET /api/projects/{id} */
|
||||
public function show(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$project = Project::with(['supplierB1', 'supplierB2', 'supplierB3']) // eager-load to avoid N+1
|
||||
$project = Project::with(['supplierProjects', 'supplierB1', 'supplierB2', 'supplierB3']) // eager-load to avoid N+1
|
||||
->withCount('supplierProjects') // ProjectResource::source_locked — анти-N+1
|
||||
->where('tenant_id', $request->user()->tenant_id)
|
||||
->findOrFail($id);
|
||||
|
||||
+22
-11
@@ -154,20 +154,32 @@ class Project extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Все связанные SupplierProject из eager-loaded BelongsTo отношений.
|
||||
* Все связанные SupplierProject: pivot project_supplier_links ПЛЮС три legacy-слота
|
||||
* supplier_b{1,2,3}_project_id, объединение без дублей.
|
||||
*
|
||||
* Используется внутри aggregateSyncStatus(), aggregateLastSyncedAt(),
|
||||
* getSupplierLinks() — устраняет N+1 (каждый из трёх методов вызывал
|
||||
* SupplierProject::find() независимо; теперь читает из уже загруженных
|
||||
* $this->supplierB1 / supplierB2 / supplierB3).
|
||||
* 🔴 Почему pivot обязателен (прод-баг 29.07.2026): ночной SyncSupplierProjectsJob —
|
||||
* единственный, кто в режиме batch реально заводит заказ у поставщика — пишет ТОЛЬКО
|
||||
* в pivot и legacy-колонок не касается. Заполняет их лишь SyncSupplierProjectJob, и
|
||||
* только если заказ УЖЕ существует в момент запуска; при создании проекта заказа ещё
|
||||
* нет (он появится в 18:00), поэтому колонки остаются пустыми навсегда — пока клиент
|
||||
* сам не дёрнет проект (пауза / снятие с паузы / «Синхронизировать» / правка).
|
||||
* Пока статус читался только из колонок, 5 из 9 работающих проектов на бою показывали
|
||||
* жёлтое «Готовим к запуску» при живом заказе (самый старый — 13 дней).
|
||||
*
|
||||
* Требует eager-load: Project::with(['supplierB1', 'supplierB2', 'supplierB3']).
|
||||
* Legacy-слоты продолжаем читать: handleBatch пишет колонку без pivot-строки, такие
|
||||
* проекты терять нельзя.
|
||||
*
|
||||
* Требует eager-load: Project::with(['supplierProjects', 'supplierB1', 'supplierB2', 'supplierB3']).
|
||||
*
|
||||
* @return Collection<int, SupplierProject>
|
||||
*/
|
||||
private function resolvedSupplierProjects(): Collection
|
||||
{
|
||||
return collect([$this->supplierB1, $this->supplierB2, $this->supplierB3])->filter()->values();
|
||||
return collect([$this->supplierB1, $this->supplierB2, $this->supplierB3])
|
||||
->filter()
|
||||
->merge($this->supplierProjects)
|
||||
->unique('id')
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,10 +235,9 @@ class Project extends Model
|
||||
*/
|
||||
public function getSupplierLinks(): array
|
||||
{
|
||||
return collect(['b1' => $this->supplierB1, 'b2' => $this->supplierB2, 'b3' => $this->supplierB3])
|
||||
->filter()
|
||||
->map(fn (SupplierProject $sp, string $platform) => [
|
||||
'platform' => $platform,
|
||||
return $this->resolvedSupplierProjects()
|
||||
->map(fn (SupplierProject $sp) => [
|
||||
'platform' => strtolower((string) $sp->platform),
|
||||
'supplier_project_id' => $sp->id,
|
||||
'sync_status' => $sp->sync_status,
|
||||
'last_synced_at' => $sp->last_synced_at?->toIso8601String(),
|
||||
|
||||
@@ -7,8 +7,10 @@ use App\Models\SupplierLead;
|
||||
use App\Models\SystemSetting;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function () {
|
||||
SystemSetting::query()->where('key', 'supplier_webhook_secret')->update(['value' => 'test-secret-32chars-aaaaaaaaaaaaaa']);
|
||||
@@ -159,3 +161,40 @@ it('accepts timestamp within ±24h window (Plan 2.6 fix #iii — partition guard
|
||||
|
||||
$response->assertStatus(202);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Журнал вебхука поставщика (31.07.2026). Прежний logSupplierWebhook() писал в
|
||||
// таблицу webhook_log, снесённую ещё 24.05 вместе с legacy-каналом, и молча
|
||||
// выходил по Schema::hasTable → отказы были невидимы. Итог: 70 отказов 404 за
|
||||
// 10 дней (поставщик долбился со СТАРЫМ паролем на старый адрес) нашлись только
|
||||
// в логе nginx, случайно. Отпечаток пароля (первые 8 символов md5) отвечает на
|
||||
// главный вопрос разбора «это чужой ключ или наш» и сам секретом не является.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('logs a rejected call with the secret fingerprint so a stale endpoint is visible', function () {
|
||||
$stale = 'stale-secret-32chars-bbbbbbbbbbbbbb';
|
||||
|
||||
$this->postJson("/api/webhook/supplier/{$stale}", [
|
||||
'vid' => 77001, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => time(),
|
||||
])->assertStatus(404);
|
||||
|
||||
$row = DB::connection('pgsql_supplier')->table('supplier_webhook_log')->latest('id')->first();
|
||||
expect($row)->not->toBeNull();
|
||||
expect($row->status)->toBe('rejected_secret');
|
||||
expect($row->secret_fingerprint)->toBe(substr(md5($stale), 0, 8));
|
||||
expect($row->supplier_lead_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('logs an accepted call with the created lead id', function () {
|
||||
Bus::fake();
|
||||
$secret = 'test-secret-32chars-aaaaaaaaaaaaaa';
|
||||
|
||||
$this->postJson("/api/webhook/supplier/{$secret}", [
|
||||
'vid' => 77002, 'project' => 'B1_test.ru', 'phone' => '79991234568', 'time' => time(),
|
||||
])->assertStatus(202);
|
||||
|
||||
$row = DB::connection('pgsql_supplier')->table('supplier_webhook_log')->latest('id')->first();
|
||||
expect($row->status)->toBe('received');
|
||||
expect($row->secret_fingerprint)->toBe(substr(md5($secret), 0, 8));
|
||||
expect((int) $row->supplier_lead_id)->toBe((int) SupplierLead::where('vid', 77002)->value('id'));
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Mail\TenantBusinessDriftAlertMail;
|
||||
use App\Models\Project;
|
||||
use App\Models\SupplierLead;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\LeadRegionResolver;
|
||||
use App\Services\Supplier\SupplierPortalClient;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Mail\Mailer;
|
||||
@@ -45,6 +46,7 @@ beforeEach(function (): void {
|
||||
}
|
||||
});
|
||||
Cache::store('redis')->forget('supplier:csv_reconcile');
|
||||
Cache::store('redis')->forget('supplier:csv_reconcile:pending');
|
||||
putSupplierSession();
|
||||
config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
|
||||
config(['services.supplier.alert_email' => 'ops@liderra.ru']);
|
||||
@@ -52,6 +54,7 @@ beforeEach(function (): void {
|
||||
|
||||
afterEach(function (): void {
|
||||
Cache::store('redis')->forget('supplier:csv_reconcile');
|
||||
Cache::store('redis')->forget('supplier:csv_reconcile:pending');
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -142,6 +145,10 @@ it('recovers a delivered lead missing from webhook — with the REAL vid (not nu
|
||||
$delivered[] = ['vid' => $missingVid, 'phone' => $missingPhone, 'project' => 'B1_a.com'];
|
||||
fakeDelivered($delivered);
|
||||
|
||||
// Отсрочка 31.07.2026: первый прогон только кладёт недостачу в карантин, добор — со
|
||||
// второго, когда стало ясно, что вебхук её не несёт (а не просто опаздывает).
|
||||
runCsvReconcile();
|
||||
Carbon::setTestNow(now()->addMinutes(16));
|
||||
runCsvReconcile();
|
||||
|
||||
$log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
|
||||
@@ -212,6 +219,10 @@ it('unparseable project in delivered ledger — skipped, counted, excluded from
|
||||
}
|
||||
fakeDelivered($delivered);
|
||||
|
||||
// Мусорные строки — тоже недостача, поэтому проходят через отсрочку: считаются
|
||||
// unparseable только на прогоне, где реально дошло до разбора project.
|
||||
runCsvReconcile();
|
||||
Carbon::setTestNow(now()->addMinutes(16));
|
||||
runCsvReconcile();
|
||||
|
||||
$log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
|
||||
@@ -219,6 +230,7 @@ it('unparseable project in delivered ledger — skipped, counted, excluded from
|
||||
expect((int) $log->matched_count)->toBe(5);
|
||||
expect((int) $log->recovered_count)->toBe(0);
|
||||
expect((int) $log->unparseable_count)->toBe(5);
|
||||
expect((int) $log->pending_count)->toBe(0);
|
||||
expect((float) $log->drift_ratio)->toBe(0.0); // только junk, реального missing нет
|
||||
expect($log->status)->toBe('ok');
|
||||
});
|
||||
@@ -395,3 +407,184 @@ it('R-05 business-drift: tenant with shortfall <= 20% → NO TenantBusinessDrift
|
||||
return $mail->tenantId === $tenant->id;
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Отсрочка добора (31.07.2026): вебхук поставщика приходит на 2-8 минут ПОЗЖЕ,
|
||||
// чем строка появляется в журнале отданного. Сверка каждые 30 мин попадала в это
|
||||
// окно и «восстанавливала» лид, который уже был в пути → ложная тревога «потеряно N»
|
||||
// + бедная карточка (в журнале нет tag/time/phones). Инцидент 31.07: 7 добранных,
|
||||
// вебхук по тем же семи пришёл через 2,5 минуты и получил «уже есть».
|
||||
// Правило: недостача добирается только если висит дольше GRACE_MINUTES.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('first sighting of a missing vid is quarantined — not recovered, no alert', function (): void {
|
||||
$vid = 930001;
|
||||
$phone = '79300000001';
|
||||
fakeDelivered([['vid' => $vid, 'phone' => $phone, 'project' => 'B1_a.com']]);
|
||||
|
||||
runCsvReconcile();
|
||||
|
||||
expect(SupplierLead::where('vid', $vid)->count())->toBe(0);
|
||||
$log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
|
||||
expect((int) $log->recovered_count)->toBe(0);
|
||||
expect((int) $log->pending_count)->toBe(1);
|
||||
expect($log->status)->toBe('ok');
|
||||
Mail::assertNotSent(CsvDriftAlertMail::class);
|
||||
Bus::assertNothingDispatched();
|
||||
});
|
||||
|
||||
it('late webhook arrives during quarantine — nothing is recovered, no phantom, no alert', function (): void {
|
||||
// Точный сценарий инцидента 31.07.2026: в 05:30 сверка увидела 7 номеров, которых у нас
|
||||
// нет; вебхук по ним пришёл в 05:32-05:33. Со старым поведением портал уже завёл их сам
|
||||
// (бедные карточки, ложная тревога «потеряно 7»). С отсрочкой — ждём и не трогаем.
|
||||
$vid = 940001;
|
||||
$phone = '79400000001';
|
||||
fakeDelivered([['vid' => $vid, 'phone' => $phone, 'project' => 'B1_a.com']]);
|
||||
|
||||
runCsvReconcile(); // 05:30 — увидели недостачу, в карантин
|
||||
webhookLead($vid, $phone, 'B1_a.com'); // 05:33 — вебхук донёс, как в жизни
|
||||
Carbon::setTestNow(now()->addMinutes(16));
|
||||
runCsvReconcile(); // 06:00 — недостачи больше нет
|
||||
|
||||
expect(SupplierLead::where('source', 'csv_recovery')->count())->toBe(0);
|
||||
expect(SupplierLead::where('vid', $vid)->count())->toBe(1);
|
||||
|
||||
$log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
|
||||
expect((int) $log->recovered_count)->toBe(0);
|
||||
expect((int) $log->pending_count)->toBe(0);
|
||||
expect((int) $log->matched_count)->toBe(1);
|
||||
expect($log->status)->toBe('ok');
|
||||
Mail::assertNotSent(CsvDriftAlertMail::class);
|
||||
Bus::assertNothingDispatched();
|
||||
});
|
||||
|
||||
it('alert counts only overdue as lost and reports in-flight separately', function (): void {
|
||||
// Один номер висит с прошлого прогона (реальная потеря), второй увиден только сейчас (в пути).
|
||||
$lostVid = 950001;
|
||||
$lostPhone = '79500000001';
|
||||
fakeDelivered([['vid' => $lostVid, 'phone' => $lostPhone, 'project' => 'B1_a.com']]);
|
||||
runCsvReconcile();
|
||||
|
||||
Carbon::setTestNow(now()->addMinutes(16));
|
||||
$freshVid = 950002;
|
||||
$freshPhone = '79500000002';
|
||||
fakeDelivered([
|
||||
['vid' => $lostVid, 'phone' => $lostPhone, 'project' => 'B1_a.com'],
|
||||
['vid' => $freshVid, 'phone' => $freshPhone, 'project' => 'B1_a.com'],
|
||||
]);
|
||||
runCsvReconcile();
|
||||
|
||||
$log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
|
||||
expect((int) $log->recovered_count)->toBe(1);
|
||||
expect((int) $log->pending_count)->toBe(1);
|
||||
|
||||
Mail::assertSent(CsvDriftAlertMail::class, function (CsvDriftAlertMail $mail) {
|
||||
return $mail->missingCount === 1 // потерей считаем только просроченный
|
||||
&& $mail->pendingCount === 1 // «в пути» показываем отдельно
|
||||
&& $mail->recoveredCount === 1;
|
||||
});
|
||||
expect(SupplierLead::where('vid', $freshVid)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('drift alert email renders and separates loss from in-flight', function (): void {
|
||||
// Смоук шаблона: битый blade иначе всплыл бы только на бою, письмом-пустышкой.
|
||||
$html = (new CsvDriftAlertMail(
|
||||
reconcileLogId: 1417,
|
||||
totalCsvRows: 21,
|
||||
missingCount: 7,
|
||||
pendingCount: 3,
|
||||
recoveredCount: 7,
|
||||
driftRatio: 0.3333,
|
||||
windowStart: Carbon::parse('2026-07-30 00:00:00'),
|
||||
windowEnd: Carbon::parse('2026-07-31 02:30:00'),
|
||||
))->render();
|
||||
|
||||
expect($html)->toContain('Ещё в пути');
|
||||
expect($html)->toContain('считаем потерей');
|
||||
expect($html)->toContain('1417');
|
||||
expect($html)->not->toContain('Пропущено webhook');
|
||||
});
|
||||
|
||||
/**
|
||||
* Разметка строки журнала «Мои сделки» — СПИСАНА С ЖИВОГО КАБИНЕТА (31.07.2026),
|
||||
* а не придумана: проект лежит в td.crm-domain-column, следом отдельная ячейка с ТЕГОМ
|
||||
* (регион или «РФ»), затем td.users__td_phones с телефоном звонившего.
|
||||
*
|
||||
* @param list<array{vid:int, phone:string, project:string, tag:string}> $leads
|
||||
*/
|
||||
function deliveredHtmlCabinet(array $leads): string
|
||||
{
|
||||
$rows = '';
|
||||
foreach ($leads as $l) {
|
||||
$vid = $l['vid'];
|
||||
$rows .= '<tr class="users-table__item" data-id="'.$vid.'">'
|
||||
.'<td><input type="checkbox" name="visit-checbox" value="'.$vid.'"></td>'
|
||||
.'<td class="crm-domain-column"> <a href="/admin/visit/view?id='.$vid.'">'.$l['project'].'</a> </td>'
|
||||
.'<td><a href="/admin/visit/view?id='.$vid.'">'.$l['tag'].'</a></td>'
|
||||
.'<td class="users__td_phones" style="position: relative;"><div class="visr-phone">'
|
||||
.'<a class=" " href="tel: " data-info="">'.$l['phone'].'</a></div></td>'
|
||||
.'<td class="users__td_user"></td>'
|
||||
.'</tr>';
|
||||
}
|
||||
|
||||
return '<table><tbody>'.$rows.'</tbody></table>';
|
||||
}
|
||||
|
||||
it('fetchDeliveredLeads picks up the region tag from the cabinet row', function (): void {
|
||||
$html = deliveredHtmlCabinet([
|
||||
['vid' => 6001, 'phone' => '79990000001', 'project' => 'B1_79089202427', 'tag' => 'Свердловская область'],
|
||||
['vid' => 6002, 'phone' => '79990000002', 'project' => 'B1_79537885612', 'tag' => 'РФ'],
|
||||
['vid' => 6003, 'phone' => '79990000003', 'project' => 'B1_a.com', 'tag' => '-'],
|
||||
]);
|
||||
Http::fake(['crm.bp-gr.ru/admin/visit/index-visit*' => Http::response($html, 200)]);
|
||||
|
||||
$result = app(SupplierPortalClient::class)->fetchDeliveredLeads(now()->subDay(), now());
|
||||
|
||||
expect($result[6001]['tag'])->toBe('Свердловская область');
|
||||
expect($result[6002]['tag'])->toBe('РФ');
|
||||
expect($result[6003]['tag'])->toBeNull(); // прочерк — это не тег
|
||||
// телефон и проект по-прежнему берутся верно
|
||||
expect($result[6001]['phone'])->toBe('79990000001');
|
||||
expect($result[6001]['project'])->toBe('B1_79089202427');
|
||||
});
|
||||
|
||||
it('recovered lead carries the supplier tag — region has a chance when DaData is silent', function (): void {
|
||||
// Повод (31.07.2026): у добранного лида в карточке было только vid+phone+project, поэтому
|
||||
// резолверу нечем было подстраховаться, когда ДаData не знает номер. Тег поставщика —
|
||||
// ровно эта подстраховка (RegionTagResolver), и он в журнале ЕСТЬ, мы его выбрасывали.
|
||||
$vid = 960001;
|
||||
fakeDelivered([['vid' => $vid, 'phone' => '79600000001', 'project' => 'B1_a.com', 'tag' => 'Красноярский край']]);
|
||||
|
||||
runCsvReconcile();
|
||||
Carbon::setTestNow(now()->addMinutes(16));
|
||||
runCsvReconcile();
|
||||
|
||||
$lead = SupplierLead::where('vid', $vid)->first();
|
||||
expect($lead)->not->toBeNull();
|
||||
expect($lead->raw_payload['tag'])->toBe('Красноярский край');
|
||||
});
|
||||
|
||||
it('tag from the journal actually yields a region when DaData is silent', function (): void {
|
||||
// Смысл правки — не «положить строчку в карточку», а вернуть региону опору.
|
||||
// ДаData выключена (как будто не знает номер) → резолвер обязан взять регион из тега.
|
||||
config(['services.dadata.enabled' => false]);
|
||||
$vid = 970001;
|
||||
fakeDelivered([['vid' => $vid, 'phone' => '79700000001', 'project' => 'B1_a.com', 'tag' => 'Красноярский край']]);
|
||||
|
||||
runCsvReconcile();
|
||||
Carbon::setTestNow(now()->addMinutes(16));
|
||||
runCsvReconcile();
|
||||
|
||||
$lead = SupplierLead::where('vid', $vid)->first();
|
||||
$resolution = app(LeadRegionResolver::class)->resolve($lead);
|
||||
|
||||
expect($resolution->source)->toBe('tag');
|
||||
expect($resolution->subjectCode)->not->toBeNull();
|
||||
|
||||
// Контроль вырезанием: без тега тот же лид даёт «регион неизвестен».
|
||||
$lead->raw_payload = ['project' => 'B1_a.com', 'phone' => '79700000001', 'vid' => $vid];
|
||||
$lead->save();
|
||||
$without = app(LeadRegionResolver::class)->resolve($lead->fresh());
|
||||
expect($without->source)->toBe('unknown');
|
||||
expect($without->subjectCode)->toBeNull();
|
||||
});
|
||||
|
||||
+8
-14
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-07-31T07:17:46.606Z
|
||||
Last updated: 2026-08-01T10:43:22.039Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -8,14 +8,14 @@ Last updated: 2026-07-31T07:17:46.606Z
|
||||
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 9 week(s) ago |
|
||||
| C4 Сигнальный статус | ✅ | This file (self-reference) |
|
||||
| C5 Observer-coverage | ⚠️ | 0 episode(s) this month · observer-stop-hook NOT registered in .claude/settings.json Stop hook; .git/hooks/post-commit not installed (run: npx lefthook install --force) |
|
||||
| C5 Observer-coverage | ⚠️ | 0 episode(s) this month · observer-stop-hook NOT registered in .claude/settings.json Stop hook |
|
||||
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 17 chains in sync |
|
||||
|
||||
## Кто на посту (оборона М1–М6)
|
||||
|
||||
⚠️ **ПОСТ ПУСТОЙ** — не зарегистрированы: enforce-floor.mjs, enforce-supreme-gate.mjs, enforce-normative-content-rules.mjs, enforce-read-path-deny.mjs, enforce-mcp-classification.mjs, enforce-judge-gate.mjs, enforce-snapshot.mjs, enforce-floor-escape-consume.mjs, enforce-skill-journaler.mjs, enforce-verify-gate.mjs, enforce-criterion-gate.mjs, enforce-coverage-verify.mjs, enforce-todowrite-skill-verifier.mjs (оборона НЕ подтверждена; SE-B/Δ8)
|
||||
|
||||
Судья М4: **live-block** (inert $0 / shadow / floor-only / live-block)
|
||||
Судья М4: **inert** (inert $0 / shadow / floor-only / live-block)
|
||||
|
||||
| Машина / страж | Хук | Зарегистрирован |
|
||||
|---|---|---|
|
||||
@@ -39,7 +39,7 @@ Last updated: 2026-07-31T07:17:46.606Z
|
||||
|
||||
- Observer evidence: 0 episodes this month, 0 observer_error markers, 0 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 0
|
||||
- Last /brain-retro: 65 day(s) ago
|
||||
- Last /brain-retro: 66 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
|
||||
## Метрики дисциплины
|
||||
@@ -112,9 +112,9 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 3544 | MsMpEng | 16.61ч | NaNч |
|
||||
| 23936 | Code | 4.76ч | 0.0ч |
|
||||
| 4 | System | 2.51ч | NaNч |
|
||||
| 3544 | MsMpEng | 30.24ч | 12286018.7ч |
|
||||
| 23936 | Code | 10.81ч | 0.0ч |
|
||||
| 4 | System | 4.49ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
@@ -130,13 +130,7 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
## Целостность журналов действий
|
||||
|
||||
🔴 Битые цепочки (3 из 120):
|
||||
|
||||
| session | broken at seq |
|
||||
|---|---|
|
||||
| `03437265-6d58-4622-aeed-c0eeac0f2c32` | 1 |
|
||||
| `54594686-843c-4ea8-bcd3-5ae6a7244e30` | 14 |
|
||||
| `9c02276d-dabb-40e4-9c04-44c18d47485a` | 14 |
|
||||
Ключ подписанта не provisioned — проверка цепи недоступна (ключ — owner-шаг A3).
|
||||
|
||||
## Алерт-индикаторы
|
||||
|
||||
|
||||
Reference in New Issue
Block a user