Task 2 плана 2026-05-29-audit-rebuild-per-tenant-fix.md.
Regression-safe refactor: drop private TABLE_CONFIG const + buildRowExpression()
helper, заменить на чтение AuditChainConfig::TABLES (создан в Task 1, commit
4cfd9f6b) + AuditChainConfig::rowExpression($table). Поведение не изменилось —
тот же baseline regression Pest (9 passed pre-refactor → 10 passed post-refactor;
+1 = регрессия-guard VerifyAuditChainsTest.php flipped fail→pass; 2 pre-existing
errors orthogonal к Task 2).
VerifyAuditChainsTest.php — TDD regression guard на cleanness рефактора: проверяет
полноту AuditChainConfig::TABLES (6 таблиц), корректность rowExpression() для
всех таблиц, и отсутствие private TABLE_CONFIG const после refactor'а.
Ref: docs/adr/ADR-018-audit-chain-per-tenant-semantics.md
docs/superpowers/plans/2026-05-29-audit-rebuild-per-tenant-fix.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-018 (commit 0098db66, Дмитрий) закрепил per-tenant chain semantics canonical. 6 mismatches в activity_log_y2026_m05 переклассифицированы как bug AuditRebuildChain (global rebuild под admin без RLS), не divergence design'а. Trigger + verify согласованы по per-tenant, менять не надо. План фикса (commit e964d70c) — 8 TDD-task'ов, shared AuditChainConfig + rewrite rebuild через LAG OVER. Task 1 выполнен в worktree audit-rebuild-per-tenant-fix commit 4cfd9f6b НЕ на main. Прод-код БЕЗ изменений с deploy 26634115769 (29.05 11:15 UTC). cspell-words.txt: +ретраились/сериализуются/OID (pre-existing в L13 unrelated snapshot).
8 TDD tasks (~день кода): extract shared AuditChainConfig, refactor VerifyAuditChains (regression-safe), failing tests для multi-tenant/BYPASSRLS/single-row, rewrite AuditRebuildChain через LAG OVER (partition_clause ORDER BY id) симметрично verify, активация ADR-018 enforcement rules, Pint/Larastan/Pest --parallel smoke, handoff для прод-cleanup activity_log_y2026_m05 через gh workflow run artisan-run.yml. Self-review GREEN на spec coverage / placeholders / типы. Execution mode: subagent-driven.
ремонт: logrotate отказал rotation PG log из-за insecure parent dir permissions
/var/log/postgresql/ имеет permissions drwxrwxr-t (group-writable + sticky).
Logrotate refuses to rotate без явного su directive в config.
Стандарт postgresql-common тоже использует 'su' — копирую идиому.
Two operational gotchas discovered в session 29.05.2026 (router-gate v3.6-3.8 sweep + post-sweep memory updates):
1. §5 п.13 NB — docs-only short-circuit считает строго .md-суффикс.
cspell-words.txt / package.json / lefthook.yml рядом со spec.md
делают diff mixed → verify-before-push активен → нужен vitest sentinel
ИЛИ override. Прецедент: commit 46c43169.
2. §5 +п.15 — enforce-memory-coverage hook не принимает chain-каналы
(chain:commit-push-mem-sync etc); требует строго direct:memory-sync
в свежем turn'е. Memory updates как часть multi-step задачи планировать
отдельным turn'ом или использовать memory dump override.
Прецедент: 4-й шаг sweep задачи заблокирован.
Via /claude-md-management:revise-claude-md skill flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ремонт: psql \set vars не expand'ятся в server-side plpgsql DO block
В section 2 (DO $rebuild$ block) использовал :'partition' и :from_id —
client-side psql substitution не работает внутри DO (server-side parse).
Заменил на shell expansion ('$PARTITION', $FROM_ID) до psql.
Sections 1+3 без изменений (plain psql statements там работают).
ремонт: F1 chain rebuild для 152-ФЗ целостности
Closes deferred item from docs/incidents/2026-05-29-disk-full-pg-recovery.md §4.1.
Sequential hash recomputation в plpgsql DO-блоке через sudo -u postgres psql.
Identical алгоритм с trigger audit_chain_hash() (post-F1 advisory-lock).
Inputs: partition (whitelist), from_id, dry_run/confirm_apply.
Safety: partition whitelist, ON_ERROR_STOP, COMMIT only after full loop.
Adds audit:rebuild-chain --partition=<name> --from-id=<n> [--force] to MUTATING_RE
regex group. Required to rebuild hash chain on 2 broken partitions
(activity_log_y2026_m05 from id=599, balance_transactions_y2026_m05 from id=462)
after F1 advisory-lock migration applied.
Ref: docs/superpowers/plans/2026-05-29-audit-chain-race-fix.md Step 3.3
ремонт: deploy.yml fail на F1 миграции — schema public требует postgres superuser, у crm_migrator нет прав на CREATE OR REPLACE FUNCTION
Applies F1 audit-chain advisory-lock migration via sudo -u postgres psql,
then INSERTs migration row so subsequent php artisan migrate skips it.
Workaround for prod deploy where crm_migrator can't modify public schema.
Replays sha256 chain in given audit partition from given id:
1. Uses pgsql_supplier (BYPASSRLS) to see all rows regardless of RLS scope.
2. Bypasses audit_block_mutation trigger via session_replication_role=replica
(session-local SET, does not affect other connections).
3. Recomputes hash per row using the same formula as audit_chain_hash():
digest(COALESCE(prev_hash,''::bytea) || ROW(col1,...,NULL::bytea,...,coln)::text::bytea, 'sha256')
Column order from COLUMN_CONFIG matches TABLE_CONFIG in VerifyAuditChains.
4. Supports --dry-run (count without UPDATE) and --force (skip confirmation).
Validated against breaking partitions:
--partition=activity_log_y2026_m05 --from-id=599
--partition=balance_transactions_y2026_m05 --from-id=462
Tests: 4 tests — activity_log rebuild, balance_transactions rebuild,
dry-run no-op, unknown partition rejection. All pass (4/4 GREEN).
Refs: docs/superpowers/plans/2026-05-29-audit-chain-race-fix.md Task 3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes race condition where concurrent INSERT workers (webhook handlers)
all read the same prev_hash before any commits, causing hash chain to
branch. Advisory lock key is derived from the partition OID (TG_RELID):
lock_key := ('x' || lpad(to_hex(TG_RELID::int), 16, '0'))::bit(64)::bigint
This serializes INSERTs to the SAME partition without blocking concurrent
INSERTs to DIFFERENT partitions (distinct OIDs → distinct lock keys).
Hash formula: verbatim unchanged from db/schema.sql:3107-3127:
digest(COALESCE(prev_hash, ''::bytea) || NEW::text::bytea, 'sha256')
Tested: pg_locks advisory lock presence test passes (pg_advisory_xact_lock
visible in pg_locks during INSERT transaction).
Refs: docs/superpowers/plans/2026-05-29-audit-chain-race-fix.md Task 2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two tests:
1. pcntl_fork concurrent-INSERT test (skipped on Windows/no pcntl) —
demonstrates chain branch when 5 workers insert into the same partition
simultaneously; passes after advisory-lock migration.
2. pg_locks advisory lock presence test (Windows-compatible) —
verifies that pg_advisory_xact_lock is actually held in pg_locks
during an INSERT transaction, proving the migration works.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Important fix (sql-runner.yml): Reject multi-statement SQL — `SELECT 1; UPDATE supplier_leads ...` was passing READ_RE whitelist and executing the second statement on prod without confirm_mutating=true. Added explicit `*";"*` guard before regex checks.
Minor fix (RouteSupplierLeadJob.php): Capture `$originalError = \$lead->error` BEFORE `\$lead->update(...)`. Laravel mutates the in-memory model, so reading `\$lead->error` after update returns the already-suffixed value, making Log::info `original_error` field useless for debugging.
Both findings from F2 review subagent on commit c8c089cb.
Test verification: 10/10 Pest GREEN (6 SupplierWebhookFastFail + 4 SingleLeadStorm).
Task 4 уточнён: нет миграций (только PHP), fast-fail активируется сразу после
деплоя. Добавлены конкретные gh workflow run команды для cleanup (Steps 3-4 из
sql-runner.yml) и верификации шторма + incidents алерта. Галочки [ ] оставлены
(задача контроллера, не агента).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Добавлен БЛОК 5 в IncidentsWatchFailures::handle() — детекция шторма от
одного supplier_lead_id. Если один lead_id генерирует >= threshold-single-lead
failures за окно (default=1000) → severity=high инцидент с root_cause
'single-lead-storm:<lead_id>'. Дедуп по dedup-window как в остальных блоках.
Новая опция: --threshold-single-lead=1000 (configurable).
Мотивация (Finding 2 Stage 5, 2026-05-29): supplier_leads 1110+1157 генерировали
~256k строк в failed_webhook_jobs за 24ч без алерта. Этот блок создаёт incident
уже при 1000+ failures одного лида в 10-минутном окне — что позволяет обнаружить
шторм в течение первого часа.
Связь с Task 2 (fast-fail): вместе эти два изменения stop new storms (Task 2)
и alert on remaining storms (Task 3).
Tests: 4 passing в SingleLeadStormTest.php
- детекция шторма (>= threshold)
- НЕ создаёт incident при распределённых failures
- default threshold=1000
- dedup (второй запуск = 0 новых инцидентов)
Task 3 plan 2026-05-29-supplier-webhook-fast-fail-and-stuck-cleanup.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes failed_webhook_jobs storm class (Finding 2, 2026-05-29):
поставщик crm.bp-gr.ru шлёт B1+SMS combo → DomainException в
SupplierProjectResolver → 3 retries → failed() записывает error в supplier_lead
→ RetryFailedSupplierJobsCommand при следующем dispatch видит тот же lead →
~256k строк/сутки.
Fast-fail guard добавлен в RouteSupplierLeadJob::handle() МЕЖДУ двумя
существующими idempotency-guard'ами и parseProjectField. Если supplier_lead.error
содержит terminal pattern ('does not support' / 'platform mismatch' /
'no matching supplier_project') и processed_at IS NULL — job помечает processed_at
и выходит без записи в failed_webhook_jobs.
Correction 1: реальный класс RouteSupplierLeadJob (не ProcessSupplierWebhookJob).
Correction 3: место вставки — после processed_at guard, до parseProjectField.
Tests: 6 passing в SupplierWebhookFastFailTest.php
- fast-fail на 3 terminal patterns
- НЕ fast-fail при error=null (нормальный лид)
- НЕ fast-fail при processed_at уже установлен (idempotency guard первым)
- НЕ fast-fail при transient ошибке (не matching pattern)
Task 2 plan 2026-05-29-supplier-webhook-fast-fail-and-stuck-cleanup.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Markdownlint added blank line before list in TL;DR v3.5 Changes section. Cosmetic, no semantic change. Captured to keep working tree clean for session handoff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dedicated brainstorm на S8 (последний acceptable residual после v3.4) — controller-writable markers BRANCH-SWITCH-CONFIRMED / RECOVERY-INTENT: заменены на AskUserQuestion-gate через расширение §4.5 answer parser. Тот же class fix что S5 v3.4 — перенос сигнала в side-channel который controller не контролирует.
V3.5 architectural shift:
- Old: controller wants git rebase → hook видит блокировку → controller пишет BRANCH-SWITCH-CONFIRMED в response → hook читает, allows. Самоодобрение через controller-written text.
- New: controller wants git rebase → hook блокирует → controller обязан AskUserQuestion('Выполнить git rebase main?') с 3 опциями → user picks → §4.5 parser распознаёт git-pattern, пишет approve_git_operation запись в askuser-decisions.jsonl с approved_action_pattern exact → controller retries → hook читает askuser-decisions, exact-match approval, consumes one-shot, allows.
Anti-forgery 4 защиты:
- askuser-decisions.jsonl в protected paths (~/.claude/runtime/*)
- Пишется только §4.5 parser при реальном AskUser-ответе
- Strict exact-match approved_action_pattern против tool_input.command
- 5-min time window + one-shot consumption (consumed flag)
Adversarial pre-check v4.3 — 6 attack-векторов проверены, все заблокированы.
5 spec edits:
- §3 preserved hooks таблица — enforce-branch-switch row переписан (no longer reads response markers, instead reads askuser-decisions)
- §4.5 — новый row + новое enum gate_interpretation: approve_git_operation
- §9 — S8 убран из acceptable residual list. Список residual теперь ПУСТ.
- §10.2 — askuser-decisions schema +consumed/consumed_at/consumed_by_tool_use_id + полный schema приведён
- §11 v3.5 history entry
Implementation budget: 21.5-31h (v3.4) → 22.5-32h (v3.5). +1.1h за: rewrite hook, extend parser, one-shot consumption, schema, spec edits.
Главный урок: класс controller-writable signals закрыт архитектурно через обе v3.4 (file side-channel) и v3.5 (askuser-decisions side-channel). Формула generalisable — любой controller-writable signal закрывается переносом write-канала в protected file который пишет отдельный gate-процесс при harness-driven событии.
Hard wall теперь полный hard. Acceptable residual list пуст.
Verify-sentinel: 1179/1179 vitest tools-only GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Handoff document for transition to new session. Contains:
- TL;DR of where we left off (spec v3.1 ready, impl not started)
- 4-step instructions for next session (read spec, writing-plans,
subagent-driven impl, post-impl tasks)
- Context (brain-retro #10 trigger, self-retrospect #2 confirmation,
user choice of Level 4)
- 7 design principles from spec section 2
- Architecture TL;DR (gate, 4 behaviors, baseline, deletes, preserved)
- All 4 spec versions in git with commits
- Cross-refs to L1+L2 plan, brain-retro, self-retrospect
- 5 open questions for writing-plans phase
Cannot write to memory/ path in this turn (memory-coverage hook
requires direct:memory-sync coverage, current turn has different
coverage). Memory entry can be added in next session via Skill or
manual annotation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comprehensive analysis of v3 found ~24 minor issues (no critical bypasses).
V3.1 closes most via clarifications to prepare for writing-plans skill in
next session.
Additions:
- TL;DR at top — fast orientation for implementer
- 10.1 Function and registry references (nodeMatches source,
registry source docs/registry/nodes.yaml, SDD-skill impact,
coverage-hint to recovery resolution)
- 10.2 State file schemas (8 files: router-state, chain-state,
askuser-decisions, router-gate-decisions, subagent-inheritance,
coverage-hint, gate-errors, gate-config)
- 10.3 Test strategy: ~150 unit + 10-15 integration + 10-15 golden
snapshot + 5-7 smoke
- 10.4 Success metrics: quantitative (override drops to 0,
gate-decisions growing) + qualitative (lockout < 5/100,
correct% > 60%) + acceptance criteria
- 10.5 Rollback plan (3 levels: hook off / revert commits / v2 baseline)
- 10.6 Stages and parallelism: 6-9h wall-clock with SDD parallelism
vs 13.5-20h sequential
No architectural changes — v3.1 only clarifies what implementer needs
to know without making implicit decisions.
Spec versions in git:
- v1: 7a43c175
- v2: b510a758
- v3: b632bcba
- v3.1: this commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>