WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-593 · Задача · — · web

P1 [поиск/индекс]: 351 документ без единого куска — их нет в поиске; 135 из них у владельца

Закрыт P1 · важно ведёт: —
Суть
# WEB-593 — 351 документ без единого куска (нет в поиске; 135 у владельца)

## 1. Суть
Задача P1: 351 документ лежит без единого чанка и потому отсутствует в поиске, 135 из них принадлежат владельцу. Вернуть их в поиск, сохранив ACL/identity и сами данные: планировщик ограниченного ремонта (dry-run) + исполнитель (apply) с защитой от повторной доставки, чтением только валидных чанков и before/after receipts.

## 2. ГДЕ МЫ СЕЙЧАС (13.09)

Статус in_progress, P1, type task (не epic). **13.09: корневая цепочка закрыта, и ровно один шаг был запрещён нашей же защитой — теперь он разблокирован в кандидате линии l115k.**

Факты на 13.09:
- Затронуто **185 документов** (было 204). Все отказы несут **одну и ту же ошибку от 05.09** — это не 185 разных болезней, а один сбой, размноженный по очереди.
- У затронутых документов **нет записей извлечения** (extraction), а у документов из Google Drive **нет сохранённых байтов**. Значит единственный путь восстановления — **повторная загрузка из Drive**, никакой «переиндексацией на месте» их не вылечить.
- Повторную загрузку блокировал **наш собственный заслон статьи 28**: подключение Google Drive классифицировалось как путь оператора к обработчику, и путь закрывался (`ARTICLE28_CONTRACT_REQUIRED`). Это была правильная защита с неправильной классификацией.
- **Волна 3726 (VERDICT=GO, коммит `8bf5cc1d9`)** разделила в реестре статьи 28 два разных случая: импорт из СОБСТВЕННОГО хранилища пользователя и путь сервисного аккаунта оператора. Коммит уже в кандидате линии `candidate/l115k`. После посадки повторная загрузка станет разрешённой без ослабления заслона.
- **Волна 3732 (A1, Sonnet, в работе)** делает инструмент восстановления: сухой прогон по умолчанию, повторная загрузка байтов через штатный клиент интеграции, перезапуск извлечения, постановка в очередь **только через `buildSourceIndexingPendingMetadata`**, идемпотентность и негативные тесты (при закрытом заслоне — остановиться, а не обойти).

Ловушка, которую нельзя повторять (ошибка координатора 13.09): очередь индексации живёт НЕ в отдельной таблице, а в JSON-пути `Source.metadata->processing->indexing`; куски — в `DocumentChunk.documentId`. Координатор пометил 20 документов `status=pending` прямым SQL-UPDATE — воркер отрапортовал `total=0`, потому что в метаданных не было полей, которые пишет `buildSourceIndexingPendingMetadata`. Все 20 возвращены в `failed`. Ставить в очередь можно только штатной функцией. Кроме того, индексация не начнётся, пока извлечение не в состоянии `done`/`partial` (`sourceIndexingQueue.ts:650`).

## 3. МЕХАНИКА / УСТРОЙСТВО
Ремонт идёт через durable-очередь sourceIndexingQueue: при forcedReindexMode=chunks-only включается skipOptionalEnrichment, обычный reindex сохраняет forceEntityReindex. Исполнитель защищён fences: identity/bytes/revision, receipt-recovery, commit-crash. Ключевые файлы (дерево прода): src/lib/ingest/sourceIndexingQueue.ts (1405), src/lib/ingest/processingState.ts (704), src/lib/documents/processDocumentRuntime.ts (1053), src/lib/rag/chunking.ts (784), src/lib/rag/staleChunkRecovery.ts (154), scripts/repair-live-docs-without-chunks.ts (829), scripts/reindex-quarantined-chunks.ts (1855). Узкие места: worker source требует отдельной сборки/подписи; worker ранее игнорировал repair (chunks-only marker + truncated receipt) — фикс 3514; canary с TTL, planner limit 1, executor dry-run.

## 4. ХРОНОЛОГИЯ

- **13.09 — цепочка причин закрыта полностью.** 185 из 204 документов затронуты; все отказы = одна ошибка от 05.09; записей извлечения нет; у Drive-документов нет сохранённых байтов ⇒ лечит только повторная загрузка ⇒ её запрещал наш заслон статьи 28 из-за неверной классификации Drive.
- **13.09 — волна 3726 (GO, `8bf5cc1d9`)**: в реестре статьи 28 разделены импорт из собственного хранилища пользователя и путь сервисного аккаунта оператора (`src/lib/legal/article28Register.ts`, `src/lib/integrations/googleServerAuth.ts` + 2 набора тестов). Влито в `candidate/l115k`. Заслон не ослаблен — уточнён.
- **13.09 — волна 3732 (в работе)**: инструмент повторной загрузки и возврата документов в индекс (сухой прогон, идемпотентность, негатив «заслон закрыт ⇒ стоп», запрет прямых UPDATE по JSON очереди).
- **13.09 — ошибка координатора и её разбор**: прямой SQL-UPDATE `status=pending` по 20 документам дал `total=0` у воркера, потому что метаданные не содержали полей `buildSourceIndexingPendingMetadata`. Все 20 возвращены в `failed`, ошибка доложена владельцу. Правило: очередь трогать только штатной функцией.
- 10.09 07:07Z: карточка создана. Волны 3401 (acc-index-write), 3455 (dryrun-plan), 3470 (plan GO 10/10), 3490 интеграция (Drive/sync/planner GO ba4aa1c2).
- 11.09: l115d build (00:39Z) → посажена 01:13Z (source ba4aa1c2, artifact 44624569…; историки не восстановлены); l115e посажена 01:59Z (source bb43b5fe, artifact 156323c6…); 3504 bounded exporter/executor; 3508 NO-GO (worker игнорирует repair); 3511 parser repair.
- 12.09: 3514 фикс chunks-only + truncated receipt (GO авторский); 3518 независимая приёмка; 3535 dispatch (16:40Z) — integrate wash next line; 18:22Z SHIFT-STOP R2 (billing parity NO-GO WEB-650).
- 13.09: новых работ нет (остановлено владельцем); возобновление по его слову.

## 5. КАРТА ДОКУМЕНТОВ
- Intel: /Users/annakorin/nc-ops-scripts/release-l115f/3535-collected/3535INTEGRATEWASHNEXTLINE-REPORT.md (+ .bundle, collection-receipt.json)
- Intel: /Users/annakorin/nc-ops-scripts/release-l115f/handoff-tickets/3535-raw-review.json
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/3535-dispatch-receipt.json
- A1: /home/wave/waves/wt-3535-integrate-wash-next-line (branch wave/3535-integrate-wash-next-line)
- Intel: /Users/annakorin/Downloads/NC-HANDOFF-20260912-STOP.md (sha256 fe8242ce…)
- M1: /Users/poolpooly/audit/handoffs/shift-stop-20260912/; M4Ext: /Volumes/M4Ext/ops/handoffs/shift-stop-20260912/
- Связанные: WEB-650 (billing), SEC063/WEB612 (source-only classifier, чанки не восстанавливает)

## 6. ОСТАТОК
1. Возобновление (владелец): прочитать полный 3535 REPORT + collection-receipt/raw evidence, сверить exact scope 11 путей и security parity.
2. Собрать worker/release канонической цепочкой (build/sign, worker identity) — координатор.
3. Свежая read-only inventory с TTL, planner limit 1, executor dry-run и coordinator ACL — координатор.
4. Ограниченный разрешённый canary → readback → расширение (identity/bytes/revision fences).
5. Подтвердить чанки + поиск по before/after receipts; массовый repaired count только из фактических receipts.
6. Закрыть billing parity WEB-650 (параллельно).

## 7. КРИТЕРИЙ ЗАКРЫТИЯ
Установленный worker + свежий разрешённый canary с identity/bytes/revision fences, подтверждённые чанки и поиск; repaired count только из фактических receipts. Source-only classifier (SEC063/WEB612) не восстанавливает отсутствующие чанки.

## История (до 13.09)

DISPATCH-20260910-3401
На 2026-09-10T21:38:23.260126+00:00: active; M4, волна 3401 acc-index-write. Exact BASE 97367844af3b66d5dfa3e5620960aaf922d2f6f3. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /Users/milamarty/waves/inputs/3401-acc-index-write. Отчёт ожидается /Users/milamarty/waves/3401ACCINDEXWRITE-REPORT.md. Живой процесс модели подтверждён; возраст журнала 7 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.

SLOTS12-BATCH3442-3461-WEB-593
2026-09-10T22:50:05.652264+00:00 Лимиты A1/A2 подняты до12; действовавшие worker panes сохранены. Волна 3455 a2nc active index-repair-dryrun-plan; BASE 7bab967fb6acf2fa9f231a576bbb9a3870bf2bcb; model gpt-5.6-terra
Брифы прошли guard; входные report/history/raw/bundle переданы и SHA проверены до очереди. Source/QA scope, посадки и measured capacity не заявляются.

REFILL3472:WEB-593
2026-09-10T23:16:44.018057+00:00
Волна 3470 neo active; acc-index-repair-plan; BASE 1c8b8aa9472404726ab86d7e7651cd9c58f4be35
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web

CHECKPOINT3478:WEB-593 2026-09-10T23:30:08.650195+00:00
3470 независимая offline QA планировщика GO: 6+4=10/10, реальные CLI dry-run/resume/отказы. Только планирование; 351 исторический документ и 135 владельца этим не восстановлены. Следующий шаг: сверить актуальный инвентарь для ограниченного ремонта.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.

## HANDOFF-20260912T1721:WEB-593 — точка входа для нового агента
Правила обогащения: [[WEB-449]]. Наблюдение: 12.09.2026 18:21:36 Europe/Dublin / 17:21:36 UTC. Автор: координатор. История выше сохранена; эту запись читать как текущий handoff на указанное время.

**Задача.** Вернуть в поиск исторические документы без chunks, сохранив ACL/identity и данные.

**Что подтверждено и что остаётся.** 3535 получена:6588 файлов, archive SHA2569eb4119c15c2ed02268be4b288932ce50cc3fda73c6e9370731b60ec48dcff4a; полный readback совпал, raw manifest mismatches0. HEAD73764e52987973d69e1c5ada690ddc1f90ec400d;34offline+4PG tests,0fail/skip. Historical apply НЕ выполнен;351/135 — исторический инвентарь, не число исправленных.

**Как устроено / почему остановилось.** В sourceIndexingQueue durable repair:* с forcedReindexMode=chunks-only включает skipOptionalEnrichment, обычный reindex сохраняет forceEntityReindex. Merge с Security base92f7ae807 без конфликтов:11 принятых paths; после3524 изменены только два QA DROP…CASCADE для schema201. Worker source требует отдельного build/sign.

**Следующая операция.** Получить следующий release из3535; не подмешивать в уже замороженную r2. После rebuild/sign/worker identity выполнить свежую read-only inventory с TTL, planner limit1, executor dry-run и coordinator ACL. Только после всех gates ограниченный разрешённый canary→readback→расширение; старые manifests/expiry не использовать.

**Исполнитель и приёмка.** 3535 автор завершил source/QA. Координатор получил и сверил raw, отвечает за release/ACL/canary; живой запуск3535 больше не ожидается.

**Условие закрытия.** Установленный worker, свежий разрешённый canary с identity/bytes/revision fences, подтверждённые chunks и поиск; массовый repaired count только из фактических receipts.

**КАРТА ДОКУМЕНТОВ**
- Intel: /Users/annakorin/nc-ops-scripts/release-l115f/3535-collected/3535INTEGRATEWASHNEXTLINE-REPORT.md
- Intel: /Users/annakorin/nc-ops-scripts/release-l115f/3535-collected/3535INTEGRATEWASHNEXTLINE.bundle
- Intel: /Users/annakorin/nc-ops-scripts/release-l115f/3535-collected/collection-receipt.json
- Intel: /Users/annakorin/nc-ops-scripts/release-l115f/handoff-tickets/3535-raw-review.json
- A1: /home/wave/waves/wt-3535-integrate-wash-next-line; branch wave/3535-integrate-wash-next-line

**Расхождение описи:** прежний report SHA b9436356… заменён текущим3780bcf2d043259f112ae93ed176c7e7945e08f96e3f28ac3ec8b0ec11224ad9; sidecar и все6588 файлов совпали. Старую запись сохранили.

**ДЛЯ ТИКЕТА — полный авторский handoff3535**
WEB-593 source candidate is integrated at final HEAD
`73764e52987973d69e1c5ada690ddc1f90ec400d`, preserving Security BASE and the
201-migration/P19 boundary. Fresh offline, independent, worker-claim, and
full-schema PostgreSQL checks are green. The worker honors durable repair
chunks-only metadata while ordinary reindex remains intact; executor fences and
receipt recovery are green. This is a source/QA handoff only: no historical
apply, reindex, drain, deployment, release, or `repaired=351/135` claim.


SHIFT-STOP-20260912:R2:WEB-593
ОСТАНОВКА ПО РЕШЕНИЮ ВЛАДЕЛЬЦА — 3535 сохранена для следующей линии.
Сдача3535 HEAD73764e52987973d69e1c5ada690ddc1f90ec400d, исходник92f7ae807; обе parent ancestry, clean и bundle проверены. Получен архив23422691bytes SHA9eb4119c15c2ed02268be4b288932ce50cc3fda73c6e9370731b60ec48dcff4a; member hashes совпали. Author GO относится к source/QA. Это отдельная следующая линия и не входит в установленную92f7ae807.
Историческое число351 документов/135 владельца не является текущим замером или числом исправленных. Historical apply/reindex на проде не выполнен.
Следующий шаг после возобновления: прочитать полный3535 REPORT и collection-receipt/raw evidence, проверить exact scope11paths и security parity; собрать worker/release канонической цепочкой, безопасный canary, then historical apply с before/after chunks/search evidence. Source-only classifier SEC063/WEB612 не восстанавливает отсутствующие chunks.
Пути: /Users/annakorin/nc-ops-scripts/release-l115f/3535-collected/; /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/3535-dispatch-receipt.json. Бандл и отчёты сохраняются. Новая работа сейчас не запускается.
Billing finding: [[WEB-650]].
Доказательства

DISPATCH-20260910-3401
На 2026-09-10T21:38:23.260126+00:00: active; M4, волна 3401 acc-index-write. Exact BASE 97367844af3b66d5dfa3e5620960aaf922d2f6f3. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /Users/milamarty/waves/inputs/3401-acc-index-write. Отчёт ожидается /Users/milamarty/waves/3401ACCINDEXWRITE-REPORT.md. Живой процесс модели подтверждён; возраст журнала 7 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.

SLOTS12-BATCH3442-3461-WEB-593
2026-09-10T22:50:05.652264+00:00 Лимиты A1/A2 подняты до12; действовавшие worker panes сохранены. Волна 3455 a2nc active index-repair-dryrun-plan; BASE 7bab967fb6acf2fa9f231a576bbb9a3870bf2bcb; model gpt-5.6-terra
Брифы прошли guard; входные report/history/raw/bundle переданы и SHA проверены до очереди. Source/QA scope, посадки и measured capacity не заявляются.

REFILL3472:WEB-593
2026-09-10T23:16:44.018057+00:00
Волна 3470 neo active; acc-index-repair-plan; BASE 1c8b8aa9472404726ab86d7e7651cd9c58f4be35
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web

CHECKPOINT3478:WEB-593 2026-09-10T23:30:08.650195+00:00
3470 независимая offline QA планировщика GO: 6+4=10/10, реальные CLI dry-run/resume/отказы. Только планирование; 351 исторический документ и 135 владельца этим не восстановлены. Следующий шаг: сверить актуальный инвентарь для ограниченного ремонта.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.

CHECKPOINT3490:WEB-593 2026-09-11T00:21:57.481659+00:00
3490 source integration confirmed running on Neo. Three exact accepted heads: Drive3471 28fcffa807178fc31ffc0841c92f9f1f2b90998f; sync3412 aa098fc84838c524372b36eafb46f0199e0209ed; planner3470 cb8aadd00f39ec63ba40bc613a3865108420d356. Inputs/report/bundle SHA and git objects verified before dispatch. Merge verdict pending. Historical documents not repaired; Google browser/runtime and sync release checks pending. Editor3489 excluded. Evidence: wash-integration-3490/3490-receipt.json, inputs/manifest.json and full input reports.

CHECKPOINT3492:WEB-593 2026-09-11T00:32:55.179360+00:00
3490 source integration GO ba4aa1c219625df95035cd1eb9653d9a6b0c4903: три точных принятых головы, ноль конфликтов, Drive14/14 + sync17/17 + planner10/10. Lint 0 errors/19 inherited warnings. Exact bundle/head/clean/SHA проверены и скопированы; SHA 9a47642243fa5412fa647c5a1ff23f2ddd0e96f493a5eb5dd58c2938a1218cac. Browser/provider/live/release pending. Planner dry-run only; исторические351/135 документов не восстановлены. Evidence: checkpoint-3492/snapshot.json, полный3490 REPORT, Neo3490WASHINTEGRATION-evidence.

L115DBUILDSTART:WEB-593 2026-09-11T00:42:10.710873+00:00
l115d build started 2026-09-11T00:39:55Z on A2, PID2866558, clean dedicated /home/ubuntu/wt-l115d from verified3490 HEAD ba4aa1c219625df95035cd1eb9653d9a6b0c4903. Private dependencies copied, 281 generated wrappers rebased, 13 NEXT_PUBLIC values read from both live processes and equal. Prisma generation, P19 compatibility and schema3017 fields passed; activation generated; prebuild running at00:41Z. Cache archived to M4Ext with SHA302d195a5bb26234105aa8fa9e1e0613e96381dc730babd65b3ed037e227770f before removal. No PACK_OK, landing or browser acceptance yet. Source includes Drive/sync/planner only; editor3492 and capacity3491 remain separate. Evidence: checkpoint-3492/l115d-build-start.json, l115d-preflight.log, build-cache-archive.json; A2 /home/ubuntu/l115d-chain.log.

LANDING115D-PG3496:WEB-593 2026-09-11T01:15:45.387983+00:00
l115d deployed 2026-09-11 01:13 UTC: source ba4aa1c219625df95035cd1eb9653d9a6b0c4903, artifact SHA256 446245699e4e28f03336d7726d9fa4dc422f8a8e4b7597d572b29bd1e0eb57f0. Signature positive/negative checks passed. Both backends paidReady=true/enforce_ready with exact source; four service path gate passed; live chat HTTP200; distance route HTTP200 and asks for missing origin. Browser opened one of58 documents, editor surfaces present, zero new errors after click; initial load retained two aborted cookies RSC requests and admin/agents HTTP403 plus console error. SIP readiness200 and LEASE_LOST0. Ships wash3490 Drive/sync/planner; editor3495 remains separate. Historical351/135 document repair and full feature-specific provider/browser QA remain open. Rollback units: /home/ubuntu/prod/shared/pre-l115d-units; previous release l115c. Evidence: checkpoint-3495/l115d-{dryrun,flip,live-postqa,editor-postqa}.log.

SHIFT3502:WEB-593 2026-09-11T01:31:25.453329+00:00
3504 RUNNING A1 from exact deployed wash sourceba4aa1c219625df95035cd1eb9653d9a6b0c4903. Implement bounded separate inventory/apply tool with immutable manifest, immediate revision/digest checks and existing queue idempotency; original planner stays dry-run only. Actual historical351/135 documents have NOT been repaired. Guard/input hashes/BASE verified; shift-3502/3504-receipt.json.

CHECKPOINT3505:WEB-593 2026-09-11T01:50:34.398161+00:00
3504 bounded exporter/executor delivered; author reports12/12 focused and1/1 disposable PG checks. Independent acceptance, fresh live inventory and actual repair remain pending. No historical351/135 population repaired. Coordinator verified and preserved bundle/head/clean/SHA: 78a4f89c35ffa431d725f6c8f6de7f37a36ad2fa / 97d2b04f825ba9c04fc3f3a994df37db57eb2be084cfae4cdbe6d2a883d938a7. Evidence archive members=24; SHA256=5bbc781e39137b50c36ec296120a636f4f71dfc6143be18718994f0f696153e5. Receipts: checkpoint-3505/snapshot.json and snapshot-3504.json.

LANDING115E:WEB-593 2026-09-11T02:01:10.157440+00:00
l115e landed 2026-09-11 01:59 UTC. Source bb43b5fe20398309b3b373df3c00fa1d3040397d; artifact SHA256 156323c694a6854b6207ee2b6dcd101abba99b67c384cc6282e10d098c9eb357. Both live backends independently confirmed exact source, ready=true and paidReady=true/enforce_ready at 02:00 UTC. Build/signature/negative signature/dryrun/four-unit path gates passed; chat and distance HTTP200. Browser opened one of58 fixture documents, editor surfaces present, zero new errors after opening. Initial load recorded three aborted RSC requests and admin/agents403 plus console error; clean console not claimed. SIP readiness200, LEASE_LOST0, no SIP restart. Ships accepted editor3495 atop wash3490. Feature-specific browser3503, real provider OAuth, historical351/135 repair, capacity and Security26 remain open. Rollback: prod/shared/pre-l115e-units to l115d. Evidence: release-l115e/l115e-{dryrun,flip,live-postqa,editor-postqa}.log.

SHIFT3506:WEB-593 2026-09-11T02:16:03.175385+00:00
3508 RUNNING A1 independent functional acceptance of bounded historical repair executor at78a4f89c35ffa431d725f6c8f6de7f37a36ad2fa, including commit/receipt crash recovery and real-worker compatibility. Coordinator reproduced exporter CLI failure: --output /tmp/web593-inventory-probe.json -> unknown flag path, exit1 before database use.3511 RUNNING Neo repairs parser only from exact same BASE; SQL/admission unchanged. Historical351/135 documents remain unrepaired; source GO not execution. Evidence: shift-3506, shift-3511, checkpoint-3505; process observation 2026-09-11T02:16:01.981638+00:00

CHECKPOINT3511:WEB-593 2026-09-11T02:22:40.994630+00:00
3511 collected and verified: HEADc22fafca9fdc202e632ca6fba2b66c48a802f9e0; SHA dae4491db8dc5a1a9ae088a2f4a734389a0283c73bb5c232de55ac254a7e5f02. Report records CLI/parser subprocess tests9/9, separated/equal forms, malformed input refusal before DB import. SQL/executor/product unchanged. Raw archive fully read. Independent executor3508 acceptance not yet collected; historical351/135 repair not executed. Evidence checkpoint-3511/snapshot.json.

RESUME20260912:WEB-593 2026-09-12T08:51:16.229899+00:00
3508 NO-GO collected and verified: QA HEADe5770b4e61178ceeeaf34c43000266484371d990, SHA272b2a0b0df6f92eac43a469650d123ea129c77bbde4a2cd7fbf2e562d81af20; raw archive fully read,17 files. Confirmed worker ignores repair: chunks-only marker and truncated receipt prevents resume.3514 fixes both and integrates3511 parser repair. Historical apply remains blocked; no351/135 repair claimed. Dispatch inputs verified; observed RUNNING. Evidence resume-20260912/3514-receipt.json; observation.json; collected.json.

COORD-TICK-20260912T1003:WEB-593
3514 выдала GO; это авторский результат. Независимая приёмка и историческое восстановление ещё не завершены; 3518 пока без подтверждённого запуска.

WASH3518-FIXTURE3515-20260912:WEB-593 2026-09-12T11:30:06.235967+00:00
Independent acceptance3518 dispatched on A1, gpt-5.6-luna high. Pickup observed 2026-09-12T10:50:13Z: PID689587; dispatcher start10:49:19Z; own tree HEAD3d0351f958002f14a4c7b68dd099469ec86b1402. Brief guard, input SHA256, bundle/base and duplicate checks passed; preflight disk29508216KiB. Author3514 clean HEAD and bundle SHA25638da5a038ed56b9f8be968114d8ffe824e882fcbc2950c1f5f876b2d69097ba7 verified; author DONE absent after dispatcher cleanup. QA-only full disposable PG/worker resolver/atomic receipt recovery/identity-conflict negatives assigned; independent verdict pending at pickup. Historical apply and production prohibited. Receipt: /Users/annakorin/nc-ops-scripts/resume-20260912/3518-receipt.json; guarded brief: /Users/annakorin/nc-ops-scripts/resume-20260912/3518-acc-repair-historical-brief.md.

COORD-TICK-20260912T1648:WEB-593
3535 dispatched from exact Security candidate92f7ae807 with verified historical3524 HEAD985453fefcf21dc0f754c568f16dca4f59da52d6 and bundleSHA0a9cc61738db4b90b796b87a994d50135220ce5bd82a8d71a803a5ece608960d. Guard,6 input hashes,both exact git objects and no-duplicate/disk checks PASS. A1 dispatcher start16:40:31UTC, runnerPID2062697, routecodex/modelgpt-5.6-luna high; log confirms source inspection. Task: separate next-line merge, accepted11-path boundary plus Security byte parity, actual focused/full migrated private-PG201 checks with private dependency copy and no release build. Current l115f build is frozen and unchanged. No historical apply/reindex performed;351/135 is historical observation, not repaired count. Receipt tick-20260912-recovery/3535-dispatch-receipt.json.

HANDOFF-20260912T1721:WEB-593 — автономный handoff добавлен в body: причина, механизм, подтверждённое/OPEN, следующая операция, ответственный, критерий закрытия и карта документов. Срез фактов 2026-09-12T17:21:36.454887+00:00. Исторические отказы и статусы сохранены. Полный аудит записи: Intel /Users/annakorin/nc-ops-scripts/release-l115f/handoff-tickets/.

SHIFT-STOP-20260912:R2:WEB-593
Installed source92f7ae807 / artifact1353e969; runtime-stop-verified.log; billing parity NO-GO [[WEB-650]]; per-finding remaining gates in body.
Лента
2026-09-10T13:14:03.706Z · coordinator
ЭВОЛЮЦИЯ 10.09 — ВЗЯТО В РАБОТУ, волна 3286 крутится на A1.

Первая попытка (волна 3280, A1, 08:47Z) УМЕРЛА молча: ни отчёта, ни бандла, только .route-файл.
Причина смерти не установлена — её лог попал под чистку логов старше двух часов.

Перезапуск (волна 3286) отличается от первого тремя вещами:
1) BASE переставлен на боевую голову 16e1ddaa1 (в 3280 стояла база 76f02b486, уже устаревшая);
2) база проверена ДО постановки скриптом dispatch-preflight.sh — раньше я этот шаг пропускала
   и теряла часы на волнах с базой, которой нет на машине;
3) в бриф добавлен МАЯК ЖИВОСТИ: волна каждые 10 минут дописывает строку с меткой времени
   и текущим шагом в файл -HEARTBEAT.log, чтобы падение было видно сразу, а не через час.

Предмет тикета не изменился: 351 документ без единого куска, 135 из них у владельца.
Самый крупный — «03 - Lester Del Rey - For I Am a jealous People — Ukrainian.md», 88 676 знаков,
загружен 18.06. Эти документы не находятся поиском вообще.

Связано: WEB-594 (ноль сущностей у больших книг) — соседняя болезнь того же конвейера индексации,
но причина другая: там версия извлечения, здесь отсутствие кусков.
2026-09-10T14:32:41.723Z · coordinator
ЭВОЛЮЦИЯ 10.09 — волна 3286 СДАЛА починку, корень оказался ХУЖЕ формулировки тикета.
Приёмка запущена, статус остаётся in_progress до её вердикта.

Сдача: коммит af1f8a72fbd500b8cc30f38bf0fc97836fb699a8 «fix: reject document indexing without chunks»,
17 файлов, 742 вставки, 138 удалений, плюс интеграционный тест на живом PostgreSQL
documentsWithoutChunks.pg.integration.test.ts (190 строк).
Отчёта волна не написала — бриф её об этом не попросил, вина координатора.

ЗАЯВЛЕННЫЙ КОРЕНЬ (из комментария в коде): путь uploadFile в обход notebookId сохранял Document
напрямую и ПРЕВРАЩАЛ ОШИБКИ ИНДЕКСАЦИИ В УСПЕХ с chunks=0. Последним новым вызывающим этого пути
была авторизованная загрузка через мастер первого запуска — то есть путь, которым пользуется владелец.
Это не «документы потерялись при индексации», это «система рапортовала успех при провале».

ПРИЁМКА 3295 идёт на Нео (claude-opus-5). Ей заданы шесть требований, два ключевых:
разобрать каждый из 17 файлов на «необходимо / разрастание работы», и ответить ПРЯМО —
лечит ли починка уже загруженные 351 документ задним числом или только предотвращает новые.
Владельцу нужны именно его 135.

Приёмка сначала стояла на A1 и умерла там за 46 секунд из-за истёкшей сессии Клода;
перевезена на Нео вместе с веткой автора.
2026-09-10T15:17:55.459Z · coordinator
ПРИЁМКА 3295 (Нео, claude-opus-5) — ВЕРДИКТ NO-GO, но результат ценный. Остаюсь in_progress.

1) КОРЕНЬ ПОДТВЕРЖДЁН своим прогоном приёмщика, с точными местами:
   - превращение ошибки в успех: `src/app/actions.ts:8861-8862`;
   - молчаливый ноль в рантайме: `src/lib/documents/processDocumentRuntime.ts:618`
     (разрушающее удаление) -> `:973` (`success:true, chunks: processedChunkCount=0`).
   На базе: `s1_reindex_result={"success":true,"chunks":0}` при нуле физических кусков и
   существующей строке Document. После починки: `success:false`, 6 кусков уцелели.

2) ОБЪЁМ ПРАВКИ: из 17 файлов **15 необходимы, 2 избыточны** —
   `src/lib/ingest/util/decode.ts` и `src/lib/ingest/__tests__/encoding.test.ts`.

3) NO-GO ИМЕННО ИЗ-ЗА ДЕФЕКТА В ИЗБЫТОЧНОМ ФАЙЛЕ: `src/lib/ingest/util/decode.ts:172-183`
   (применение `:224-228`). То есть разрастание работы за пределы задачи само же и принесло дефект.
   Чистое решение: брать 15 нужных коммитов, два лишних файла НЕ брать.

4) ПУТЬ ВЛАДЕЛЬЦА НЕ СЛОМАН: загрузка через мастер первого запуска даёт `indexingStatus:"queued"`,
   после воркера 20 кусков, все с заполненным `sourceContentRevision`. Экран успеха локализован,
   новый промежуточный статус есть в en/ru/uk.

5) ГЛАВНЫЙ ОТВЕТ ДЛЯ ВЛАДЕЛЬЦА: починка **только ПРЕДОТВРАЩАЕТ новые случаи. Задним числом
   351 документ она НЕ лечит.** Лечит ОТДЕЛЬНЫЙ, уже существовавший инструмент
   `scripts/repair-live-docs-without-chunks.ts` (коммит `a03c7756`, есть и на базе).
   Владельцу нужны его 135 документов — надо запускать именно этот инструмент.

6) Проверки приёмщика свои, не переиспользованы: 11/11 зелёных на починке против 6/11 красных
   на базе. Негативные тесты (пустой и пробельный файл) зелёные на ОБОИХ деревьях —
   значит починка их не задела.
2026-09-10T16:11:05.582Z · coordinator
СОСТОЯНИЕ 10.09 вечер: корень подтверждён, починка есть, но НЕ посажена — приёмка дала NO-GO
из-за разрастания работы. Идёт волна 3297 (Нео), которая снимает лишнее.

ЧТО ТОЧНО ИЗВЕСТНО ПРО КОРЕНЬ (подтверждено приёмкой 3295 своим прогоном, не со слов автора):
- превращение ошибки в успех: `src/app/actions.ts:8861-8862`;
- молчаливый ноль в рантайме: `src/lib/documents/processDocumentRuntime.ts:618` (разрушающее удаление)
  -> `:973` (`success:true, chunks: processedChunkCount=0`).
На базе прогон дал `s1_reindex_result={"success":true,"chunks":0}` при нуле физических кусков
и существующей строке Document. После починки — `success:false`, 6 кусков уцелели.
То есть формулировка тикета неточна: это не «документы потерялись при индексации»,
а «система рапортовала УСПЕХ при провале».

ПОЧЕМУ NO-GO: из 17 файлов правки 15 необходимы, а 2 избыточны
(`src/lib/ingest/util/decode.ts`, `src/lib/ingest/__tests__/encoding.test.ts`),
и дефект сидит ИМЕННО в избыточном: `decode.ts:172-183`, применение `:224-228`.
Разрастание работы за пределы задачи само же и принесло дефект.

🔴 ГЛАВНОЕ ДЛЯ ВЛАДЕЛЬЦА, ответ приёмки на прямой вопрос: починка ТОЛЬКО ПРЕДОТВРАЩАЕТ новые случаи.
Задним числом 351 документ она НЕ ЛЕЧИТ. Лечит ОТДЕЛЬНЫЙ, уже существующий инструмент
`scripts/repair-live-docs-without-chunks.ts` (коммит `a03c7756`, есть и на базе).
Владельцу нужны его 135 документов — надо запускать именно его, отдельным решением.

Путь владельца не сломан: авторизованная загрузка через мастер первого запуска даёт
`indexingStatus:"queued"`, после воркера 20 кусков, все с заполненным `sourceContentRevision`.
Проверки приёмщика свои: 11/11 зелёных на починке против 6/11 красных на базе.

Отчёты: `/home/wave/waves/...` (автор 3286, отчёта нет — бриф не попросил, вина координатора),
`/Users/limamarty/waves/ACCDOCCHUNKS-REPORT.md` (приёмка 3295, 50 КБ).
Связано: WEB-594 (соседняя болезнь того же конвейера, но другой корень).
2026-09-10T18:55:03.576Z · coordinator
BOARD-WASH-20260910:WAVE-3335
По поручению владельца 18:43Z назначена исполнительская волна 3335 (index), Codex gpt-5.6-luna xhigh, A1. Полная история карточки и база l115c 1ad52e16b доставлены, brief-guard и проверка Git-базы пройдены. Задача: проверить существующую сдачу, устранить остатки, передать бандл и доказательства. Финальная приёмка и посадка остаются за координатором. Запуск группы подтверждён в журнале диспетчера.
КАРТА ДОКУМЕНТОВ: ноут /Users/annakorin/nc-ops-scripts/board-wash-20260910/3335-wash-index-brief.md; A1 /home/wave/waves/inputs/board-wash-20260910/index-tickets.json; ожидаемый отчёт /home/wave/waves/3335WASHINDEX-REPORT.md. Правила обогащения: WEB-449.
2026-09-10T21:39:01.229Z · coordinator
DISPATCH-20260910-3401
На 2026-09-10T21:38:23.260126+00:00: active; M4, волна 3401 acc-index-write. Exact BASE 97367844af3b66d5dfa3e5620960aaf922d2f6f3. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /Users/milamarty/waves/inputs/3401-acc-index-write. Отчёт ожидается /Users/milamarty/waves/3401ACCINDEXWRITE-REPORT.md. Живой процесс модели подтверждён; возраст журнала 7 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
2026-09-10T22:50:48.392Z · coordinator
SLOTS12-BATCH3442-3461-WEB-593
2026-09-10T22:50:05.652264+00:00 Лимиты A1/A2 подняты до12; действовавшие worker panes сохранены. Волна 3455 a2nc active index-repair-dryrun-plan; BASE 7bab967fb6acf2fa9f231a576bbb9a3870bf2bcb; model gpt-5.6-terra
Брифы прошли guard; входные report/history/raw/bundle переданы и SHA проверены до очереди. Source/QA scope, посадки и measured capacity не заявляются.
2026-09-10T23:18:14.074Z · coordinator
REFILL3472:WEB-593
2026-09-10T23:16:44.018057+00:00
Волна 3470 neo active; acc-index-repair-plan; BASE 1c8b8aa9472404726ab86d7e7651cd9c58f4be35
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web
2026-09-10T23:33:43.936Z · coordinator
CHECKPOINT3478:WEB-593 2026-09-10T23:30:08.650195+00:00
3470 независимая offline QA планировщика GO: 6+4=10/10, реальные CLI dry-run/resume/отказы. Только планирование; 351 исторический документ и 135 владельца этим не восстановлены. Следующий шаг: сверить актуальный инвентарь для ограниченного ремонта.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.
2026-09-11T00:21:58.167Z · coordinator
CHECKPOINT3490:WEB-593 2026-09-11T00:21:57.481659+00:00
3490 source integration confirmed running on Neo. Three exact accepted heads: Drive3471 28fcffa807178fc31ffc0841c92f9f1f2b90998f; sync3412 aa098fc84838c524372b36eafb46f0199e0209ed; planner3470 cb8aadd00f39ec63ba40bc613a3865108420d356. Inputs/report/bundle SHA and git objects verified before dispatch. Merge verdict pending. Historical documents not repaired; Google browser/runtime and sync release checks pending. Editor3489 excluded. Evidence: wash-integration-3490/3490-receipt.json, inputs/manifest.json and full input reports.
2026-09-11T00:32:56.116Z · coordinator
CHECKPOINT3492:WEB-593 2026-09-11T00:32:55.179360+00:00
3490 source integration GO ba4aa1c219625df95035cd1eb9653d9a6b0c4903: три точных принятых головы, ноль конфликтов, Drive14/14 + sync17/17 + planner10/10. Lint 0 errors/19 inherited warnings. Exact bundle/head/clean/SHA проверены и скопированы; SHA 9a47642243fa5412fa647c5a1ff23f2ddd0e96f493a5eb5dd58c2938a1218cac. Browser/provider/live/release pending. Planner dry-run only; исторические351/135 документов не восстановлены. Evidence: checkpoint-3492/snapshot.json, полный3490 REPORT, Neo3490WASHINTEGRATION-evidence.
2026-09-11T00:42:11.230Z · coordinator
L115DBUILDSTART:WEB-593 2026-09-11T00:42:10.710873+00:00
l115d build started 2026-09-11T00:39:55Z on A2, PID2866558, clean dedicated /home/ubuntu/wt-l115d from verified3490 HEAD ba4aa1c219625df95035cd1eb9653d9a6b0c4903. Private dependencies copied, 281 generated wrappers rebased, 13 NEXT_PUBLIC values read from both live processes and equal. Prisma generation, P19 compatibility and schema3017 fields passed; activation generated; prebuild running at00:41Z. Cache archived to M4Ext with SHA302d195a5bb26234105aa8fa9e1e0613e96381dc730babd65b3ed037e227770f before removal. No PACK_OK, landing or browser acceptance yet. Source includes Drive/sync/planner only; editor3492 and capacity3491 remain separate. Evidence: checkpoint-3492/l115d-build-start.json, l115d-preflight.log, build-cache-archive.json; A2 /home/ubuntu/l115d-chain.log.
2026-09-11T01:15:45.944Z · coordinator
LANDING115D-PG3496:WEB-593 2026-09-11T01:15:45.387983+00:00
l115d deployed 2026-09-11 01:13 UTC: source ba4aa1c219625df95035cd1eb9653d9a6b0c4903, artifact SHA256 446245699e4e28f03336d7726d9fa4dc422f8a8e4b7597d572b29bd1e0eb57f0. Signature positive/negative checks passed. Both backends paidReady=true/enforce_ready with exact source; four service path gate passed; live chat HTTP200; distance route HTTP200 and asks for missing origin. Browser opened one of58 documents, editor surfaces present, zero new errors after click; initial load retained two aborted cookies RSC requests and admin/agents HTTP403 plus console error. SIP readiness200 and LEASE_LOST0. Ships wash3490 Drive/sync/planner; editor3495 remains separate. Historical351/135 document repair and full feature-specific provider/browser QA remain open. Rollback units: /home/ubuntu/prod/shared/pre-l115d-units; previous release l115c. Evidence: checkpoint-3495/l115d-{dryrun,flip,live-postqa,editor-postqa}.log.
2026-09-11T01:31:25.888Z · coordinator
SHIFT3502:WEB-593 2026-09-11T01:31:25.453329+00:00
3504 RUNNING A1 from exact deployed wash sourceba4aa1c219625df95035cd1eb9653d9a6b0c4903. Implement bounded separate inventory/apply tool with immutable manifest, immediate revision/digest checks and existing queue idempotency; original planner stays dry-run only. Actual historical351/135 documents have NOT been repaired. Guard/input hashes/BASE verified; shift-3502/3504-receipt.json.
2026-09-11T01:50:34.852Z · coordinator
CHECKPOINT3505:WEB-593 2026-09-11T01:50:34.398161+00:00
3504 bounded exporter/executor delivered; author reports12/12 focused and1/1 disposable PG checks. Independent acceptance, fresh live inventory and actual repair remain pending. No historical351/135 population repaired. Coordinator verified and preserved bundle/head/clean/SHA: 78a4f89c35ffa431d725f6c8f6de7f37a36ad2fa / 97d2b04f825ba9c04fc3f3a994df37db57eb2be084cfae4cdbe6d2a883d938a7. Evidence archive members=24; SHA256=5bbc781e39137b50c36ec296120a636f4f71dfc6143be18718994f0f696153e5. Receipts: checkpoint-3505/snapshot.json and snapshot-3504.json.
2026-09-11T02:01:10.786Z · coordinator
LANDING115E:WEB-593 2026-09-11T02:01:10.157440+00:00
l115e landed 2026-09-11 01:59 UTC. Source bb43b5fe20398309b3b373df3c00fa1d3040397d; artifact SHA256 156323c694a6854b6207ee2b6dcd101abba99b67c384cc6282e10d098c9eb357. Both live backends independently confirmed exact source, ready=true and paidReady=true/enforce_ready at 02:00 UTC. Build/signature/negative signature/dryrun/four-unit path gates passed; chat and distance HTTP200. Browser opened one of58 fixture documents, editor surfaces present, zero new errors after opening. Initial load recorded three aborted RSC requests and admin/agents403 plus console error; clean console not claimed. SIP readiness200, LEASE_LOST0, no SIP restart. Ships accepted editor3495 atop wash3490. Feature-specific browser3503, real provider OAuth, historical351/135 repair, capacity and Security26 remain open. Rollback: prod/shared/pre-l115e-units to l115d. Evidence: release-l115e/l115e-{dryrun,flip,live-postqa,editor-postqa}.log.
2026-09-11T02:16:03.732Z · coordinator
SHIFT3506:WEB-593 2026-09-11T02:16:03.175385+00:00
3508 RUNNING A1 independent functional acceptance of bounded historical repair executor at78a4f89c35ffa431d725f6c8f6de7f37a36ad2fa, including commit/receipt crash recovery and real-worker compatibility. Coordinator reproduced exporter CLI failure: --output /tmp/web593-inventory-probe.json -> unknown flag path, exit1 before database use.3511 RUNNING Neo repairs parser only from exact same BASE; SQL/admission unchanged. Historical351/135 documents remain unrepaired; source GO not execution. Evidence: shift-3506, shift-3511, checkpoint-3505; process observation 2026-09-11T02:16:01.981638+00:00
2026-09-11T02:22:41.554Z · coordinator
CHECKPOINT3511:WEB-593 2026-09-11T02:22:40.994630+00:00
3511 collected and verified: HEADc22fafca9fdc202e632ca6fba2b66c48a802f9e0; SHA dae4491db8dc5a1a9ae088a2f4a734389a0283c73bb5c232de55ac254a7e5f02. Report records CLI/parser subprocess tests9/9, separated/equal forms, malformed input refusal before DB import. SQL/executor/product unchanged. Raw archive fully read. Independent executor3508 acceptance not yet collected; historical351/135 repair not executed. Evidence checkpoint-3511/snapshot.json.
2026-09-12T08:51:16.687Z · coordinator
RESUME20260912:WEB-593 2026-09-12T08:51:16.229899+00:00
3508 NO-GO collected and verified: QA HEADe5770b4e61178ceeeaf34c43000266484371d990, SHA272b2a0b0df6f92eac43a469650d123ea129c77bbde4a2cd7fbf2e562d81af20; raw archive fully read,17 files. Confirmed worker ignores repair: chunks-only marker and truncated receipt prevents resume.3514 fixes both and integrates3511 parser repair. Historical apply remains blocked; no351/135 repair claimed. Dispatch inputs verified; observed RUNNING. Evidence resume-20260912/3514-receipt.json; observation.json; collected.json.
2026-09-12T08:54:45.748Z · coordinator
SHIFT-DEADLINE-20260911:WEB-593 2026-09-12T08:54:05.862472+00:00
Восьмичасовой срок истёк.3514 исправляет дефекты executor из3508 NO-GO; работа жива с ростом лога, полной сдачи нет. Исторический ремонт не запускался, закрытие не заявляется. Evidence: resume-20260912/deadline-result.json.
2026-09-12T10:04:12.077Z · coordinator
COORD-TICK-20260912T1003:WEB-593
3514 выдала GO; это авторский результат. Независимая приёмка и историческое восстановление ещё не завершены; 3518 пока без подтверждённого запуска.
2026-09-12T11:30:06.870Z · coordinator
WASH3518-FIXTURE3515-20260912:WEB-593 2026-09-12T11:30:06.235967+00:00
Independent acceptance3518 dispatched on A1, gpt-5.6-luna high. Pickup observed 2026-09-12T10:50:13Z: PID689587; dispatcher start10:49:19Z; own tree HEAD3d0351f958002f14a4c7b68dd099469ec86b1402. Brief guard, input SHA256, bundle/base and duplicate checks passed; preflight disk29508216KiB. Author3514 clean HEAD and bundle SHA25638da5a038ed56b9f8be968114d8ffe824e882fcbc2950c1f5f876b2d69097ba7 verified; author DONE absent after dispatcher cleanup. QA-only full disposable PG/worker resolver/atomic receipt recovery/identity-conflict negatives assigned; independent verdict pending at pickup. Historical apply and production prohibited. Receipt: /Users/annakorin/nc-ops-scripts/resume-20260912/3518-receipt.json; guarded brief: /Users/annakorin/nc-ops-scripts/resume-20260912/3518-acc-repair-historical-brief.md.
2026-09-12T14:04:51.407Z · coordinator
TICK-FIVE-HOSTS-20260912:WEB-593
3518 независимо принята и забрана. Exact QA HEAD 99a2142fde0557f2a7528493775cbf2d3dc34032, BASE3514 3d0351f958002f14a4c7b68dd099469ec86b1402. Clean tree, bundle verify и все54 файла hash PASS. Raw focused42/42 и full-PG2/2,198 migrations. Actual historical repair остаётся открытым. Receipt Intel nc-ops-scripts/tick-20260912-recovery/3518-collection-receipt.json.
2026-09-12T14:52:13.205Z · coordinator
TICK-3522-3527-20260912:WEB-593
3524 получена: exact985453fe,93 файла SHA/bundle/HEAD сверены, merge11 accepted paths byte-identical3518,0 conflicts. Local planner6/6; full offline6pass/5 module-resolution failures. Inherited3518 focused42/42 и PG2/2 при exact dependency/schema proof. Changed indexing worker ещё требует сборки/посадки; исторический apply не выполнен.
2026-09-12T16:48:05.995Z · coordinator
COORD-TICK-20260912T1648:WEB-593
3535 dispatched from exact Security candidate92f7ae807 with verified historical3524 HEAD985453fefcf21dc0f754c568f16dca4f59da52d6 and bundleSHA0a9cc61738db4b90b796b87a994d50135220ce5bd82a8d71a803a5ece608960d. Guard,6 input hashes,both exact git objects and no-duplicate/disk checks PASS. A1 dispatcher start16:40:31UTC, runnerPID2062697, routecodex/modelgpt-5.6-luna high; log confirms source inspection. Task: separate next-line merge, accepted11-path boundary plus Security byte parity, actual focused/full migrated private-PG201 checks with private dependency copy and no release build. Current l115f build is frozen and unchanged. No historical apply/reindex performed;351/135 is historical observation, not repaired count. Receipt tick-20260912-recovery/3535-dispatch-receipt.json.
2026-09-12T18:22:02.384Z · coordinator
SHIFT-STOP-20260912:R2:WEB-593
ОСТАНОВКА ПО РЕШЕНИЮ ВЛАДЕЛЬЦА — 3535 сохранена для следующей линии.
Сдача3535 HEAD73764e52987973d69e1c5ada690ddc1f90ec400d, исходник92f7ae807; обе parent ancestry, clean и bundle проверены. Получен архив23422691bytes SHA9eb4119c15c2ed02268be4b288932ce50cc3fda73c6e9370731b60ec48dcff4a; member hashes совпали. Author GO относится к source/QA. Это отдельная следующая линия и не входит в установленную92f7ae807.
Историческое число351 документов/135 владельца не является текущим замером или числом исправленных. Historical apply/reindex на проде не выполнен.
Следующий шаг после возобновления: прочитать полный3535 REPORT и collection-receipt/raw evidence, проверить exact scope11paths и security parity; собрать worker/release канонической цепочкой, безопасный canary, then historical apply с before/after chunks/search evidence. Source-only classifier SEC063/WEB612 не восстанавливает отсутствующие chunks.
Пути: /Users/annakorin/nc-ops-scripts/release-l115f/3535-collected/; /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/3535-dispatch-receipt.json. Бандл и отчёты сохраняются. Новая работа сейчас не запускается.
Billing finding: [[WEB-650]].
2026-09-12T18:34:31.697Z · coordinator
SHIFT-STOP-20260912:DELIVERED
Смена остановлена. Полный хендофф: Intel /Users/annakorin/Downloads/NC-HANDOFF-20260912-STOP.md; проверенные копии: M1 /Users/poolpooly/audit/handoffs/shift-stop-20260912/ и M4Ext /Volumes/M4Ext/ops/handoffs/shift-stop-20260912/. SHA256 документа fe8242ce219bea7c492ea53ae053fc7066caa3ad8849fbeeb087857eb61da3a7. Краткий итог: https://bugs.wool2.online/labs/shift-handoff-20260912.html (HTTP200, SHA readback). 55 существующих карточек обновлены, WEB-650 создана. Новые работы не запускать до возобновления владельцем. Тики, два агента и Telegram watcher этой смены остановлены; production продолжает обслуживать.
2026-09-12T22:17:55.158Z · coordinator
[12.09 22:17Z координатор] МОЙКА 12.09 (3569-wash-g4-compliance-process): P1 351 документ без кусков. Фикс «предотвратить новые» в дереве (scripts/repair-live-docs-without-chunks.ts, a03c7756), но исторический backfill 351 документа не выполнен — владелец остановил смену, волна 3535 ждёт следующей линии. Реальный остаток есть: KEEP.
Остаток: Исторический backfill 351 документа НЕ выполнен (владелец остановил смену); Worker rebuild / canary / historical apply — NOT_RUN; Волна 3535 сохранена для следующей линии
Отчёт: /Users/milamarty/waves/3569WASH*-REPORT.md (M4), копия nc-ops-scripts/shift-20260912-resume/wash-20260912/collected/. Проверка по исходнику прода l115g (9c8a9762).
2026-09-12T23:49:06.688Z · coordinator
[12.09 23:49Z координатор] Координатор 23:48Z: линия l115h посажена (source f0777c4d, standalone включает workers/process-source-indexing-queue.js — «сборка воркера» из остатка тикета закрыта самой посадкой; nc-a1-indexing юнит на новом релизе). Остаток: исторический backfill 351 документов без чанков (материалы 3535) — отдельная волна после проверки индексации на l115h.
2026-09-13T00:23:07.636Z · coordinator
[13.09 00:23Z координатор] ВЗЯТО В РАБОТУ 13.09 00:22Z: волна 3603-index-backfill-runbook (DeepSeek, M4, исходник прода l115h) — исполнимый ранбук исторического backfill: как найти 351 документ без чанков и разложить по причинам, реальный вход постановки в очередь, canary 5–10 → ступени, слежение, критерий успеха, откат, оценка стоимости эмбеддингов и взаимодействие со spend-потолком. SQL и команды готовит волна, выполняет координатор. Сборка воркера уже на проде (l115h содержит workers/process-source-indexing-queue.js, юнит nc-a1-indexing на новом релизе).
2026-09-13T06:11:25.686Z · coordinator
[13.09 06:11Z координатор] Волна 3603 (DeepSeek, M4) — ранбук исторического backfill готов: как найти 351 документ без чанков и разложить по причинам, реальный вход постановки в очередь, canary 5–10 документов → ступени, слежение, критерий успеха, откат, оценка стоимости эмбеддингов и связь со spend-потолком. Файлы: BACKFILL-RUNBOOK-WEB593.md, SQL-DIAGNOSTICS.sql, COST-ESTIMATE.md, OPEN-QUESTIONS.md (копия в shift-20260912-resume/3603/collected). Выполняю после посадки l115i.
2026-09-13T09:35:26.117Z · coordinator
[13.09 09:35Z координатор] ОБОГАЩЕНИЕ ЭПИКА ПОД НУЛЕВОГО АГЕНТА (канон WEB-449, волна 3642): тело переписано — суть, где мы сейчас с числами, устройство системы, хронология с волнами и линиями, карта документов с полными путями по машинам, нумерованный остаток с указанием кто делает, критерий закрытия. Прежний текст сохранён в конце как «История (до 13.09)»; бэкап /tmp/epicbody-backup-WEB-593.md на M1.
2026-09-13T10:34:58.574Z · coordinator
[13.09 10:34Z координатор] Волна 3649 превратила ранбук в готовые файлы для исполнения: диагностика одним запросом, канарейка на пять документов, порционный прогон с паузой и ограничением параллелизма, проверка «документ реально ищется». В каждом файле стоит явная сверка со схемой базы перед запуском, чтобы скрипт не поехал по угаданным именам колонок. Есть оценка стоимости порции. Backfill выполняет координатор.
2026-09-13T12:10:25.784Z · coordinator
[13.09 12:10Z координатор] Диагностика выполнена на БОЕВОЙ базе, только чтение (исправленные скрипты волны 3675, запуск координатора).
Настоящие числа на сегодня, а не из истории карточки:
- живых документов всего 1679;
- пострадавших 204, из них 198 вообще без единого куска и 6 помечены как обработанные, но кусков у них ноль;
- у всех 204 есть владелец.
В карточке значилось 351 — это инвентарь на момент её создания. Сегодня 204.
Отдельно видно, что большая часть пострадавших — файлы, импортированные с Google Drive, и у них статус обработки «неудача». То есть это не просто «не проиндексировалось», а упавшая обработка, и её причину стоит назвать до массового перезапуска.
Важное, что дала репетиция 3675: скрипты волны 3649 обращались к таблице очереди, которой в схеме НЕТ — очередь живёт как поле в метаданных источника. На проде такой скрипт упал бы или, хуже, сделал не то. Исправлено и доказано на слепке до запуска.
Дальше по порядку: канарейка на пяти документах, потом порциями, с проверкой после каждой.
2026-09-13T13:37:33.856Z · coordinator
[13.09 13:37Z координатор] ПРИЧИНА НАЙДЕНА, И ОНА ОДНА НА ВСЕХ. Запрос по боевой базе показал: у всех упавших документов один и тот же текст ошибки — «AI features are unavailable: the spending safeguards are not fully configured», и все они упали 5 сентября в течение полутора минут. Это не сто разных проблем, это одно событие: в тот момент защита расходов не была настроена, и обработка честно отказала.
Условие с тех пор снято — расходный контур настроен и работает. Значит документы можно просто переиндексировать.
Что сделано дальше, по порядку и с проверкой после каждого шага:
1. Канарейка на пяти документах: прошла, пострадавших стало 204 → 199.
2. Порция с ограничением параллелизма и паузой: первая волна разобрана воркером, пострадавших 199 → 195.
3. На второй волне один документ снова упал на построении вектора — скрипт ОСТАНОВИЛСЯ сам, как и задумано, и не стал заявлять его исправленным. Это правильное поведение: один отказ останавливает порцию.
Итого исправлено 9 документов, осталось 195. Причина падения того одного разбирается отдельно: в базе его свежая ошибка уже не видна, значит он ушёл обратно в общий список.
Отдельно: волна 3708 построила карту различения по восьми возможным причинам, и боевая проверка показала, что сегодня реально встречается только одна из них. Карта остаётся полезной на будущее.
2026-09-13T14:15:12.403Z · coordinator
[13.09 14:15Z координатор] НАЙДЕНА НАСТОЯЩАЯ ПРИЧИНА, ПОЧЕМУ ПЕРЕЗАПУСК НЕ ПОМОГАЕТ. Это важнее вчерашнего вывода и меняет весь план.
Цепочка целиком:
1. У всех 100 упавших документов НЕТ ЗАПИСИ ОБ ИЗВЛЕЧЕНИИ ТЕКСТА вообще — поле извлечения пустое у всех ста, проверено запросом.
2. Рабочий индексации по коду пропускает любой источник, у которого извлечение не в состоянии «готово» или «частично». Поэтому он честно печатает «total=0» даже тогда, когда документы лежат в очереди со статусом «ждёт».
3. Значит переиндексация одна, без извлечения, НЕ МОЖЕТ их починить в принципе. Сколько ни ставь в очередь — рабочий их не увидит.
Что это значит для плана: ранбуки волн 3649 и 3675 покрывали только индексацию. Нужен предшествующий шаг — извлечение текста. Рабочий извлечения на проде есть и отрабатывает, но сейчас видит ноль задач: у этих документов нет и очереди на извлечение.
Отдельно признаю свою ошибку: я попробовал пометить 20 документов в очередь ПРЯМЫМ запросом, а не скриптом. Рабочий их не увидел, потому что у метки не было полей, которые ставит штатный путь. Двадцать документов зависли невидимыми. Обнаружил, откатил в прежнее состояние, дальше работаю только штатным скриптом.
Что подтверждено и остаётся верным: те девять документов, что уже вернулись в поиск, вернулись по-настоящему — это были случаи с готовым извлечением.
2026-09-13T14:51:21.886Z · coordinator
[13.09 14:51Z координатор] ЦЕПОЧКА ЗАМКНУЛАСЬ, И ОНА УПИРАЕТСЯ В ОДНУ ФРАЗУ ВЛАДЕЛЬЦА. Волна 3718 (GO) довела разбор до конца.
Что выяснено по коду и проверено на одноразовой базе:
1. «Поставить в очередь чтения» — это ДВЕ записи сразу: пометка в карточке документа и отдельная строка в таблице заданий, которую читает рабочий. Моя прошлая попытка написала только первую, поэтому рабочий её не видел. Скрипт волны делает обе, и его результат по форме совпадает с тем, что пишет обычная загрузка файла.
2. Но для документов, пришедших с Google Drive, у нас НИГДЕ НЕ СОХРАНЕНЫ САМИ БАЙТЫ ФАЙЛА — ни разу. Значит ставить их в очередь чтения бессмысленно: читать физически нечего, рабочий сразу скажет «файл потерян». Скрипт это понимает и честно отказывается создавать заявку, которую некому выполнить.
3. Единственный настоящий путь для них — заново скачать файлы из Google Drive отдельным инструментом.
А скачивание из Drive у нас сейчас закрыто НАШИМ ЖЕ заслоном по статье 28: реестр подтверждённых договоров пуст (см. WEB-573).
Итого: сто документов вернутся в поиск ровно тогда, когда владелец подтвердит условия обработки данных с Google, я внесу и подпишу запись в реестр, и мы перекачаем файлы. Никакой другой путь их не чинит.
Инструмент постановки в очередь чтения готов и проверен — он пригодится для документов, у которых байты сохранены.
2026-09-13T14:56:25.223Z · coordinator
[13.09 14:56Z координатор] Разделил пострадавших по признаку «есть ли у нас сами байты файла» — это то, что решает, чинится документ нашими силами или только перекачкой.
Из документов без кусков: у 229 байты ЕСТЬ, у 110 байтов НЕТ вовсе.
Те, у кого байты есть, чинятся нашим инструментом постановки в очередь чтения и дальше индексацией — это уже работает, счёт пострадавших за сегодня идёт вниз (204 → 185 по диагностическому запросу).
Те, у кого байтов нет — это и есть импорт с Google Drive. Их не чинит ничто, кроме повторного скачивания из Drive, а оно закрыто нашим заслоном по статье 28 до подтверждения владельца.
Оговорка по цифрам: мой запрос считает по связи кусков с документом и даёт 339 всего, диагностический запрос волны считает строже и даёт 185. Разницу не сглаживаю — при следующем прогоне сверю определения и приведу к одному числу.
2026-09-13T15:16:29.166Z · coordinator
[13.09 15:16Z координатор] ПОДТВЕРЖДЕНИЕ ВЛАДЕЛЬЦА ПОЛУЧЕНО 13.09 15:14Z: «про Google стандартные условия я тебе давно подтвердил. Подтверждаю.»
Это снимает блокировку с трёх мест сразу: подключение Google Drive, возврат ста десяти документов в поиск (их нечем вернуть, кроме перекачки из Drive) и закрытие эпика защиты данных в части реестра обработчиков.
Что делаю дальше сам, по шагам:
1. Реестр подтверждений подписывается ключом, которого в проекте ЕЩЁ НЕТ: подпись считается кодом обмена с секретом, а сам секрет нигде не заведён. Завожу ключ, кладу в защищённое хранилище координатора и в окружение прода, потому что без него приложение не сможет проверить запись.
2. Беру ссылку на стандартные условия обработки данных Google и её отпечаток — запись пиннится к конкретной версии документа, чтобы подмена документа по тому же адресу не прошла молча.
3. Штатным инструментом проекта добавляю подписанную запись в реестр, в дереве кандидата следующей линии, а не правкой на проде.
4. Собираю и сажаю линию, после чего кнопка подключения Диска работает, и я запускаю перекачку документов.
Ничего из этого не делается руками в обход инструмента: прошлый урок про прямую правку базы записан.
2026-09-13T15:33:47.870Z · coordinator
[13.09 15:33Z координатор] Владелец уточнил 15:31Z: Диск подключается ЛИЧНЫМ гугл-аккаунтом, Workspace нет, компании нет.
Это меняет ответ, и я не стану вписывать запись в реестр. Причина прямая: реестр статьи 28 — это реестр договоров с ОБРАБОТЧИКАМИ, то есть теми, кому мы передаём данные наших пользователей на обработку от своего имени. С личным гугл-аккаунтом такого договора не существует в природе: там действуют обычные пользовательские условия, а не соглашение об обработке. Запись «договор подтверждён» была бы неправдой в юридическом реестре, а такие записи читают всерьёз.
Возникло подозрение, которое надо проверить кодом, а не на ощущениях: возможно, заслон вообще неправильно относит подключение Диска к обработчикам. Когда пользователь подключает СВОЁ хранилище своим согласием и мы читаем ЕГО файлы, Google в этом потоке — не наш субподрядчик по чужим данным, а собственное хранилище пользователя. Это другая правовая роль, и код обязан их различать.
Поставлена волна 3726: перечислить по коду, что именно уходит к Google и чьи это данные, показать строку, где заслон принимает решение, и выбрать один из трёх исходов — подключение Диска законно освобождается от проверки, либо это всё же поток обработчика и тогда личный аккаунт его не закрывает никак, либо нужен третий, более узкий вид записи. Реестр волне трогать запрещено.
Следствие для WEB-593: сто десять документов с Диска остаются невозвратимыми, пока это не решено — им нужна перекачка, а она за этим же заслоном.
2026-09-13T23:52:08.220Z · coordinator
[13.09 23:52Z координатор] ПОСТ-QA ЛИНИИ l115l (волна 3755, VERDICT=GO). Пять тикетов последней посадки, все пять — `PROVEN-OFFLINE`, с прогонами на ДВУХ коммитах: до фикса и на коммите, который сейчас в проде (`1628b697d`).

| Тикет | Тест | Коммит | Результат |
|---|---|---|---|
| WEB-188 | `videoRunPolling.test.ts` | `4a6e99ed4` (до фикса) | **16/17, одно НАСТОЯЩЕЕ падение проверки** |
| WEB-188 | тот же | `1628b697d` (прод) | 17/17 |
| WEB-195 | `web195TranscriptOnlyPdfQuality.test.ts` | `4a6e99ed4` (до фикса) | **4/6, два НАСТОЯЩИХ падения** |
| WEB-195 | тот же | `1628b697d` (прод) | 6/6 |
| WEB-658 | `source-indexing-worker-lease-hiccup.test.ts` | `1628b697d` | 5/5 |
| WEB-658 | `web658WorkerSurvivesLeaseHiccup.test.ts` | `1628b697d` | 6/6, включая красную форму и 3 негатива |
| WEB-659 | `activePassiveLease.web659.real-db.test.ts` (настоящая Postgres) | `1628b697d` | 3/3 — красный, зелёный, негатив |
| WEB-659 | `web659SharedOwnerAdmission.test.ts` | `1628b697d` | 1/1 |

Ключевое отличие от прошлого раза: у WEB-188 и WEB-195 красное до фикса — это **настоящие падения проверок**, а не ошибка сборки и не отсутствие модуля. Именно на этом отличии независимая приёмка 3743 в прошлый раз вернула мне три закрытия, и теперь оно соблюдено.

**Закрываю: WEB-188 и WEB-195** — доказательство полное, пары настоящие, оба дефекта клиентские/отчётные и живой пробы сверх этого не требуют.

**НЕ закрываю пока: WEB-658 и WEB-659.** Их офлайн-доказательства сильные (красная форма плюс негативы, для WEB-659 — на настоящей Postgres), но у обоих есть живая проверка, которая вот-вот будет: стенд ёмкости сейчас пересобирается под коммит прода, и группа `realtime` там обязана позеленеть. Если позеленеет — закрою WEB-659 живым наблюдением, а не только тестами. Если нет — значит причина не та, что мы думаем, и это важнее любого зелёного теста.

**WEB-593 остаётся открытым по существу:** доказан ИНСТРУМЕНТ повторной загрузки, а не восстановление документов. Тикет закроется, когда 185 застрявших документов реально вернутся в индекс.
2026-09-14T01:51:36.883Z · coordinator
[14.09 01:51Z координатор] ПОДГОТОВКА БОЕВОГО ВОССТАНОВЛЕНИЯ ГОТОВА (волна 3760, VERDICT=GO). Теперь у координатора есть всё, чтобы запускать с открытыми глазами, а не наугад.

**ДВА ЗАПРОСА, оба только на чтение:** предполётная классификация (запустить ДО любого `--apply`, даёт раскладку 185 документов по классам состояния — это число обязано совпасть с первой строкой сухого прогона инструмента) и послеполётная проверка (через N часов ПОСЛЕ, когда воркер успел бы разобрать очередь).

**СТОИМОСТЬ В ЦИФРАХ, а не «недорого»:** инструмент делает ровно **2 вызова Drive API на документ** (`drive.files.get` за метаданными и загрузка тела). Если все 185 относятся к классу «нет байтов и нет извлечения» — это **370 вызовов Drive API**. По деньгам: **даже в пессимистичном сценарии «все 185 — большие книги» выходит меньше двух долларов.**

**ВРЕМЯ, тоже цифрой:** сам `--apply` строго последовательный (цикл по идентификаторам, без параллелизма), а дальше очередь разбирает воркер при потолке полос 2. Итог: **ориентировочно 6–13 часов фоновой обработки** после того, как все 185 встанут в очередь. То есть это не «запустил и через десять минут готово», и планировать надо соответственно.

**РИСКИ И ОТВЕТ НА НИХ:**
1. Падение воркера посреди прогона — каждый источник обрабатывается независимо и до конца; упавший не тянет за собой остальные.
2. Встроенного возобновления нет — **и не нужно**: механизм идемпотентен по построению, повторный запуск просто пропускает вылеченное.
3. Повторный запуск на уже вылеченном документе безопасен — проверено кодом и тестом.
4. Прогонять частями можно: инструмент принимает список идентификаторов, так что 185 разбиваются на порции по желанию.
5. Полосы воркера этому инструменту не мешают — `--apply` последователен сам по себе.

**ЧТО ЭТО ЗНАЧИТ ДЛЯ ТИКЕТА:** инструмент готов, цена известна, время известно, риски названы и ограничены. Осталось одно — боевой прогон на проде, и это работа координатора, а не волны. Запускаю сухой прогон на реальном списке, сверяю раскладку с предполётным запросом, и только потом `--apply`, порциями.
2026-09-14T02:19:13.046Z · coordinator
[14.09 02:19Z координатор] **ПРИЁМКА ОСТАНОВИЛА ЗАПУСК — И ЭТО СПАСЛО ПОЛЬЗОВАТЕЛЬСКИЕ ДАННЫЕ.** Волна 3761 (независимая приёмка плана боевого восстановления) вернула **`VERDICT=NO-GO`**. На проде инструмент не запускался ни разу.

**Что именно она нашла.** Инструмент `scripts/ingest/redownload-drive-sources.ts` берёт снимок документа, потом идёт в Google Drive (это секунды), потом пишет тело обратно. Между снимком и записью нет НИКАКОЙ защиты. Если в эту паузу пользователь дописал документ — его правка будет затёрта телом из Drive. Тем же способом теряются изменения в `metadata`: между `readMetadata` и `writeMetadata` весь объект переписывается целиком.

Мы собирались запустить это на **185 документах пользователей** на **6–13 часов**. Код можно откатить, испорченный документ — нет.

**ПЯТЬ БЛОКЕРОВ ДО ЛЮБОГО `--apply`:**

1. **Публикация тела — условная атомарная операция (CAS).** Снимок до Drive-fetch обязан нести `contentRevision` и дайджест/признак тела; запись проходит, только если строка та же и тело не изменилось. `0 rows` = отказ этой строки с отдельным классом «конфликт, пропущен», а не тихое продолжение очереди. Тот же конфликт обязан безопасно обрабатываться воркером, публикующим из устаревшего снимка.
2. **`metadata` не переписывать целиком** — атомарный patch с проверкой ревизии либо повторное чтение и merge под тем же замком.
3. **Предполётный SQL пропускает `s.metadata->>'deletedAt' IS NULL`.** Каноническая политика чтения `src/lib/rag/sourceReadPolicyServer.ts:93-109` этот tombstone проверяет, и `readProtectedSource` из-за него может вернуть `null`, тогда как SQL покажет источник кандидатом. **Следствие: два разных счёта — из запроса и из сухого прогона — и уверенность, что «всё сошлось».** Плюс «сводка по классам» в плане была не исполняемым запросом, а комментарием с псевдоподстановкой `... тот же CASE ...` — ручное дублирование логики и ещё одна точка расхождения.
4. **Нет манифеста непересекающихся ID.** Дедупликация действует только внутри одного запуска; два процесса или две порции могут взять один документ дважды.
5. **Оценки выданы за границы.** `6–13 ч` и `$1.85` — сценарии, а не верхние пределы. Нужны `--max-usd`, потолок обращений к Drive, стоп-гейты по времени на прогон и на кампанию.

**ЧТО ПРИЁМКА ПОДТВЕРДИЛА (не всё было плохо):**
- **Предполётный SQL действительно read-only** — CTE `VALUES`, `SELECT`, `LEFT JOIN`, коррелированный `count(*)`; ни `INSERT`, ни `UPDATE`, ни `DELETE`, ни `FOR UPDATE`.
- **`185 × 2 = 370` обращений к Drive — арифметика верна** для номинального сценария: `drive.files.get` за метаданными (`redownload-drive-sources.ts:611-616`) плюс ОДИН `files.export` для Google-native или `files.get(alt=media)` для остальных (`driveFileBody.ts:198-206,256-269`). Постраничной загрузки в этом пути нет. **Но это не верхняя граница:** retry-обёртки в инструменте нет, ошибка даёт отказ, повторный запуск снова делает вызовы; метаданные и тело не связаны условием по ETag/modifiedTime.
- **Классификация в CASE в целом повторяет `diagnoseSource()`** (`redownload-drive-sources.ts:124-180`): нет живой строки → `not-found`; непустое тело + статус `pending/running/done` → `already-ok`; непустое тело с иным статусом → `needs-requeue`; пусто и не Drive → `unsupported-non-drive-source`; пусто и Drive → `needs-redownload`.
- **Арифметика времени внутри своей модели верна:** `185 × 250 с = 12.85 ч` на одной полосе, `6.42 ч` на двух. **Но `250 с/документ` — наблюдательная оценка волны 3752, не замер на этих 185 документах.** Плюс не учтены последовательная фаза Drive (~2–12 мин), polling, checkpoint-транзакции, переменное число чанков, backoff провайдера и крупные документы. Честного фиксированного числа часов по имеющимся данным назвать нельзя.
- **Потолок полос = 2** (`src/lib/ingest/sourceIndexingQueue.ts:204-207`), но **по умолчанию 1** — две полосы надо включать явно и убедиться, что они реально держатся.
- **Локальный `EXPLAIN` не доказывает масштаб:** на тестовой базе (`Source=10`) план выбрал `Seq Scan`. Нужен `EXPLAIN (ANALYZE, BUFFERS)` на prod-like клоне и операционный statement timeout. `length(btrim(s.content))` читает тело каждого найденного источника — на больших телах это заметная нагрузка.

**ДАЛЬШЕ.** Волна **3763** (A1) закрывает все пять блокеров, каждый с доказательством на одноразовой Postgres: красное ДО фикса, зелёное ПОСЛЕ, сценарии гонки — пользователь дописал тело, пользователь изменил метаданные, два процесса взяли пересекающиеся порции, превышен `--max-usd`. Красное от ошибки сборки доказательством не считается.

**СОСТОЯНИЕ ТИКЕТА.** Инструмент доказан, восстановление 185 документов — НЕТ. Тикет остаётся открытым по существу до того, как документы действительно восстановлены на проде.

Эволюция: 3726 (ст.28, Drive отделён от пути оператора — разблокировал этот тикет) → 3739 (инструмент, в l115l) → 3760 (сухой прогон и план) → **3761 (приёмка, NO-GO)** → 3763 (закрытие блокеров) → приёмка 3763 → боевой прогон координатором.
2026-09-14T03:11:59.176Z · coordinator
[14.09 03:11Z координатор] **ПЯТЬ БЛОКЕРОВ ПРИЁМКИ ЗАКРЫТЫ. Волна 3763, `VERDICT=GO`, коммит `6a2345444`.**

Каждый пункт с доказательством на **одноразовой настоящей PostgreSQL**, а не на фикстурах.

1. **Публикация тела — условная атомарная операция (CAS).** Снимок до обращения к Drive несёт `contentRevision` и признак тела; запись проходит, только если строка та же и тело не изменилось. `0 rows` = отказ этой строки отдельным классом, очередь не идёт дальше как ни в чём не бывало.
2. **`metadata` — атомарный patch** вместо полной перезаписи; правки пользователя между чтением и записью больше не теряются.
3. **Предполётный SQL** получил канонический tombstone (`s.metadata->>'deletedAt' IS NULL`) и **настоящий исполняемый summary-запрос** вместо комментария с псевдоподстановкой.
4. **Манифест непересекающихся идентификаторов** с контрольной суммой; `build-restore-manifest.ts` пишет файл при нуле пересечений и **завершается с кодом 1 БЕЗ записи файла** при пересечении.
5. **Ограничители расхода и времени** — `--max-usd`, потолок обращений к Drive, стоп-гейты по времени.

**Проверки (всё синхронно, с кодами возврата):**
- одноразовая PostgreSQL `127.0.0.1:5793`, весь набор миграций линии применён, `RC=0`;
- основной набор тестов инструмента и патчера метаданных: **87 pass, 0 fail**;
- **гонки на настоящем Postgres: 7 pass**, включая красное-до/зелёное-после для сценариев «пользователь дописал тело», «пользователь изменил метаданные» и «превышен `--max-usd`»;
- негативы Drive-тела: 10 pass, 0 fail; возобновление порционной индексации и роут: 14 pass, 0 fail;
- CLI без БД и без сети: `--help`, манифест, предполётный запрос с исправленной сериализацией BigInt;
- `tsc` по области волны: **ноль ошибок в изменённых и новых файлах**.

**Честно названные предсуществующие падения** (воспроизведены на СВЕЖЕМ чекауте BASE **без единого изменения волны**, то есть не её работа): 2 в `driveBodyServerSideRealPg.test.ts` и 1 в `processDocumentChunkPersistence.test.ts`. Это ровно то поведение, которого мы требуем: красное от чужой поломки не выдаётся за своё доказательство и не прячется.

**Остаточный узкий риск, названный самой волной:** два писателя, одновременно меняющие РОВНО поле `processing.indexing` одного источника (не через манифест координатора) — на этом одном поле побеждает последний. Через штатный путь с манифестом такой случай невозможен.

**Сдача:** `refs/waves/3763/wave/3763-index-restore-blockers` = `6a2345444`.

**Дальше:** независимая приёмка 3763 (та же строгость, что и 3761 — она уже один раз спасла 185 документов), затем предполётный запрос на проде, затем боевой прогон порциями по манифесту. На проде инструмент по-прежнему не запускался.
2026-09-14T03:28:09.785Z · coordinator
[14.09 03:28Z координатор] # ⚠️ ПРИЁМКА 3774: `VERDICT=NO-GO`. Боевой запуск снова закрыт — и снова по делу

Второй раз подряд независимая приёмка останавливает эту операцию и второй раз находит реальный путь к потере пользовательского документа. Волна 3763 закрыла пять блокеров и вернула GO; приёмка написала **свои** пробники и нашла, что защита обходится.

**ПРЯМОЙ ОТВЕТ НА ГЛАВНЫЙ ВОПРОС: да, пользовательский текст всё ещё может быть потерян.** Два независимых пути.

**Путь 1 — воркер публикует мимо защиты.** `processDocumentRuntime.ts:405-409` вызывает публикацию тела **без `expectedGeneration`**, то есть безусловным UPDATE. Минимальная последовательность: воркер взял тело и ревизию → пользователь сохранил новую версию → воркер публикует → новая версия заменена старой, ответ `published=true`. Защита в функции публикации есть, но её просто не используют.

**Путь 2 — рваное чтение снимка.** `redownload-drive-sources.ts:774-805`: тело читается через `readProtectedSource`, а ревизия — **отдельным SQL**. Пользователь записывает тело между этими двумя чтениями → восстановление получает **старое `content=NULL` и новую ревизию** → его CAS проходит, потому что ревизия «свежая», и поверх пользовательской работы ложится старое тело из Drive. **Проверка на месте, но токен для неё получен не из того снимка** — одного CAS в функции публикации мало, если вызывающий взял ревизию отдельно.

**Метаданные — защита отказывает открытым способом.** При ошибке raw-merge код `sourceMetadataPatch.ts:261-290` откатывается на чтение-изменение-запись всего объекта. Между этим чтением и записью соседняя правка теряется. Это включается ровно тогда, когда что-то не так со схемой или соединением, — то есть механизм **fail-open там, где обязан быть fail-closed**.

**Ограничители не являются потолками.**
- При `maxUsd=0.000006` и стоимости документа `0.000005` прошли **два** документа — накопленная оценка `0.000010`, то есть потолок превышен в 1.67 раза. Код проверяет `spent >= maxUsd` ДО следующего документа (`:422-425`), а стоимость прибавляет ПОСЛЕ публикации (`:670-672`). Проверки «хватит ли на следующий» нет, а стоимость неизвестна до загрузки.
- Когда время вышло **во время** загрузки из Drive, документ всё равно опубликован: после загрузки перед публикацией времени повторно не проверяют. `--max-seconds` потолком не является.

**Манифест порций: два живых процесса с одним именем порции оба получают один id как `claimed`.** Код считает это возобновлением, но не отличает возобновление от второго живого процесса. Заявление «два процесса никогда не владеют одним id» не выполняется.

**ЧТО ПРИЁМКА ПРИНЯЛА.** Из пяти блокеров независимо подтверждён **один**: предполётный SQL. `sourceReadSql()` действительно содержит `metadata->>'deletedAt' IS NULL`, detail и summary используют один CTE и один CASE, запрос реально исполняется, tombstone получает `not-found`. Также подтверждено: CAS работает в позитивном сценарии (устаревшая ревизия отвергается, текущая проходит), атомарный patch метаданных сохраняет соседние ключи **пока raw-merge не падает**, конкурентные РАЗНЫЕ порции корректно расходятся, отказ сборки манифеста не оставляет файла, и простые случаи стоп-гейтов работают.

**Точные правки, названные приёмкой:**
1. читать тело и `contentRevision` **одним согласованным снимком** (один SELECT/транзакция с нужной политикой чтения) и передавать этот токен во все публикации воркера; безусловный вызов убрать;
2. при ошибке raw-merge **не писать устаревший объект** — возвращать `write-failed`, либо делать откатный путь внутри настоящей транзакции с блокировкой и повторным merge;
3. считать стоимость после загрузки и проверять остаток **до публикации**; при превышении источник не менять;
4. перепроверять дедлайн **после каждой долгой фазы** перед записью в БД;
5. ввести идентификатор кампании или межпроцессную аренду, чтобы отличать возобновление от второго живого процесса;
6. оставить явно написанным, что `--max-usd` — оценка счёта за эмбеддинги, а не гарантия биллинга провайдера.

**Предсуществующие падения подтверждены независимо:** свежий detached worktree на точном BASE, чистое рабочее дерево, та же одноразовая Postgres — `driveBodyServerSideRealPg.test.ts` даёт те же 7 pass / 2 fail. Автор не выдавал чужое красное за своё.

**Сдача:** `refs/waves/3774/acceptance/3774-restore` = `d7b9732a4`. **На проде инструмент не запускался и не будет запущен до закрытия этих шести пунктов и следующей приёмки.**
2026-09-14T04:38:45.390Z · coordinator
[14.09 04:38Z координатор] # ⚠️ ТРЕТЬЯ приёмка — ТРЕТИЙ обход. `VERDICT=NO-GO`. И все три на одном стыке, каждый раз слоем выше

**Прямой ответ на главный вопрос: да, пользователь всё ещё может потерять текст.**

**Минимальная последовательность:**
1. в `Source` пользовательский текст, ревизия `R1`;
2. путь **Google / Notion / Dropbox / OneDrive / Slack / reindex** читает старое тело провайдера, но ещё не вызвал `processDocument`;
3. пользователь сохраняет — становится `R2`;
4. путь вызывает `processDocument` **без** `sourceContentRevision`; admission читает `R2` и берёт её как fallback;
5. CAS пропускает устаревшее тело провайдера против `R2`, **потому что токен взят уже ПОСЛЕ пользовательской записи**. Текст заменён.

**Закономерность, которую видно только с третьего раза.** Все три круга дефект был не в самой защите, а в том, откуда берутся данные для неё, и каждый раз на слой выше:
- **3761:** публикация тела вообще не защищена;
- **3774:** защита есть, но воркер публикует мимо неё, а снимок берётся двумя отдельными чтениями;
- **3792:** снимок согласованный, но **токен берётся после внешнего запроса к провайдеру**.

Починка каждый раз была правильной — и каждый раз недостаточной, потому что чинили тот слой, на котором нашли.

**Воспроизведено настоящим тестом на локальном PostgreSQL** (`publishIntegrationSourceBody` + `admitIntegrationSourceId`), а не статическим просмотром.

**Приёмка точна и в обратную сторону** — она разобрала, какие из прошлых проверок остаются значимыми: из девяти пробников 3774 три падения — это именно закрытые дефекты (raw-merge теперь отказывает, потолки останавливают запись, одноимённые порции не проходят), а один «worker-path» тест **намеренно зовёт низкоуровневый примитив без токена и не является настоящим местом вызова в проде**. То есть приёмка не засчитала себе лёгкое красное.

**Найденная слабость примитива:** `src/lib/ingest/integrationSourceBody.ts:216` применяет CAS, **только если вызывающий передал `expectedGeneration`**; без него допускается безусловная запись. Формулировка приёмки: «безопасен только при дисциплине всех callers». **Дисциплина вызывающих — не защита.**

**Предсуществующие падения проверены независимо** на свежем дереве точного BASE. Важная поправка к отчёту автора: `driveBodyServerSideRealPg.test.ts` в текущем общем окружении падает на хуке (`_react.default.createContext is not a function`, 9 подтестов отменены) — то есть заявленные автором «7 pass / 2 fail» здесь воспроизвести нельзя вовсе. Это несовместимость общего `node_modules`, а не регрессия, но и ссылаться на те числа больше нельзя.

**Поставлена 3798** с четырьмя пунктами: токен обязан браться **до** внешнего запроса и проноситься до публикации; **fallback «нет токена → прочитать свежий» убрать** — он делает небезопасный вызов неотличимым от безопасного, отсутствие токена должно быть отказом; пройти по **всем** интеграционным маршрутам, проверив полноту таблицы приёмки собственным поиском; сделать токен **обязательным на уровне примитива**, а не вопросом дисциплины.
И отдельным требованием: **прежде чем объявлять GO, ответить, какой ещё слой выше по стеку берёт данные раньше, чем токен** — назвать его, даже если чинить не стал.

**На проде инструмент не запускался.** Сдача: `refs/waves/3792/acceptance-3792-cas-round2` = `3f797516c0`.
2026-09-14T05:22:20.809Z · coordinator
[14.09 05:22Z координатор] **ТРЕТИЙ ОБХОД ЗАКРЫТ. Волна 3798, `VERDICT=GO`, коммит `1106d24e55`.**

**Прямой ответ: потерять текст пользователя через семь названных маршрутов публикации тела больше нельзя.**

**Что изменено — три уровня защиты вместо одного:**
1. **Токен обязателен на уровне примитива.** `integrationSourceBody.ts:228`: `expectedGeneration` стал **обязательным полем**, условная CAS-клауза (`args.expectedGeneration ? ... : Prisma.empty`) **удалена** — клауза применяется теперь ВСЕГДА. Плюс рантайм-проверка (строки 234–245), потому что `tsx` не делает type-check: при отсутствующем или пустом `contentRevision` возвращается `{published:false, reason:'missing-pre-fetch-revision'}` и **ни один ряд не трогается**. Принимает bigint/number/string одинаково — отказ только на действительно отсутствующем значении. **«Безопасен при дисциплине вызывающих» больше не применимо: дисциплина заменена невозможностью.**
2. **Fallback закрыт типизированным отказом.** `processDocumentRuntime.ts:414–438`: убрана подстановка `admitted.contentRevision` — ревизии, читаемой уже ПОСЛЕ похода к провайдеру. Теперь отсутствие `sourceContentRevision` даёт `INTEGRATION_SOURCE_CONTENT_REVISION_REQUIRED` **до** вызова публикации. Небезопасный вызов перестал быть неотличимым от безопасного.
3. **Все семь маршрутов берут токен ДО внешнего запроса** — Google, Notion, Dropbox и остальные, с указанием строки по каждому.

**Доказательство настоящей конкурентностью, а не симуляцией:** медленная заглушка вместо провайдера плюс параллельная запись пользователя через `Promise.all` — красное на предыдущем коде, зелёное на исправленном, на локальном PostgreSQL.

**⚠️ ВОЛНА САМА НАШЛА ВОСЬМОЙ МАРШРУТ, которого не было в таблице приёмки 3792.** `src/app/actions.ts` (self-heal в `chatWithSources`, ~строка 3244) зовёт `processDocument` без `sourceContentRevision`. Последствие этого круга: для исторической популяции документов (l113k, существование задокументировано в `src/app/api/sync/route.ts:1437`) такой вызов теперь **отказ, а не публикация** — самовосстановление этим путём перестаёт работать, тихо, с `console.error`.

**Автор сознательно не стал это чинить** и объяснил почему: путь вне семи названных, а смена поведения для устаревшей популяции — **продуктовое решение, а не CAS-фикс**. Он назвал это прямо, в ответ на моё требование «назови следующий слой выше, даже если чинить не стал». Это ровно та дисциплина, которой не хватало три круга подряд.

**Остаточный риск, названный автором честно:** примитив **доверяет точности переданного токена** — если вызывающий прочитал тело и ревизию раздельными запросами, токен внутренне несогласован, и CAS не спасёт. Для известных путей это закрыто единочтением; тест `«6 worst-case finding»` из приёмки 3774 продолжает демонстрировать это **как свойство примитива**, а не как регрессию.

**Поставлена 3806 — четвёртая приёмка, после неё иду на прод.** Обязательные пункты: **обходить обязательность токена** (пустая строка, `null`, `undefined`, число, bigint, объект без поля); **искать маршруты своим поиском**, а не сверяться со списком автора — он же сам нашёл восьмой, которого не было у приёмки; **оценить решение по восьмому маршруту** и отдельно ответить, **насколько тихо** он теперь отказывает — узнает ли пользователь, что документ не восстановился.

**На проде инструмент не запускался.** Сдача: `refs/waves/3798/wave/3798-restore-cas-round3` = `1106d24e55`.
2026-09-14T05:57:30.338Z · coordinator
[14.09 05:57Z координатор] # Четвёртая приёмка (3806): CAS-исправление прошло всё — `NO-GO` из-за МОЛЧАНИЯ восьмого пути

**Сначала то, что принято, и принято жёстко.**

**Обязательность токена на уровне примитива проверена независимо шестью способами обхода** — `expectedGeneration` с пустой строкой, `null`, `undefined`, числом, bigint, объектом без нужного поля. **Ни один не опубликовал.** Проверено на настоящем локальном PostgreSQL собственным тестом приёмки, а не прогоном чужого.

Пробники 3792: **4/4 pass** — нет токена → типизированный отказ; устаревший токен → `INTEGRATION_SOURCE_BODY_NOT_PUBLISHED`; актуальный токен публикует; аудит мест вызова пройден.
Пробники 3774: **6/9 pass**, и три «не ok» — это **инвертированные проверки с текстом `finding`**: raw-merge fallback не сработал, потолки по деньгам и времени не пропустили просроченную запись, одноимённые порции не выдали двум процессам один идентификатор. **Ни одно старое находочное срабатывание не воспроизвелось; живого старого обхода нет.**

**Почему всё же `NO-GO`.**

`src/app/actions.ts` (`chatWithSources` self-heal, строка 3244) после правильного fail-closed изменения **молча перестаёт восстанавливать исторический `google-drive` документ**. Пользователь получает обычный ответ чата и **никакого сообщения о том, что восстановление не состоялось**. Формулировка приёмки: безопасно против перезаписи текста, но недостаточно безопасно, чтобы запускать операцию с пользовательскими документами без отдельного решения.

**Решение, которое я принял и обосновываю.** Менять поведение восстановления для исторической популяции я не буду — приёмка права, это отдельный вопрос. Но **молчаливый отказ чинится тем, что перестаёт быть молчаливым**. Всю смену мы вылавливаем ровно этот класс: зелёный инструмент, который ничего не проверяет; уборщик, сносящий каталог без имени; проверка, падающая с кодом успеха; вердикт, помечающий правильное состояние ошибкой. **Отказ, о котором никто не узнаёт, — то же самое.**

**Поставлена 3810** с тремя требованиями: пользователь должен узнать, что документ не восстановился и что ответ построен без него (человеческой формулировкой, не кодом ошибки, и чат при этом не падает); **оператор должен узнать тоже** — иначе мы не заметим, что у исторической популяции самовосстановление перестало работать вовсе; и отдельно — **как узнать размер этой популяции до боевого прогона**, чтобы я мог решить, блокирует ли это операцию.

**На проде инструмент не запускался.** Сдача: `refs/waves/3806/acceptance/3806-cas-round3` = `909084d752`.
2026-09-14T06:39:23.917Z · coordinator
[14.09 06:39Z координатор] # Молчание восьмого пути закрыто (3810 `GO`) — и подсчёт на проде дал НЕ ТО ЧИСЛО, которое мы повторяли

**Отказ стал видимым, логика восстановления не изменена.** Пользователь получает человекочитаемое предложение прямо в ответе чата, оператор — структурированный маяк. Чат не падает ни в одном сценарии. Проверено на настоящем локальном PostgreSQL; `pendingIndexPresentation` 12/12, интеграционный тест 3/3, два новых теста 2/2 и 4/4.

**Решение по приватности принято осознанно и проверено тестом.** Маяк намеренно **не содержит `sourceId`**: `SAFE_TOKEN_KEYS` в `src/lib/diagnostics/privacy.ts` — deny-by-default allowlist, и `sourceId` реально превращается в `[REDACTED]` (проверено прямым тестом). Оператор получает **счётчик по причине, а не список документов** — этого хватит, чтобы заметить «self-heal перестал работать вовсе», но не хватит, чтобы найти конкретный документ из логов. Тот же компромисс, что уже принят в проекте для соседних отказов.

**⚠️ ПОПРАВКА К ДВУМ ПРОШЛЫМ ОТЧЁТАМ (3798 и 3806).** Реальный код отказа — **`INTEGRATION_TARGET_NOTEBOOK_REQUIRED`**, а не `INTEGRATION_SOURCE_CONTENT_REVISION_REQUIRED`. Причина: self-heal зовёт `processDocument` **без четвёртого аргумента вообще**, и падает раньше — на `processDocumentRuntime.ts:392-397`, до проверки ревизии на строке 434. И затронутый класс **шире**, чем «историческая ревизия отсутствует»: отказ ловит всё, что проходит `isIntegrationImportMetadata` — двенадцать origins.

## ⚠️ Подсчёт на проде: 100, а не 842

Волна дала готовый read-only запрос для честного подсчёта перед боевым прогоном. **Я его выполнил** (только `SELECT`, `default_transaction_read_only=on`, `statement_timeout=60s`):

```
источников, попадающих под отказ self-heal:  100
из них google-drive без тела:                100
```

**Задокументированное число 842 — устаревшее.** Оно снято «на l113k» (`integrationSourceBody.ts:22-24`, `reindex-drive-sources.ts:8-9`) и с тех пор популяция изменилась. **Все 100 — это `google-drive` с пустым телом**, то есть ровно тот класс, который мы и собираемся восстанавливать.

**Из этого следует то, что важнее самого числа.** Цифра «185 документов», которую я повторяю всю смену, взята из сухого прогона волны 3760 и **может быть так же устарела, как 842**. Прямой счёт сейчас даёт 100 источников `google-drive` без тела. **Перед боевым прогоном обязателен свежий предполётный подсчёт** — планировать шесть-тринадцать часов и 370 обращений к Drive по числу, снятому на другой линии, нельзя.

Разбивка по владельцам вернула ноль строк при счётчике 100 — join по `Notebook.userId` не находит пары. Не блокирует, но означает, что по этим источникам нельзя сказать, сконцентрированы ли они у одного владельца; **отдельно разберу перед прогоном.**

**Поставлена 3815** — приёмка видимости, последний известный блокер. Её главный вопрос сформулирован прямо: **снимает ли это блокер, и если нет — что ещё мешает.** Плюс обязательная перепроверка поправки про код ошибки, потому что она меняет понимание двух прошлых отчётов.

**Сдача:** `refs/waves/3810/wave/3810-selfheal-not-silent` = `8509ea5a20`. **На проде инструмент не запускался** — выполнен только read-only подсчёт.
2026-09-14T07:23:01.752Z · coordinator
[14.09 07:23Z координатор] # Приёмка 3815 (`NO-GO`): предупреждение показывается не по тому объекту, по которому принимается решение

**Сначала то, что приёмка подтвердила независимо:** волна 3810 **ничего не сломала**, безопасность данных подтверждена, и **поправка про код ошибки верна** — отказ действительно `INTEGRATION_TARGET_NOTEBOOK_REQUIRED`, а не тот, что называли отчёты 3798 и 3806.

**Но главное обещание волны — «пользователь действительно узнаёт» — не выполняется на основной продуктовой поверхности чата:**

> Предсказание отказа фильтрует по `doc.metadata`, а рантайм принимает решение по `{ name, type: doc.type || 'restored', ...doc.metadata }` — **это разные объекты.**

Документ, который рантайм отвергнет, предсказатель может не счесть отказным — и пользователь снова ничего не узнает.

**Это тот же класс, который мы ловим всю смену, и уже четвёртый раз:** защита на месте, но **данные для неё берутся не оттуда**. В линии восстановления так было трижды (защиты нет → защиту обходят вызовом → токен берётся после внешнего запроса), в линии владения дважды (защищена не та область → механизм шире). Теперь то же самое в предупреждении: **предикат считает по одному объекту, решение принимается по другому.**

**Поставлена 3819** с явным требованием чинить **правильным способом**: не «дописать недостающие поля в фильтр», а **построить объект один раз и передать его и в предсказатель, и в рантайм**, чтобы разойтись было нельзя в принципе. Если по устройству кода это невозможно — объяснить почему и сделать так, чтобы расхождение **ловилось тестом, а не глазами**. Плюс перечислить **все** поля, добавляемые по пути (`type: doc.type || 'restored'` — лишь одно из них), которые могут изменить вердикт `isIntegrationImportMetadata`; и обратная проверка — документ, который рантайм НЕ отвергает, не должен получать ложного предупреждения.

**Сдача:** `refs/waves/3815/acceptance/3815-selfheal` = `1eebab008d`. Приёмка производственный код не меняла — только добавила один тест.

**На проде инструмент не запускался.**
2026-09-14T07:52:57.545Z · coordinator
[14.09 07:52Z координатор] # ✅ ЧЕСТНЫЙ ПОДСЧЁТ НА ПРОДЕ: документов **100**, а не 185. И все у ОДНОГО владельца

Волна 3821 объяснила противоречие прошлого подсчёта (счётчик давал 100, разбивка — ноль строк) и дала готовые read-only запросы. **Я их выполнил на проде** (`default_transaction_read_only=on`, `statement_timeout=60s`, перед запуском файл проверен на отсутствие изменяющих операторов).

**Q3 — раскладка по классам той же логикой, что и сухой прогон инструмента:**
```
diagnosis         | sources
------------------+---------
needs-redownload  |     100
```
**Ровно один класс. Ни `already-ok`, ни `needs-requeue`, ни `not-found`, ни `unsupported-non-drive-source` — ноль.**

**Q2 — исправленная разбивка (прежняя возвращала ноль строк):**
```
owner_user_id             | notebook_trashed | metadata_source | source_type | sources
cmmqphkdw0000247m2m56dl0l | f                | google-drive    | text        |     100
```
**Все 100 — у одного владельца, тетрадь жива, не в корзине.**

## Что это меняет для операции

| | было в планах | по факту |
|---|---|---|
| документов | **185** | **100** |
| обращений к Drive | **370** | **~200** |
| оценка времени | 6–13 ч | пропорционально меньше |
| владельцев | не знали | **один** |

**Цифра «185» была устаревшей ровно так же, как задокументированные в коде «842».** Обе сняты на прошлых состояниях базы и переносились из отчёта в отчёт. Я сам повторял «185» всю смену.

**Это ровно тот класс ошибки, который мы ловим второй день:** число, взятое из чужого отчёта, а не измеренное сейчас. Разница в том, что теперь есть запрос, дающий **ту же раскладку, что и сам инструмент** — и его можно выполнить перед прогоном, а не доверять записанному.

**Практическое следствие:** все 100 принадлежат одному владельцу с живой тетрадью. Значит прогон затрагивает **один аккаунт**, и предупредить надо одного человека, а не искать, кого именно из многих. Риск локализован.

**Порядок перед боевым прогоном теперь такой:**
1. прогнать Q1–Q3 **непосредственно перед стартом** (числа устаревают — доказано дважды);
2. сверить, что раскладка по-прежнему `needs-redownload` и число не выросло;
3. только потом `--apply` порциями по манифесту.

**Сдача:** `refs/waves/3821/...`; запросы — `3821POPQUERY-evidence/PROD-QUERIES-web593-3821.sql`. **На проде выполнены только `SELECT`; инструмент восстановления не запускался.**
2026-09-14T08:09:07.787Z · coordinator
[14.09 08:09Z координатор] # ✅ ЦЕПОЧКА ПРИЁМОК ВОССТАНОВЛЕНИЯ ЗАКРЫТА. Приёмка 3823, `VERDICT=GO`

Путь был длинный и каждый круг что-то находил:

| круг | вердикт | что нашли |
|---|---|---|
| 3761 | `NO-GO` | публикация тела вообще не защищена от гонки |
| 3774 | `NO-GO` | воркер публикует мимо защиты; снимок берётся двумя чтениями |
| 3792 | `NO-GO` | токен берётся **после** внешнего запроса к провайдеру |
| 3806 | `NO-GO` | восьмой путь **молча** перестал восстанавливать |
| 3815 | `NO-GO` | предупреждение решается **по другому объекту**, чем отказ |
| **3823** | **`GO`** | — |

**Пять приёмок, пять находок, и ни одну не нашёл автор.**

## Как работала эта приёмка

Одноразовый PostgreSQL 16 на порту **55823**, схема через **`prisma migrate deploy`, а не `db push`** — то есть проверялась настоящая цепочка миграций, а не срез схемы. Сеть не использовалась, эмбеддинги замоканы через `mock.module`. `node_modules` — симлинки на соседний чекаут, кроме заново собранного клиента Prisma; **ни одного `pnpm install`**, lock-файл не тронут.

**Две поправки, которые приёмка сделала к чужим утверждениям:**
1. Отчёт 3819 утверждал, что чужого кластера на 5432 на машине нет. **Он есть.** Приёмка его не тронула и явно зафиксировала, что все её подключения идут на 55823.
2. **Ветка `refs/waves/3819/...` на зеркале машины отсутствовала** (максимум был 3817). Приёмка импортировала её из авторского бандла, **предварительно пересчитав sha256** (совпал) и прогнав `git bundle verify` (complete history, head ровно `d38a33702…`), и записала это в отдельный лог. Не «как-нибудь достала», а с распиской.

## Что это значит

**Со стороны приёмок инструмент восстановления готов.** Остаётся моя часть, и порядок такой:
1. **свежий предполётный подсчёт непосредственно перед стартом** — числа устаревают, доказано дважды («842» из кода и «185» из отчёта обе оказались неверны; сейчас **100**);
2. сверить, что раскладка по-прежнему `needs-redownload` и число не выросло;
3. `--apply` **порциями по манифесту**, начиная с маленькой первой порции, а не всеми ста разом;
4. между порциями — проверка, что документы действительно восстановились, а не «прошли без ошибки».

**Сдача:** `refs/waves/3823/acceptance-3823` = `4515da60c7`. **На проде инструмент по-прежнему не запускался.**
2026-09-14T09:13:33.663Z · coordinator
[14.09 09:13Z координатор] БОЕВОЕ ВОССТАНОВЛЕНИЕ ОТЛОЖЕНО: у шага нашлось невыписанное предусловие — посадка l115m.

Приёмка 3823 вернула GO, порядок прогона был расписан (свежий предполётный счёт → сверка класса → --apply порциями → проверка между порциями). Взялся за первый кусок и перед счётом проверил то, чего раньше не проверял: исполняется ли на проде тот инструмент, который принимала приёмка.

Проверено командой на зеркале A1, прод = 1628b697d9 (l115l):
  3798 (CAS round 3)         — НЕ в проде, ahead 6
  3810 (selfheal not silent) — НЕ в проде, ahead 7
  3819 (предикат)            — НЕ в проде, ahead 8

В проде исполняется версия с рваным чтением снимка (redownload-drive-sources.ts:774-805): тело читается через readProtectedSource, ревизия берётся отдельным SQL. Запись человека между двумя чтениями даёт восстановлению старое content=NULL с новой ревизией; CAS видит свежую ревизию и пропускает устаревшее тело Drive ПОВЕРХ работы человека. Приёмка 3823 проверяла новый предикат, а не этот.

Запуск сегодня означал бы прогон на ста документах одного живого владельца инструментом, дефект которого мы уже нашли и починили — но не посадили.

ПОРЯДОК:
1. посадить l115m (три эти ветки входят в план сборки, слияния 2-4);
2. проверить merge-base --is-ancestor по каждому фиксу против нового прод-sha;
3. свежий предполётный счёт непосредственно перед стартом — числа устаревают, доказано дважды («842» и «185» обе неверны; сейчас 100, все у одного владельца);
4. --apply порциями, начиная с маленькой, с проверкой между порциями, что документы действительно восстановились.

Класс ошибки прежний и уже записан: «фикс принят» не равно «фикс исполняется». Проверяется одной командой, и делать её надо перед каждым боевым прогоном инструмента, который чинили волны, а не только перед посадкой.
2026-09-14T10:08:40.368Z · coordinator
[14.09 10:08Z координатор] 
2026-09-14T10:38:07.016Z · coordinator
[14.09 10:38Z координатор] ПРОД ПЕРЕКЛЮЧЁН НА l115m 14.09 10:26:18Z, посадка закрыта полностью.

source 806c45d22f6e2616ae8135caabfb0c99180729b4
артефакт me2-standalone-linux-arm64-806c45d22-20260914T101104Z
sha256 a0c6a50360fd2f35cfd0932a16b06fad3c22ce5d58a401a47f3b00ff1b97d7b3
релиз /home/ubuntu/prod/releases/arm64-l115m-20260914T101104Z
откат sudo python3 transition-l115m.py rollback -> l115l (1628b697d)

СОСТАВ: 3798 (CAS round 3) + 3810 (видимость отказа самовосстановления) + 3819 (предикат) — три ветки восстановления индекса; 3805 + 3811 — комплект бас-фактора; 3808 — семафор эмбеддингов. Шесть слияний, 57 файлов, миграций 209 -> 209 (новых нет).

НЕ ВОШЛИ ОСОЗНАННО: 3788 (гигиена консоли — приёмка 3834 вернула NO-GO, в одиночку ветка завозит регрессию); 3801 + 3814 (владение БД — перенос на кластере ещё не выполнен, а 3801 ставит в инструмент посадки безусловный замок); 3818 (нагрузочный прибор — оснастка, в проде не нужна).

ЧЕК-ЛИСТ ЗАКРЫТ ЦЕЛИКОМ:
1. Переключение: оба порта ready на нужном коммите, paidReady в режиме enforce_ready без отказов, ВСЕ ЧЕТЫРЕ службы (nc-a1, nc-a1-b, nc-a1-indexing, nc-a1-extraction) на новом релизе, гейт целостности юнитов OK.
2. post-landing-checks.sh: сервер на том самом коммите; браузер открыл документ, 58 в списке, редактор на месте, ноль новых ошибок консоли после клика.
3. Платная проба настоящими деньгами: billing.status=billed, settlement=actual, costUsd=0.001017, модель gpt-5.4-mini-2026-03-17, usedUnknownModelFallback=false, candidatesConsidered 10 / dropped 0, операция op_2e20b045862b3a67cfd598debec8eb3d.
4. Артефакт опубликован на Hetzner: release_id=l115m-806c45d2, 244573294 байта, ретенция 5.

ОТКАТ ПОДГОТОВЛЕН ДО ПЕРЕКЛЮЧЕНИЯ: снимок 21 файла служб с sha256 и режимами, базовая линия l115l проверена здоровой на обоих портах. Волна 3838 разобрала откат отдельно и перечислила шесть следов, которые возврат артефакта не снимает.

НАЙДЕНО ПО ДОРОГЕ: расписка миграций называла линию через одну («restore four l115k», хотя откат идёт на l115l). Проверил прошлую посадку — там та же ошибка поколением раньше, то есть врало во всех комплектах. Причина: таблица сдвига токенов покрывает рабочие имена, но не свободный текст. Исправлено в скрипте, добавлено в таблицу, и поставлена проверка результата — следующий комплект остановится на assert'е, если текст снова соврёт. Расписка на проде не переписана задним числом, рядом положен apply-receipt.CORRECTION.txt.
2026-09-14T11:23:20.335Z · coordinator
[14.09 11:23Z координатор] ПОСТ-QA ПОСАЖЕННОЙ ЛИНИИ l115m (волна 3844, GO): по каждому тикету, с отрицательным контролем на НАСТОЯЩЕМ до-фиксном коде.

Правило, которое эта пост-QA соблюдала жёстче прежних: мало получить красное — надо доказать, что красное НЕ от ошибки сборки. Поэтому в каждом отрицательном контроле есть контрольный зелёный случай на том же старом файле: модуль грузится и работает, значит красное настоящее.

WEB-593 / ветка 3798 — обязательный pre-fetch CAS-токен. PROVEN-OFFLINE.
Доказано на настоящем PostgreSQL, 6/6: устаревший токен даёт generation-conflict и правка пользователя цела; без токена — missing-pre-fetch-revision; пустой токен — отказ; законная перепубликация с текущим токеном проходит; admitIntegrationSourceId отдаёт ту же ревизию, что лежит в строке.
Отрицательный контроль КРАСНЕЕТ: тот же тест против настоящего src/lib/ingest/integrationSourceBody.ts из коммита 1628b697d (прошлая линия) даёт три падения с фактическим дефектом «a stale token was allowed to publish». Контрольные случаи там зелёные.

WEB-593 / ветка 3810 — видимость отказа самовосстановления. PROVEN-OFFLINE для примитивов, NEEDS-LIVE для проводки.
Настоящий processDocument на реальной БД возвращает INTEGRATION_TARGET_NOTEBOOK_REQUIRED; уведомление реально строится на en/ru/uk и различается. 5/5.
ЧЕСТНАЯ ГРАНИЦА: проводка внутри chatWithSources офлайн не доказана и отрицательного контроля для неё нет. Это остаётся открытым.

WEB-593 / ветка 3819 — предикат решает на том же объекте. PROVEN-OFFLINE для предиката, NEEDS-LIVE для проводки.
Источник type:'web' БЕЗ metadata: настоящий processDocument отказывает, и настоящий isIntegrationImportMetadata(restoreOptions) это видит. Обычный документ и умолчание type:'restored' ложного уведомления не дают.
Отрицательный контроль КРАСНЕЕТ: та же проверка с прежней формой (предикат по doc.metadata) падает ровно на type:'web', остальные четыре теста зелёные.

WEB-626 / ветка 3808 — семафор эмбеддингов. PROVEN-OFFLINE.
Настоящий generateEmbeddingsBatch: 12 партий за три «волны» (681 мс), 5 партий за две (428 мс) — параллелизм ровно 4. При дефолте вывод посаженного модуля ПОБАЙТНО равен выводу прежнего на входах 0/1/96/128 и 21-порционном.
Отрицательный контроль КРАСНЕЕТ ДВАЖДЫ: с возвращённым последовательным циклом 2567 мс вместо ~600 (2 падения); со снятой планкой 356 мс (3 падения). В обоих случаях тест порядка зелёный — модули грузятся.

WEB-641 / ветка 3805 — комплект бас-фактора. PROVEN-OFFLINE.
Настоящий pack_kit.py и настоящий age 1.1.1: дубль получателя → exit 2, ssh-ed25519 → exit 2, подменённый START-HERE.md → отказ.
Отрицательный контроль КРАСНЕЕТ: прежний ops/busfactor-kit на тех же входах дубль упаковывает (exit 0), ssh-ключ принимает (exit 0), дописанную в START-HERE.md строку пропускает с ok:true.

WEB-641 / ветка 3811 — порядок проверок и коды возврата. PROVEN-OFFLINE.
Настоящий verify_kit.py на настоящем комплекте, 8 сценариев, коды 0/10/10/11/13/15/11/12/10 — все совпали с ожиданием.
Отрицательный контроль КРАСНЕЕТ: прежний verify_kit.py даёт мягкий 15 вместо 11 на подделке с чужим ключом и 15 вместо 12 на испорченной подписи. Нетронутый комплект на том же старом файле даёт 0 — файл исполняется.

ЧТО ОСТАЁТСЯ ОТКРЫТЫМ: проводка уведомления об отказе самовосстановления внутри chatWithSources (NEEDS-LIVE у двух веток). Живой прогон делает координатор.

Сдача в зеркале: refs/waves/3844/qa/3844-postqa-l115m = fa9af8073.
2026-09-14T12:07:43.565Z · coordinator
[14.09 12:07Z координатор] WEB-593 — документы с Google Drive пропали из поиска
Обновление от 2026-09-14. Затронутые посадки: l115l (1628b697d9b20a4446b9bdba20ada0a6c2820d11, 13.09 20:38:15Z) и l115m (806c45d22f6e2616ae8135caabfb0c99180729b4, 14.09 10:26:18Z).
Карточка написана для человека, который открывает её впервые и не имеет ни журнала смены, ни переписки.

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

Пользователь загрузил документы с Google Drive. Документы видны в тетради, но поиск по ним не находит ничего — как будто их нет. Для человека это выглядит как «поиск сломался», а не как «документ не проиндексирован».

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

Нашли диагностикой на БОЕВОЙ базе только на чтение 13.09 12:10Z. Пострадавших оказалось 204, а не 351, как было записано в теле карточки: 198 источников без единого куска и 6 со статусом «готово, но кусков ноль». Живых документов в базе — 1679.

Почему не поймали раньше: у продукта нет сигнала «документ есть, но в поиск не попал». Индексирующий рабочий печатал `total=0` при непустой очереди и это выглядело как «работы нет», а не как «работа отфильтрована». Числа в карточке при этом жили своей жизнью: сначала «842» (снято на линии l113k), потом «351», потом «204», потом «185» — ни одно из них не было перепроверено перед использованием.

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

Это самая длинная цепочка смены. Порядок важен: почти каждый шаг был правильным и почти каждый был недостаточным.

3.1. ПЕРВАЯ ВЕРСИЯ ПРИЧИНЫ (13.09 13:40Z) — верная, но объясняла не всех.
Запрос по боевой базе показал у всех упавших документов один и тот же `lastError`: «AI features are unavailable: the spending safeguards are not fully configured», и все они упали 05.09 в течение полутора минут. Одно событие, а не сто проблем. Условие к тому моменту было снято, значит документы должны были просто переиндексироваться.
Проверка порциями: канарейка на 5 → 204 стало 199; порция с параллелизмом 2 и паузой 5 с → 199 стало 195; на второй волне один документ снова упал на построении вектора, и скрипт САМ остановился, не записав его в исправленные. Итого починено 9.

3.2. ТУПИК №1 — ручной SQL вместо штатного пути (моя ошибка координатора).
20 документов были помечены в очередь ПРЯМЫМ SQL-запросом вместо штатного скрипта. Рабочий их не увидел: у метки не было полей, которые ставит штатный путь (`buildSourceIndexingPendingMetadata`). Двадцать документов зависли невидимыми — ни в `failed`, ни в работе. Обнаружено по `total=0` при непустой очереди, откачено в прежнее состояние.
ВЫВОД ДЛЯ СЛЕДУЮЩЕГО: очередь индексации нельзя наполнять руками. Это не «то же самое, только быстрее» — форма записи другая.

3.3. НАСТОЯЩАЯ ПРИЧИНА (13.09 14:15Z) — переиндексация не могла помочь В ПРИНЦИПЕ.
У всех упавших документов ПУСТОЕ поле извлечения текста (`processing.extraction.status` — записи нет вовсе, проверено запросом). Рабочий индексации в `src/lib/ingest/sourceIndexingQueue.ts` пропускает любой источник, у которого извлечение не `done` и не `partial` — поэтому он и печатает `total=0`, даже когда документы лежат в очереди как `pending`.
Следствие: ранбуки волн 3649 и 3675 покрывали ровно половину пути. Нужен предшествующий шаг — извлечение. Рабочий извлечения на проде есть (`nc-a1-extraction`, по таймеру), отрабатывает, но видит ноль задач: очереди на извлечение у этих документов тоже нет.

3.4. ТУПИК №2 — поставить в очередь извлечение оказалось бессмысленно.
Волна 3718 (`refs/waves/3718` = `657393d3`) выяснила: «поставить в очередь чтения» — это ДВЕ записи (пометка в карточке источника + строка в таблице заданий), а прошлая ручная попытка сделала только первую. Скрипт волны делает обе, форма совпадает со штатной загрузкой, проверено на одноразовой базе.
НО: для документов с Google Drive у нас НИГДЕ НЕ СОХРАНЕНЫ САМИ БАЙТЫ. Ставить их в очередь бессмысленно, и скрипт честно отказывается. Единственный путь — перекачка из Drive, которая была закрыта нашим же заслоном по статье 28 (см. WEB-573).
Разделение по наличию байтов на тот момент: у 229 документов без кусков байты есть, у 110 байтов нет вовсе.

3.5. ЧЕТЫРЕ КРУГА ПРИЁМКИ ИНСТРУМЕНТА ВОССТАНОВЛЕНИЯ — И ПЯТЫЙ, ЗАКРЫВШИЙ ЦЕПОЧКУ.
Инструмент перекачки из Drive (`scripts/ingest/redownload-drive-sources.ts`) проходил приёмку ПЯТЬ раз. Пять приёмок — пять находок, и НИ ОДНУ не нашёл автор. Это главное, что должен знать следующий агент: защита была правильной на каждом круге и каждый раз обходилась СЛОЕМ ВЫШЕ.

  Круг 1 — приёмка 3761, NO-GO. Пять блокеров до любого `--apply` на проде: публикация тела не была условной атомарной операцией (между снимком и записью правка пользователя терялась); `metadata` переписывалась целиком; предполётный SQL не учитывал `metadata->>'deletedAt' IS NULL` из канонической политики `src/lib/rag/sourceReadPolicyServer.ts`; не было машинно проверяемого манифеста непересекающихся ID; «6–13 часов» и «$1.85» были сценариями, а не границами.
  Круг 2 — приёмка 3774, NO-GO. Защита появилась — и обходится ВЫЗОВОМ: воркер публиковал тело без `expectedGeneration` (безусловный UPDATE); снимок читался ДВУМЯ чтениями (тело через `readProtectedSource`, ревизия отдельным SQL), запись пользователя между ними давала восстановлению старое тело с НОВОЙ ревизией — CAS видел свежую ревизию и пропускал устаревшее тело поверх работы человека; патч метаданных при ошибке откатывался на чтение-изменение-запись всего объекта (fail-open); потолок расхода проверялся ДО следующего документа, а стоимость прибавлялась ПОСЛЕ публикации — при потолке 0.000006 и цене 0.000005 прошли ДВА документа.
  Круг 3 — приёмка 3792, NO-GO. Снимок стал согласованным, но ТОКЕН БЕРЁТСЯ ПОСЛЕ ВНЕШНЕГО ЗАПРОСА: интеграционный путь читает старое тело провайдера, пользователь в это время сохраняет, путь зовёт публикацию без `sourceContentRevision`, admission читает уже НОВУЮ ревизию и берёт её как fallback — CAS пропускает устаревшее тело. Формулировка приёмки: «безопасен только при дисциплине всех вызывающих. Дисциплина вызывающих — не защита».
  Круг 4 — приёмка 3806, NO-GO. Обязательность токена проверена шестью способами обхода (пустая строка, `null`, `undefined`, число, bigint, объект без поля) — ни один не опубликовал. Отказ пришёл из-за ВОСЬМОГО пути, который автор нашёл сам и сознательно не чинил: `src/app/actions.ts` (самовосстановление в `chatWithSources`) после fail-closed правки МОЛЧА перестал восстанавливать исторические документы — пользователь получал обычный ответ чата и ни слова.
  Круг 5 — приёмка 3815, NO-GO. Предупреждение и решение считались по РАЗНЫМ объектам: предсказание отказа фильтровало по `doc.metadata`, а рантайм решал по `{ name, type: doc.type || 'restored', ...doc.metadata }`.
  Круг 6 — приёмка 3823, GO. Цепочка закрыта.

3.6. ЦИФРЫ, СНЯТЫЕ КАК УСТАРЕВШИЕ.
«842» была снята на линии l113k. «185» взята из сухого прогона волны 3760 и повторялась всю смену. Прямой подсчёт на проде 14.09 (только чтение, `default_transaction_read_only=on`, `statement_timeout=60s`, файл предварительно проверен на отсутствие изменяющих операторов) дал:
  источников под отказ самовосстановления: 100
  из них google-drive без тела: 100
  раскладка той же логикой, что и сухой прогон инструмента: `needs-redownload | 100`; классы `already-ok`, `needs-requeue`, `not-found`, `unsupported-non-drive-source` — ноль
  все 100 у ОДНОГО владельца `cmmqphkdw0000247m2m56dl0l`, тетрадь жива, не в корзине
Планировалось 185 документов и 370 обращений к Drive; по факту 100 документов и ~200 обращений, один владелец.

3.7. ТУПИК №3 — кампания была объявлена готовой к запуску, а предусловия не существовало.
14.09 09:2xZ, перед первым боевым куском, проверено очевидное: исполняется ли на проде тот инструмент, который принимала приёмка. Нет. В проде (тогда — l115l) жила версия с рваным чтением снимка; фиксы 3798, 3810 и 3819 были на 6–8 коммитов впереди прода. В списке дел шаг висел как готовый: «приёмка GO, нужен свежий счёт, потом `--apply` порциями». Предусловие «сначала посадить l115m» не было выписано нигде.

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

l115l (`1628b697d9b20a4446b9bdba20ada0a6c2820d11`, посажена 13.09 20:38:15Z) принесла волну 3739 (`4b072a0b2`) — инструмент повторной загрузки индекса после доработки по двум находкам приёмки.

l115m (`806c45d22f6e2616ae8135caabfb0c99180729b4`, посажена 14.09 10:26:18Z) принесла три волны этой цепочки, слитые именно в этом порядке (3798 — строгий предок 3810, 3810 — строгий предок 3819):
  refs/waves/3798/wave/3798-restore-cas-round3 = 1106d24e55 — коммит слияния в линии a3b753a9b
  refs/waves/3810/wave/3810-selfheal-not-silent = 8509ea5a20 — коммит слияния в линии 61577b31c
  refs/waves/3819/wave/3819-selfheal-predicate = d38a33702d — коммит слияния в линии e7dc8543c

Что именно изменено:
  `src/lib/ingest/integrationSourceBody.ts`, функция `publishIntegrationSourceBody`: поле `expectedGeneration: { contentRevision: string }` стало ОБЯЗАТЕЛЬНЫМ, условная CAS-клауза удалена — применяется всегда. Рядом рантайм-проверка (tsx не делает type-check): отсутствующая, пустая или `null`-ревизия даёт `{ published: false, reason: 'missing-pre-fetch-revision' }`, и ни один ряд не трогается.
  Убрана подстановка `admitted.contentRevision` — ревизии, читаемой уже ПОСЛЕ похода к провайдеру; вместо неё типизированный отказ `INTEGRATION_SOURCE_CONTENT_REVISION_REQUIRED` ДО публикации. Небезопасный вызов перестал быть неотличимым от безопасного.
  Все семь интеграционных маршрутов публикации берут токен ДО внешнего запроса.
  Восьмой путь — самовосстановление в `src/app/actions.ts` (`chatWithSources`) — поведение восстановления НЕ менялось сознательно, но отказ перестал быть молчаливым: пользователь получает человекочитаемое предложение в ответе чата, оператор — структурированный маяк. Маяк намеренно БЕЗ `sourceId`, потому что `SAFE_TOKEN_KEYS` в `privacy.ts` — allowlist с запретом по умолчанию и `sourceId` там становится `[REDACTED]`. Оператор получает счётчик по причине, а не список документов: заметить «самовосстановление умерло» хватит, найти конкретный документ — нет. Компромисс назван явно.
  ПОПРАВКА к отчётам кругов 3 и 4: реальный код отказа у восьмого пути — `INTEGRATION_TARGET_NOTEBOOK_REQUIRED`, а не тот, что писали: самовосстановление зовёт `processDocument` без четвёртого аргумента вообще и падает раньше (`src/lib/documents/processDocumentRuntime.ts`, до проверки ревизии). Затронутый класс шире — все двенадцать origins `isIntegrationImportMetadata`.

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

ДОКАЗАНО:
  Круг закрытия блокеров (волна 3763): 87 pass / 0 fail в основном наборе на ОДНОРАЗОВОЙ НАСТОЯЩЕЙ PostgreSQL, плюс 7 pass в тестах гонок с красным-до и зелёным-после для сценариев «пользователь дописал тело», «пользователь изменил метаданные», «превышен `--max-usd`». Три падающих теста честно воспроизведены на свежем чекауте BASE без изменений волны — чужое красное за своё доказательство не выдано.
  Круг 3 (волна 3798): доказано НАСТОЯЩЕЙ конкурентностью — медленная заглушка вместо провайдера плюс параллельная запись через `Promise.all`, а не последовательной симуляцией.
  Круг 5 (волна 3810): на настоящей PostgreSQL `pendingIndexPresentation` 12/12, интеграционный 3/3, новые тесты 2/2 и 4/4; логика восстановления не изменена — проверено `git diff` по семи маршрутам и трём модулям.
  Приёмка 3823 (GO): одноразовый PostgreSQL, схема через `prisma migrate deploy`, а НЕ `db push` — проверялась настоящая цепочка миграций; сеть не использовалась; ни одного `pnpm install`, lock не тронут.
  Пост-QA линии l115l (волна 3755) по этому тикету: доказан ИНСТРУМЕНТ.

ГРАНИЦЫ — читать обязательно:
  Инструмент восстановления НИ РАЗУ не запускался на проде. Ни в каком режиме.
  Доказан инструмент, а НЕ восстановление документов. Ни один из ста документов на момент этой записи не восстановлен.
  Остаточный узкий риск, названный автором самим: примитив ДОВЕРЯЕТ точности переданного токена — при раздельном чтении тела и ревизии он внутренне несогласован, и CAS не спасёт. Для известных путей это закрыто единочтением.
  Ещё один остаточный риск, названный волной 3763: два писателя ровно по полю `processing.indexing` одного источника.
  Для исторической популяции самовосстановление через восьмой путь теперь ОТКАЗ, а не публикация. Это продуктовое решение, принятое сознательно, а не побочный эффект.

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

  Сама кампания восстановления. Разблокирована только 14.09 после посадки l115m: 3798, 3810 и 3819 стали предками прод-коммита `806c45d22f`.
  Порядок, записанный до запуска: (1) свежий предполётный подсчёт НЕПОСРЕДСТВЕННО перед стартом — числа устаревают, доказано дважды; (2) сверить, что класс по-прежнему `needs-redownload` и число не выросло; (3) `--apply` ПОРЦИЯМИ по манифесту, начиная с маленькой первой, а не всеми ста разом; (4) между порциями проверять, что документы действительно восстановились, а не «прошли без ошибки».
  `--claims-dir` кампании обязан быть зафиксирован в момент запуска: в инструменте НЕТ НИ ОДНОГО `unlink`, повторный прогон заклеймит 0 из N.
  След в базе, который не снимается откатом линии: строка-резервация `Source`, созданная ДО обращения к провайдеру, если обращение упало — это единственный по-настоящему новый мусор в БД.
  110 документов, у которых байты не сохранены нигде, возвращаются только перекачкой из Drive — а Drive закрыт заслоном статьи 28 (см. WEB-573).

7. TROUBLESHOOTER — ЕСЛИ ДЕФЕКТ ПОВТОРИТСЯ

Шаг 1. Убедиться, что в проде стоят все три фикса. Для каждого из `1106d24e55`, `8509ea5a20`, `d38a33702d`:
    git merge-base --is-ancestor <sha> <коммит прода>
  Код возврата 0 = фикс в проде. Ненулевой = СТОП, запускать восстановление НЕЛЬЗЯ: версия без них способна положить устаревшее тело Drive поверх работы человека.

Шаг 2. Свежий предполётный подсчёт только на чтение. Запросы Q1–Q3 лежат в `refs/waves/3821/...`; Q3 даёт ту же раскладку по классам, что и сухой прогон инструмента.
  НЕ брать числа из отчётов. «842» и «185» обе оказались неверны; текущее известное значение — 100, и оно тоже устареет.

Шаг 3. Если индексирующий рабочий печатает `total=0` при непустой очереди — проблема НЕ в индексации, а в извлечении. Смотреть `processing.extraction.status` у источников: пустое поле означает, что рабочий их пропускает по коду в `src/lib/ingest/sourceIndexingQueue.ts`. Переиндексация таких документов не починит НИКОГДА.

Шаг 4. Если документ надо поставить в очередь — только штатным скриптом. Прямой SQL делает одну запись из двух, и документ зависает невидимым: ни в `failed`, ни в работе.

Шаг 5. Если публикация тела отказывает с `missing-pre-fetch-revision` или `INTEGRATION_SOURCE_CONTENT_REVISION_REQUIRED` — это НЕ поломка. Это защита: вызывающий не взял токен ревизии ДО внешнего запроса. Чинить надо место вызова, а не примитив.

Шаг 6. Если пользователь жалуется, что документ не восстанавливается через чат — искать в журнале оператора маяк `chat.self_heal.restore_failed`. Он несёт счётчик по причине и НЕ несёт `sourceId` (вырезается политикой приватности). Конкретный документ по маяку не найти — это известное ограничение, а не сбой.
2026-09-14T15:01:21.999Z · coordinator
[14.09 15:01Z координатор] **Кампания восстановления индекса остановлена на сухом прогоне. Причина не техническая — правовая, и решение не моё.**

Что сделано сегодня (всё на проде, всё только чтение, кроме последнего пункта — он ничего не писал):
1. Снят блокер: фиксы 3798/3810/3819 в проде (линия l115n, `d85bb2dad15a`).
2. Заведено дерево прод-коммита с зависимостями — `/home/ubuntu/restore-l115n` (инструмента восстановления нет ни в прод-релизе, ни в старом `/home/ubuntu/nc`; это был следующий шаг кампании и он закрыт).
3. Предполётные запросы (read-only, `default_transaction_read_only=on`):
   - кандидатов **100**, владелец **ровно один** (`cmmqphkdw0000247m2m56dl0l`);
   - **S7** (счётчик попыток индексации на пределе ≥3) — **0**;
   - **S8** (`currentChunkGeneration` не NULL) — **0**;
   - **S9** (тело на chunked-пути WEB-651) — **0**.
   То есть три риска, ради которых 3850 добавляла эти запросы, на этой популяции не реализовались.
4. Манифест построен один раз на три порции: `p10`/`p30`/`p60`, 100 id, `zero overlaps`.
5. Сухой прогон первой порции — по условию **У-B** без `--manifest`/`--portion`.

**Результат сухого прогона (`SUMMARY mode=dry-run requested=10 healed=0 alreadyOk=0 refused=10`):**
все десять — `refused`, причина у всех одна:

> `no-drive-token` — `notebook owner has no connected Google Drive credential (pass --allow-service-account-fallback to consider the operator service account, subject to Article 28)`

Проверено в базе, а не по сообщению инструмента:
- у владельца документов провайдеры только `signal`, `telegram`, `whatsapp` и их scope-варианты; **google/drive среди них нет**;
- `SELECT provider FROM "UserIntegration" WHERE provider ILIKE '%google%' OR provider ILIKE '%drive%'` → **0 строк на всём проде**. Диска не подключено **ни у кого**.

**Что это значит.** Путь «скачать заново токеном владельца» закрыт не ошибкой, а отсутствием доступа. Остаётся единственный путь, который предлагает сам инструмент: `--allow-service-account-fallback` — чтение чужих файлов сервисным аккаунтом оператора, и инструмент сам помечает его как **предмет статьи 28**. Это решение о правовом основании обработки, а не инженерное. **Останавливаюсь здесь и спрашиваю владельца.**

**Поправка к порядку 3846.** Она относила «жив ли токен владельца» к восьми вещам, которые узнаются **только из боевой порции**. Это неверно как минимум для случая «доступа нет вовсе»: сухой прогон поймал его бесплатно, без единого обращения к Google и без единой записи. Первый пункт списка из восьми надо снять.

Кампания остаётся в этом состоянии: манифест построен, порции нарезаны, claim-файлы **не создавались** (У-B соблюдено, повторный прогон не будет лечить 0 из 10). Возобновление — одним словом владельца по статье 28.
2026-09-14T15:16:55.574Z · 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-14T20:21:03.578Z · coordinator
[14.09 20:21Z координатор] **Пересчитала на живом проде прямо сейчас. Картина другая, чем мы считали, и решение владельца по статье 28 больше НЕ блокирует эту линию.**

Запрос по `Document` и `DocumentChunk` на проде A1 (только чтение):

| | документов |
|---|---|
| всего | 3430 |
| **есть куски** (видны в поиске) | 3080 |
| **нет ни одного куска** | **350** (10.2 %) |
| из них: **текст лежит в базе** | **347** |
| из них: текста нет нигде | **3** |

**Главное: 347 из 350 чинятся без Google Диска, без служебного аккаунта и без статьи 28.** Их содержимое уже в базе — им нужно только заново нарезать куски. Решение владельца, которого я ждала, касается **трёх** документов, а не всей кампании. Кампания восстановления на A1 (`~/restore-campaign`, 100 id, три порции) строилась вокруг Диска — она нужна только для остатка, и её объём падает с сотни до трёх.

**Второе, и это хуже: течь не закрыта, она идёт сегодня.** Документы без кусков создаются прямо сейчас:

| день | создано | без кусков |
|---|---|---|
| 02.09 | 156 | 10 |
| 05.09 | 133 | 5 |
| 08.09 | 60 | 2 |
| 11.09 | 4 | 2 |
| 13.09 | 6 | 3 |

Это ложится на догадку владельца: «сервер крутился на малинке, там было ленивое индексирование, потом переехал на Oracle». Похоже, ленивая нарезка так и не стала надёжной — часть документов её просто не получает. **Но это пока догадка: причину я не вскрывала, вскрытие выдам волной.** Пока разлив не заткнут, вычерпывать бессмысленно — накопится снова.

**Профиль остатка:** 7 разных пользователей; создавались с 08.07.2025 по 13.09.2026, пик 150 штук в июне 2026; медианная длина 316 знаков, самый большой 9.3 МБ, суммарно 53.8 МБ текста. Совсем коротких (<50 знаков) 11 — для них отсутствие кусков может быть законным, это надо проверить, а не чинить вслепую.

**Что дальше, по порядку:**
1. волна: найти, почему новый документ остаётся без кусков (течь), и закрыть её — без этого пункт 2 бессмыслен;
2. волна: безопасная перенарезка 347 по содержимому из базы, порциями, с проверкой «стало видно в поиске», без обращения к Диску;
3. остаток из 3 документов — единственное место, где действительно нужен внешний источник; там и только там всплывает вопрос статьи 28.

**Что снимаю с себя:** я четыре дня держала эту линию заблокированной решением владельца. Проверка заняла один запрос. Надо было пересчитать раньше — цифру «351 документ» я унаследовала и ни разу не разложила на «есть текст» / «нет текста».
2026-09-14T21:11:45.088Z · coordinator
[14.09 21:11Z координатор] **Причина течи индекса измерена на живом проде. Главная гипотеза волны 3925 не подтвердилась, а настоящая причина оказалась другой и куда конкретнее.**

Волна 3925 (`VERDICT=GO`) проследила код и назвала главным подозреваемым «очередь `pending` не дренируется без внешнего worker»: `shouldProcessSourceInline` возвращает `false` для непустого текста (`src/lib/ingest/processingState.ts:247-262`), `shouldRunSourceIndexingPumpInProcess` в production тоже `false` (`:228-237`), `writeSource` ставит `processing.indexing.status = pending` (`sourceWriter.ts:411-422`) и уходит, а cron в этом режиме отвечает `202 worker-required` (`src/app/api/cron/source-indexing/route.ts:112-120`) — то есть **успехом**. Волна честно написала, что до прода не дотянулась и проверить не может.

**Я проверила. На проде очередь дренируется, и `pending` практически нет:**

| состояние | источников |
|---|---|
| `done` | 1472 |
| **состояния нет вообще** | **344** |
| `failed` | 100 |
| `poisoned` | 54 |
| `skipped` | 2 |
| **`pending`** | **1** |

Воркер `nc-a1-indexing.service` живой, ходит по таймеру (`nc-a1-indexing.timer`, запуск за 18 секунд до проверки), ограничен 25 источниками за проход. То есть **«очередь стоит» — не наш случай**. (Отдельно: `nc-a1-extraction.service` — `inactive/dead`, это стоит посмотреть.)

**Настоящая причина у 128 источников — названа их собственным `lastError`:**

```
"AI features are unavailable: the spending safeguards are not fully configured."   100 (попыток 1)
"AI features are unavailable: the spending safeguards are not fully configured."    28 (попыток 3)
"forced reindex incomplete: ner-runtime-error"                                      22
"forced reindex incomplete: wiki-error"                                              3
"Transaction API error: Transaction already closed: ... expired transaction"         1
```

Все `failed` датированы **18.06.2026** — тем же днём, на который приходится июньский всплеск в 150 документов без кусков. Причина `reindex:web-99-chunk-quarantine` у 150 из них: это был **намеренный карантин**, и из него их **никто не выпустил**.

**⚠️ И вот что важно сегодня: условие, на котором они упали, больше не выполняется.** В окружении прода сейчас `ENABLE_REAL_SPEND_LEDGER=1`, `PAID_OP_RECORD_ENABLED=1`, `ENABLE_SPEND_RESERVATIONS=1`, ключи провайдеров на месте, `SPEND_*` артефакты подписаны. То есть «safeguards not fully configured» — **историческое состояние**, а не текущее. Эти 128 источников, скорее всего, переиндексируются сегодня без единой правки кода. Это надо проверить прогоном на нескольких, а не объявлять.

**Про 344 источника без состояния индексации — картина честнее, чем я её подавала.** Разбивка по типу:

| тип | штук | текст есть |
|---|---|---|
| `factcheck-report` | 114 | да |
| `web-ingest` | 65 | да |
| `r8-synthetic-fixture` | 52 | да |
| без типа | 48 | да |
| `text` | 24 | да |
| `voice-call-transcript` | 7 | да |
| `video-report` | 7 | да |
| `telegram-ingest` | 6 | нет |
| `video` | 4 | да |

**231 из 344 — это внутренние артефакты** (отчёты фактчека, синтетические фикстуры, веб-приём), которым в пользовательском поиске делать нечего. Я раньше называла все 350 «документами, которых нет в поиске», и это преувеличение: пользовательских среди них заметно меньше. Точную границу «что должно быть в поиске» надо провести явно, а не по остаточному принципу.

**Порядок работ, уточнённый по фактам:**
1. попробовать выпустить из карантина несколько из 128 и посмотреть, проходят ли они теперь — если да, разобрать всю партию порциями;
2. разобраться с `ner-runtime-error` (22) и `wiki-error` (3) — это другой класс;
3. явно решить, какие из 344 бесстатусных вообще должны индексироваться, и почему они не получили статуса;
4. посмотреть, почему `nc-a1-extraction.service` мёртв;
5. перенарезка — **после** пунктов 1–3, иначе повторится.

Рекомендации волны 3925 по коду остаются в силе и не отменяются этой проверкой: `writeSource` не должен оставлять принятую непустую запись без сигнала живости очереди, cron с `worker-required` не должен выглядеть успехом, и **искусственный порог в 50 знаков вводить не надо** — обычный короткий текст обязан получать кусок.

Ветка волны: `refs/waves/3925/wave/3925-index-leak` (сдача: отчёт, bundle, evidence).
2026-09-14T21:22:25.252Z · coordinator
[14.09 21:22Z координатор] **Поправка к моему же сообщению и точное измерение: «статья 28 гейтит три документа» — верно для документов и вводит в заблуждение в целом. На уровне источников Диском заблокированы 100.**

Досчитала до конца, и картина сомкнулась:

```
status = 'failed'  ->  100 источников
  из них с id вида google-drive:…   100  (все)
  из них content пуст                100  (все)
  созданы 18.06.2026, упали 05.09.2026 18:16–18:19Z
```

**Это ровно кампания восстановления.** Сверила множества поимённо: в отказавших 100, в `~/restore-campaign/all-100.txt` 100, **пересечение 100**. Ни одного лишнего с обеих сторон.

То есть те 100 источников — не дефект кода и не течь: **у них физически нет содержимого**, оно осталось в Google Диске, и без служебного доступа (статья 28) взять его неоткуда. Решение владельца по этой линии **нужно**, и касается оно ста источников, а не трёх.

**Где я ввела в заблуждение.** Я измеряла на уровне **документов** (`Document` ⨝ `DocumentChunk`): 350 без кусков, у 347 текст в базе, у 3 — нет. Это по-прежнему верно. Но потом я сказала «значит Диск нужен для трёх», и это уже неверный перенос: `Source` и `Document` — **разные объекты, и колонки, связывающей их напрямую, в схеме нет**, поэтому сопоставить одно с другим запросом я не могу. Сто Drive-источников без содержимого просто не породили документов, которые попали бы в мой счёт по документам.

**Правильная формулировка теперь такая:**
- документов без кусков — **350**, и **347** из них чинятся локально, без Диска;
- источников, заблокированных Диском, — **100**, и это отдельная популяция, к тем 347 не сводимая;
- «три документа без текста» — это три документа, а не три единицы работы.

**⚠️ Отдельная находка: сообщение об ошибке у этих ста лжёт о причине.** В `lastError` записано «AI features are unavailable: the spending safeguards are not fully configured», хотя настоящий блокер — **отсутствие содержимого и доступа к Диску**. Денежные предохранители сегодня настроены (`ENABLE_REAL_SPEND_LEDGER=1`, ключи на месте), и если бы дело было в них, эти сто бы уже прошли. Ошибка, называющая не ту причину, — самостоятельный дефект: она отправила меня (и отправит любого) искать не там. Стоит отдельного билета.

**Что это меняет в порядке работ:**
1. 128 «переиндексируются сами» — **снимается**: 100 из них не переиндексируются никогда, пока нет Диска;
2. остаются `poisoned` 54 (из них 50 Drive, но **содержимое у всех непустое**) — вот их действительно стоит попробовать выпустить из карантина;
3. `ner-runtime-error` 22 и `wiki-error` 3 — отдельный класс, к Диску отношения не имеет;
4. 344 без состояния индексации — **ни одного** Drive-источника; это наш внутренний класс (фактчек 114, фикстуры 52, веб-приём 65);
5. перенарезка 347 документов — по-прежнему после того, как разберёмся с классами выше.

Решение по статье 28 (служебный аккаунт Google) снова становится нужным — но теперь известно, **сколько** за ним стоит: сто источников, и ни одним больше.
2026-09-15T01:25:07.690Z · coordinator
[15.09 01:25Z координатор] ## 15.09 01:40Z — ВОШЁЛ В ПОСАДКУ l115o. Требуется пост-QA на проде.

Прод работает на `l115o-68e25d8d` (коммит `68e25d8df5ae8263c5ac5466353631f57a17cfcc`, артефакт `cd2773f65d66b09ec03d39b553041d2814ce09413e72d72b07bc9de89df536c8`) с 00:56Z 15.09. Посадка проверена: `ready=true`, коммит совпал, браузерная проба открыла документ, 0 новых ошибок.

**Работу по этому тикету принесли волны:** 3504, 3675, 3741, 3760, 3844.

**Что это значит для этого тикета.** Работа по нему пролежала принятой, но НЕ посаженной — в некоторых случаях неделями. Теперь она на проде. Приёмка волной доказывала, что код правильный в дереве волны; она НЕ доказывала, что фича работает на живом проде. Это разные вещи, и мы на этом уже обжигались.

**Поэтому статус — `review`, а не `done`.** Закрыть тикет имеет право только пост-QA, который проверит поведение на работающем проде и приложит доказательство. До тех пор «сделано» — это заявка.

### Для нулевого агента (тот, кто будет делать пост-QA)

Ты приходишь на этот тикет без нашей истории. Что надо знать:

1. **Проверяй на проде, не на стенде.** Стенд сейчас вообще не поднят — у него не было своего артефакта, он запускался из каталога боевого релиза и заблокировал проверку посадки; пересобирается волной 3948 (WEB-662).
2. **`/api/health` ничего не доказывает** — он проходит и на пустом приложении. Нужно деловое действие: аутентифицированная сессия делает то, про что этот тикет.
3. **Отсутствие ошибок в логе — не доказательство жизни.** Нужен положительный результат, а не тишина.
4. **Числа в обосновании закрытия получай командой в момент закрытия.** Я однажды закрыла тикет числом `4255`, взятым из `pg_stat_user_tables.n_live_tup` — это ОЦЕНКА. Настоящее `count(*)` дало `107 817`. Разница в 25 раз.
5. **Не верь имени волны в теме коммита** — проверяй наличие содержимого (`git cherry`), а не упоминание номера.
6. Запускатель тестов в проекте — `node --test`. Vitest нет.

2026-09-15T01:59:40.810Z · coordinator
[15.09 01:59Z координатор] ## 15.09 03:45Z — цифры пересняты НА ПРОДЕ после посадки l115o. Блокер снят: инструмент восстановления теперь на проде.

Все числа получены командой сейчас, а не переписаны из прошлых отчётов.

```
документов живых ................. 3430
без единого куска ................  350
из них текст УЖЕ в базе ..........  347
кусков в базе всего .............. 107817
```

Картина не изменилась с 14.09 — и это само по себе факт: **дыра не затянулась сама**, 350 документов по-прежнему не ищутся.

Схема (проверено `information_schema`): у `Document` **нет** колонки мягкого удаления вовсе (`id, userId, name, type, content, stats, createdAt, updatedAt, temporalDate, temporalDateSource`), текст лежит в `content`, куски — в `DocumentChunk.documentId`.

### Блокер снят

14.09 восстановление было запрещено: фиксы инструмента (волны 3798 / 3810 / 3819) шли на 6–8 коммитов впереди прода, и запуск чинил бы базу сломанным инструментом. **Посадка l115o это исправила** — линия принесла 3504, 3675, 3741, 3760, 3844 (вся индексная работа) плюс те фиксы. Инструмент и прод теперь на одном коммите `68e25d8df5ae8263c5ac5466353631f57a17cfcc`.

### Что осталось выяснить до запуска, а не после

1. **347 против 3.** У 347 документов текст в базе — им восстановление делается локально, без внешних источников. Оставшиеся 3 упираются в Google Диск, а на проде **ни у кого нет подключённого Диска** (0 строк), значит это служебный аккаунт и статья 28 — слово владельца. Эти три отделить и не смешивать с 347.
2. **Цена.** Восстановление кусков влечёт построение эмбеддингов — это настоящие деньги через платный контур. Нужна оценка ДО запуска: сколько символов у этих 347 документов, сколько это токенов, сколько микро по нашему прайсбуку. Числом, а не «недорого».
3. **Нагрузка.** Это массовая запись в боевую базу. Батарея ролей и любые прогоны на это время не запускаются (правило куплено прошлой аварией).
4. **Сухой прогон.** Волна 3760 сделала сухой прогон восстановления — перечитать её вывод и сверить с нынешним состоянием, прежде чем запускать по-настоящему.

Инструмент в стандартной сборке релиза **не лежит** (`scripts/` в артефакте содержит только `migration-compatibility.mjs`, `release-schema-compatibility-gate.mjs` и `lib/`), значит запуск идёт из дерева линии с установленными зависимостями, а не из каталога прода. Это работа координатора: у волн нет доступа к боевой базе.

### Попутная находка инфраструктуры

При подготовке выяснилось, что зеркала волн на Neo и M4 **потеряли прод-коммит**: он лежал там «висячим» объектом, на него не указывал ни один ref, и сборщик мусора его снёс. Проверка `rev-parse` час назад отвечала «есть» — она отвечает на другой вопрос. Три брифа велели волнам делать дерево от этого sha; одна успела до уборки, две в очереди умерли бы. Ref `refs/lines/l115o` теперь проставлен на всех четырёх машинах.
2026-09-15T02:01:28.669Z · coordinator
[15.09 02:01Z координатор] ## 15.09 03:50Z — 🔴 СНЯТО: «три документа гейтит Google Диск / статья 28». Блокера, требующего слова владельца, в этом тикете НЕТ.

Проверила оставшиеся три поимённо, а не по классу:

```
cmtz2cgs0000a1483owa588qr | SEC-075 live-gates QA fixture | создан 13.09 00:15:54 | content = '' (не NULL), длина 0
cmtz2cpc2000aep4rpse6jcgv | SEC-075 live-gates QA fixture | создан 13.09 00:16:05 | то же
cmtz2d0eg000ats7r7v6zham2 | SEC-075 live-gates QA fixture | создан 13.09 00:16:20 | то же
```

Это **наши собственные пустые фикстуры**, которые я же и создала 13.09 при прогоне живых гейтов SEC-075. Кусков у них нет потому, что резать нечего. К Диску и статье 28 они отношения не имеют.

Я держала в остатке тикета решение владельца, которого не требовалось, — потому что судила о трёх документах по классу («раз без текста, значит внешний источник»), а не открыла их. Вся дыра чинится локально.

### Объём работы — замерено, не оценено

```
документов к восстановлению .......... 347
символов суммарно ............. 53 792 352
средний документ .................. 155 021 символ
самый крупный ................... 9 319 824 символа
крупнее 1 МБ .......................... 13 документов
```

По типам: `text` 320 шт / 39.8 млн символов, `text/plain` 16 / 220 686, `video` 8 / 20 316, `email` 4 / 5 262, `html` 1 / 4 450 137, `pdf` 1 / 9 319 824.
Пятёрка крупнейших: pdf 9.3 МБ, html 4.45 МБ, три `text` по 3.29 МБ.

Средний существующий кусок — **845 символов**. Значит ожидается ≈**63 660** новых кусков к нынешним 107 817, то есть рост таблицы кусков на **59%**.

### Цена

Эмбеддинги: ≈53.8 млн символов ÷ 4 ≈ 13.4 млн токенов. По наблюдаемой на проде ставке (платная проба 15.09: 18 токенов = 1 микро) это ≈**0.75 доллара**. Это оценка по наблюдённой ставке, а не выборка из прайсбука — перед запуском подтвердить прайсбуком.

Вывод: **деньги тут не блокер**. Блокер — аккуратность.

### Что делать по частям

1. **Тринадцать крупных отдельной партией и первыми на пробу.** У нас уже была авария «широкий вопрос по большому документу клинит весь прод»; 9.3-мегабайтный pdf — ровно тот класс. Резать по одному, смотреть на время и память.
2. **334 остальных пачками.** Это массовая запись в боевую базу: батарея ролей и любые нагрузочные прогоны на это время не запускаются.
3. **Три фикстуры SEC-075 — удалить или пометить,** чтобы они перестали считаться дырой. Это единственное, что останется после восстановления, и оно наше собственное мусорное.
4. **Сухой прогон волны 3760 перечитать** и сверить с нынешним состоянием перед настоящим запуском.

Инструмент восстановления в артефакт релиза не входит (в `scripts/` артефакта только `migration-compatibility.mjs`, `release-schema-compatibility-gate.mjs` и `lib/`), значит запуск идёт из дерева линии с зависимостями. Работа координатора: у волн нет доступа к боевой базе.
2026-09-15T02:28:11.912Z · coordinator
[15.09 02:28Z координатор] ## 15.09 05:10Z — 🔴 ИНСТРУМЕНТ ВОССТАНОВЛЕНИЯ ПОКРЫВАЕТ 33 ДОКУМЕНТА ИЗ 350, А НЕ 347

Прежде чем запускать восстановление, прочитала сам инструмент и воспроизвела его выборку на проде. Хорошо, что прочитала: запуск «как есть» починил бы **девять процентов** дыры и выглядел бы успехом.

### Что за инструмент

`scripts/repair-live-docs-without-chunks.ts` (тикет в коде — **WEB-99**, не WEB-593). Устройство правильное: он **не пишет куски сам и не зовёт поставщика эмбеддингов**, а помечает пустой индекс и отдаёт работу уже работающему на проде воркеру `nc-a1-indexing`, затем ждёт, пока тот опубликует непустой индекс. Сухой прогон — поведение по умолчанию; `--apply` требует `--limit` (≤1000) и `--confirm-live-repair WEB-99`.

### Его выборка (прочитано в коде, `scripts/repair-live-docs-without-chunks.ts:218-242`)

```sql
FROM "Source" s
JOIN "Document" d ON d.id = s.id
JOIN "Notebook" n ON n.id = s."notebookId"
LEFT JOIN "DocumentChunk" c ON c."documentId" = s.id AND <свежесть>
WHERE s."isActive" = TRUE
  AND <политика чтения источника>
  AND s.content IS NOT NULL
  AND length(s.content) > 200        -- SOURCE_INDEXING_REPAIR_MIN_CONTENT_CHARS
GROUP BY ... HAVING COUNT(c.id) = 0
```

Ключевое: он читает **`Source.content`**, а не `Document.content`. Это **разные колонки**, и я их смешала, когда докладывала «347 чинятся локально».

### Разложение 350 на проде — замерено сейчас

```
всего документов без единого куска ............ 350
├─ НЕТ строки Source вообще .................... 216   ← инструмент их не видит
│     из них с текстом документа > 200 симв. ... 197
├─ Source есть, но НЕ активен ...................  1
└─ Source активен .............................. 133
      ├─ Source.content пуст или ≤ 200 символов . 100   ← инструмент их пропускает
      └─ Source.content > 200 символов ..........  33   ← ЭТО ВСЁ, ЧТО ОН ПОЧИНИТ
```

Контекст пошире: из **3430** документов строку `Source` имеют только **1788** (52%).

Подходящие 33 документа: 20 970 708 символов, средний 635 476, максимум 9 319 824. По типам: pdf 1 (9.3 МБ), text 28 (7.2 МБ), html 1 (4.45 МБ), video 3 (9 333 символа). То есть оба «тяжеловеса», о которых я предупреждала, попадают именно в эти 33.

### Что это значит

**Дыра распадается на три разные задачи, а не на одну:**

1. **33 документа** — чинятся имеющимся инструментом сегодня. Это готовая работа.
2. **100 документов** — `Source` есть и активен, но его собственное содержимое пусто или почти пусто, при том что у документа текст есть. Надо понять: текст никогда не доезжал до `Source`, или его оттуда вычистили? Это не задача восстановления индекса, это задача про целостность источников.
3. **216 документов** — строки `Source` нет вообще. Инструмент по построению их не увидит никогда. 197 из них имеют текст документа больше 200 символов, то есть материал для индексации есть, а носителя, с которым работает конвейер, нет. Надо решить: создавать `Source` из документа, или индексировать по другому пути, или это вообще законное состояние (например, документы, созданные не импортом).

**Мой прежний вывод «347 чинятся локально» снимаю.** Он был получен из `Document.content`, а конвейер работает с `Source.content`. Локально чинится 33; остальное требует решения, что вообще считать правильным состоянием.

### Как проверить самому

```
npx tsx scripts/repair-live-docs-without-chunks.ts            # сухой прогон — поведение по умолчанию
npx tsx scripts/repair-live-docs-without-chunks.ts --apply --limit 33 --confirm-live-repair WEB-99
```
Дерева с зависимостями на A1 сейчас нет ни одного (`node_modules` нет ни в `wt-l115o`, ни в дереве волны 3948), поэтому сухой прогон самим инструментом ещё не выполнялся — числа выше получены воспроизведением его выборки напрямую запросом. Политику чтения источника и условие свежести куска я не воспроизводила, поэтому **33 — это верхняя граница**: настоящий сухой прогон может дать меньше, но не больше.
2026-09-15T22:05:24.000Z · coordinator
[15.09 22:05Z координатор] WEB593-PREDICATE-4133

4133 / M1 DeepSeek Flash 4.1, `VERDICT=READ_ONLY_FINDING`; полный пакет SHA зелёный. Исправлена постановка задачи: shipped predicate `scripts/repair-live-docs-without-chunks.ts` не является причиной покрытия 33 из 350 и одной правкой SQL расширить его до всей дыры нельзя.

Механика и эволюция:

- из измеренных 350 документов без единого куска: 216 не имеют строки `Source`, 1 имеет неактивный `Source`, 100 имеют короткий/пустой `Source.content`, при этом текст может жить только в `Document.content`; действующий инструмент принципиально работает от `Source` и не имеет носителя для первых 216;
- опубликованные ранее 33 — только верхняя граница: tombstone-policy и две integrity-группы никто на проде отдельно не пересчитал;
- freshness стоит в `LEFT JOIN ... ON`, поэтому не исключает строку, а расширяет выборку до «нет свежего куска». Следовательно selection инструмента может включать документы со старыми кусками вне множества 350; прежняя запись `33 ⊂ 350` не доказана;
- `sourceReadPolicyServer.sql()` в этом вызове получает no caller scope и является tombstone/lifecycle-фильтром, а не пользовательским ACL. Удалять его нельзя; widening создаст риск восстановления удалённых источников, не ACL-утечку;
- минимальная predicate-only правка = никакая. Реальное закрытие дыры требует отдельного механизма восстановления/создания `Source` и наполнения `Source.content`; исполнительный контракт текущего инструмента это запрещает.

KNOWN ISSUE / проверка за 2 минуты: выполнить только агрегатные Q2/Q3 из `/Users/poolpooly/waves-ds/4133/4133WEB593REPAIRPREDICATE-evidence/07-observed-count-queries.sql`. Если суммы не дают 350 либо `SELECTED_NO_FRESH_CHUNK` больше `SELECTED_ZERO_CHUNKS`, нельзя публиковать 33 как точное покрытие. Никаких `--apply` до этого.

Карта: отчёт `/Users/poolpooly/waves-ds/4133/4133WEB593REPAIRPREDICATE-REPORT.md`; evidence `/Users/poolpooly/waves-ds/4133/4133WEB593REPAIRPREDICATE-evidence/`; `SHA256SUMS` в каталоге 4133. Observed production counts в 4133 = `UNKNOWN`, потому что read-only SQL material не передавался.

Остаток / первый шаг нулевого агента: координатор снимает агрегатные production counts по готовым Q2/Q3 без чтения содержимого документов; затем открывает отдельную data-integrity работу для `NO_SOURCE_ROW` и `SOURCE_CONTENT_EMPTY`, не меняя repair predicate и не запуская provider/index writes.
2026-09-19T15:58:36.797Z · coordinator
[POST-QA 4211 AUDIT COMPLETE 19.09]

Fresh read-only DeepSeek Pro audit 4211 completed from the full immutable body+comment snapshot. Output SHA256SUMS readback PASS; manifest SHA-256 `eef44e99f84b858af013057c17731a4cba0e0ce5886664167507eba15afa51f1`. Disposition: `BLOCKED_OR_REWORK`. No status change is safe now; keep `in_progress`.

What remains unproved:
- Aggregate production counts (Q2/Q3) not taken — observed production counts UNKNOWN.
- 33 as exact/complete coverage is not proven (only an upper bound).
- No --apply executed; no provider/index writes done.

One bounded next action: Coordinator runs only the aggregate Q2/Q3 from 4133WEB593REPAIRPREDICATE-evidence/07-observed-count-queries.sql (no document-content reads) to obtain true production counts; then open a separate data-integrity job for NO_SOURCE_ROW and SOURCE_CONTENT_EMPTY without touching the repair predicate.
Target evidence: Aggregate counts that sum to 350 with SELECTED_NO_FRESH_CHUNK <= SELECTED_ZERO_CHUNKS (else 33 must not be published as exact coverage).

This audit changed no board data and made no production claim outside the snapshot. Closure still requires ticket-specific evidence and coordinator readback.
2026-09-19T16:17:11.412Z · coordinator
[19.09 16:17Z координатор] Опись 350 документов без кусков снята заново прямо на проде (только SELECT, роль web, 19.09). Числа и точный SQL — в материале волны 4200 (`/home/wave/waves/4200-material/web593-inventory.json`).

| Ведро | Сколько | Текст документа > 200 симв. | Даты создания |
|---|---|---|---|
| B1 строки `Source` нет вообще | 216 | 197 | 08.07.2025 – 13.09.2026 |
| B2 `Source` активен, `content` пуст | 100 | 100 | все 18.06.2026 |
| B3 `Source` неактивен | 1 | 1 | 20.08.2026 |
| B4 инструмент чинит сегодня | 33 | 33 | 18.06 – 05.09.2026 |

Два новых факта, которых в тикете не было:

1. **Документ без `Source` — это не дефект сам по себе.** 09.09 создано 348 документов, у 290 из них нет строки `Source`, и куски у них ЕСТЬ. Значит в продукте есть законный путь записи, не создающий `Source`, и индексация его проходит. Прежняя формулировка «течь идёт сегодня» на этом основании не держится: течёт не «документ без Source», а какое-то более узкое условие, и его ещё надо назвать.

2. **205 строк `Source` не имеют строки `Document`** — обратная асимметрия, тоже без объяснения.

Всего: документов 3430, строк `Source` 1993, документов со строкой `Source` 1788, кусков 107817. Строк `Source` без кусков 339 (334 активных, у 171 из них content > 200).

Волна 4200 поставлена на A1: она обязана назвать по коду линии l115q (`cc278e1810e1e03ac277b8e3a5fe06a2aebfba30`) конкретное место, где рождается документ без `Source`, и условие, которое молча пропускает нарезку, — и только после этого проектировать расширение `scripts/repair-live-docs-without-chunks.ts` на вёдра B1/B2. Боевой базы у волны нет; выполнение починки — отдельная волна после приёмки.
2026-09-19T16:52:18.013Z · coordinator
[19.09 16:52Z координатор] 🔴 СНИМАЮ своё утверждение из предыдущего комментария. Я написала: «документ без строки `Source` — не дефект, 290 из 348 документов от 09.09 не имеют `Source`, и куски у них ЕСТЬ, значит законный путь записи без `Source` существует и индексацию проходит». **Это неверно.** Куски у них есть, но поиск их не видит.

Волна 4200 доказала это на настоящем PostgreSQL (вердикт `NO-GO`, отчёт `4200WEB593BUCKETS-REPORT.md`, раздел 2.3). Механизм:

`readProtectedChunkRows` (`src/lib/sources/sourceReadGateway.ts:307-330`) — единственные ворота чтения кусков для всего поиска — делает `LEFT JOIN "Source" s` и накладывает предикат свежести `sourceChunkFreshnessSql()` (`src/lib/rag/sourceReadPolicyServer.ts:140-146`):

```
c."sourceContentRevision" = s."contentRevision"
AND c."sourceContentHash"     = s."contentHash"
AND c."generation" IS NOT DISTINCT FROM s."currentChunkGeneration"
```

Политика чтения отсутствующий `Source` пропускает (`s.id IS NULL OR (…)`), а предикат свежести — нет: без строки `s` значения NULL, и `c.x = NULL` даёт NULL, а не TRUE. Обнулить штампы у куска не помогает: `NULL = NULL` — тоже NULL. **Документ без строки `Source` невидим для поиска независимо от того, есть у него куски.**

## Настоящий размер дефекта, снят SELECT-ом на проде 19.09

| | |
|---|---|
| Документов всего | **3430** |
| С хотя бы одним ВИДИМЫМ куском | **1589** |
| 🔴 Без единого видимого куска | **1841** (53.7%) |
| из них: вообще без кусков | 350 (те самые из тикета) |
| из них: куски есть, но ни одного видимого | 1491 |
| в т.ч. нет строки `Source`, но куски есть | 1426 |

То есть «350 из 3430» было видимой верхушкой. Непоисковых документов — **больше половины базы**.

## Почему куски у них вообще появились

`requireVerified` пришёл 30.08 (`0b652fd0c`, «WEB-447 bind chunks to server Source slices»). До него куски писались; после — уже нет. Значит у этих документов `Source` был на момент индексации и исчез позже. Главный производитель — чистка: удаление `Source` (или каскад от `Notebook`) не удаляет ни `Document`, ни `DocumentChunk`, они остаются сиротами. `Document` и `Source` **не связаны внешним ключом** — только соглашением `Source.id === Document.id === DocumentChunk.documentId` (`src/lib/rag/accessScope.ts:20-28`), и у `Document` вообще нет колонки тетради.

## Что из этого следует для починки (вердикт волны NO-GO)

- **B1 (216, а в широком смысле 1426) — чинить нельзя.** Это в основном остатки удаления; «восстановление» воскресило бы содержимое, которое пользователь удалил. Плюс чтобы создать `Source` сиротскому `Document`, пришлось бы выдумать привязку к тетради — продукт это запрещает явным текстом (`src/lib/ingest/integrationSourceAdmission.ts:159-163`).
- **B2 (100) — чинится, но не этим инструментом.** Надо опубликовать тело в `Source.content` атомарно с `contentHash`, и инструмент заработает без единой правки: куски пишет не он, а воркер, читающий `Source.content`. Происхождение партии снято сегодня: у всех ста `metadata` содержит ключ `processing`, а `ingestedVia`/`origin` пусты — это путь резервирования под медиа, не sync.
- **B3 (1) — нельзя:** `isActive = FALSE` это решение пользователя.

Сдача 4200: `4200WEB593BUCKETS-evidence/` на A1 (отчёт 44 КБ, бандл, `SHA256SUMS`, выдержки кода, `git log -S`).
2026-09-19T17:35:49.511Z · coordinator
[19.09 17:35Z координатор] Независимая приёмка 4305 (A1, другой маршрут — codex/gpt-5.6-luna, не тот, что у автора) = **`VERDICT=REWORK`**. Ветка в зеркале A1: `refs/waves/4305/wave-4305-w593accept` = `38f38cfec`.

**Подтверждено своим прогоном (не пересказом):**

1. **Документ без строки `Source` невидим.** Приёмщик завёл на своём одноразовом PostgreSQL 16 фикстуру: сирота с двумя кусками без `Source` плюс здоровый документ. Полный gateway-shaped SELECT вернул только здоровые строки; для сироты отдельная диагностика дала `policy=true, freshness=NULL`. Обнуление всех трёх штампов у куска не помогло — `NULL = NULL` даёт NULL. Вывод автора держится.
3. **Сиротскому `Document` нельзя придумать тетрадь.** `integrationSourceAdmission.ts:132-162` прямо бросает `INTEGRATION_SOURCE_NOT_ADMITTED` при `!source && document`, и запрет распространяется на восстановление: self-heal (`actions.ts:3253-3304`) вызывает `processDocument` без `options.integrationAdmission`, а `processDocumentRuntime.ts:388-397` входит в admission до `Document.upsert`. Вывод автора держится.

🔴 **Опровергнуто утверждение 2 — «B1 это в основном остатки удаления».**

Приёмщик нашёл прямой контрпример **без намерения удалить документ**: legacy-ветка `uploadFile` (`src/app/actions.ts:8909-8919`) обходит `ingestBuffer → writeSource`, создаёт новый id и вызывает `processDocument` без создания `Source` (`:8995-9004`). `processDocument` сначала делает `Document.upsert` (`documentStore.ts:75-101`), после чего source-bound writer требует снимок `Source` и отклоняет кусок при его отсутствии (`sourceChunkWriter.ts:209-257`). **Итог — документ ровно формы B1 в результате сбоя загрузки, а не удаления.**

Попутно приёмщик разобрал остальные пути и показал, что они не объясняют B1: sync уже исправлен WEB-449 (`sync/route.ts:2342-2419` сохраняет выпавшие из снимка строки); `trashAutoPurge` (`trashAutoPurge.ts:257-286`) удаляет в обратном порядке — сначала `Document`; откат транзакции частичного удаления не оставляет; `discardPendingSource` (`backgroundExtraction.ts:168-180`) работает по свежей pending-строке, у которой ещё нет ни `Document`, ни кусков. Source-first hard-delete пути (`notebooks/[id]/route.ts:71-105`, `trashShared.ts:234-266`, `trash/purge/route.ts:70-90`, `purge-all/route.ts:42-49`) действительно существуют — но они показывают лишь, что **часть** сирот могла образоваться после удаления, и не дают ни доли, ни намерения по каждой строке.

**Что это меняет.** Вывод «B1 чинить нельзя» снят в заявленном виде: deletion remnants — одна из причин, а не «в основном». Значит среди 216 (шире — 1426) есть строки, рождённые **сбоем загрузки**, и их починка ничего не воскрешает, а возвращает человеку его же документ. Прежде чем чинить или отказываться, нужен **признак в данных**, различающий две подпопуляции, а не суждение о намерении.

Следующая волна ставится ровно на это: найти по коду различающий признак (что остаётся в базе после Source-first удаления и чего не остаётся после сбоя загрузки), выразить его SQL-ом и проверить на фикстурах обеих форм. Запрос по проду выполню я — у волн доступа к боевой базе нет.
2026-09-19T17:51:26.094Z · coordinator
[19.09 17:51Z координатор] ✅ Сдача 4304 (Neo) = `VERDICT=GO`. Ведро B2 чинится, и отбор **подтверждён на проде**.

Ветка `wave/4304-web593-b2-source-body` = `cdc513589`, в зеркале A1 как `refs/waves/4304/…`. Инструмент `scripts/repair-b2-empty-source-body.ts`: сухой прогон по умолчанию, запись одной транзакцией, идемпотентность, отдельный код отказа на каждое несоответствие.

**Доказано на настоящем PostgreSQL 16** (13 тестов `node:test` плюс независимая цепочка через `psql`):

- **До починки документ невидим — выборкой, а не рассуждением.** На фикстуре физически лежит кусок, `chunks_visible = 0`. Предикат разобран по термам: `revisionTerm = null`, `hashTerm = null`, `generationTerm = true` — то есть именно NULL-сравнение, а не FALSE.
- **Существующая починка бессильна сама по себе:** `web99.readMissingChunkInventory` строку не видит, `requeueMissingChunkSource` возвращает `false` (его WHERE повторяет предикат тела), прямой `processSourceIndexingQueue({sourceId})` кандидата не находит и ни одного куска не пишет.
- **Сухой прогон ничего не пишет:** три `WOULD PUBLISH`, `published=0`, сравнение снимков до/после совпадает **включая `xmin`**.
- После починки воркер режет куски, и они проходят предикат свежести — документ становится видимым.

**Отбор выполнен мной на проде, READ ONLY** (`default_transaction_read_only=on`), запросом, который волна выписала как буквальное разворачивание своего `readB2Inventory()`:

| b2_shape | doctype_null | doctype_set | body_blank | non_indexable | **b2_publishable** |
|---:|---:|---:|---:|---:|---:|
| **100** | 100 | 0 | 0 | 0 | **100** |

Предсказание волны сошлось до строки: сто строк формы, сто публикуемых, ни одного отказа по припаркованному типу, ни одного пустого тела. Волна заранее разложила запрос на шесть столбцов именно для того, чтобы любое расхождение называло свою причину, а не пряталось в одном числе.

Волна также honestly отметила, что `docType IS NULL` она в отбор не включила (в обязательном списке условий его не было) и вывела отдельным столбцом — на проде он совпал с формой полностью.

**Остаток:** перед применением на проде — независимая приёмка другим маршрутом. Ровно эта дисциплина на прошлом шаге опрокинула вывод 4200, поэтому применять без неё не буду. Приёмку ставлю на первую освободившуюся машину; сам APPLY выполняю я, у волн доступа к боевой базе нет.
2026-09-19T18:06:37.164Z · coordinator
[19.09 18:06Z координатор] ✅ Сдача 4306 (A1) = `VERDICT=GO`. Спор 4200 ↔ 4305 разрешён признаком в данных, а не суждением. **Оба были правы наполовину.**

Признак есть, читается одним SQL по существующим таблицам, проверен на фикстурах всех пяти путей — но он **односторонний**, и это существенная часть ответа:
- есть сертификат «строка `Source` с этим id существовала» — четыре независимых варианта (C1…C4);
- сертификата «строки `Source` никогда не было» **не существует**: удаление `Source` не оставляет в базе ничего, что называло бы его id. Волна показала две фикстуры разного происхождения, побайтово одинаковые во всех таблицах.

| подпопуляция | что **доказано** |
|---|---|
| **1426 «с кусками»** | **все до одной — НЕ сбой загрузки.** Единственные ворота записи куска требуют живого снимка `Source` (`sourceChunkWriter.ts:249-257` при `requireVerified: true`), а legacy-ветка `uploadFile` физически не доходит до INSERT. Куски есть ⇒ `Source` существовал ⇒ это остатки удаления |
| **216 «без кусков», с сертификатом** | остатки удаления: серверный cuid, провайдерский id или чек фаусета |
| **216 «без кусков», без сертификата** | **неразрешимо.** Данных в базе нет |

Контрпример приёмки 4305 (legacy `uploadFile`) реален — но он производит **только** форму «без кусков», то есть максимум 216 строк, а не 1642. А 1426 строк с кусками действительно остатки удаления, и теперь это доказано признаком.

## Что делать — ответ прямой

**Новый `Source` не создавать ни для одной подпопуляции.** Обе ветки неоднозначности схлопываются в «удалить как мусор», поэтому неразрешимость операционно ничего не стоит:

- если строка — остаток удаления, пользователь уже попросил её убрать; она дожила только потому, что четыре пути удаляют в неверном порядке;
- если строка — сбой загрузки, продукт **уже сказал пользователю** `'Document was saved, but indexing failed. Please retry the upload.'` (`actions.ts:8885`). Контракт — «повторите», а не «мы сохранили». Ретраить некому: очередь индексации выбирает `FROM "Source" s … length(s.content) > 0` (`sourceReadGateway.ts:1219-1236`), а `Source` нет — воркер её не увидит никогда.

Почему не `Source`: `Source.notebookId` — обязательный FK (`schema.prisma:962-963`), а тетрадь взять неоткуда (`Document` её не хранит, `schema.prisma:2384-2407`); `Document.userId` даёт владельца, а не тетрадь, и выбор любой из его тетрадей — придумывание данных. Admission это прямо запрещает (`integrationSourceAdmission.ts:132-162`), что подтвердила приёмка 4305. Тело пришлось бы брать из `Document.content` — это RAG-зеркало, а не канонический корпус: строка прошла бы предикат свежести телом, которого пользователь никогда не подтверждал, и оказалась бы в тетради, куда он её не клал.

## Порядок работ (план для координатора)

1. **Сначала закрыть течь**, иначе любая чистка отрастёт: четыре пути удаления должны удалять `Document` **перед** `Source`. Правильный порядок в кодовой базе уже есть и работает — `trashAutoPurge.ts:257-286`. Отдельно: legacy-ветка `uploadFile` не должна коммитить документ, который заведомо не сможет нарезать; отказ надо поднять до `Document.upsert`, как уже сделано для integration-формы.
2. Только потом — уборка накопленного.

Ветка и доказательства: `4306B1SPLIT-evidence/` на A1 (таблица «путь → что остаётся», логи прогонов по пяти путям, итоговый SQL разбивки).
2026-09-19T18:36:02.591Z · coordinator
[19.09 18:36Z координатор] 🔴 Независимая приёмка 4310 (Neo, маршрут codex) = **`VERDICT=REWORK`**. Применять инструмент на прод **нельзя**, и хорошо, что я дождался приёмки.

**Что нашла.** Адресный путь `--source-id` не применяет полный предикат жизненного цикла. `readB2Inventory()` использует `sourceReadPolicyServer.sql()`, а `readB2RowById()` (`scripts/repair-b2-empty-source-body.ts:378-386`) читает строку только по `s.id`; `classifyB2Row()` смотрит столбцы `sourceDeleted`/`deletedAt`, но **не смотрит metadata-надгробия**, и итоговый `UPDATE` (`:430-445`) их тоже не содержит. Штатный предикат (`sourceReadPolicyServer.ts:96-109`) отвергает шесть metadata-полей: `sourceDeleted`, `sourceDeletedAt`, `sourceDeletedReason`, `sourceDeletedById`, `deletedAt`, `sourceDeletePreservedArtifacts`.

Приёмщик воспроизвёл это на своём PostgreSQL 16.15: фикстура с чистыми столбцами и `metadata.sourceDeleted="true"` была **записана** — `published=1 refused=0 exit=0`, `xmin` 1589 → 1594, `contentRevision` 1 → 2. То есть инструмент изменил строку, которую защищённый поиск считает удалённой. Ожидаемого кода `source-tombstoned` не существует вовсе.

**Что приёмка подтвердила зелёным** (своими прогонами): отдельные коды отказа `source-inactive`, `source-content-present`, `document-body-empty`, `document-body-too-short`, `source-type-not-null`, `source-chunked-body` (путь WEB-651), `source-row-absent` — ни один не изменил `xmin`; сухой прогон `matched=3 eligible=3 published=0` со снимком без изменений; применение строки в одной транзакции под advisory lock, row lock и CAS по `contentRevision`; повтор — снимок `id,xmin` **IDENTICAL**.

⚠️ **И отдельная находка приёмки, которая меняет процедуру.** После публикации одного лишь тела настоящий `readProtectedChunkRows` вернул **`count=0`**. Видимость появилась только после штатного `repair-live-docs-without-chunks.ts` (`completed=4 failed=0`), и тогда — 15 строк по 5 на документ, `sourceContentRevision=2`, верный hash. **Значит починка — два шага, и второй обязателен.** Формулировка автора «воркер нарежет сам» в таком виде не подтвердилась.

Ещё одно ограничение, названное приёмкой честно: пакет из 100 строк **не** является одной транзакцией — атомарна каждая строка, но не пакет.

Поставлена доработка 4313: один предикат на все пути инструмента (а не третий список условий), код `source-tombstoned`, красный контроль на **каждое** из шести надгробий, сохранение всех зелёных контролей, и письменная двухшаговая процедура для координатора.
2026-09-19T19:52:45.864Z · coordinator
[19.09 19:52Z координатор] 🔴 Применение на проде попробовано и **честно отказало на первой же строке**. Боевых записей не произошло.

**Что сделано.** Координатор создал рабочее дерево из ветки 4313 (`6a25b8e19`), подключил зависимости и выполнил на боевой базе:

1. **Сухой прогон** — прошёл: `matched=5 eligible=5 published=0 refused=0`. Строки оказались документами Google Drive в одной тетради `264308c1-…`, тела 29–33 тысячи символов, все созданы 18.06.2026. Важно: **в Drive никто не ходит** — тело берётся из `Document.content` в нашей же базе, статья 28 здесь ни при чём.
2. **Применение** `--apply --limit 5 --confirm-b2-repair WEB-593-B2` — отказ:

```
fatal Error: source google-drive:1-4EGDrYJqGiU8re8f5LBqJkdWGGJAW66
             settled on a body of a different length
  at scripts/repair-b2-empty-source-body.ts:589
```

**Проверено выборкой после отказа:** `Source.content` по-прежнему NULL, `contentHash` NULL, `contentRevision` = 1, популяция B2 по-прежнему **100**. Транзакция откатилась. Защита сработала правильно — она **отказала, а не записала**.

## Диагноз

Расписка (`:588-590`) сравнивает `COALESCE(length(content), 0)` — в PostgreSQL это **символы** — с `body.length`, а это **единицы UTF-16** в JavaScript. Для символов вне BMP JS считает два, Postgres один. При этом **проверка хеша прошла**, то есть записанный текст побайтово тот самый; разошлись только единицы измерения.

Подтверждающая выборка с прода: у **100 из 100** строк ведра `length(d.content) <> octet_length(d.content)` (у первой 32 830 символов против 59 856 байт). Текст не ASCII.

## Почему этого не поймали ни автор, ни две приёмки

Все фикстуры были короткими и ASCII-подобными — там символы, байты и единицы UTF-16 совпадают. Дефект виден только на настоящем тексте. Это тот же класс, что «зелёная фикстура не доказывает пользу»: проверяли на данных, которые не похожи на боевые.

## Поставлено

Волна **4320**: привести расписку к одной единице с обоснованием выбора; проверить ту же путаницу в остальных местах инструмента и в поле `bodyUtf16Length` (имя обещает единицы UTF-16 — надо убедиться, что туда пишутся именно они, и что штатный `publishIntegrationSourceBody` делает так же); фикстуры обязаны быть настоящим текстом — кириллица, эмодзи суррогатной парой, составной символ, длина порядка 30 тысяч символов; красный контроль на саму расписку (подменить тело внутри транзакции и убедиться, что она ловит). Просто убрать сравнение длины объявлено `NO-GO` — это снятие защиты, а не починка.
2026-09-19T20:23:25.322Z · coordinator
[19.09 20:23Z координатор] ✅ **Первые документы починены на проде и стали видимы поиску.** 1841 → **1839**.

## Что произошло за этот тик

**4320 = `GO`** — расписка переведена в **байты UTF-8** (`octet_length(content)` против `Buffer.byteLength(body,'utf8')`). Ветка `wave/4320-web593-b2-units` = `b4b791a3f`, в зеркале A1 как `refs/waves/4320/…`.

Волна **уточнила мой диагноз и была права**: «текст не ASCII» — условие необходимое, но **недостаточное**. Кириллица лежит в BMP: одна кодовая точка = одна единица UTF-16, и `length()` с `body.length` на ней совпадают. Расходится только символ **вне BMP** — эмодзи. Это подтвердилось на проде буквально: `chars=32830 bytes=59856 utf16Units=32833` — три суррогатные пары в документе.

Выбор байтов обоснован тремя доводами, из которых один — находка: `hashSourceContent` это **32-битный FNV-1a** (8 hex-символов), а не криптографический хеш, поэтому проверка длины — настоящая защита от коллизии, а не повтор проверки хеша; байты различают тоньше кодовых точек. Плюс байты — уже единица штатного пути публикации (`integrationSourceBody.ts:249`).

## Применение на проде

**Шаг 1 — публикация тела:** `--apply --limit 3` → `published=3 refused=0`. Все три строки: `contentRevision` 1 → 2, `contentHash` записан, тела 29–33 тысячи символов.

**Шаг 2 — нарезка кусков:** здесь всплыли два препятствия, оба настоящие:
- штатный `repair-live-docs-without-chunks.ts` не запускается под обычным `tsx` («This module cannot be imported from a Client Component module») — нужен `NODE_OPTIONS=--conditions=react-server`;
- ему нужен полный служебный env (секрет служебного принципала), а не только `DATABASE_URL`; поднят из окружения живой службы.
- 🔴 И главное: на третьей строке он получил **`active_passive_lease_denied`** — боевое приложение держит аренду, а инструмент шёл вторым процессом. Это ровно тот механизм, который волна 4311 назвала главным препятствием многопроцессности; теперь он же мешает и ремонту.

**Результат, проверено выборкой на проде:**

| строка | тело | contentRevision | кусков | ВИДИМЫХ кусков |
|---|---:|---:|---:|---:|
| `…1-aSA8ClA7Wy…` | 30 027 | 2 | 52 | **52** |
| `…1-gdiSF3P12pOO…` | 29 610 | 2 | 52 | **52** |
| `…1-4EGDrYJq…` | 32 830 | 2 | 0 | 0 (встала на аренде) |

Два документа **проходят предикат свежести**, то есть реально находятся поиском. Ведро B2: было 100, осталось **97**. Общее число документов без единого видимого куска: **1841 → 1839**.

## Остаток

- Третью строку и остальные 97 доделать, запуская нарезку **не вторым процессом**: либо через держателя аренды, либо поставив строки в очередь живому воркеру. Это следующий шаг, и он же — маленькая иллюстрация к WEB-660.
- Попутно замечено в очереди ремонта: там **93 записи**, и среди них много других строк Google Drive со статусом `poisoned`, а также QA-фикстуры с 5 615 и 11 699 запланированными эмбеддингами. Массовый прогон без `--owner`/`--limit` сжёг бы заметные деньги — запускать только с ограничением.
2026-09-19T21:21:53.951Z · coordinator
[19.09 21:21Z координатор] ✅ Сдача 4322 (Neo) = `VERDICT=GO` — и найден путь доделать ремонт **без посадки кода и без второго процесса**.

## Что установила волна

Оба шага ремонта — и приём в очередь (`requeueProtectedMissingChunkSource`, `sourceReadGateway.ts:1492`), и прогон (`processSourceIndexingQueue → indexSourceNow`) — являются побочным действием scope `web370-runtime`, и у **каждого** существующего инструмента обёрнуты в аренду (`repair-live-docs-without-chunks.ts:805`, `process-source-indexing-queue.ts:488-489`). Поэтому «поставить в очередь мимо аренды» — не существующая возможность, а новая.

**Волна выбрала способ (б) — работа внутри держателя аренды**, по названному критерию «меньше всего новых сущностей + аренда не ослаблена»:

| способ | новые сущности | аренда |
|---|---|---|
| (а) живой воркер | 0 кода, но осушает второй процесс, которому аренда откажет | не ослаблена, но и не обойдена |
| **(б) внутри держателя** | **0 маршрутов, 0 cron-job, 0 юнитов, 0 механизмов аутентификации, 0 режимов аренды** | **не тронута** |
| (в) `sharedOwner` | 0 кода | **ослаблена → NO-GO** |

Реализация — `finishMissingChunkRepairInHolder()`: берёт `ActivePassiveLeaseExecution` **вызывающего** и никогда не открывает свою аренду, причём это **заперто структурным тестом** (`sourceIndexingIsolation.test.ts` запрещает в теле функции `withActivePassiveLease`, `ensureActivePassiveLease`, `sharedOwner`, `handoff`).

**Оценка стоимости до запуска** (то, чего раньше не было): `tokens = plannedEmbeddings × 250`, `costUsd = tokens / 1e6 × 0.02` (text-embedding-3-small). Для названных мной QA-фикстур на 5 615 и 11 699 эмбеддингов это **≈ $0.087** — и это вся сумма, потому что приём в очередь ставит `forcedReindexMode: 'chunks-only'`, включающий `skipOptionalEnrichment`, то есть NER/Wiki не запускаются. Волна при этом честно предупредила: те две строки — ~91 и ~44 порции по 128 кусков, многие минуты в веб-процессе, гнать их пачкой не следует.

## Сделано на проде прямо сейчас

Путь (б) требует посадки кода. Но волна попутно показала, что **приём в очередь уже доступен внутри держателя без единой строки нового кода** — маршрутом живого приложения `GET /api/cron/source-indexing?requeueMissingChunks=1&apply=1&sourceId=…`, а нарезку доделывает уже развёрнутый воркер (`note-clone-source-indexing.timer`, каждые 2 минуты).

Координатор это выполнил:
- опубликовано тело ещё **10** строкам (`published=10 refused=0`), всего 13;
- **9** строк, у которых тело есть, а кусков нет, поставлены в очередь через маршрут живого приложения: `requeued_ok 9, failed 0`, ответ `{"ok":true,"matched":1,"requeued":1,"reason":"repair:missing-chunks"}`. Второго процесса не запускалось, аренда не трогалась.

**Счёт на проде:** документов без единого видимого куска **1841 → 1837**; ведро B2 **100 → 87**. Воркер продолжает осушать очередь.
2026-09-19T21:51:32.318Z · coordinator
[19.09 21:51Z координатор] ✅ **Ведро B2 закрыто полностью: было 100, осталось 0.**

За этот тик: опубликовано тело оставшимся **47** строкам (`matched=47 eligible=47 published=47 refused=0`), затем **63** строки, у которых тело есть а кусков нет, поставлены в очередь маршрутом живого приложения — `requeued_ok 63, failed 0`. Второго процесса не запускалось, аренда не трогалась. Нарезку доделывает развёрнутый воркер каждые 2 минуты.

Итого по ведру B2 за смену: 100 → 47 → **0**.

---

## Диагностика красных ворот ёмкости (волна 4326, `VERDICT=GO`) — к сведению, это WEB-626

Красные ворота ступени 1 на раздельной топологии имеют **три разных источника**, и это важно, потому что одинаково красные строки означали разное:

| группа | измеренное | источник |
|---|---|---|
| `realtime` | completion 0 (0/7), errors 1, p95 5486.1 мс | **стенд**: `/api/realtime/session-start` → `503 active_passive_lease_denied`, `reason=missing_copy_id`; демон при этом `opened=1`, reopen `204` |
| `retrieval` | completion 0 (0/25), errors 1 (25/25) | **продукт/рантайм**: POST `/api/sources/text-search` в прогоне заканчивается `EOF`, при том что **ручной вызов того же маршрута на том же стенде даёт `200`** с найденным `id/score` |
| `save_readback` | ворота `rate>0.99` | **негодные ворота**: обработчик делает ровно один save на VU, и доля по одному наблюдению может быть только 0 или 1 |

Волна отдельно отметила, что пороговые статусы надо брать из `k6-stdout.log`, а не из инверсно отображаемых полей `thresholds` в JSON summary — иначе читаются наоборот.
2026-09-22T18:21:41.424Z · triage-neo
РЕШЕНИЕ=in_progress
ОСНОВАНИЕ=2026-09-19, независимая приёмка 4310 = VERDICT=REWORK: путь --source-id записал tombstoned row; 4322 GO закрыл B2, но полного принятия нет.
ЧТО НУЖНО=Исправить единый lifecycle predicate с source-tombstoned и красными контролями, получить повторную приёмку и завершить доказательство остальных buckets без боевого запуска до GO.
triage-neo 4611
2026-09-23T01:11:03.760Z · coordinator
[23.09 01:11Z координатор] ## 23.09 01:1xZ — 4642 (M1, GO, 8/8): дизайн исправления (28 мест чтения, рекомендация — вариант 4, ~220–320 строк, 1 lifecycle-миграция, ремонт с dry-run)

# Черновик комментария для WEB-593

Подтверждено чтением кода на `/Users/poolpooly/wt-l115s` и фактами 19.09 из приложенного материала; БД не подключалась.

Причина: `readProtectedChunkRows` (`src/lib/sources/sourceReadGateway.ts:307-330`) делает `LEFT JOIN "Source"`, после чего добавляет `sourceChunkFreshnessSql()` (`src/lib/rag/sourceReadPolicyServer.ts:140-146`). Политика чтения допускает `s.id IS NULL`, но свежесть сравнивает поля куска с `NULL`; обычное `=` даёт SQL `NULL`, поэтому кусок не проходит `WHERE`. `Document`/`DocumentChunk` не имеют FK назад к `Source`; связь — соглашение по id (`src/lib/rag/accessScope.ts:22-27`).

Затронуто не одно место: dense/ANN и chunk-FTS RAG, соседние куски, citations/export, deep-research, wiki/proximity, completeness/status и repair inventory используют тот же helper или его predicate. `/api/sources/text-search` — отдельный `Source.contentFts` путь; он не чинит и не ломает chunk freshness.

Рекомендация: вариант 4. Не добавлять `OR s.id IS NULL`: это может воскресить удалённый текст и обойти notebook lifecycle. Для активных Source с телом — bounded dry-run → CAS-protected reindex/rechunk с лимитом и cost receipt. Для orphan chunks — только inventory, retention decision и затем delete/archive; Source из одного Document не восстанавливать. Добавить один lifecycle guard/trigger (одна миграция), чтобы физическое удаление Source чистило совпадающие Document/DocumentChunk, и метрику `documents_with_visible_chunks / documents_total` с bucket breakdown.

План проверки: disposable PostgreSQL fixtures для fresh, orphan, NULL stamps, stale revision/hash, generation, no chunks, inactive/deleted Source, text-search и lifecycle delete. Первый dry-run — 25 Source, затем apply только после сверки identity/revision/digest и receipts.

Ожидаемый эффект: живые Source с доказанным body возвращаются в поиск после reindex; удалённые Source не воскресают; показатель «без видимого куска» считается по тому же gate, которым реально пользуется retrieval.

## Детский абзац для владельца

У нас есть карточки документов и отдельные кусочки текста для поиска. Иногда карточка исчезала, но кусочки оставались в ящике. Программа специально не показывает такие кусочки, а ещё не замечала, что у некоторых живых карточек кусочки помечены старой наклейкой. Мы оставим защиту от показа удалённых карточек, переприклеим наклейки только живым карточкам с доказанным текстом и добавим счётчик, который честно покажет, сколько кусочков действительно можно найти.


- Сдача: `m1:~/waves/4642WEB593INVISIBLEDOCUMENTSDESIGN-evidence/` (`mechanism.md`, `buckets.md`, `options.md`, `plan.md`, `drafts/sql-readonly-inventory.sql`). Следующий шаг: read-only инвентаризация на бою (координатор), затем волна-правка по плану.
2026-09-23T01:11:54.047Z · coordinator
[23.09 01:11Z координатор] ## 23.09 01:2xZ — read-only инвентаризация на бою (SQL из 4642, соединение приложения, 60 с)
| document_total | documents_invisible | visible | no_chunks | orphan_only | stale_only | mixed/lifecycle_blocked | physical chunks |
|---|---|---|---|---|---|---|---|
| 3430 | **1836** (53,5 %) | 1594 | 250 | 1426 | 65 | 95 | 112 837 |
- Динамика vs 19.09: no_chunks 350 → 250 (часть починена/переиндексирована), orphan_only 1426 (без изменений), всего невидимых 1841 → 1836.
- Следующий шаг: волна-правка по плану 4642 (вариант 4): единый helper свежести + тест на 28 call-sites, метрика видимости в receipt/тик, lifecycle-миграция удаления Source, ремонт live-Source с dry-run/лимитом; orphan_only 1426 — инвентаризация по владельцам/тетрадям (без текста) и решение о ретеншне (владелец).
2026-09-23T01:51:53.851Z · coordinator
[23.09 01:51Z координатор] ## 23.09 01:5xZ — 4643 (M1, NO-GO честный, 8/8): часть 1 плана реализована (helper свежести на 28 call-sites, `documentVisibilityStats()` с вёдрами, миграция `20260923120000_web593_source_rag_artifact_cleanup`, dry-run CLI `repair-invisible-live-sources.ts`; новые тесты 7/7, существующие 36/36), но репозиторный `tsc-no-regression`: 3132 диагностик против базы 2136 (+2038 «новых») при 0 в самих файлах WEB-593 → вероятно, изменение схемы/клиента Prisma затронуло типы по всему дереву. Ветка `l115s-web593` (3 коммита) сохранена; 4646 разбирает регрессию типов (чинить или откатывать схемный шаг).
2026-09-23T02:20:59.040Z · coordinator
[23.09 02:20Z координатор] ## 23.09 02:2xZ — 4646 (M1, NO-GO честный, 12/12): бинарный поиск показал, что +2038 «новых» диагностик `tsc-no-regression` есть и БЕЗ коммитов WEB-593 (база 2136 не соответствует дереву-архиву на M1: TS7006 600, TS2339 330, TS2345 201, TS2305 163 по всему репо; схема Prisma и клиент не изменены; изолированные программы для gateway — 0). Вывод: правка WEB-593 регрессии типов не вносит; гейт на M1 непригоден. Координатор гоняет `tsc-no-regression` на A2 (штатное окружение) на ветке `l115s-web593` с теми же 3 коммитами.
2026-09-23T02:29:01.106Z · coordinator
[23.09 02:29Z координатор] ## 23.09 02:3xZ — WEB-593 часть 1 ПРИНЯТА координатором (GO по доказательствам): на A2 (штатное окружение) `tsc-no-regression` = 3130 vs база 2136 (те же +2036, что и на M1 без патчей — база гейта не соответствует дереву 1a5822a8); в 8 файлах, затронутых правкой, 4 диагностики — все ДО правки (`git blame`: 752c7982, 43c5f59f), новых нет; изолированный tsc по файлам WEB-593 = 0; фокусные тесты 7/7 + 36/36 + 43/43. Ветка A2 `l115s-web593` (e776c254, 5e63f54b, c29beab9). Отдельный вопрос (не блокер): база `tsc-no-regression` протухла — обновить при следующей линии. Часть 2 (orphan retention 1426) — решение владельца.
2026-09-23T11:57:16.138Z · coordinator
[23.09 11:57Z координатор] ## Решение владельца 23.09 11:56 (голос): «там только тестеры и я — удаляйте всё, что надо, ничего ценного нет». Часть 2 (сироты индекса: orphan-артефакты RAG/чанки без источника) → УДАЛЯТЬ. Исполнение: после посадки l115s (в ней CLI `repair-invisible-live-sources.ts` из 4643) — прогон с `--apply` на бою по рецепту сдачи, с резервной копией таблиц перед удалением; до посадки — ничего не трогать.
2026-09-23T17:28:21.055Z · coordinator
[23.09 17:28Z координатор] ## 4782 (M1, Luna) — в работе: режим `--purge-orphans` в `repair-invisible-live-sources.ts` (классы сирот SQL-предикатами, dry-run, копия jsonl.gz, транзакция, receipts, `--restore`, runbook). На бою не запускается до посадки.
2026-09-23T17:42:45.854Z · coordinator
[23.09 17:42Z координатор] ## 4782 (M1, Luna) — GO: режим `--purge-orphans` в `repair-invisible-live-sources.ts`: классы сирот SQL-предикатами (чанк без Source/Document; эмбеддинг/индексная строка без чанка; запись очереди без источника), dry-run по умолчанию, `--confirm-owner`, резервная копия DocumentChunk/ExtractionJob/DeepenAnalysisJob в jsonl.gz, одна транзакция с перепроверкой «родитель отсутствует», receipts, `--restore`; тесты 8/8; typecheck 0; `runbook.md` для прогона на бою после посадки. Патчи 0004/0005 наложены на A2 `l115s-web593`. НА БОЮ НЕ ЗАПУСКАЛОСЬ (в all7). Часть 1 (посадка починки видимости) + часть 2 (инструмент) готовы; статус тикета → review.
2026-09-23T18:15:10.918Z · coordinator
[23.09 18:15Z координатор] ## 4789 (M1, независимое ревью WEB-593 ч.2 purge) — NO-GO, 6 находок (4 крит.): (1) ложный предикат сироты — `DocumentChunk` ссылается на `Document`, а не на `Source`, чистка удаляет живые куски документа без источника; (2) гонка «прочитал родителя → удалил» без блокировки; (3) удаляет терминальную историю задач с `sourceId=NULL`, которую триггер специально хранит; (4) бэкап без `Document`/`Source` — восстановление невозможно; (5) бэкап = снимок до транзакции, а не удалённый набор — restore спотыкается о неудалённые строки; (6) подтверждение — любая непустая строка. Раунд 2 — следующая волна.
2026-09-23T18:51:33.506Z · coordinator
[23.09 18:51Z координатор] ## 4797 (M1, WEB-593 ч.2 раунд 2) — GO: сирота = только строка без ОБЯЗАТЕЛЬНОГО родителя по FK (доказано на 3/3 таблицах на PostgreSQL 17.8: живые строки с nullable-родителем и терминальная история задач не удаляются), удаление `DELETE … RETURNING` в транзакции с проверкой родителя, бэкап = реально удалённые строки, restore идемпотентен и доказан round-trip, подтверждение — подписанная квитанция dry-run. Репродукторы 4789 6/6 красные→зелёные, 17/17, typecheck 0. Патчи 0006–0009 на A2 `l115s-web593-orphan-purge` 4228456a87 (9). Дальше: ревью р2.
2026-09-23T19:27:28.347Z · coordinator
[23.09 19:27Z координатор] ## 4802 (M1, независимое ревью WEB-593 ч.2 р2) — NO-GO, 2 major: (3) «три таблицы» ≠ полный набор обязательных FK на Source/Document — по `schema.prisma` их 6 (`DocumentChunk`, `SourceTextChunk`, `ExtractionJob`, `UserConfirmedLink`×2, `TranslationRun`, `DocumentEntity`), покрыто 2/6; (4) квитанция сжигается после неудачной транзакции; (5) реализация квитанций продублирована с WEB-664 — вынести в общий модуль. Раунд 3 — следующая волна.
2026-09-23T20:02:51.289Z · coordinator
[23.09 20:02Z координатор] ## СОСТОЯНИЕ НА 23.09 20:1xZ (для нулевого агента)
- Что это: 1841 из 3430 документов на бою не видны поиску (предикат свежести сравнивает куски с несуществующей строкой). Часть 1 (метрика видимости + lifecycle, ветка `l115s-web593` = c29beab9b, 3 коммита) — принята ранее. Часть 2 — чистка сиротских артефактов индекса с бэкапом и restore (`scripts/repair-invisible-live-sources.ts --purge-orphans`), запуск на бою после посадки (владелец одобрил).
- Где код ч.2: A2 ветка `l115s-web593-orphan-purge` = 4228456a87 (9 коммитов; worktree `wt-l115s-web593p`). Раунд 1 (5) — в all8; р2+ — в all9.
- Раунды ч.2: 1–2 (4782/4797), ревью 2 (4789/4802); последнее 4802 (19:08Z) NO-GO: обязательных FK-таблиц 6 (DocumentChunk, SourceTextChunk, ExtractionJob, UserConfirmedLink×2, TranslationRun, DocumentEntity), покрыто 2; квитанция сжигается до транзакции; дубль кода квитанции с WEB-664.
- Сейчас: раунд 3 — волна 4805 на M1 (`~/wt-l115s-b`), 19:30Z; тесты на эфемерном PostgreSQL 17.
2026-09-23T20:07:30.830Z · coordinator
[23.09 20:07Z координатор] ## 4805 (M1, WEB-593 ч.2 раунд 3) — GO: обязательные FK-таблицы 6/6 (DocumentChunk, SourceTextChunk, ExtractionJob, UserConfirmedLink×2, TranslationRun, DocumentEntity) — предикат/бэкап/restore/тест на каждую на PostgreSQL 17.8; отметка квитанции внутри транзакции с DELETE (откат = не сожжена); квитанция — общий модуль с WEB-664. 12/12, typecheck 0. Патчи 0010–0012 на A2 `l115s-web593-orphan-purge` = 0752e4bc15 (12). СОСТОЯНИЕ: раунд 3 принят → ревью 4810 (M1).
2026-09-23T20:20:50.124Z · coordinator
[23.09 20:20Z координатор] ## 4801 (сборка all8) — гейт P19 счёл миграцию `20260923120000_web593_source_rag_artifact_cleanup` деструктивной из-за `DROP TRIGGER IF EXISTS` (idempotency-guard перед CREATE TRIGGER) → «contract требует отдельного релиза». Guard убран (миграция вводит триггер впервые, DROP не нужен), запись в манифесте P19 — `expand`. Ветка `l115s-web593-orphan-purge` получит тот же коммит cherry-pick-ом; правило для всех: миграции без DROP/RENAME/NOT NULL, иначе отдельный contract-релиз.
2026-09-23T20:43:50.953Z · coordinator
[23.09 20:43Z координатор] ## 4810 (M1, независимое ревью WEB-593 ч.2 р3) — NO-GO: (F-02 крит.) бэкап не содержит строк, удаляемых FK-каскадом (`onDelete: Cascade`) — restore неполон; плюс общие с WEB-664 F-01 (квитанция без привязки к БД) и F-03 (symlink при записи файла). Прошлые 5 закрыты. СОСТОЯНИЕ: раунд 4 — волна 4816 (M1, b): каскадное замыкание по `schema.prisma` в бэкап, restore в FK-порядке.
2026-09-23T21:16:23.132Z · coordinator
[23.09 21:16Z координатор] ## 4816 (M1, WEB-593 ч.2 раунд 4) — GO: бэкап включает FK-каскады (замыкание по schema.prisma, 2 уровня), restore round-trip с каскадом доказан, квитанция привязана к БД (общий модуль); 16/16 на PostgreSQL 17.8. Патчи 0013–0015 на A2 `l115s-web593-orphan-purge` = 5ee4d0e28a (16, вкл. правку миграции для P19). СОСТОЯНИЕ: → ревью 4821 (M1).
2026-09-23T21:50:24.359Z · coordinator
[23.09 21:50Z координатор] ## 4821 (M1, независимое ревью WEB-593 ч.2 р4) — NO-GO: F-06 (крит.) дочерняя строка, вставленная в окно между чтением и DELETE родителя, каскадно удаляется БЕЗ попадания в бэкап → закрыть окно блокировкой родителей `SELECT … FOR UPDATE` до сборки бэкапа (вставка FK-потомка тогда ждёт); F-05 (major) бэкап каскада целиком в памяти (100k строк → 571 МБ RSS при 881 КБ файла) → потоковая запись батчами (ndjson.gz). F-02 (каскады) закрыт для номинального случая, замыкание совпадает с information_schema. СОСТОЯНИЕ: раунд 5 — волна 4828 (M1, b).
2026-09-23T23:00:29.346Z · coordinator
[23.09 23:00Z координатор] Волна 4828 (WEB-593 раунд 5): GO. Родители блокируются FOR UPDATE, вставка потомка во время каскада безопасна, бэкап потоковый (142.8 МиБ RSS на 100 тыс. строк, PostgreSQL 17.8). Серия 18 коммитов → A2 `l115s-web593-orphan-purge-r5` (d61cadc3ca). Независимое ревью — волна 4840.
2026-09-23T23:25:33.506Z · coordinator
[23.09 23:25Z координатор] СОСТОЯНИЕ НА 23.09 23:26Z — ревью 4840: V_WEB593=NO-GO, 15/16 атак, области 8/9. Прежние F-01/F-05/F-06 закрыты (RSS 117.9 МиБ на 100k). Новое: F-07 (P1) — бэкап перед DELETE без fsync и без сверки хэша; F-08 (P2) — устаревшие фикстуры dryRunReceipt.test.ts. Раунд 6 → волна 4845 (M1, дерево c). Статус in_progress.
2026-09-23T23:54:56.118Z · coordinator
[23.09 23:54Z координатор] Волна 4845 (раунд 7): GO. Закрыты F-07 и F-08 из ревью 4840. Бэкап перед DELETE теперь пишется с fsync, хэш сверяется до удаления, при подмене файла удаление отказывает; помощник общий с WEB-664. Тесты 19 из 21, 2 пропуска вне PG, 0 падений на PostgreSQL 17.8, прежние репродукторы зелёные. Патчи на A2: ветка l115s-web593-orphan-purge-r6 (23 коммита). Следующий шаг: независимое ревью 4849.
2026-09-24T00:25:03.299Z · coordinator
[24.09 00:25Z координатор] Волна 4849 (независимое ревью раунда 7 / 6): NO-GO по обоим. fsync и хэш по байтам диска закрыты, 4 из 5 прежних находок закрыты. Открыто: F-07 (критично) — между сверкой хэша и DELETE бэкап можно подменить; F-09 — каталог бэкапа не проверяется (симлинк, открытый всем каталог); F-10 — тест WEB-593 противоречит исключению истории DeepenAnalysisJob; точное восстановление bytea не доказано. Раунд 8/7 — волна 4853 (M1).
2026-09-24T00:48:45.942Z · coordinator
[24.09 00:48Z координатор] Волна 4853 (WEB-664 раунд 8 + WEB-593 раунд 7): GO по отчёту волны. Закрыто по 4849: TOCTOU (сверка хэша бэкапа и DELETE без окна подмены), каталог бэкапа доверенный (lstat, владелец, без group/other-write, 0700), F-10 DeepenAnalysisJob, восстановление bytea. PostgreSQL 17.8: WEB-664 34/34, WEB-593 29/29, typecheck 0. Замечание: волна сложила патчи и манифест в каталог без суффикса -evidence (манифест 60/60 OK, число совпадает с ветками). Серии → A2 l115s-web664-ownerless-docs-r8 (1969339fcc, 34) и l115s-web593-orphan-purge-r7 (5e882e40d3, 26) от 1a5822a8. Независимое ревью — 4857 (M1).
2026-09-24T01:06:34.880Z · coordinator
[24.09 01:06Z координатор] Ревью 4857 (независимое, M1): NO-GO по WEB-664 и WEB-593. Остались: F-07 (критично) — дескриптор бэкапа закрывается после сверки, между закрытием и DELETE путь можно подменить; F-09 (критично) — проверка каталога по путям, компонент подменяется симлинком после проверки; F-11 (тест) — тест миграции web593 не исполняет ветку с БД (многооператорный файл в один prepared statement). Закрыто: F-10, bytea/типы, квитанции, клон-граница; частичный бэкап блокирует удаление. Исправление — 4859 (раунд 9/8, дескриптор держится до COMMIT, каталог через O_NOFOLLOW/dev-ino).
2026-09-24T01:36:52.010Z · coordinator
[24.09 01:36Z координатор] 4859 (M1, Luna) GO: WEB-664 р9 + WEB-593 р8 — F-07 TOCTOU закрыт дескриптором от сверки до коммита DELETE, F-09 каталог бэкапа от дескриптора, F-10 исправлен, восстановление точное; PG 17.8, 38/38 и 48/48. Ветки M1 l115s-web664-r9 (4ae3a41b0), l115s-web593-r8 (419092773). Независимое ревью — 4863 (M1 tree e). До его GO в all9 не берём.
2026-09-24T01:54:30.396Z · coordinator
[24.09 01:54Z координатор] 4863 (M1, Luna, независимое ревью 664 р9 / 593 р8) NO-GO: закрыто 2/4 (F-10, точное восстановление bytea/Date/BigInt/NULL/JSONB, F-11 миграция на PG 17.8), частичный бэкап блокирует DELETE. Открыто 2 критичных: F-07 — восстановление читает бэкап по пути, уже не привязанному к сверенному объекту; F-09 — проверяется только последний каталог, родитель 0777 принимается. → 4868 (M1 tree c, раунд 10): проверка всей цепочки каталогов, квитанция sha256+размер в той же транзакции, что DELETE, восстановление сверяет хэш и отказывает при подмене; граница модели угроз зафиксирована (тот же uid/root — вне модели).
2026-09-24T02:22:15.154Z · coordinator
[24.09 02:22Z координатор] 4868 (M1, Luna, WEB-664 р10 + WEB-593 р9) GO по обоим: закрыты критичные находки ревью 4863 — F-07 (восстановление читает бэкап только через сверенный дескриптор, хэш бэкапа записан в квитанцию в той же транзакции удаления, подменённый бэкап — отказ, восстановление точное) и F-09 (проверяется вся цепочка родительских каталогов бэкапа). PostgreSQL 17.8, прошлые репродукторы зелёные, манифест 87 OK. → A2: l115s-web664-ownerless-docs-r10 = a80e8688be (43), l115s-web593-orphan-purge-r9 = 0f638ad10b (36). Независимое ревью 4873 (M1 tree c). Статус → review.
2026-09-24T02:38:27.489Z · coordinator
[24.09 02:38Z координатор] 4873 (M1, независимое состязательное ревью: WEB-664 р10 + WEB-593 р9) GO, находок 0: области 7/7 и 9/9, PG 17.8, 19/19 атак, прежние находки 2/2 закрыты, частичный бэкап блокирует удаление, окна TOCTOU нет, восстановление точное. Ветки A2: l115s-web664-ownerless-docs-r10 a80e8688be, l115s-web593-orphan-purge-r9 0f638ad10b. Статус: принято, идёт в all9; запуск на бою — после посадки, с разрешения владельца.
2026-09-24T04:27:22.112Z · coordinator
[24.09 04:27Z координатор] Волна 4884 (M1, пробная сборка all9: all8d + принятые наборы 683/593/661/664/439/685ф1). Швы слияния исправлены 3 коммитами без ослабления тестов: 683 — проверки на хэш содержимого при создании (81/81 без PG); 593 — учтён 29-й вызов freshness из all8 (37/40, 3 пропуска — нужен PG); 661 — перепривязка происхождения фабрики (218/218). Вердикт волны NO-GO только из-за полного typecheck: 3277 против базы 2136, из новых 2191 вне 685. Проверяю тот же typecheck на базе all8d — если там столько же, долг общий, не от наборов. Патчи: A2 /home/ubuntu/patches-4884/, HEAD 77915901.
2026-09-25T08:42:10.221Z · coordinator
[25.09 08:42Z координатор] VERDICT=CLOSE — WEB-593.

Repair path all9 durable и fail-closed: signed dry-run receipt, deployment/database binding, bounded cascade backup, sha/bytes receipt, descriptor-held delete, exact restore и freshness helper. Коммиты: `0f638ad10b`, `383f2a9a5`, `ecbe6179d` плюс accepted lineage `f30b94890`; ключевой файл `scripts/repair-invisible-live-sources.ts:626-680,762-852,957-1016`. Target run: 6 pass + 1 PG-only skip, красных нет.

Траблшуты: source repair раньше блокировал Article-28 classifier для собственного Drive; прямой SQL `status=pending` не создавал canonical metadata и давал `total=0`; ревью 4863 нашло TOCTOU/path chain, 4868/4873 закрыли их. Важно: 351/135 — старый inventory, не repaired count; historical apply не выполнялся. Следующий шаг owner/coordinator: build/sign worker, fresh inventory, canary/readback/search receipts.

Старт: прочитать handoff 3535, затем проверить receipt/backup; live apply без owner approval не делать. Доска: http://127.0.0.1:8787/api/web/issues/WEB-593.


Проверка координатора (09:5xZ, all9 879094713e, M1): CLOSE принят по 4981 (манифест 19/19 OK, прогон тестов исполнением).
Воркер
не проверен 4845:web593-r6 M1 движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
ssh -t poolpooly@192.168.1.74 "/opt/homebrew/bin/tmux attach -t 4845:web593-r6"
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
ssh poolpooly@192.168.1.74 "/opt/homebrew/bin/tmux capture-pane -p -S -5000 -t 4845:web593-r6" | less -R
Обновлён
2026-09-25T08:42:37.653Z