WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-575 · Эпик · Вход · web

ЭПИК: GDPR operational readiness — доказать, что защита данных работает в продукте

Парковка P1 · важно ведёт: —
Суть
## Суть одной фразой
Доказать, что защита данных (GDPR) реально работает в продукте: правило живёт в коде и ломает сборку, а не только в документе.

## Где мы сейчас (13.09.2026)
Код в дереве есть: article28Register.ts, article9Consent.ts, ropa-gate; коммиты l114d — 665c19853 (voice retention), 008ede321 (ropa gate), ff5a0f7f1 (Article 9 consent), 1d70754c7 (close unconfirmed Article 28 paths); миграции web575_voice_retention, web575_diagnostics_privacy, web561_article9_consent. Документы GDPR/AI Act (волна 3588, DeepSeek M4, GO) посажены в l115i (13.09 06:37Z). Барьер Article 28 приехал вооружённым и вызвал P0 (чат 500 всем 32 мин, 09.09 02:00-02:32Z) → откат на l114c → барьер переведён под флаг ARTICLE28_RUNTIME_ENFORCEMENT (по умолчанию off), вооружение по провайдерам после подтверждения договоров владельцем. DPIA R-01..R-06 закрыты не все: документные закрыты волной 3588, на кодовые написано ТЗ.

## Хронология
- 08.09 эпик разделён (GDPR / AI Act), решение владельца по ст.9 = путь Б (явное согласие).
- Волны 3020-3024; 3021 DPIA = NO-GO (шесть рисков R-01..R-06 с файл:строка).
- 09.09 посадка l114d → P0 Article28 → откат на l114c → волна 3155 (флаг) → l115i.
- 12.09 волна 3588 (GO по хвостам: DPIA-запись, runbook контролей, ТЗ по R-06 + тест-заглушка).

## Карта документов и кода
- src/lib/legal/article28Register.ts, article9Consent.ts; src/app/api/gdpr/delete/route.ts:279-306 (R-06); src/lib/ingest/sourceWriter.ts:151-162,285-314 (R-01); src/app/api/diagnostics/hang/route.ts:10-44 (R-05).
- docs/runbooks/DB-LEAST-PRIVILEGE-ROLES.md:42-49; docs/dr/PI-COLD-STANDBY.md:10-35; GDPRDPIA-REPORT.md; refs/waves/3588 = 286c6f5b.

## Остаток (ответственный)
1. Решение владельца о включении ARTICLE28_RUNTIME_ENFORCEMENT (вооружение по провайдерам по мере подтверждения договоров) — владелец.
2. Закрыть кодовые DPIA R-01..R-06 по ТЗ волны 3588 (волна).
3. R-06 (резервные копии переживают удаление): срок жизни копий + список удалённых субъектов при любом восстановлении + честный текст в публичных документах (волна).

## Критерий закрытия
Флаг runtime enforcement включён по решению владельца, провайдеры вооружены по подтверждённым договорам; DPIA R-01..R-06 закрыты (код+документ+цифры); R-06 реализован по честной схеме и описан публично. Тогда эпик done.

---

## История (старый текст, сохранён ниже)

## ДЛЯ НУЛЕВОГО АГЕНТА (создан 2026-09-08)
- **Суть одной строкой:** не переписывать правовые документы, а ДОКАЗАТЬ, что защита данных реально работает в продукте.
- **Откуда взялось:** внешний рецензент 08.09 разделил остаток на две независимые линии; владелец подтвердил и просил не смешивать их между собой и с линией Stripe/легал r9 (WEB-516).
- **РЕШЕНИЕ ВЛАДЕЛЬЦА 08.09 по статье 9: путь Б — явное согласие.** Спрашивать ТОЛЬКО перед первым использованием функции с особыми данными, НЕ галочкой при регистрации. Согласие: отдельное действие, по умолчанию не отмечено, под конкретную цель, с версией текста и временем ДО обработки, с отзывом на будущее.
- **Не закрывается путём Б:** особые данные ТРЕТЬЕГО лица. Согласие владельца учётки не является согласием человека, о котором написано в загруженном документе. Для личной беты нужен отдельный заслон; для командной — определить контролёра и обработчика.
- **Срок:** на переходный период не рассчитывать — внешней беты ещё не было, готовность нужна ДО первого внешнего запуска.
- **Текущее состояние:** пять волн отданы 08.09 ~23:10Z, база — боевая линия l113z `30000baf9939f94d062b0e216f023a11faa4af3f`.
- **Свой отдельный вердикт:** GDPR READY / CONDITIONAL / NO-GO. Не смешивать с вердиктом линии AI Act (WEB-576).

## Волны
| Волна | Что делает | Хост |
|---|---|---|
| 3020 | Реестр обработок (RoPA) + проверка «новый путь обработки без записи в реестре ломает сборку» | M4 |
| 3021 | Полноценная оценка воздействия (DPIA) с разбором наших реальных рисков | A2 |
| 3022 | Реестр обработчиков ст. 28 + правило В КОДЕ «нет договора → путь ЗАКРЫТ» | A1 |
| 3023 | Поток явного согласия ст. 9 (путь Б) + особые данные третьего лица | A1 |
| 3024 | Доказательства живых процессов: запрос субъекта, отзыв, сроки, cookie, 72 ч, журналы согласий, закрытые пути | A2 |

## Сдача
`ROPA`, `DPIA`, `PROCESSOR/ARTICLE28 REGISTER`, `ARTICLE9 FLOW SPEC`, тесты операционных контролей, итоговый вердикт.

## KNOWN ISSUES / ТРАБЛШУТИНГ
- Симптом: «GDPR закрыт, потому что есть политика конфиденциальности» → проверка: попросить доказательство удаления по запросу субъекта → причина: политика ≠ процесс → лечение: волна 3024, синтетический человек с данными в пяти местах после удаления не находится ни в одном.
- Симптом: поставщик включён «с оговоркой» без подтверждённого договора → проверка: реестр ст. 28 → причина: правило жило в документе, а не в коде → лечение: волна 3022 переносит правило в код.

---

## история (тело до 13.09.2026)

## ДЛЯ НУЛЕВОГО АГЕНТА (создан 2026-09-08)
- **Суть одной строкой:** не переписывать правовые документы, а ДОКАЗАТЬ, что защита данных реально работает в продукте.
- **Откуда взялось:** внешний рецензент 08.09 разделил остаток на две независимые линии; владелец подтвердил и просил не смешивать их между собой и с линией Stripe/легал r9 (WEB-516).
- **РЕШЕНИЕ ВЛАДЕЛЬЦА 08.09 по статье 9: путь Б — явное согласие.** Спрашивать ТОЛЬКО перед первым использованием функции с особыми данными, НЕ галочкой при регистрации. Согласие: отдельное действие, по умолчанию не отмечено, под конкретную цель, с версией текста и временем ДО обработки, с отзывом на будущее.
- **Не закрывается путём Б:** особые данные ТРЕТЬЕГО лица. Согласие владельца учётки не является согласием человека, о котором написано в загруженном документе. Для личной беты нужен отдельный заслон; для командной — определить контролёра и обработчика.
- **Срок:** на переходный период не рассчитывать — внешней беты ещё не было, готовность нужна ДО первого внешнего запуска.
- **Текущее состояние:** пять волн отданы 08.09 ~23:10Z, база — боевая линия l113z `30000baf9939f94d062b0e216f023a11faa4af3f`.
- **Свой отдельный вердикт:** GDPR READY / CONDITIONAL / NO-GO. Не смешивать с вердиктом линии AI Act (WEB-576).

## Волны
| Волна | Что делает | Хост |
|---|---|---|
| 3020 | Реестр обработок (RoPA) + проверка «новый путь обработки без записи в реестре ломает сборку» | M4 |
| 3021 | Полноценная оценка воздействия (DPIA) с разбором наших реальных рисков | A2 |
| 3022 | Реестр обработчиков ст. 28 + правило В КОДЕ «нет договора → путь ЗАКРЫТ» | A1 |
| 3023 | Поток явного согласия ст. 9 (путь Б) + особые данные третьего лица | A1 |
| 3024 | Доказательства живых процессов: запрос субъекта, отзыв, сроки, cookie, 72 ч, журналы согласий, закрытые пути | A2 |

## Сдача
`ROPA`, `DPIA`, `PROCESSOR/ARTICLE28 REGISTER`, `ARTICLE9 FLOW SPEC`, тесты операционных контролей, итоговый вердикт.

## KNOWN ISSUES / ТРАБЛШУТИНГ
- Симптом: «GDPR закрыт, потому что есть политика конфиденциальности» → проверка: попросить доказательство удаления по запросу субъекта → причина: политика ≠ процесс → лечение: волна 3024, синтетический человек с данными в пяти местах после удаления не находится ни в одном.
- Симптом: поставщик включён «с оговоркой» без подтверждённого договора → проверка: реестр ст. 28 → причина: правило жило в документе, а не в коде → лечение: волна 3022 переносит правило в код.
Лента
2026-09-08T22:19:08.911Z ·
Волны отданы 08.09 23:10Z на базе боевой линии l113z 30000baf9939f94d062b0e216f023a11faa4af3f: 3020 реестр обработок (M4), 3021 оценка воздействия (A2), 3022 обработчики ст. 28 (A1), 3023 согласие ст. 9 путь Б (A1), 3024 доказательства процессов (A2). Все пять прошли гард-проверку запретов. Главное требование ко всем: ценность не в документе, а в проверке, живущей в коде — новый путь обработки без записи в реестре обязан ломать сборку, поставщик без подтверждённого договора обязан оставаться закрытым.
2026-09-08T22:32:44.785Z ·
ОЦЕНКА ВОЗДЕЙСТВИЯ (DPIA) ЗАКОНЧЕНА — волна 3021, VERDICT=NO-GO. Это правильный вердикт: волна не поставила галочку, а назвала шесть рисков со ссылками файл:строка на линии l113z `30000baf9`. Отчёт: /home/ubuntu/waves/GDPRDPIA-REPORT.md (A2).

| Риск | Уровень | Доказательство | Волна |
|---|---|---|---|
| R-06 Резервные копии переживают удаление | **КРИТИЧЕСКИЙ** | Основное удаление не вызывает вычистку из выгрузки базы, журнала, зеркала и холодного резерва: `src/app/api/gdpr/delete/route.ts:279-306`, `docs/runbooks/DB-LEAST-PRIVILEGE-ROLES.md:42-49`, `docs/dr/PI-COLD-STANDBY.md:10-35` | 3040 |
| R-01 Особые категории данных третьих лиц | NO-GO | Нет доказанного обнаружения, согласия и заслона: `src/lib/ingest/sourceWriter.ts:151-162,285-314` | 3041 |
| R-04 Голос и транскрипты | NO-GO | Каскад есть, но пробел в схеме голосовой сессии, очистка объектов «по возможности», единого срока нет | 3042 |
| R-05 Персональные данные в журналах и диагностике | ВЫСОКИЙ | Приёмник отчётов о зависании сознательно без входа, без ограничения содержимого и срока хранения: `src/app/api/diagnostics/hang/route.ts:10-44`. Координатор проверил снаружи — отвечает 200 на проде | 3043 |
| R-03 Ссылка на общую тетрадь шире ожиданий владельца | NO-GO | Владелец, срок, отзыв и область есть, но публичный короткий адрес даёт доступ любому предъявителю без подтверждения аудитории | 3044 |
| R-02 Чужой документ в контексте модели | NO-GO | Права доступа и видимость есть, но происхождение, право на раскрытие и вычёркивание перед отправкой провайдеру не доказаны: `src/app/actions.ts:2875-29xx` | входит в 3041 |

ВАЖНОЕ ПО R-06, чтобы следующий агент не искал невозможного: полное удаление из УЖЕ СДЕЛАННЫХ резервных копий физически невозможно ни у кого. Признанная честная схема — срок жизни копий, за который данные гарантированно уходят, ПЛЮС список удалённых субъектов, применяемый при ЛЮБОМ восстановлении, чтобы удалённый человек не воскресал. Это и поручено волне 3040, и это же надо будет честно написать в публичных документах отдельным кругом.

Пять волн отданы на A2 08.09 22:31Z.
2026-09-09T02:01:17.860Z · coordinator
[09.09 02:01Z координатор] ПОСАЖЕН l114d на A1, 09.09 01:59Z. Коммит aa3ee917c178a962ef514554a8bd1d10af5c98ed, 35 коммитов поверх l114c. Артефакт me2-standalone-linux-arm64-aa3ee917c-20260909T014659Z (sha b21f5e5125d29cc7e1130f07185de794dbf63656fd049b52fedda617a987124b). Релиз /home/ubuntu/prod/releases/arm64-l114d-20260909T014659Z. Откат = l114c (bd4108aeecbb1f8b67472abb71c72275eb225f5e).

ТРИ МИГРАЦИИ ПРИМЕНЕНЫ (проверено по _prisma_migrations):
- 20260908100000_web575_voice_retention (класс contract, шесть drop-constraint — переклассифицирована волной 3084 из ошибочного expand)
- 20260908100000_web575_diagnostics_privacy
- 20260908120000_web561_article9_consent

GDPR-коммиты в линии:
- 665c19853 feat(gdpr): make voice retention and blob cleanup durable
- dd82e339a fix(gdpr): apply voice retention to audit derivatives
- e17c69b42 / 23284fbbc docs(gdpr): document audit retention clock
- 230688496 fix(web575): redact and expire diagnostics
- 008ede321 feat(privacy): add code-backed GDPR ropa gate
- ff5a0f7f1 feat: add Article 9 consent-gated document flow
- 1d70754c7 feat(legal): close unconfirmed Article 28 paths

ДОКАЗАТЕЛЬСТВА ПОСАДКИ (снаружи, 02:00Z): корень https://app.sixbyy.com/ = 200 ×4; вебхук POST = 401; /api/ops/live-sha = aa3ee917c…; /api/ready = ready:true, failures пусто; /api/ready/paid = paidReady:true, posture=enforce_ready, failures пусто; гейт целостности юнитов OK; sip readiness 4080 = 200, LEASE_LOST = 0.

ДВЕ ПОЛОМКИ, ПОЙМАННЫЕ ДО ПРОДА, обе в разделе KNOWN ISSUES ниже.

## KNOWN ISSUES / ТРАБЛШУТИНГ

| Симптом | Проверка | Причина (файл:строка) | Лечение | Ссылки |
|---|---|---|---|---|
| Сухой прогон: `/api/ready/paid` = paidReady:false, failures=[egress_boundary], detail «Article28ExternalCallUnregisteredError: … unregistered_external_host», при этом accountingHealth.ready=true | `curl :3011/api/ready/paid` на сухом прогоне | `src/lib/legal/article28Register.ts` — `article28ProviderForUrl()` возвращает null И для петли, И для незарегистрированного внешнего хоста; `assertArticle28FetchDestination()` трактовал оба одинаково и бросал. Барьер ставится в `src/instrumentation.ts:110`, поэтому умирал любой fetch на 127.0.0.1, включая пробу живости платного egress-прокси (`SPEND_EGRESS_PROXY_HEALTH_URL=http://127.0.0.1:35083/health`) | Отдельная `article28IsLoopbackDestination()` и ранний возврат до броска; негативный контроль сохранён. Коммит aa3ee917c | Тест `src/lib/legal/__tests__/article28Register.test.ts` 10/10, включая новый «lets loopback destinations through untouched» |
| После флипа контроллер апстрима: «backend nc-a1 not eligible: check_failed:background» на ОБОИХ бэкендах, при `/api/ready.ready=true` и пустых failures | `curl :3010/api/ready` → `checks.background` | `src/app/api/ready/route.ts:244-270` — l114d добавил две обязательные проверки: `webhookRetries` и `scheduledPosts`. На A1 юнит `nc-a1-webhook-inbox-retry.service` имел `WorkingDirectory=/home/ubuntu/prod/releases/arm64-l105-20260904T221737Z` (каталог давно удалён) и падал `status=200/CHDIR` на каждом тике — пульс не писался. Юнита `nc-a1-scheduled-posts` не существовало вовсе | drop-in `20-workdir.conf` с `WorkingDirectory=/home/ubuntu/prod/shared`; создан `nc-a1-scheduled-posts.service` + `.timer` (раз в минуту, `/api/cron/process-scheduled-posts`). Оба пульса зелёные, `background ok=true` | Сайт не падал: контроллер отказался публиковать пустой апстрим и держал прежний |
| Сборка: `'language' is not defined at runtime` в трёх вебхуках мессенджеров | гейт prebuild | Сведение l114b поверх l114c: `26411f35b` добавил `language` в объект опций вне блока, где эта переменная объявлена — signal:1145, telegram:4361, whatsapp:1110 | Брать язык тем же способом, что рядом в тех же файлах: `detectLanguage(ragInput)` и `detect*(inboundText)`. Коммит 5a5c74a53 | — |
| Сборка: «STOP — new non-paid API perimeter debt: src/app/api/cron/voice-retention/route.ts» | гейт prebuild | WEB-575 добавил маршрут, а в `scripts/api-perimeter-baseline.json` его нет | Записан так же, как все прочие cron-маршруты. **Долг не закрыт**: маршруту нужны настоящие ограничение скорости и наблюдаемость, тогда его надо убрать из реестра. Коммит 51d986398 | Сортировка в реестре — по `localeCompare`, не по байтам |

## ДЛЯ ТИКЕТА
Уроки в ранбук: (1) у барьера, который что-то запрещает, обязан быть тест на то, что он ПРОПУСКАЕТ разрешённое — существующий тест Article 28 проверял только отклонение внешних адресов, поэтому дыра на петле была невидима; (2) посадка обязана чинить не только nc-a1/nc-a1-b/воркеры, но и все периодические юниты — `nc-a1-webhook-inbox-retry` две недели падал молча, потому что ни один гейт не смотрел на юниты вне списка посадки; (3) цикл ожидания здоровья в land-скрипте под `set -e` падал с RC=7 (curl не смог подключиться, пока порт не занят) — добавлено `|| true` во все три цикла.
2026-09-12T22:17:55.135Z · coordinator
[12.09 22:17Z координатор] МОЙКА 12.09 (3569-wash-g4-compliance-process): эпик GDPR. Код Article 28/ROPCA/Article 9 в дереве есть (article28Register.ts, article9Consent.ts, 6 коммитов l114d найдены), но флаг ARTICLE28_RUNTIME_ENFORCEMENT остаётся off, риски DPIA R-01..R-06 не все закрыты, включение ждёт владельца. Эпик с открытыми детьми — не DONE. Статус не меняю: KEEP.
Остаток: Эпик с открытыми дочерними волнами (DPIA R-01..R-06 закрыты не все); Флаг ARTICLE28_RUNTIME_ENFORCEMENT остаётся off — runtime enforcement не включён; Решение владельца о включении флага не принято
Отчёт: /Users/milamarty/waves/3569WASH*-REPORT.md (M4), копия nc-ops-scripts/shift-20260912-resume/wash-20260912/collected/. Проверка по исходнику прода l115g (9c8a9762).
2026-09-12T23:18:34.904Z · coordinator
[12.09 23:18Z координатор] ВЗЯТО В РАБОТУ 12.09 23:17Z (владелец: «мойка = брать тикеты в работу»): волна 3588-gdpr-aiact-remainders (DeepSeek, M4): статус DPIA R-01..R-06 по документам и коду, закрыть документные, ТЗ на кодовые; флаг Article28 не трогаем. Сдача — бандл + отчёт, комментарий сюда по вердикту.
2026-09-12T23:40:17.008Z · coordinator
[12.09 23:40Z координатор] Волна 3588 (DeepSeek, M4) — GO по хвостам без решения владельца: DPIA-запись R-01..R-06 (статус против кода, файл:строка), runbook операционных контролей, ТЗ по R-06 + тест-заглушка. Флаг ARTICLE28_RUNTIME_ENFORCEMENT не трогали (решение владельца). refs/waves/3588 = 286c6f5b (документы, в кандидат l115i).
2026-09-13T10:34:57.627Z · coordinator
[13.09 10:34Z координатор] Волна 3648 закрыла документные хвосты оценки рисков R-01…R-06. По каждому риску теперь либо готовый документ, либо короткое задание на код: разграничение ролей и третьих лиц, срок хранения голоса, вычистка диагностики, удаление из резервных копий. Флаг принуждения по статье 28 НЕ трогали — для него отдельно описаны последствия, откат и чек-лист владельца. Файлы волны: DOC-R01/R04/R05/R06, TZ-R01/R02/R03, FLAG-ARTICLE28-ENABLEMENT.md.
2026-09-13T10:37:39.947Z · coordinator
[13.09 10:37Z координатор] Мойка доски, волна 3654. RETURN. Код GDPR в дереве (article28Register/article9Consent/ropa-gate), документы волны 3588 посажены в l115i; runtime-барьер Article28 под флагом off — решение владельца; DPIA R-01..R-06 не все закрыты. Статус не меняю.
2026-09-13T12:06:19.208Z · coordinator
[13.09 12:06Z координатор] Волна 3660 (GO, refs/waves/3660 = 1156532d) закрыла КОДОВУЮ часть рисков оценки воздействия. Реализовано по заданиям волны 3588: особые данные третьих лиц с заслоном, происхождение документа и вычистка, подтверждение аудитории у публичного короткого адреса, и механизм, при котором удаление переживает восстановление из резервной копии. Покрыто 49 новыми тестами, регрессий ноль.
Риски со сроком хранения голоса и персональными данными в диагностике не трогались — они были закрыты кодом ещё до этой волны, и волна подтвердила это повторным прогоном, а не на слово.
Флаг принуждения по статье 28 не трогался ни в какую сторону — это решение владельца.
2026-09-13T15:16:28.360Z · coordinator
[13.09 15:16Z координатор] ПОДТВЕРЖДЕНИЕ ВЛАДЕЛЬЦА ПОЛУЧЕНО 13.09 15:14Z: «про Google стандартные условия я тебе давно подтвердил. Подтверждаю.»
Это снимает блокировку с трёх мест сразу: подключение Google Drive, возврат ста десяти документов в поиск (их нечем вернуть, кроме перекачки из Drive) и закрытие эпика защиты данных в части реестра обработчиков.
Что делаю дальше сам, по шагам:
1. Реестр подтверждений подписывается ключом, которого в проекте ЕЩЁ НЕТ: подпись считается кодом обмена с секретом, а сам секрет нигде не заведён. Завожу ключ, кладу в защищённое хранилище координатора и в окружение прода, потому что без него приложение не сможет проверить запись.
2. Беру ссылку на стандартные условия обработки данных Google и её отпечаток — запись пиннится к конкретной версии документа, чтобы подмена документа по тому же адресу не прошла молча.
3. Штатным инструментом проекта добавляю подписанную запись в реестр, в дереве кандидата следующей линии, а не правкой на проде.
4. Собираю и сажаю линию, после чего кнопка подключения Диска работает, и я запускаю перекачку документов.
Ничего из этого не делается руками в обход инструмента: прошлый урок про прямую правку базы записан.
2026-09-13T15:33:46.217Z · coordinator
[13.09 15:33Z координатор] Владелец уточнил 15:31Z: Диск подключается ЛИЧНЫМ гугл-аккаунтом, Workspace нет, компании нет.
Это меняет ответ, и я не стану вписывать запись в реестр. Причина прямая: реестр статьи 28 — это реестр договоров с ОБРАБОТЧИКАМИ, то есть теми, кому мы передаём данные наших пользователей на обработку от своего имени. С личным гугл-аккаунтом такого договора не существует в природе: там действуют обычные пользовательские условия, а не соглашение об обработке. Запись «договор подтверждён» была бы неправдой в юридическом реестре, а такие записи читают всерьёз.
Возникло подозрение, которое надо проверить кодом, а не на ощущениях: возможно, заслон вообще неправильно относит подключение Диска к обработчикам. Когда пользователь подключает СВОЁ хранилище своим согласием и мы читаем ЕГО файлы, Google в этом потоке — не наш субподрядчик по чужим данным, а собственное хранилище пользователя. Это другая правовая роль, и код обязан их различать.
Поставлена волна 3726: перечислить по коду, что именно уходит к Google и чьи это данные, показать строку, где заслон принимает решение, и выбрать один из трёх исходов — подключение Диска законно освобождается от проверки, либо это всё же поток обработчика и тогда личный аккаунт его не закрывает никак, либо нужен третий, более узкий вид записи. Реестр волне трогать запрещено.
Следствие для WEB-593: сто десять документов с Диска остаются невозвратимыми, пока это не решено — им нужна перекачка, а она за этим же заслоном.
2026-09-14T12:07:40.979Z · coordinator
[14.09 12:07Z координатор] WEB-575 — удаление данных и аудитория ссылок на встраивание: две миграции, приехавшие с l115k
Обновление от 2026-09-14. Затронутая посадка: l115k (ac4eb8c68d70de829608bcae5a7acef693ab85fb, 13.09 17:27:44Z).
Карточка написана для человека, который открывает её впервые и не имеет ни журнала смены, ни переписки.

⚠️ ЧЕСТНАЯ ОГОВОРКА О ПОЛНОТЕ ЭТОЙ КАРТОЧКИ. Материал смены фиксирует по этому тикету факт двух миграций, их классификацию, доказательство обратимости и последствия на проде. ПОЛЬЗОВАТЕЛЬСКОГО ОПИСАНИЯ СИМПТОМА В МАТЕРИАЛЕ НЕТ, и дописывать его по догадке нельзя. Раздел 1 заполнен ровно тем, что есть.

1. ЧТО БЫЛО СЛОМАНО

Тикет относится к теме защиты персональных данных: удаление данных пользователя должно оставлять проверяемый след («надгробие»), а ссылки на встраивание должны знать свою аудиторию. В материале смены тема проходит как часть эпика защиты данных; ПОЛЬЗОВАТЕЛЬСКОЙ ФОРМУЛИРОВКИ СИМПТОМА В ЖУРНАЛЕ НЕТ. Прежде чем ссылаться на этот тикет как на закрытый пользовательский дефект, формулировку надо восстановить из исходного тела карточки.
Что точно известно из материала: работа по теме шла двумя дорогами — кодовые риски защиты данных (49 тестов) и отдельный разбор поверхностей; тела карточек по теме переписывались по канону.

2. КАК НАШЛИ И ПОЧЕМУ НЕ ПОЙМАЛИ РАНЬШЕ

Отдельного расследования в материале нет. Тикет двигался в рамках мойки доски и эпика защиты данных; общий знаменатель почти всех тогдашних возвратов карточек в работу был один — код посажен, а не хватает живой пробы или формально записанного финального вердикта.

3. ЭВОЛЮЦИЯ, ВКЛЮЧАЯ ТУПИКИ

3.1. ТУПИК, ОБЩИЙ ДЛЯ ЭПИКА ЗАЩИТЫ ДАННЫХ. Волна, проверявшая остаток эпика ПО ДЕРЕВУ, вернула NO-GO по эпику и назвала остаток перечнем с файлами и строками. NO-GO по эпику НЕ означал, что конкретные механизмы сломаны, — он означал, что остаток эпика не закрыт. Эти две вещи легко перепутать при чтении истории.

3.2. ГЛАВНОЕ СОБЫТИЕ ПО ЭТОМУ ТИКЕТУ — ДВЕ МИГРАЦИИ, ПРИЕХАВШИЕ С ЛИНИЕЙ l115k. Кандидат линии принёс ТРИ новые миграции, не объявленные в манифесте совместимости, и сборка ПРАВИЛЬНО встала на гейте. Две из трёх — этого тикета:
  `20260913100000_web575_erasure_tombstone`
  `20260913110000_web575_embed_share_audience`
Волна 3729 объявила их в манифесте с классификацией, выведенной РЕАЛЬНЫМ запуском инспектора SQL: обе — `expand`, то есть чистые CREATE TABLE/INDEX. Третья миграция того же набора (`web651`) оказалась `contract` и потребовала отдельного решения — её след в WEB-651.
Негатив показан дважды: ложная классификация роняет гейт на конкретном месте, порча контрольной суммы базиса роняет гейт с ошибкой устаревшего базиса.

3.3. ПОСЛЕДСТВИЕ НА ПРОДЕ, НАЙДЕННОЕ СЛЕДУЮЩЕЙ ПОСАДКОЙ. Таблицы, созданные этими миграциями (`ErasureTombstone`, `EmbedShareLinkAudience`) вместе с `SourceTextChunk`, ПРИНАДЛЕЖАЛИ РОЛИ МИГРАЦИЙ, и у роли приложения не было на них никаких прав — `pg_dump` упал с отказом. Выданы те же права, что соседним таблицам, плюс `ALTER DEFAULT PRIVILEGES` для обеих создающих ролей. Полный след — в WEB-078.

3.4. ВТОРОЕ ПОСЛЕДСТВИЕ, ОБНАРУЖЕННОЕ СУТКИ СПУСТЯ. Список таблиц, зарегистрированных в подписке репликации, ОТСТАЛ ровно на те таблицы, что принесли эти миграции: живых 219, зарегистрировано 195, не зарегистрировано 24 — и среди незарегистрированных `ErasureTombstone`, `EmbedShareLinkAudience`, `SourceTextChunk`. Последствие измерено и описано в WEB-078: если подписка когда-нибудь применит поток, зарегистрированные таблицы получат данные, а незарегистрированные — молча ничего, причём внешние ключи в этом режиме не срабатывают.

4. ЧТО СДЕЛАЛИ В ИТОГЕ

  Миграции объявлены в `prisma/migration-compatibility-manifest.json` как `expand` с классификацией, полученной реальным запуском инспектора, а не написанной от руки. Контрольная сумма набора миграций пересчитана КОДОМ ПРОЕКТА.
  Линия l115k = `ac4eb8c68d70de829608bcae5a7acef693ab85fb`, посажена 13.09 17:27:44Z. МИГРАЦИИ НА ПРОДЕ: 206 → 209, личность применения `web_migrator`, расписка `prod/shared/l115k-migration/apply-receipt.json`.
  Права на новые таблицы выданы; запись внесена в журнал изменений ролей БД.

5. ЧЕМ ДОКАЗАНО И ГРАНИЦЫ ЗАЯВЛЕНИЯ

ДОКАЗАНО:
  Пост-QA линии l115k (волна 3777) на НАСТОЯЩЕЙ PostgreSQL: обе `expand`-миграции ОБРАТИМЫ БЕЗ СЛЕДА. Это прямая проверка цели отката, а не декларация из манифеста.
  Гейт совместимости показал негатив дважды (ложная классификация и порченый базис роняют сборку).

ГРАНИЦЫ — читать обязательно:
  Доказана ОБРАТИМОСТЬ МИГРАЦИЙ. ФУНКЦИОНАЛЬНОЕ ПОВЕДЕНИЕ механизмов удаления данных и аудитории ссылок на встраивание этими доказательствами НЕ ПОКРЫТО.
  Пользовательской формулировки симптома в материале нет, поэтому утверждение «пользовательская жалоба закрыта» было бы ЗАЯВКОЙ, а не фактом.
  Приёмка 3734 отдельно доказала СВОИМИ тестами, что сам МЕХАНИЗМ гейта миграций дырявый (пара `contract` + `expand` с разными ярлыками релиза в одном наборе проходит; `DROP DEFAULT` на колонке NOT NULL, объявленный как безопасный для отката, тоже проходит). К классификации ЭТИХ двух миграций это не относится, но означает, что гейт — не абсолютная гарантия.

6. ЧТО ОСТАЛОСЬ ОТКРЫТЫМ

  Восстановить и записать в карточку пользовательскую формулировку симптома.
  Функциональное доказательство механизмов (удаление с надгробием, аудитория ссылок) — офлайн-тестами или живьём.
  Устаревший список таблиц подписки (24 незарегистрированные) — см. WEB-078.
  Дырявость механизма гейта миграций — отдельный тикет.

7. TROUBLESHOOTER

Шаг 1. Если посадка встала на гейте совместимости миграций — это НОРМАЛЬНОЕ поведение для миграций, не объявленных в манифесте. Объявлять надо с классификацией, полученной РЕАЛЬНЫМ запуском инспектора SQL, а не на глаз, и обязательно показывать негатив: ложная классификация обязана ронять гейт.

Шаг 2. Если после посадки сломалась резервная копия (`pg_dump: permission denied for table X`) — X создана миграцией под ролью миграций. Выдать те же права, что соседним таблицам, и проверить `ALTER DEFAULT PRIVILEGES` для обеих создающих ролей.

Шаг 3. После посадки, приносящей новые таблицы, сверить их со списком таблиц, зарегистрированных в подписке репликации. Список НЕ обновляется сам и уже отстал на 24 таблицы.

Шаг 4. Если надо доказать, что миграция обратима — прогонять на НАСТОЯЩЕЙ PostgreSQL через `prisma migrate deploy`, а не проверять запись в манифесте. Манифест — это объявление, а не доказательство.
2026-09-14T15:16:39.991Z · coordinator
[14.09 15:16Z координатор] # WEB-575 - блок для вставки

Источники, проверенные отсюда: тело тикета из live API; все 12 комментариев из локального `web-board.sqlite`, последний комментарий `id=4991` от 2026-09-14T12:07:40.979Z. Статус на live API при финальной сверке: `in_progress`. Legal/runtime/prod deletion flows отсюда не проверялись.

## Что болит словами пользователя

GDPR readiness должен быть доказан поведением продукта и кодовыми барьерами, а не только политикой приватности или документом. По позднему `id=4991` пользовательской формулировки симптома для двух миграций в материале нет, это надо честно восстановить.

## Что уже сделано и чем доказано

По телу: в дереве есть Article 28 register, Article 9 consent, RoPA gate and related migrations. Wave 3588 closed document tails and produced specs. Comment `id=4751` is a key update: wave 3660 closed the code part of DPIA risks with a `GO`, implemented shields for special third-party data, document provenance/redaction, audience confirmation and erasure surviving backup restore; it also says voice-retention and diagnostics risks were already closed and rechecked.

Comment `id=4991` adds l115k migration evidence: `web575_erasure_tombstone` and `web575_embed_share_audience` were classified as `expand`, reversible on real PostgreSQL according to post-QA, and permissions/replication consequences were found elsewhere. This proves migration reversibility, not functional GDPR behavior.

## Что осталось

Functional proof: erasure tombstone behavior and embed share audience behavior need tests or live-safe proof. Article28 runtime enforcement/Google Drive role remains unresolved after comment `id=4810`: owner first confirmed Google standard terms in `id=4805`, then clarified personal Google account/no Workspace, so writing processor-register confirmation would be false. Wave 3726 must decide whether Google Drive is user-owned storage flow, processor flow, or a third narrow record type. Also open by `id=4991`: stale replication table list and migration gate weaknesses are separate risks.

## Противоречия между комментариями

Body and `id=4651` say DPIA code risks not all closed; later `id=4751` cancels that for the code part. `id=4805` says owner confirmation unlocks Google/Article28; `id=4810` cancels this: personal Google account is not a processor contract, so no register entry should be written. `id=4991` warns not to confuse reversible migrations with functional GDPR proof.

## С ЧЕГО НАЧАТЬ НУЛЕВОМУ АГЕНТУ

Смотреть: `src/lib/legal/article28Register.ts`, `article9Consent.ts`, RoPA gate, `src/app/api/gdpr/delete/route.ts`, migration files `web575_erasure_tombstone` and `web575_embed_share_audience`, wave 3660 and wave 3726 materials, comments `id=4751`, `id=4810`, `id=4991`.

Первый шаг: split the ticket into current proof matrix: code risks closed by 3660, functional behavior not proven, Google Drive legal role unresolved. Then write tests/proofs for deletion tombstone and embed audience without destructive prod actions.

Готово: owner/legal decision for Article28/Google Drive is represented in code; functional tests show deletion/audience behavior; public docs describe backup erasure limits honestly; runtime flag decision is recorded.

Нельзя: claim GDPR ready because docs exist, insert false Google processor contract for personal account, trigger destructive deletion on prod, use paid providers, treat migration reversibility as user-facing behavior proof.

Размер: несколько смен. Большим это делает legal role decision, functional privacy proofs, backup/replication implications and owner-controlled runtime flag.

## Закрытие и связи

Не закрыто. Code subpart was closed by `id=4751`, but epic remains open. Duplicates inside this set not proven.
2026-09-14T15:16:54.548Z · coordinator
[14.09 15:16Z координатор] **Связка с WEB-593, найденная сегодня двумя разными путями.**

Обогащение 3869 отметило по этому эпику: комментарий `id=4810` **отменяет** путь «Google как processor в реестре», и вопрос роли Google Drive / статьи 28 остаётся открытым.

Сегодня в тот же вопрос упёрлась кампания восстановления индекса (WEB-593) — с другой стороны и практически:
- сухой прогон первой порции дал `refused=10` из 10, причина `no-drive-token`;
- проверка в базе: `UserIntegration` с провайдером google/drive — **0 строк на всём проде**, диск не подключён ни у кого;
- единственный оставшийся путь инструмента — `--allow-service-account-fallback`, и он сам помечен как **предмет статьи 28**.

То есть открытый вопрос этого эпика сейчас **блокирует конкретную работу**, а не лежит теоретически. Решение запрошено у владельца. Если ответ «нет» — WEB-593 закрывается как невыполнимая на текущем правовом основании, и это тоже ответ.
2026-09-14T15:38:27.305Z · coordinator
[14.09 15:38Z координатор] VERDICT=NO-GO

# 3876-web575-gdpr-readiness — WEB-575: удаление и выгрузка выполнены, а не объявлены

Дата прогона: 14.09.2026. Дерево: `refs/waves/l115n` = `d85bb2dad15a3ba52c773b0a2362748009a2c3b9`,
ветка `wave/3876-web575-gdpr-readiness`, коммит `ec3067fbec2902208a0527e757fffa4d1bc06fc7`.
Все ссылки вида `файл:строка` — по этому дереву.

**Вердикт NO-GO означает ровно одно:** механизмы удаления и выгрузки я **выполнил**,
и выполнение показало, что они не делают того, что продукт обещает субъекту данных.
Это не отказ от работы и не «не проверял» — ниже цифры, каждую из которых я получил сам.

---

## 0. Что и где я выполнял (граница прогона)

Одноразовый PostgreSQL 16, поднятый только для этой волны:
каталог `/tmp/wave3876-pgdata`, сокет `/tmp/wave3876-sock`, порт 55999,
запуск с `-h ''` — **не слушает ни одного TCP-адреса**.
Боевые кластеры этой машины (`main:5432`, `gate14bk:5440`, `noteclone:55432`)
не открывались ни на чтение, ни на запись.
Сборок не было: ни `next build`, ни полного `tsc`. Тесты — только `node --test`.

В одноразовую базу применены **все 204 файла миграций** из `prisma/migrations`
(`evidence/run/00-migrations.log`, 0 отказов). Получилась настоящая боевая схема:
**211 таблиц** в `public`.

⚠️ **Честная оговорка о методе.** Маршруты `/api/gdpr/delete` и `/api/gdpr/export`
я выполнял не как TypeScript, а как построчную транскрипцию в SQL
(`evidence/sql/40-delete-transcription.sql`, `evidence/sql/35-export-transcription.sql`):
в дереве нет `node_modules`, а ставить их и собирать — вне разрешений волны.
Каждый оператор SQL подписан номером строки `route.ts`, которую он повторяет.
Основную работу при удалении делает не транскрипция, а **собственный каскад
PostgreSQL** — он выполнялся по-настоящему, со всеми `ON DELETE`, триггерами и
проверками схемы. Всё, что ниже помечено «доказано», доказано именно этим прогоном.

---

## 1. Что продукт ОБЕЩАЕТ субъекту данных, и где это живёт в коде

| Обещание | Где обещано | Где реализовано (файл:строка) | Статус |
|---|---|---|---|
| Удаление учётной записи | `src/content/legal/real/PRIVACY_POLICY.md:71` — «Verified account deletion has a zero-day grace period and **initiates hard deletion immediately**» | `src/app/api/gdpr/delete/route.ts:159` (POST), UI `src/app/(app)/account/delete/AccountDeleteClient.tsx:107-109` | код есть, **работает не так, как обещано** (раздел 2) |
| Доступ / выгрузка / переносимость | `PRIVACY_POLICY.md:86`; `docs/operator/DSAR-RUNBOOK.md:39` | `src/app/api/gdpr/export/route.ts:10`, `src/lib/gdpr/accountExport.ts:179`, UI `src/app/(app)/account/export/AccountExportClient.tsx:27` | код есть, **выгрузка неполна** (раздел 5) |
| Исправление | `PRIVACY_POLICY.md:86` | обычные экраны аккаунта | вне этой волны |
| **Ограничение обработки** | `PRIVACY_POLICY.md:86` — «you may request … restriction, objection» | **нет кода.** Поиск по дереву не нашёл ни флага на субъекте, ни ветки, которая бы приостановила обработку по его требованию. Единственное упоминание — `docs/operator/DSAR-RUNBOOK.md:51`: «pause the relevant optional processing **where technically possible**» | ⛔ **ДЕФЕКТ: обещание без кода.** `User.isSuspended` — это блокировка администратором, а не ограничение по требованию субъекта |
| **Возражение против обработки** | `PRIVACY_POLICY.md:86` | нет кода (то же место) | ⛔ **ДЕФЕКТ: обещание без кода** |
| **Сведения о получателях** | `PRIVACY_POLICY.md:86`; публичный список `/legal/vendors` и `/legal/subprocessors` → `src/lib/legal/publicLegalRegistry.ts:52-61` → `src/content/legal/real/PUBLIC_VENDOR_LIST.md` | список **категорий** получателей есть и публикуется. **Персональных сведений нет**: в выгрузке нет поля получателей, а таблицы, которые знают, каким именно третьим лицам ушли данные ИМЕННО ЭТОГО человека — `SocialIntegration`, `UserIntegration`, `AuthOAuthAccount` — в выгрузку не входят (доказано, раздел 5) | ⚠️ **ЧАСТИЧНО.** Ст. 15(1)(c) допускает категории, но человек не может узнать свои конкретные подключения |

---

## 2. Удаление выполнено. Сценарий S1: удаление ПАДАЕТ ЦЕЛИКОМ

Синтетический субъект `VICTIM_USER_575` засеян генератором, который заполняет
**каждый столбец, чьё имя обозначает пользователя**, а не список таблиц из чьей-то
памяти (`evidence/sql/10-seeder-functions.sql`). Засеяно **205 из 209** таблиц;
субъект присутствует в **127 таблицах**, 134 попадания «столбец = субъект»,
231 строка в базе (`evidence/run/30-census-before.log`).

Дальше я выполнил транскрипцию `POST /api/gdpr/delete` без единого изменения.

**Результат — `evidence/run/40-delete-S1.log`:**

```
ERROR:  update or delete on table "User" violates foreign key constraint
        "DeckSession_userId_fkey" on table "DeckSession"
DETAIL:  Key (id)=(VICTIM_USER_575) is still referenced from table "DeckSession".
```

`prisma.$transaction([...])` откатывается целиком. **Стёрто ноль строк.**

Причина, доказанная и по схеме, и по базе:

* `prisma/schema.prisma:2318` — `user User @relation(fields: [userId], references: [id])`
  **без `onDelete`**. Prisma по умолчанию для обязательной связи ставит `Restrict`.
* В базе это единственный из **85** внешних ключей на `"User"`, у которого
  `ON DELETE RESTRICT` — остальные `CASCADE` или `SET NULL`
  (проверено запросом по `pg_constraint`, тест B-1).
* `RESTRICT` проверяется немедленно, поэтому каскад `User → Deck → DeckSession`
  (он в схеме есть, и в прогоне `Deck` принадлежал субъекту —
  `evidence/run/backup-before-erasure.sql`) **не успевает** сработать.

**Это не теория.** Строку `DeckSession` создаёт живой маршрут
`src/app/api/game/decks/[id]/session/route.ts:77` для залогиненного пользователя.
**Любой человек, который хоть раз дошёл до конца одной колоды карточек,
не может удалить свою учётную запись.** Он получит 500.

### Хуже, чем просто отказ

Две записи фиксируются **до** транзакции и переживают откат:

* `route.ts:254` — `AuditLog` со строкой `gdpr.delete.requested`;
* `route.ts:276` — `ErasureTombstone` (доказано: после провала S1
  `SELECT count(*) FROM "ErasureTombstone"` = 1, а `User` на месте).

То есть система утверждает «субъект удалён и внесён в список стёртых», хотя
не удалено ничего. Это рассинхронизация между заявлением и фактом.

---

## 3. Сценарий S2: убрал блокировку — и измерил остаток

Одно задокументированное отступление (`evidence/sql/45-drop-restrict.sql`):
удаляю строку `DeckSession`, чтобы стало возможным измерить всё остальное.
Повторяю ту же транскрипцию — транзакция фиксируется.

| Измерение | Значение |
|---|---|
| строк в базе до / после | **231 → 114** |
| таблиц опустело полностью | **112** |
| строк с идентификатором субъекта осталось | **37 в 35 таблицах** |

Разбор этих 37 вручную — `evidence/survivors-classified.csv`:

* **32 строки в 31 таблице — это данные самого субъекта, которые пережили удаление;**
* 1 — `ErasureTombstone`, так и задумано;
* 1 — `ActivePassiveLease.ownerId`: это владелец блокировки-процесса, а не человек;
  сюда идентификатор попал из-за совпадения имени столбца (артефакт засева, признаю);
* 3 — чужие записи, где субъект был бы администратором-действующим лицом
  (`ChannelConfig.updatedBy`, `TakedownAction.adminUserId`, `LedgerEntry.adminActorUserId`).

Что именно осталось (выдержка):

| Таблица | Что это про человека |
|---|---|
| `TelephonyEndpoint` | его SIP-адрес, добавочный номер, отображаемое имя |
| `WebPushSubscription` | адрес push-подписки его браузера |
| `ChronicleSendJournal` | журнал доставки ему еженедельных сообщений, `messageId`, `lastError` |
| `LedgerEntry`, `UserBalance`, `BalanceHold`, `PaidOperationRecord`, `TokenUsage`, `UsageCall` | его деньги и потребление |
| `SpendReservation`, `TerminalSpendEvent`, `TerminalCounterDelta`, `SpendActionReplayReceipt` | его платёжная история |
| `DeepResearchRetrievalCache`, `FactcheckWebSearchCache`, `ArcadeGenerationCache` | кэши с содержимым, извлечённым для него |
| `BossFightSession`, `BossMissReviewItem`, `DailyBossStreak` | его игровое поведение |
| `DeepenAnalysisJob` | производная аналитика по его документу |
| `MeBridgeRequest`, `MeBridgeUserMode`, `StudioRoomPresence`, `OutboundCallConfirmation` | его сессии и звонки |

Корень один и общий: в схеме **49 столбцов в 47 таблицах обозначают пользователя,
но не имеют внешнего ключа на `User`** — значит, каскада у них нет вообще.
Маршрут удаления перечисляет руками **12** таких таблиц
(`analyticsOutbox, auditLog, dailyMetrics, document, gameEvent, messengerGuardEvent,
shortLink, subscription, usageRecord, userMetrics, userSession, voiceSession` —
зафиксировано тестом A-5). Остальные не перечислены никем.

Две таблицы стереть **нельзя в принципе**: на `SpendActionReplayReceipt` и
`TerminalSpendEvent` висят триггеры `forbid_..._delete` /
`..._append_only_trigger`. Это осознанное решение про целостность денег, но
оно противоречит обещанию «hard deletion immediately» и нигде публично не оговорено.

**Побочная проверка:** посторонний пользователь `BYSTANDER_USER` после удаления цел
(тест B-3). Удаление не сносит лишнего — оно сносит недостаточно.

---

## 4. Отрицательный контроль: производные без прямого внешнего ключа

Это главная часть задания, и она дала находку.

### NC-1 — осиротевшая RAG-копия. **Механизм её НЕ ЗАМЕЧАЕТ.**

Полностью штатный путь продукта:

1. Человек загружает документ → `Source(id=X)` в его тетради **плюс** устаревшее
   RAG-зеркало `Document(id=X, userId=NULL)` и строки `DocumentChunk` с текстом
   и **вектором `embedding vector(1536)`**.
2. Человек кладёт этот источник в корзину и нажимает «очистить».
   `src/app/api/trash/purge/route.ts:89` выполняет `prisma.source.delete({where:{id}})`.
   Он **не трогает** ни `Document`, ни `DocumentChunk`.
   Триггера, который бы их снёс, тоже нет: на `Source` висят ровно два триггера —
   `entityboost_source_delete_cleanup` и инвалидация кэша фактчекинга.
   ⚠️ Комментарий `route.ts:296-299` утверждает: «Source/Document/Notebook database
   triggers provide the same guarantee inside the transaction». **Это неверно.**
   Я перечислил все триггеры базы — ни один не удаляет `Document`/`DocumentChunk`.
3. Человек требует удаления. `route.ts:151-158` (`getUserSourceDocumentIds`) строит список идентификаторов
   из **живых** строк `Source`. Источника X больше нет — значит, список его не содержит.

**Результат прогона:** `Document` = 1 строка, `DocumentChunk` = 2 строки,
в обеих текст `SECRET-NC1 …` в открытом виде и непустой вектор.
Прямой поиск по базе после удаления: `DocumentChunk.content LIKE 'SECRET-%'` → **2**,
`Document.content` → **1** (`evidence/run/60-residue.log`, раздел F; тест B-5).

Это прямо опровергает `docs/legal/PRIVACY-DRAFT.md:39`
(«Embeddings … удаляются через тот же Document→DocumentChunk cascade»)
и `PRIVACY-DRAFT.md:144` («current и legacy RAG Documents удаляются application-level»)
для этого пути.

### NC-2 — производная, отвязанная через `ON DELETE SET NULL`. **Не замечена.**

`DeepenAnalysisJob.sourceId` — `SET NULL`, `DeepenAnalysisJob.userId` — без внешнего ключа.
После удаления строка жива с идентификатором субъекта.
Таких рёбер `SET NULL` от `Source` пять: `DeepenAnalysisJob`, `InlineFactcheck`,
`VerificationRun`, `VideoPipelineRun`, `VisualRun`.

### NC-3 — кэш, помеченный, а не стёртый. **Замечен, но не удалён.**

`route.ts:301-305` зовёт `invalidateFactcheckVerdictCacheFor*`. Внутри
(`src/lib/factcheck/verdictCache.ts:1097`) — `updateMany({data:{invalidatedAt: now}})`.
Строка `FactcheckVerdictCache` остаётся со всеми полями:
`claimText`, `normalizedClaim`, `summary`, `evidenceBundle`, `sourcesJson`, `rationale` —
то есть с содержимым, выведенным из документов человека.
После удаления: 1 строка, `invalidatedAt` проставлен, текст `SECRET-NC3 …` на месте
(тест A-6 + B-4). Есть `expiresAt`, но это срок годности кэша, а не исполнение права на стирание.

**Вывод по пункту 3 задания: механизм ищет по списку таблиц, а не по признаку
«ссылается на субъекта». Все три производные, не связанные прямым внешним ключом,
пережили удаление. Это важнее зелёного прогона, и зелёного прогона здесь нет.**

---

## 5. `erasure_tombstone`: что он гарантирует на самом деле

Название таблицы обещает «нельзя воскресить». Читаем сторону ЧТЕНИЯ, как и требовалось.

**Находка A — у механизма нет ни одного вызывающего в проде.**
`restoreFromBackup()` объявлен в `src/lib/legal/erasureTombstone.ts:51`.
Поиск по `src/`, `scripts/`, `ops/`: вызовов нет нигде, кроме собственных тестов
(тест A-2). Ни один сценарий восстановления — ни продвижение холодного резерва,
ни `pg_restore`, ни перезаливка дампа — его не зовёт. Миграция
`prisma/migrations/20260913100000_web575_erasure_tombstone/migration.sql`
сама пишет: «Not applied by this wave (no database available)».
То есть сегодня надгробие гарантирует **только то, что запись пометили**.

**Находка B — даже если бы его позвали, фильтр не убирает самого человека.**
Фильтр целиком:

```ts
export type BackupRow = { userId?: string | null | undefined };
return params.rows.filter((row) => !row.userId || !tombstoned.has(row.userId));
```

Строка без поля `userId` всегда сохраняется. **У таблицы `User` поле называется `id`,
а не `userId`.** Значит, учётная запись стёртого человека проходит фильтр насквозь.

Я это **выполнил**, а не вывел (`evidence/sql/70-restore-simulation.sh`):
беру резервную копию, снятую ДО удаления, заливаю её в отдельную базу
`web575_restore` и применяю ровно этот фильтр — удаляю строки только из таблиц,
у которых есть столбец с именем ровно `userId`.

**Результат — `evidence/run/70-restore.log`:**

```
в сырой копии: 127 таблиц / 134 строки субъекта
фильтр удалил: 106 строк из 106 таблиц
после фильтра:
  id              | name           | email
  VICTIM_USER_575 | Erasure Victim | victim575@example.test     ← учётка ЖИВА
  24 строки в 20 таблицах всё ещё несут идентификатор субъекта
  (ownerId, requesterId, solverId, billingUserId, createdBy, player1Id, actorUserId …)
  живы производные без столбца userId:
  Source 3, Message 1, Note 1, Corpus 1, SourceTextChunk 1,
  DocumentChunk 3, VoiceTurn 1, VoiceArtifact 1, TelephonyEventLog 1
  секреты снова в открытом виде: DocumentChunk.content 2, Document.content 1
всего живых строк: 124
```

**Ответ на вопрос 4 задания:** `erasure_tombstone` гарантирует, что **запись пометили**.
Он не гарантирует, что человека нельзя восстановить: восстановления, которое бы его
применяло, в дереве нет, а сам фильтр пропускает и учётную запись субъекта,
и всё его содержимое.

Побочное наблюдение из первого прогона: если применять тот же фильтр как обычный
`DELETE` с включённой ссылочной целостностью, он падает на первой же таблице
(`DeckCard_noteId_fkey`). То есть фильтр ещё и не готов к настоящему восстановлению —
он либо упадёт, либо оставит висячие записи.

---

## 6. Выгрузка выполнена. Что человек получит и чего в ней НЕ будет

Транскрипция `createAccountExportStream()` выполнена на состоянии ДО удаления.
Готовый файл — `evidence/run/account-export.json` (15 132 байта, 20 полей верхнего уровня).

**Человек получит:** `user` (id, email, name, createdAt, twoFactorEnabled),
`notebooks` (с sources / messages / notes / corpora), `sharedNotebooks`,
`clipperKeys` (ключ заменён на `[redacted]`), `activities`, `subscriptions`,
`usage`, `metrics` (+ dailyMetrics), `auditLog`, `sessions` (токен `[redacted]`),
`voiceSessions` (+ turns, artifacts), `messageLogs`, `documents`, `documentChunks`,
`voiceTranscripts`, `voiceArtifacts`, `telephonyEventLogs`,
`realtimeSessionReceipts`, `exportDecisions`.

**Измерено (`evidence/run/36-export-coverage.log`):**

| | |
|---|---|
| таблиц, где лежат строки этого человека | **127** |
| таблиц, до которых дотягивается выгрузка | **23** |
| **таблиц с его данными, которых в выгрузке НЕТ** | **114** (121 попадание «таблица+столбец») |

Чего в выгрузке не окажется, хотя это про него (выборка из 114):

* **кому ушли его данные:** `SocialIntegration`, `UserIntegration`, `AuthOAuthAccount`
  — именно они отвечают на вопрос «каким третьим лицам меня передали»;
* **его телефон и проверка личности:** `PstnDIDNumber`, `PstnDIDKyc`, `TelephonyEndpoint`;
* **его деньги:** `Transaction`, `Purchase`, `LedgerEntry`, `UserBalance`,
  `CurrencyBalance`, `SpendReservation`, `TokenUsage`, `UsageCall`;
* **его согласие на особые категории:** `Article9ConsentEvent` — та самая запись
  ст. 9, ради которой владелец выбрал «путь Б»;
* **то, что он опубликовал:** `Podcast`, `PublicNotebook`, `EmbedConfig`,
  `EmbedShareLink`, `EmbedLead`;
* **его настройки и устройства:** `UserSettings`, `EmailPreference`,
  `UserDeviceFingerprint`, `WebPushSubscription`;
* **его поведение:** `GameEvent`, `AnalyticsOutbox`, `MessengerGuardEvent`,
  `ArcadeRun`, `Deck`, `DeckSession`, `UserXP`, `UserProgress`;
* **производные работы над его текстами:** `AgentTask`, `AutonomyRun`, `CouncilRun`,
  `TranslationRun`, `InlineFactcheck`, `DeepenAnalysisJob`, `VisualRun`, `VerificationRun`.

Три исключения продукт объявляет честно, прямо в файле выгрузки
(поле `exportDecisions`): векторы `embedding` («excluded-derived-data»),
байты голосового аудио («excluded-pending-owner-decision»),
байты артефактов («metadata-and-storage-paths-only»). Остальные 114 таблиц
не объявлены нигде — человек не узнает, что чего-то не получил.

**Ответ на вопрос 5: выгрузка НЕ полна.** Она покрывает ядро (аккаунт, тетради,
документы, голос, журналы) и молча опускает деньги, подключения, телефонию,
согласия и почти всё игровое/производное.

---

## 7. ДЛЯ ТИКЕТА — детским языком, полный след для нулевого агента

**Что нам обещали.** В политике конфиденциальности написано: если человек
подтвердит удаление, всё его стирается сразу и насовсем. И ещё: он может
скачать всё, что мы о нём знаем. И ещё: он может попросить ограничить обработку
и узнать, кому мы его данные отдали.

**Что я проверил.** Я не читал документы и не верил комментариям в коде.
Я поднял отдельную, ни с чем не связанную базу, залил в неё **настоящую схему**
(все 204 миграции), посадил туда одного выдуманного человека и разложил его
данные **по всем таблицам, где вообще упоминается пользователь** — их получилось
127. Потом нажал «удалить» ровно так, как это делает продукт.

**Что получилось.**

1. **Удаление вообще не сработало.** База отказалась удалять человека, потому что
   у него была запись об игре в карточки (`DeckSession`). Всё откатилось,
   не стёрлось ничего. Это не редкий случай: такую запись получает каждый,
   кто доиграл одну колоду до конца. При этом система успела записать
   «удаление запрошено» и поставить «надгробие» — то есть в журналах написано
   «человек стёрт», а на деле он целиком на месте.
2. **Я убрал эту помеху и удалил ещё раз.** Тогда из 231 строки стёрлось 117.
   112 таблиц опустело. Но **32 строки в 31 таблице — это данные самого человека,
   и они остались**: его SIP-номер, адрес push-подписки, журнал отправленных ему
   писем, его деньги и платежи, кэши с текстами, добытыми для него, его игровая история.
3. **Отрицательный контроль сработал против нас.** Я подложил данные так, чтобы
   одна производная не была связана прямой ссылкой. Сценарий простейший:
   человек удалил один свой файл из корзины. После этого копия текста этого файла
   и его числовой вектор остаются в базе, и удаление аккаунта их **не находит**,
   потому что оно ищет по живым файлам. Я потом искал по базе строку-метку —
   она нашлась дважды, открытым текстом.
4. **«Надгробие» (`erasure_tombstone`) не мешает воскреснуть.** Его вообще никто
   не вызывает при восстановлении — во всём дереве нет ни одного такого места.
   А если бы вызвали — я это проверил, залив резервную копию и применив их же
   фильтр, — то **сама учётная запись человека вернулась бы с именем и почтой**,
   потому что фильтр смотрит на поле `userId`, а у таблицы пользователей поле
   называется `id`. Вместе с ней вернулись бы его тексты и голосовые расшифровки.
5. **Выгрузка неполна.** Данные человека лежат в 127 таблицах, выгрузка достаёт
   из 23. Он не получит: кому мы его передали, его телефон и проверку личности,
   его платежи, его согласие на особые категории данных, почти всё, что он опубликовал.
6. **Два права обещаны, но кода нет вовсе:** ограничение обработки и возражение
   против неё. Есть только строчка в инструкции для оператора «приостановите,
   если технически возможно».

**ГДЕ ГРАНИЦА — чего я НЕ доказал и не могу доказать этим прогоном:**

* Я **не запускал** TypeScript-маршруты — в дереве нет `node_modules`, а ставить
  и собирать запрещено. Я выполнял их построчную SQL-транскрипцию. Не проверены
  поэтому: проверка входа, ограничение частоты, второй фактор, блокировки
  abuse/DMCA/legal-hold и **очистка голосовых файлов на диске**
  (`cleanupVoiceSessionBlobsOrThrow`) — всё это до транзакции и к базе отношения не имеет.
* Я сажал **по одной строке в таблицу**. Мои числа — это покрытие таблиц, а не
  объёмы прода. «37 строк» значит «37 мест», а не «37 записей у реального человека».
* Я **не трогал боевые базы** и ничего не измерял на проде. Сколько сегодня в
  проде людей с записью `DeckSession` — **не измерено**, для этого нужен запрос
  к боевой базе, а это вне разрешений волны.
* Резервные копии, зеркала и холодный резерв я **не проверял** — я проверил
  только тот механизм, который должен их обезвредить (`restoreFromBackup`).
* Файловая система, объектное хранилище, подкасты-аудио, векторное хранилище вне
  Postgres — **не проверялись**.
* Четыре таблицы из 209 генератор не заполнил
  (`MeBridgeSession`, `TerminalProjectionIncident`, `TerminalProjectionRepairReceipt`,
  `TerminalSpendProjectionReceipt`). В них нет ни одного столбца, обозначающего
  пользователя, поэтому идентификатор субъекта в них попасть не может — но
  формально они не покрыты, и я это говорю прямо.
* Классификация 37 выживших строк на «данные субъекта» и «не его запись» —
  **моя ручная оценка** по смыслу модели (`evidence/survivors-classified.csv`),
  а не машинное измерение. Сами 37 — измерены.

**ЧТО ОСТАЛОСЬ ОТКРЫТЫМ (по убыванию важности):**

1. `DeckSession.userId` → добавить `onDelete: Cascade` в `prisma/schema.prisma:2318`
   и миграцию. Без этого удаление не работает ни у кого, кто играл в карточки.
2. Перенести порядок в `route.ts`: `AuditLog`-подтверждение и `ErasureTombstone`
   сейчас пишутся ДО транзакции и переживают её откат. Нужно либо внутрь транзакции
   (для надгробия это невозможно — оно должно пережить `User`), либо компенсация при провале.
3. Заменить рукописный список из 12 таблиц на правило: для каждого столбца,
   обозначающего пользователя, должен быть либо внешний ключ с каскадом, либо
   явная строка в удалении. 49 столбцов в 47 таблицах сегодня не покрыты ничем.
   Хорошая форма — гейт сборки: новый такой столбец без покрытия ломает сборку.
4. Путь «очистка одного источника из корзины» (`trash/purge`, `trash/purge-all`)
   обязан удалять и `Document`/`DocumentChunk`, как это уже делает
   `trashAutoPurge.ts:258`. Иначе тексты и векторы остаются навсегда.
5. Убрать из `route.ts:299-302` неверное утверждение про триггеры базы,
   которые «дают ту же гарантию» — таких триггеров нет.
6. `FactcheckVerdictCache`: решить, удалять строки при стирании субъекта или
   явно объявить это исключением в `exportDecisions`/политике.
7. `SpendActionReplayReceipt` и `TerminalSpendEvent` удалить нельзя по триггеру.
   Это законный конфликт «право на стирание против финансовой целостности» —
   его надо назвать в политике, а не молчать.
8. `restoreFromBackup()` нужен вызывающий в реальном сценарии восстановления,
   а его фильтр — по всем столбцам, обозначающим субъекта, включая `User.id`.
9. Выгрузка: либо добавить недостающие категории, либо перечислить исключения
   в `exportDecisions` честно, как уже сделано для векторов и аудио.
10. Ограничение обработки и возражение: либо код, либо убрать обещание из политики.

---

## 8. Как это перепроверить (troubleshooter)

| Симптом | Проверка | Причина (файл:строка) | Что делать |
|---|---|---|---|
| Пользователь жмёт «удалить аккаунт», получает 500, в логах Prisma `P2003` | `SELECT count(*) FROM "DeckSession" WHERE "userId"='<id>'` | `prisma/schema.prisma:2318` — связь без `onDelete`, в базе `ON DELETE RESTRICT` | `onDelete: Cascade` + миграция. Временно: удалить строки `DeckSession` до вызова удаления |
| В журналах есть `gdpr.delete.requested` и `ErasureTombstone`, а пользователь на месте | `SELECT * FROM "User" WHERE id='<id>'` | `route.ts:254` и `:276` пишут до `$transaction` (`:306`), откат их не снимает | см. пункт 2 открытых вопросов |
| После удаления в базе находится текст пользователя | `SELECT count(*) FROM "DocumentChunk" dc JOIN "Document" d ON d.id=dc."documentId" WHERE d."userId" IS NULL` | `route.ts:151-158` берёт идентификаторы из живых `Source`; `trash/purge/route.ts:89` оставил `Document` сиротой | чинить `trash/purge*`, затем разовая чистка сирот |
| Заявили «триггеры базы дают ту же гарантию» | `SELECT tgname FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid WHERE NOT tgisinternal AND c.relname IN ('Source','Document','Notebook')` | таких триггеров нет: только `entityboost_source_delete_cleanup` и три инвалидации кэша | убрать утверждение из комментария `route.ts:296-299` |
| «У нас есть надгробие, значит восстановление безопасно» | `grep -rn restoreFromBackup src scripts ops` | вызывающих нет; фильтр смотрит только на `row.userId` | `evidence/sql/70-restore-simulation.sh` показывает результат за 30 секунд |
| «Выгрузка полная» | сравнить ключи `account-export.json` с `w575.census_before` | `accountExport.ts` перечисляет 23 таблицы из 127 | `evidence/run/36-export-coverage.log` — готовый список недостающих |
| Прогон не воспроизводится: `psql: could not connect` | `pg_ctl -D /tmp/wave3876-pgdata status` | одноразовый кластер не поднят | `bash evidence/sql/00-pg-setup.sh`, затем `bash evidence/sql/run-all.sh ./run` |
| Миграции падают на `monet_w32_additive_repair` | `SELECT to_regclass('"_prisma_migrations"')` | три миграции требуют историю Prisma и отказываются работать без неё | использовать `00-apply-migrations.sh` — он создаёт таблицу истории и пишет туда каждую применённую миграцию |

---

## 9. KNOWN ISSUES этой волны

1. **Маршруты выполнены как SQL-транскрипция, а не как TypeScript.** Причина —
   отсутствие `node_modules` и запрет на сборки. Каждый оператор подписан строкой
   `route.ts`. Не покрыты: аутентификация, ограничение частоты, 2FA, блокировки
   abuse/DMCA/legal-hold, очистка голосовых файлов на диске.
2. **По одной строке на таблицу.** Числа — покрытие таблиц, не объёмы.
3. **4 таблицы из 209 не засеяны** (перечислены выше). В них нет столбцов,
   обозначающих пользователя.
4. **`ActivePassiveLease.ownerId` попал в остаток ошибочно** — это владелец
   блокировки-процесса. Признак «имя столбца» здесь дал ложное срабатывание;
   в `survivors-classified.csv` помечено как артефакт засева.
5. **Классификация выживших строк — ручная.** Измерены 37; разбиение
   «32 его / 5 не его» — моя оценка по смыслу моделей.
6. **Тесты группы B — характеризующие.** Они закрепляют сегодняшнее ДЕФЕКТНОЕ
   поведение и помечены `KNOWN GAP`. При исправлении их надо перевернуть,
   иначе они начнут падать и будут выглядеть регрессией.
7. **Тесты группы B требуют прогона `run-all.sh`.** Без одноразовой базы группа
   пропускается (skip), а не падает — это сделано намеренно, но значит, что
   зелёный `node --test` сам по себе ещё ничего не доказывает.
8. **Прод не измерялся.** Сколько реальных пользователей затронуто пунктом 1 —
   не измерено.
9. **Доля файлов `DPA-SUBPROCESSORS-DRAFT.md` / `PRIVACY-DRAFT.md`** — черновики
   с пометками `TODO-OWNER`. Я сравнивал обещания с **опубликованным**
   `src/content/legal/real/PRIVACY_POLICY.md`, который отдаётся на `/legal/privacy`.

---

## 10. Сдача

| Что | Где | Контроль |
|---|---|---|
| Отчёт | `/home/wave/waves/3876WEB575-REPORT.md` | первая строка `VERDICT=NO-GO` |
| Бандл | `/home/wave/waves/3876WEB575.bundle` | `git bundle verify` → `is okay`, `records a complete history` |
| Контрольная сумма бандла | `/home/wave/waves/3876WEB575.bundle.sha256` | `sha256sum -c` → OK |
| Головы бандла | `ec3067fbec2902208a0527e757fffa4d1bc06fc7 refs/heads/wave/3876-web575-gdpr-readiness` | `git bundle list-heads` |
| Доказательства | `/home/wave/waves/3876WEB575-evidence/` | `SHA256SUMS`, 38 файлов |
| Тесты | `tests/web575/erasure-proof.test.mjs` | `node --test` → 14 pass, 0 fail (`evidence/node-test-web575.log`) |
2026-09-14T16:23:58.040Z · coordinator
[14.09 16:23Z координатор] VERDICT=GO

# 3888-acc-web575-gdpr — независимая приёмка

## Для тикета, детским языком

Я проверил не только слова смены, а сам код и отдельную временную базу PostgreSQL.

Главное подтвердилось: если у человека есть `DeckSession`, кнопка удаления до удаления аккаунта доходит до базы и получает ошибку внешнего ключа. Моя база оставила `User=1`, `Deck=1`, `DeckSession=1`. Значит, транзакция откатывается.

Но фраза «стёрто ровно ноль строк во всей базе» неточна: перед транзакцией маршрут пишет `AuditLog` и tombstone. В моём полном порядке действий после падения остались `User=1`, `Deck=1`, `DeckSession=1`, `AuditLog=1`, `ErasureTombstone=1`. Данные не удалились, но две служебные записи появились.

Также подтвердилось, что после purge файла из корзины могут остаться его копии. В моём сценарии живой `Source` удалён, а `Document=1`, `DocumentChunk=2`; открытая метка нашлась в `2` chunk-строках и в `1` строке `Document`, непустой вектор остался у `2` chunk-строк. Список живых Source после purge этот id не содержал, поэтому route cleanup его не нашёл.

## Проверка заявлений по пунктам

### 1. Удаление и `DeckSession_userId_fkey` — подтверждено с границей

Я накатывал миграции из проверяемого worktree в PostgreSQL 16.15 на Unix socket; `inet_server_addr()` вернул `NULL`. Применено `204` migration SQL-файла.

Сценарий с одним `User`, `Deck` и `DeckSession`:

```text
before: User=1, Deck=1, DeckSession=1
DELETE: update or delete on table "User" violates foreign key constraint
        "DeckSession_userId_fkey" on table "DeckSession"
after:  User=1, Deck=1, DeckSession=1
```

Мой запрос по FK, ссылающимся на `User`, получил `85` связей: `1` `RESTRICT`, `68` `CASCADE`, `16` `SET NULL`. Единственная `RESTRICT` — `DeckSession_userId_fkey`.

Это доказывает дефект для пользователя с `DeckSession`. Это не доказывает, что любой пользователь во всех состояниях обязательно имеет такую строку.

### 2. `127` и `23` — сходятся, но это не полный поиск свободного текста

Я заново посчитал seeded-состояние своим запросом: колонки выбирались по FK к `User`, по именам субъектных идентификаторов и отдельно по `User.id`; авторскую функцию census я не вызывал.

Получено: `127` таблиц со строками субъекта, `134` найденные субъектные колонки/строки. По чтению `src/lib/gdpr/accountExport.ts` и его набора Prisma-моделей экспорт охватывает `23` таблицы/модели. Числа `127` и `23` сходятся с заявлением.

Это граница метода: поиск по колонкам не доказывает отсутствие идентификатора или текста в произвольном JSON, blob-хранилище или внешнем сервисе. Дополнительный set-difference, посчитанный моим набором имён, дал `113` таблиц вне export-списка; это не переношу как авторское число, потому что определения субъектной колонки различаются.

### 3. Оставшийся текст и вектор — подтверждено для orphaned RAG-пути

`src/app/api/trash/purge/route.ts:88-89` инвалидирует кэш и удаляет `Source`. `Document`/`DocumentChunk` там не удаляются. `src/app/api/gdpr/delete/route.ts:151-156` получает document ids только из живых `Source`, а затем route удаляет legacy document только по этому списку или по `Document.userId`.

Мой SQL-сценарий удалил Source и выполнил эти же predicates. После этого осталось `1` `Document`, `2` `DocumentChunk`, `2` строки с открытой меткой в chunks, `1` строка с меткой в Document и `2` непустых vector. Это подтверждает дефект именно для orphaned RAG-копии; это не утверждение о каждом виде файла или каждом внешнем storage object.

### 4. Tombstone — заявление автора смешивает два разных места

Заявление «вызова нет нигде» неверно в буквальном виде: `src/app/api/gdpr/delete/route.ts:276` вызывает `recordErasureTombstoneBestEffort` до `prisma.user.delete`.

Отдельно, моя проверка чтением нашла, что `restoreFromBackup` определён, но production caller в `src`, `scripts` и `ops` не найден; вызовы есть только в тестах. Это означает, что список tombstone не подключён к реальному restore-пути.

Фильтр действительно смотрит не на все формы строки:

```ts
return params.rows.filter((row) => !row.userId || !tombstoned.has(row.userId));
```

Тип `BackupRow` знает только `userId`. В таблице `User` ключ называется `id`, столбца `userId` нет; моя проверка каталога получила `id_columns=1` и `userid_columns=0`. Поэтому строка самой учётной записи прошла бы этот фильтр. Таблица `ErasureTombstone` сама не имеет FK к `User` (`0` FK), что согласуется с идеей пережить удаление пользователя.

Итог пункта: write-вызов существует; restore wiring отсутствует; фильтр для строки User неверен.

### 5. Restriction/objection — blanket claim слишком широкий, общий механизм не найден

По смыслу и по альтернативным именам я проверил routes, User schema и legal/privacy код. Найдены соседние, но более узкие механизмы:

- `src/app/api/emails/preferences/route.ts` — opt-out только для категорий email;
- messenger `recordStopOptOut` — opt-out messenger-канала;
- Article 9 consent — grant/revoke отдельного согласия на специальную обработку;
- `/api/admin/users/[id]/suspend` — административная блокировка, не просьба субъекта об ограничении обработки.

Не найдено пользовательского endpoint/флага/обработчика, который по просьбе субъекта ставит ограничение обработки, и не найдено общего data-subject objection flow. Поэтому исходная фраза «кода нет вовсе» слишком абсолютна, но основной дефект — обещанные права не реализованы как общие права субъекта — подтверждён.

## Меняет ли SQL-транскрипция вывод

Да, она может менять внешнее поведение: настоящий TypeScript route сначала проверяет auth, rate limit, JSON, подтверждение, 2FA, legal/security hold и удаление voice blobs; также до основной транзакции пишет audit/tombstone и инвалидирует кэши. Эти шаги я не выдаю за runtime-запуск.

Но в главном вопросе язык не меняет результат базы. В исходном route есть `prisma.user.delete`, нет удаления `DeckSession`, а база имеет немедленный `RESTRICT` на `DeckSession_userId_fkey`. Как только этот delete реально отправлен, PostgreSQL отклоняет его и откатывает операции основной транзакции. Мой SQL-прогон это подтвердил. Следовательно, транскрипция может отличаться по тому, дойдёт ли запрос до delete и что будет записано до него, но не может превратить этот FK в каскад.

## Вердикт

`VERDICT=GO`: существенные дефекты подтверждены моими замерами — блокирующий FK и rollback, неполный охват экспорта, orphaned RAG text/vector, отсутствие restore caller и неверная форма tombstone-фильтра, отсутствие общего restriction/objection flow.

Это не означает, что каждое слово автора верно: вызов write-side tombstone есть, а существующие узкие opt-out/consent механизмы есть. Поэтому граница вердикта — именно подтверждённые дефекты, а не буквальная формулировка всех пяти пунктов.

## KNOWN ISSUES / что осталось открытым

- Реальный TypeScript route не запускался: в worktree нет `node_modules`; установка зависимостей и сборки запрещены условиями приёмки.
- Не проверены auth, rate limit, 2FA, legal hold, voice blob storage и ответы HTTP в runtime.
- Не доказано полное удаление байтов во внешних storage и произвольных JSON/blob-полей; census проверяет выбранные субъектные колонки.
- Не найден реальный restore caller, поэтому фактический production backup-restore сценарий остаётся открытым; доказан только static gap и поведение фильтра при строке с `User.id`.
- Исправления в код не вносились.
2026-09-14T16:23:59.114Z · coordinator
[14.09 16:23Z координатор] **Независимая приёмка 3888 подтвердила дефекты удаления данных. `VERDICT=GO`** — ветка `refs/waves/3888/acceptance/3888-acc-web575`. Проверяющий поднял свою PostgreSQL 16.15 (Unix socket, `inet_server_addr()` вернул `NULL`), накатил те же 204 миграции и проверил каждый пункт своим замером, авторскую функцию переписи не вызывая.

**Подтверждено:**
- удаление аккаунта у человека с `DeckSession` **падает на внешнем ключе и откатывается**: `before User=1, Deck=1, DeckSession=1` → `after` то же самое. Собственный подсчёт связей на `"User"`: **85 всего — 1 `RESTRICT`, 68 `CASCADE`, 16 `SET NULL`**, и единственная `RESTRICT` это `DeckSession_userId_fkey`;
- **127 таблиц** со строками субъекта против **23** в выгрузке — числа сошлись при независимом способе подсчёта;
- остаток после очистки корзины: `src/app/api/trash/purge/route.ts:88-89` удаляет `Source`, но не `Document`/`DocumentChunk`, а `gdpr/delete/route.ts:151-156` берёт document id **только из живых `Source`**. После сценария осталось `1 Document`, `2 DocumentChunk`, метка открытым текстом в `2` chunk-строках и `1` строке `Document`, непустой вектор у `2` chunk-строк.

**Три моих формулировки приёмка уточнила — публикую поправки:**
1. ⛔ «Стёрто ровно ноль строк во всей базе» — **неточно**: до транзакции маршрут пишет `AuditLog` и tombstone, и после падения остаются `AuditLog=1`, `ErasureTombstone=1`. Данные не удалились, но две служебные записи появились.
2. ⛔ «`erasure_tombstone` не вызывается нигде» — **в буквальном виде неверно**: `src/app/api/gdpr/delete/route.ts:276` вызывает `recordErasureTombstoneBestEffort`. Верно другое и оно важнее: **`restoreFromBackup` не имеет ни одного вызывающего в `src`, `scripts`, `ops`** — только в тестах. То есть список надгробий **не подключён к реальному пути восстановления**. Плюс фильтр `!row.userId || !tombstoned.has(row.userId)` действительно смотрит не на все формы строки.
3. ⛔ «Ограничения обработки и возражения нет кода вовсе» — **слишком абсолютно**. Есть более узкие механизмы: opt-out категорий писем, opt-out мессенджер-канала, grant/revoke согласия по статье 9, административная блокировка. Но **пользовательского пути «субъект просит ограничить обработку» и общего flow возражения не найдено** — основной дефект подтверждён.

Граница метода, названная приёмкой честно: поиск по колонкам не доказывает отсутствие идентификатора в произвольном JSON, blob-хранилище или внешней службе.
2026-09-23T12:42:42.648Z · triage-neo
РЕШЕНИЕ=parked
ОСНОВАНИЕ=2026-09-14, независимая приёмка 3888 GO: удаление падает на DeckSession_userId_fkey, остаются Document/DocumentChunk; 14.09 вопрос роли Google Drive заблокировал WEB-593 и ждёт владельца.
ЧТО НУЖНО=владелец: подтвердить правовую роль личного Google Drive (processor или user storage), чтобы выбрать разрешённый путь WEB-593; triage-neo 4712
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-575","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-23T12:42:43.003Z