WEB-324 · Задача · — · web
Движок Gate D: controlled writer — движок владеет записью в ограниченных сценариях (NO-GO до закрытия WEB-323 + owner-одобрение)
Закрыт
P2
ведёт: —
Суть
## Карта: ДВЕ лестницы движка (зафиксировано 23.08 после вопроса owner)
ЛЕСТНИЦА ГЕЙТОВ DocumentSession (доверие движку): Gate A контракт ✅ (document-session-contract-v1) → Gate B библиотека ✅ (F01-F06, 311+135 тестов, 8 раундов V4→V4Q) → Gate C legacy shadow ✅ (ЗАКРЫТ 23.08: пакет R22G-F4, вердикт аудита GO — браузерные пруфы selection/clipboard/edit-mirror, ON/OFF артефакты, WEB-089 CLOSED-VERIFIED) → Gate D controlled writer (NO-GO, ждёт интеграции+одобрения) → Gate E mutating AI (NO-GO) → Gate F production collaboration (NO-GO).
ПРОДУКТОВАЯ лестница этапов редактора (другая!): этап A P0-фиксы ✅ (в проде) → этап B docsession-веер фич (WEB-283) → этап C CodeMirror-гибрид для ТЯЖЁЛЫХ ФАЙЛОВ (WEB-086) → этап D спайк ProseMirror.
ВАЖНО: движок-библиотека живёт в ОТДЕЛЬНОМ дереве (wq/o3/docsession-*) и НЕ является предком прод-линии (WEB-304) — т.е. Gates B/C приняты аудитором, но этот код НЕ в serving. Первый шаг к проду = WEB-323 (интеграция ship-dark).
Открывается ТОЛЬКО после WEB-323 (интеграция + телеметрия шейдоу без расхождений) и отдельного одобрения owner. Скоуп: запись под флагом, путь отката на легаси, пакет ревьюеру.
## 2026-08-23 18:20 UTC — КОД Gate D пишется в параллель (A1 gated, Luna xhigh)
Суд и включение — только после прод-телеметрии тени (формальный вход не меняется), но код controlled-writer (белый список 3 сценариев, отдельный флаг OFF, kill-switch fallback на легаси, 4 негатива) пишем уже сейчас в engine-line. Ожидаю GATED_WRITER_CODE_READY.
## 2026-08-23 18:55 UTC — КОД ГОТОВ (GATED_WRITER_CODE_READY)
controlled-writer.ts на ветке gated-writer (3ff5b60ef, engine-line): флаг DOCUMENT_SESSION_WRITER_ENABLED (default OFF, отдельный от shadow; factory бросает DisabledError до engine-вызовов), белый список ровно 3 сценария (plain-text-edit c pinned-read проверкой, undo/redo через operationHistory-порт, exact-save через flushPendingInput→persistWithCAS), kill-switch: любой refusal/divergence/barrier-failure → bounded receipt + отключение сессии + legacy-победитель (dual-write нет). 7 layer/negative тестов, DOM-free scoped tsc, NEXT_PUBLIC-алиас default '0'. Product callsite НЕ подключался — по плану после телеметрии. Bundle спасён на Интел (d6ef398c…). Формальный Gate D GO не заявлен (честно).
## 2026-08-23 21:10 UTC — план телеметрии согласован с owner (юзеров нет — активность делаем сами)
Owner напомнил: реальных юзеров на проде нет. План сбора телеметрии тени для суда Gate D:
1. После посадки 34: env-флип DOCUMENT_SESSION_SHADOW_W1_ENABLED=1 (runtime, ship-dark активация; клиентский NEXT_PUBLIC-флаг — учесть, что он build-inline: если для клиентской части нужен ON — потребуется сборка с флагом или серверная выдача — уточнить по коду web323, где граница).
2. Источник активности: скриптованные браузерные сессии с M4 (playwright, QA-учётка): открыть/править/сохранить/undo/redo/jump по разным документам ВКЛЮЧАЯ большие — сотни операций за ночь. Бесплатно (редактор не платный путь). Ручная работа owner тоже засчитывается.
3. Каждая операция → расписка сравнения (divergence recorder, JSONL). Утром: свод расхождений (цель 0 или объяснимые) → пакет судье → суд Gate D → при GO подключение writer-callsite (код уже готов, gated-writer 3ff5b60ef).
## 2026-08-23 22:25 UTC — телеметрийная ON-сборка запущена (l34shadow, тот же источник b21dea6a0, клиентский флаг build-inline=1). После неё: посадка shadow-варианта → час скриптованных сессий (M4) → свод расхождений → суд Gate D сегодня.
## 2026-08-24 04:25 UTC — П��САДКА 35s СЕЛА (arm64-35s, artifact 66974112…, пин-гейт 7/7): клиентская тень ON (байт-пруф сборки) + серверный флаг + realtime-поза в dropin. Соук №2 запущен (shadowrun2, M4): создание доков теперь работает (web334), час операций → суд Gate D.
## 2026-08-24 05:05 UTC — соук №2: web334 работает (малый+средний доки писчие ✅), но встал на большом (220К клиентский отказ, инструкция «стоп»). Тень активна, console-ошибок shadow 0. Перезапуск shadowrun3 на малом+среднем (без большого) для набора операций → суд Gate D. Большой док = отдельный WEB-337.
## 2026-08-23 BOARDSWEEP: IN_PROGRESS-LIVE
- Evidence: updatedAt=2026-08-23T15:36:16.169Z, within 24h; Gate-D controlled-writer work is current and explicitly depends on WEB-323/owner approval. No status change.
- buildFixed: <empty>; canonical: v4-081ff4fb5 / 081ff4fb5f6110f8001daec7ed910ccef2e395dc.
- Status preserved by sweep: in_progress.
Доказательства
[2026-08-25 ~03:30] OWNER-ОДОБРЕНИЕ ПОЛУЧЕНО: голос 23:27 «можем ли делать Gate E/F — если можно, срочно делай» = одобрение продвижения лестницы; E/F технически зависят от D (controlled writer) → D разблокирован (WEB-323 закрыт ранее + одобрение теперь есть). Ответ owner с планом: tg 13330. Волна Gate D — в первый свободный слот A1/M4 (сейчас: A1=R9.17 роли+замер миграции, M4=r28 фактчек+shadowrun5 телеметрия). Вход для судьи Gate D: JSONL телеметрии (456+ записей, растёт соуком) + evidence WEB-347/354.
[2026-08-25 ~05:20] GATED_W1_DONE — Gate D построен тёмным (волна gated-w1 на A1, Luna xhigh, ветка gated-w1, commit 0c699d6c7, worktree wt-gated-w1). Дизайн: NEXT_PUBLIC_DOCUMENT_SESSION_GATED_WRITER (default 0), DOM-free adapter src/lib/document-session/gated-writer.ts — фаза 1 только plain-insert/delete/undo/redo, движок коммитит первым, легаси shadow; рантайм kill-switch (localStorage+URL, работает без пересборки — урок WEB-342); fallback на легаси при любом исключении + safety beacon (правило 10); телеметрия v2 producer=engine (backward-compat producer=legacy для Gate C). Пруфы: Gate D targeted 16/16, W1/W2+telemetry+final regression 25/25, soak 1000+ ops, ЗАМОРОЖЕННАЯ 311-сюита 311/311 rc=0, scoped tsc rc=0 (full-repo tsc падает на pre-existing baseline вне движка), ESLint 0 errors. Не вошло (фаза 2+): paste/formatting/rich-команды, collaboration, save-protocol. Отчёт: A1 /home/ubuntu/waves/GATED-W1-REPORT.md. Следующее: посадка 49 (ship-dark, флаг 0) после освобождения build-слота (сейчас на A1 идёт замер бэкапа — webpack исказил бы цифры); ВКЛЮЧЕНИЕ D — по вердикту телеметрии Gate C (соук идёт, 5298+ записей).
[2026-08-25 ~12:05] ПОСАДКА 49 (l53) СЕЛА: прод v4-70ab8c1 (l48-salvage + Gate D port fa58f4e + WEB-352 70ab8c1), артефакт sha 2be7aba1 (171918242Б), аттестация подписана координатором ЛОКАЛЬНО (make-activation-attestation.mjs на ноуте, все хеши детерминированно сошлись с l52), paidReady=true, attestation.ok=true, migrations ok. Хроника флипа: попытка 1 упала (sed задвоил WorkingDirectory и путь server.js в inner-скрипте) → авто-откат на l52 отработал (прод зелёный через секунды) → точечный фикс путей → попытка 2: PAID_READY за 5с. Суммарный простой ~3 мин. Автодокладка в сейф: артефакт l53 в Hetzner app-artifact/ (теперь 2 версии, политика keep-3). Gate D в проде ТЁМНЫМ (NEXT_PUBLIC_DOCUMENT_SESSION_GATED_WRITER не вшит, GATED-WRITER-DARK-OK гейт сборки). Включение — по вердикту телеметрии Gate C + отдельный флип.
[2026-08-25 ~15:40] Посадка 50 (Gate E+F merged, fd5e1b7) НЕ загрузилась на Pi — расследование: (1) L50FIX доказал merged КОД грузится нормально (Ready in 0ms, listen, health 200) — E/F не в серверном startup-графе (клиентские), не они виноваты. (2) Структура release-dir l54 идентична рабочей l53; env (inner exec, artifact-sha ea8cb439, live-sha fd5e1b7) консистентны. (3) Симптом systemd-boot l54: egress-self-check идёт каждые 5с, но «Ready in» НЕ печатается — застревание в РАННЕЙ init (до Next.js listen, до boot-env-contract), вероятно spend/attestation-init. (4) Мой ручной порт-тест НЕВАЛИДЕН: bash source env-файла подавился SPEND_DEPLOYMENT_MANIFEST (запятые/пайпы), EMAIL_FROM ложно unset — systemd грузит EnvironmentFile правильно, EMAIL_FROM+SMTP_HOST оба заданы, контракт проходит. Прод БЕЗОПАСНО на l53 (посадка 49, авто-откат сработал). Gate E/F КОД готов и в линии (l48-salvage); посадка отложена до воспроизведения под systemd-env (не bash) и локализации ранней init-блокировки. Урок: воспроизводить boot ТОЛЬКО через enforce-start-wrapper/systemd, не bash source (env-файл с манифестом не парсится bash).
[2026-08-25 ~16:20] КОРЕНЬ boot-затыка посадки 50 НАЙДЕН точечным диагом (systemd на тест-конфиге, снят /api/ready/paid): attestation.ok=False reason=attestation_manifest_hash_mismatch. server биндит порт (Ready in, db-pool, email, egress-guard), но paidReady=false → авто-откат (прод безопасно на l53). Причина: рантайм сверяет attestation.manifestSha256 с манифестом ВСТРОЕННЫМ в артефакт; я подписал manifestSha256 из исходника (36d060fa), затем из ручной регенерации (97859706) — оба не совпали. Gate E изменил paid-entrypoints/инвентарь, сборка регенерирует манифест иначе ручного генератора (prebuild регенерит generatedActivationManifest.ts → git restore после). Урок: для посадок, меняющих billing-манифест, брать manifestSha256 из BUILD-версии (до restore), не из исходника. → L50ATT волна: извлечь точный встроенный manifestSha256 из артефакта, собрать правильный preimage → подпись координатора → переаттестация → флип. Прочие хеши (migration/estimator/coverage) тоже проверить build-методом. Прод НЕ пострадал (3 попытки флипа, все авто-откат).
[2026-08-25 ~16:50] 🛬 ПОСАДКА 50 СЕЛА (v4-fd5e1b7) — Gate E+F В ПРОДЕ ТЁМНЫМИ. paidReady=true, attestation.ok=true, sourceCommit fd5e1b7, migrations ok, аноним-телеметрия 401 (флаги OFF). Автодокладка: артефакт в Hetzner app-artifact/ (3 версии, keep-3). Gate D controlled writer тёмным (посадка 49) + теперь E+F. Лестница C→D→E→F: код всех в проде, включение по вердикту телеметрии C + owner-слово.Путь к посадке (3 барьера аттестации, все ПОЙМАНЫ тест-флипом через /api/ready/paid, прод не пострадал — авто-откат): (1) attestation_manifest_hash_mismatch — сборка регенерит billing-манифест (Gate E добавил mutating-ai paid-путь), правильный manifestSha256=e3439ec9 извлечён из артефакта волной L50ATT (не исходник 36d060fa, не ручная регенерация 97859706); (2) attestation_signature_verification_failed — preimage кончался \n (1000Б), рантайм канонизирует без него (999Б) → подписал 999Б; (3) runtime_comparandum_mismatch priceSnapshot — estimator/price статичны (source d962fb63), не build-значение 76112d3c что дала волна. v5 аттестация: manifest=build, price/estimator=source → GO за 5с. Уроки в памяти: attestation-manifest-from-build-not-source, reproduce-boot-via-systemd-not-bash-source.
[25.08 ~14:15] Owner дал ход лестнице: включать тёмные волны ПО ОЧЕРЕДИ с приёмкой. Запущена волна l54gd (A1): аттестуемый артефакт от c3fb44e (код не меняется) с NEXT_PUBLIC_DOCUMENT_SESSION_GATED_WRITER=1 запечённым (флаг build-inline — env-флипом не включить), E/F остаются тёмными. Гейт сборки перевёрнут: GATED-WRITER-ON-OK + DARK×2 для E/F. Стоп на L54GD_ARTIFACT_READY → подпись+деплой+браузерная приёмка ON/OFF оператором. Инвентарь флага и тесты gated-writer — в отчёт волны.
[25.08 ~15:15Z] GATE D ВКЛЮЧЁН В ПРОДЕ (по слову owner «включать по очереди с приёмкой»): релиз arm64-l58-20260825T124126Z, тот же source c3fb44e (код не менялся — чисто флаговый билд l54gd, артефакт 4ca56d5f). Активация подписана (те же хеши что 52/53, manifest 36d060fa BUILD), paidReady=true enforce attestation.ok, health 200 локально+публично. БАЙТ-ПРУФ из ЖИВОГО процесса: served chunk 93207… содержит Object.freeze({[nQ]:"1"}), l57 для контраста — "0". E (mutating AI) и F (collab) ОСТАЮТСЯ ТЁМНЫМИ. Runtime env также несёт NEXT_PUBLIC_…=1 (SSR-паритет). Откат: standalone-artifact.conf.bak-l57-20260825. Осталось: браузерная приёмка сценария controlled writer глазами → потом слово на Gate E.
📎 Трейл Gate D: волна l54gd (брief/лог/отчёт L54GD-REPORT.md на A1), релиз l58 (байт-пруф {[nQ]:"1"}), затем сохранён ON в посадке 54 (l59). Приёмка браузером — pending.
[2026-08-27 ~08:45Z БРАУЗЕРНАЯ ПРИЁМКА GATE D — НАХОДКА-РЕГРЕССИЯ] Приёмка на живом проде (QA qa-mailprobe-l43, EditorModal 'QA shadow probe'): runtime-дифференциал (синтетический beforeinput insertText) дал defaultPrevented=false → перехвата НЕТ. Байт-проверка served-чанка 52990.51ba57b065c407dc.js: freeze({[n0]:"0"}) — GATE D ЗАПЕЧЁН ВЫКЛЮЧЕННЫМ в l59. Контраст: l58-25.08(l54gd) чанк 93207 = "1"; уже l58-пересборка 26.08T150412Z = "0" → Gate D тихо погас 26.08, запись 'сохранён ON в l59' была неверна (сделана без байт-чека — урок). КОРЕНЬ: бриф l59build не нёс NEXT_PUBLIC_DOCUMENT_SESSION_GATED_WRITER=1 (build-inline; env-флипом не включить). SHADOW_W1=0 в l59 — намеренный (Gate C wiring-флаги =1 на месте). OFF-половина приёмки ПРОЙДЕНА (легаси-путь = текущий прод): ввод ✓, Backspace ✓, undo ✓, автосейв ✓; redo (Cmd+Shift+Z) на легаси видимого эффекта не дал — квирк нативного contenteditable, не Gate D. Маркер 'проба-GateD-OFF-l59' в QA-доке. ДЕЙСТВИЕ: запущена флаговая пересборка l59gd на M4 (тот же HEAD 2c6c2276, env l59build + GATED_WRITER=1, байт-гейты ON/EF-dark в брифе) → подпись по доверенности → деплой → ON-половина приёмки браузером (дифференциал должен дать defaultPrevented=true + kill-switch URL/localStorage откат).
[2026-08-27 ~09:45Z ПОСАДКА l59gd — Gate D ON запечён, но НЕ проявляется] Сборка l59gd (M4, тот же HEAD 2c6c2276, отличие только флаг): байт-гейты сборки PASS (GATED-WRITER "1", SHADOW_W1 "0", MUTATING_AI/COLLAB DARK x2), артефакт 29ad6759, аттестация подписана координатором по доверенности (pin PEM 0b83e0d3 = DER da2e5642 — ОДИН И ТОТ ЖЕ ключ, расхождение было в кодировке; positive verify + 2 негатива PASS). Доставка laptop->Pi sha сквозной, run-dir l59gd-2c6c2276 (все sha перепатчены: SPEND_RUNTIME_ARTIFACT_SHA256=29ad6759, RELEASE_SOURCE_COMMIT=2c6c2276 — в старом drop-in он был СТАЛЫЙ 5c5ccb1e от l58), wrapper self-check OK, drop-in с бэкапом .bak-l59-*. ПЕРЕКЛЮЧЕНО: active, health 200 (локально+Host), paidReady=true enforce_ready, 0 ошибок, sw v4-2c6c2276. Байт-пруф ИЗ ЖИВОГО БРАУЗЕРА: served chunk 80666.e2c5d5d473301672.js = freeze({[n0]:"1"}); старый чанк 52990 не грузится. НО ПРИЁМКА ЧЕСТНО НЕ ЗАКРЫТА: поведение неотличимо от OFF — redo (Cmd+Shift+Z) не восстанавливает, engine history НЕ primary; kill-switch (URL+localStorage) пуст, документ не read-only, React onBeforeInput навешен. Отдельно: синтетический InputEvent('beforeinput') — НЕВАЛИДНЫЙ тест активации (React мапит onBeforeInput на 'textInput'), прежний вывод по нему снят. Гипотеза №1 к проверке: Gate C product-wiring cohort=assigned — QA-юзер может быть вне когорты, тогда writer не создаётся. Волна gdiag (Luna xhigh) диспатчнута: найти корень + добавить НАБЛЮДАЕМЫЙ маркер активности (data-атрибут/window-стейт) для всех будущих приёмок ступеней. Откат l59gd: drop-in из .bak-l59-* + restart (артефакт l59 цел).
[2026-08-27 09:31Z accgd NO-GO — приёмка зарубила seam-фикс, ПОСАДКА ОТМЕНЕНА] Два блокера: GD-01 ДВОЙНОЕ ПРИМЕНЕНИЕ — автор ДОБАВИЛ native beforeinput listener, но ОСТАВИЛ React onBeforeInput (React 19.2 шлёт его через textInput/paste/composition; проверено в react-dom 19.2 client dev:27428-27433); event-local guard nativeEvent.defaultPrevented не связывает два РАЗНЫХ native события -> один ввод коммитится дважды ('hello' + '!' -> 'hello!!', ACCGD-independent.test.ts:99-112). GD-02 ПОРЧА СУРРОГАТОВ — fromOffset=cursor-1 рвёт emoji (A😀B -> A\ud83dB), engine расходится с legacy shadow (diverged:true), НО UI всё равно публикует engine value (EditorModal.tsx:6409-6423) = прямой класс web374. Также вскрыто: фикс gdiag был НЕ ЗАКОММИЧЕН (указатель ветки = l59-merge), работа жила в грязном worktree. PASS в матрице: OFF/kill-switch не подключают listener; обычные UTF-16 insert/delete паритетны; paste/drag/IME намеренно остаются legacy. gdfix2 диспатчнута: сначала КОММИТ работы, затем архитектурное решение «один ввод — один commit» (либо только native, либо общий дедуп-guard со стампом), графемы через Intl.Segmenter (ZWJ/флаги/скин-тоны/комбинирующие) и FAIL-CLOSED при diverged (никогда не публиковать engine value, откат на legacy + телеметрия). СОСТОЯНИЕ ПРОДА: l59gd с флагом ON стоит, но seam неполный -> writer не перехватывает -> пользователи фактически на легаси-пути, регрессии нет.
[2026-08-27 10:33Z accgd2 NO-GO — 3-й круг] GD-02 ЗАКРЫТ независимо (графемы + fail-closed при diverged подтверждены). GD-01 НЕ закрыт: cross-event stamp-guard принципиально не отличает paired React replay от ЛЕГИТИМНОГО второго одинакового символа, когда второй приходит через другой seam source; подавленный replay не потребляет stamp => обратная ловушка ('aa','!!' съедаются). gdfix3 диспатчнута с АРХИТЕКТУРНЫМ требованием вместо эвристики: ОДИН источник — native beforeinput перехватывает, React onBeforeInput при активном writer НЕ навешивается вовсе (либо строго непересекающиеся множества inputType, зафиксированные тестом-инвариантом). Обязательные тесты: 'aa'/'!!' проходят, автоповтор 10 символов = 10 коммитов, IME = 1 коммит, paste/drop = 1, Backspace-автоповтор по графемам = N, независимые сюиты обоих приёмщиков зелёные.
[2026-08-27 ~11:45Z accgd3 NO-GO — блокер ИНФРАСТРУКТУРНЫЙ, не по сути фикса] Статический аудит приёмщика ПОДТВЕРДИЛ переделку native-only: ровно один addEventListener('beforeinput'), символы onBeforeInput/handleGatedWriterBeforeInput/cross-event stamp отсутствуют. NO-GO выставлен потому, что последний гейт не зелёный по окружению: (а) не все legacy независимые сюиты зелёные, (б) полный type-check не зелёный, (в) браузерная приёмка не доходит до редактора — локальный dev-бандл падает на PRE-EXISTING compile/runtime ошибках. Маркер ACCGD3_DONE намеренно не создан. gdinfra диспатчнута: развести НАШИ регрессии от pre-existing (проверкой на чистом клоне 2c6c2276), наши — починить, pre-existing — зафиксировать как техдолг и починить минимально только то, что блокирует запуск dev; цель — зелёный tsc + зелёные сюиты ACCGD/ACCGD2/ACCGD3 + работающий редактор в браузере + инструкция запуска для следующего круга приёмки.
[2026-08-27 ~13:40Z accgd4 NO-GO — впервые ЖИВАЯ браузерная приёмка] Инфраструктура снята (gdinfra): dev поднимается, редактор открывается, HTTP smoke 200, скриншот есть; наших регрессий НЕ найдено, наборы зелёные (writer/wiring/telemetry 44/44, ACCGD3-independent 7/7, Gate C/E/sync/large-doc 33/33); полный repo tsc красный, но это PRE-EXISTING долг (совпадает с чистым 2c6c2276) — в рамках Gate D не чиню. НО в браузере при NEXT_PUBLIC_DOCUMENT_SESSION_GATED_WRITER=1: data-gated-writer="off", data-gated-writer-engine-applied="0", native beforeinput не перехватывается, всё идёт legacy. Значит корень уже НЕ в seam (он переделан на native-only и статически подтверждён), а в АКТИВАЦИИ writer'а. Это ТРЕТЬЕ проявление одного симптома «флаг включён — writer не работает»: (1) на проде l59gd (корень был React 19 seam), (2) ACCGD3 не смог проверить из-за инфраструктуры, (3) теперь writer сам объявляет себя выключенным. gddiag2 диспатчнута с требованием ИНСТРУМЕНТИРОВАТЬ маркер причиной (buildEnabled/killSwitchActive/killSwitchSource/rawEnvValue/почему writer не создан) и проверить 5 гипотез, включая «в dev NEXT_PUBLIC инлайнится иначе, чем в prod-build» и «Object.freeze env на модульном уровне берёт значение до его доступности». Доказательство фикса — маркер "on" и рост engine-applied при вводе в браузере.
[2026-08-27 ~14:25Z gddiag2 — КОРЕНЬ АКТИВАЦИИ НАЙДЕН (commit 593e37fa)] Корень оказался в LIFECYCLE, а НЕ в NEXT_PUBLIC-инлайне и не в React seam: cleanup в EditorModal.tsx:1318-1322 имел dependency [gatedWriter]; при dev StrictMode effect replay вызывался gatedWriter.dispose(), а ref оставался живым; следующий open() на том же (уже disposed) writer в gated-writer.ts:952-953 БЕЗЗВУЧНО делал return. Поэтому DOM показывал data-gated-writer="off"/engine-applied="0", хотя writer был создан и open() вызывался. Это классический пр.10 (тихая ошибка): молчаливый return прятал причину ТРИ круга приёмки подряд (accgd2/accgd3/accgd4). Волна добавила диагностические атрибуты (причина активации выводится в DOM). accgd5 диспатчнута — финальный гейт: живой браузер (маркер "on" + рост engine-applied при вводе, эмодзи/ZWJ/'aa'/'!!'), запрет тихого return на disposed, lifecycle-стресс (перемонтирование модалки, переключение документов), OFF-путь, fail-closed, все четыре прежние сюиты своими руками.
[2026-08-27 ~16:10Z accgd5 NO-GO — ПЕРВАЯ полноценная живая браузерная приёмка (Playwright + скриншоты в ACCGD5-EVIDENCE/)] Два блокера: (1) УДАЛЕНИЕ ВЫДЕЛЕНИЯ с ZWJ-эмодзи: выделили 👩🏽💻 и удалили — видимый текст верный, но engine и legacy-shadow РАЗОШЛИСЬ: data-gated-writer=off, reason=session-disabled, engine-applied=1; телеметрия content-divergence, legacyFallbacks=1, sessionDisables=1, divergences=1. Fail-closed сработал ПРАВИЛЬНО (порча не опубликована), но валидная операция не должна его вызывать. Похоже, графемная нормализация применена к caret-удалению, но не к RANGE-удалению. (2) КРИТИЧНО ДЛЯ БЕЗОПАСНОСТИ: kill-switch из localStorage ТЕРЯЕТСЯ до активации — init выставил document-session-gated-writer-kill-switch=disabled (initStoredKillSwitch=disabled), но приложение увидело storageKillSwitch=null, СОЗДАЛО writer и приняло три engine-коммита. Аварийный выключатель не работает. Ограничение приёмки честно зафиксировано: точный клон не поднял dev (Turbopack отверг симлинк зависимостей, затем pre-existing отсутствующий undici), поэтому живой прогон шёл на дереве автора с byte-identical Gate D файлами на том же 593e37fa. gdfix4 диспатчнута: (1) найти расхождение именно на range-удалении + тесты на ZWJ-семью/скин-тон/флаг/é/суррогаты/type-over, fail-closed НЕ ослаблять; (2) kill-switch читать ДО создания writer и при каждом открытии, при недоступности storage считать выключатель ВКЛЮЧЁННЫМ, вывести killSwitchSource и сырое значение в маркер.
[2026-08-27 ~16:35Z gdfix4 — ОБА блокера ACCGD5 закрыты] (1) SELECTION DIVERGENCE: delete/type-over теперь используют operation-local snapshot с сохранением ZWJ и РАСШИРЯЮТ фактический DOM Selection до тех же грапемных границ, что получает engine — поэтому range deletion и shadow execCommand работают над ОДНИМ диапазоном; для legacy-html серии обычных пробелов приводятся к сериализации contenteditable ( ), пробелы в HTML-атрибутах не трогаются. (2) KILL-SWITCH (был критичен: не работал вовсе): читается при создании И ПОВТОРНО на каждом canHandle/open/stats/marker read; localStorage.clear() (account purge) СОХРАНЯЕТ emergency key; отсутствующий или бросающий storage остаётся fail-closed (writer не создаётся); в marker добавлены kill-switch-source и raw-kill-switch-value, причина содержит источник и сырое значение. accgd6 диспатчнута — финальная живая браузерная приёмка с прямым заданием ПОПЫТАТЬСЯ ОБОЙТИ выключатель любым способом, плюс графемы через выделение, новая нормализация пробелов (риск регрессии обычного текста/вставки), 'aa'/'!!', IME, OFF-путь. При GO — сборка и посадка Gate D. ⚠️ В бриф добавлено ограничение «tsc только затронутых файлов»: сегодня четыре одновременных полных repo-tsc (по 3-6 ГБ) уронили A1 в своп (load 93, SSH не отвечал).
[2026-08-27 ~19:26Z ACCGD7 — GO]
Независимый клон A1:/home/ubuntu/waves/wt-accgd7, dev server 127.0.0.1:3199 с Gate D включённым. Авторское дерево не изменялось, next build не запускался, прод не затрагивался.
Доказательства: ACCGD7-EVIDENCE/live-final10/ACCGD7-live-results.json; скриншоты 02-trailing-three.png, 06-attribute-spaces.png, 09-artificial-drift-fail-closed.png, 11-undo-redo-space.png; ACCGD7-EVIDENCE/accgd6-live-final/ACCGD6-live-results.json (регрессия); ACCGD7-EVIDENCE/focused-unit.log; ACCGD7-EVIDENCE/runtime-scoped-tsc.log (tsconfig.accgd7-runtime-scoped.json — 0 диагностик). Отчёт: A1:/home/ubuntu/waves/ACCGD7-REPORT.md.
ЭВОЛЮЦИЯ ПРИЁМОК: ACCGD3 -> ACCGD4 -> ACCGD5 -> ACCGD6 -> ACCGD7, каждый круг новый приёмщик и новые классы, ослабления ассертов не допускались.
[2026-08-27 ~19:40Z ЛОВУШКА ПРИ СБОРКЕ — важно для всех будущих посадок]
Отчёт приёмки указал «HEAD и author/gdiag = 593e37fa», но САМ ФИКС legacyHtmlWhitespace.ts в этом коммите ОТСУТСТВОВАЛ — лежал незакоммиченным в дереве автора, а приёмочный клон унаследовал ровно тот же рабочий diff. Сборка «по HEAD» дала бы прод БЕЗ исправления при полностью зелёном отчёте приёмки.
СВЕРЕНО ПЕРЕД КОММИТОМ: tracked-diff обоих деревьев sha256 e5a3604b (совпал побайтно); 4 продуктовых untracked-файла совпали пофайлово (instrumentation.edge.ts 6347abe1, gated-writer.gdfix5.test.ts d1e7fcd9, legacyHtmlWhitespace.test.ts d4617f91, legacyHtmlWhitespace.ts e1cc5f2b); лишнее у приёмщика — только его harness (ACCGD7-live.mjs, tsconfig.accgd7-*.json). Diff прочитан глазами на чужое: clientAccountIsolation.ts — по делу (kill-switch), metrics.ts — os -> node:os.
ПРИНЯТОЕ ЗАКОММИЧЕНО: ветка gdiag-l62, коммит 11c57182 (18 файлов, +872/-61). ПОСЛЕ коммита сверено git show 11c57182:<файл> против дерева приёмщика — MATCH по legacyHtmlWhitespace.ts, gated-writer.ts, clientAccountIsolation.ts.
АРТЕФАКТ К ПОСАДКЕ: бандл gdiag-l62 sha256 e45f3738f5c0860ce64a21922fc087804a156ae0842cce50256373798c8e46fe, доставлен A1 -> ноут -> M4 со сверкой sha на каждом хопе, с ноута удалён.
СТАТУС: ждёт сборочный слот на M4 (правило «сборки строго по одной»; на M4 идёт приёмка ACC393F, память в натяг: 45% свободно, своп 2.1/3 ГБ).
[2026-08-27 ~20:10Z СЛИЯНИЕ НА ЛИНИЮ ПРОДА — посадка разблокирована]
Ветка gdiag-l62 сидела на линии l59-merge и коммита прода 7d24c3a8 НЕ содержала: посадка в лоб откатила бы починку ролей (l61/web414council). Живая линия найдена ВНУТРИ сборочной VM noteclone-build на M4: /home/milamarty.guest/merge-l61 — единственное место с коммитом прода.
СДЕЛАНО: дельта-патч `git diff 2c6c2276..gdiag-l62` (5 коммитов, 20 файлов, 2924 строки), sha256 40e756f5ebd8a715b251ddf5, перевезён A1 -> ноут -> M4 -> VM со сверкой sha на КАЖДОМ хопе (4 совпадения). Артефакты прошлой сборки (monet-w34/w35-*.json, public/sw.js, generatedActivationManifest.ts) убраны в stash pre-l62-buildartifacts — в коммит не попали. Сухой прогон `git apply --check --3way` без конфликтов, затем наложение на новую ветку l62-gdiag.
КОММИТ b3b03cfd42ea860d573a713368d5869d846541ae, ровно 20 файлов, +1957/-127.
ДВА ГЕЙТА ПРОЙДЕНЫ: `git merge-base --is-ancestor 7d24c3a8 HEAD` = да (роли в родословной, откат исключён) И `git cat-file -e HEAD:src/lib/editor/legacyHtmlWhitespace.ts` = да (фикс движка внутри коммита).
СБОРКА l62 ЗАПУЩЕНА на M4 (tmux -L gui l62build). Гейты по эталону l61: паритет флагов с l61roles-byte-gates.json (GATED_WRITER=1, SHADOW_W1=0, семейства MUTATING_AI/COLLAB не вшиты), аттестация из СБОРКИ, пакетные гейты, канон-печать байт-в-байт, сухой прогон загрузки, плюс отдельный гейт: доказать след legacyHtmlWhitespace в собранных чанках (нет следа = провал сборки). Подпись НЕ в волне — координатор подписывает локально.
ЛОВУШКА СРЕДЫ: codex внутри VM не установлен, волна идёт на хосте M4 и командует VM через limactl shell; дома хоста и VM разные, файлы только через limactl copy.
[2026-08-27 ~21:02Z ПОСАДКА l62 — ДВИЖОК НА ПРОДЕ]
Сборка прошла ВСЕ гейты. Паритет флагов: полная инвентаризация 37 ключей NEXT_PUBLIC_* с контекстом чанков в l62-byte-gates.json; GATED_WRITER вшит как "1", SHADOW_W1 как "0" (chunks/14656.6f939b5443d7f98d.js), тёмные семейства MUTATING_AI/COLLAB — 0 совпадений. Мой отдельный гейт «фикс движка реально в бандле» — PASS: в том же чанке найдены маркеры nonbreakingspace / insertionOffset / insertedText. 373/373 статических страниц, канон-печать байт-в-байт, пакетные гейты PASS, сухой прогон загрузки ok=true.
АРТЕФАКТ: me2-standalone-linux-arm64-b3b03cfd-20260827T202106Z.tar.gz, 207 415 840 байт, sha256 eeee17d1baed066dcf8929630f41721858ca20696763e91e9c50f6429423023c. Аттестация подписана ЛОКАЛЬНО ключом owner (доверенность 26.08), отпечаток ключа сошёлся (0b83e0d3...), verify: позитив PASS + два негатива (подмена artifactSha, перевёрнутый байт подписи) корректно отвергнуты.
ДОСТАВКА: VM -> M4 -> M1 -> Pi, sha сверена на КАЖДОМ из четырёх хопов, все совпали.
РЕЛИЗ: /home/pi/note-clone/releases/arm64-l62-20260827T205714Z, live-sha b3b03cfd42ea860d573a713368d5869d846541ae, run-dir /home/pi/note-clone/shared/run/l62-b3b03cfd. Откат сохранён: /home/pi/l61-standalone-artifact.conf.rollback.
ДВА СБОЯ ПРИ ПЕРЕКЛЮЧЕНИИ (одного класса, оба вылечены): (1) enforce_blocked «runtime artifactSha256 mismatch» — в env-файле остался SPEND_RUNTIME_ARTIFACT_SHA256 от l61; в drop-in я задал верный, но EnvironmentFile ПЕРЕБИВАЕТ Environment=; (2) enforce_blocked «runtime sourceCommit mismatch» — остались RELEASE_SOURCE_COMMIT и его двойник под другим именем RELEASE_ARTIFACT_SHA. Лечение: сплошная сверка — ЛЮБОЕ 40/64-символьное hex-значение в run-dir обязано присутствовать в подписанной аттестации; после неё «чисто».
ПРОВЕРКА ПОСАДКИ (против ложного «Deployed»): systemctl active; /api/ready/paid = ok, paidReady=true, enforce_ready, failures=[]; /api/db/health 200; метка sw.js ИЗ ПРОЦЕССА v4-b3b03cfd == метка НА ДИСКЕ v4-b3b03cfd; публичный https://nb.wool2.online/sw.js = v4-b3b03cfd, health 200.
РОЛИ НЕ ОТКАЧЕНЫ: линия l61 (web414council, 7d24c3a8) — предок этого коммита, проверено merge-base до сборки и подтверждено в отчёте сборки.
[29.08 КОРЕНЬ ЛОЖНОГО РАСХОЖДЕНИЯ НАЙДЕН, коммит aa2a5380, приёмка ACCPOLISH заряжена]
Симптом на живом проде l68: Polish показывает «Done!», текст не меняется. Телеметрия: engineApplied:1, divergenceKind:"content", cleanMatchCount:0, legacyFallbacks:1, sessionDisables:1.
Корень: EditorModal кладёт HTML в contenteditable (src/components/modals/EditorModal.tsx:2641-2665), Chromium канонизирует CSS-атрибут — <p style="margin:0;"> становится <p style="margin: 0px;">. Legacy shadow читает DOM обратно через clone+innerHTML+serializer (:3426-3443), normalizeLegacyEditorHtml намеренно сохраняет байты тегов/атрибутов (src/lib/editor/legacyHtmlWhitespace.ts:115-173), а сравнение шло СЫРОЙ строкой (legacyValue !== engineValue). Одинаковый пользовательский текст с разной сериализацией разметки → ложное расхождение → disableSession() + откат на legacy.
Ложное «Done!»: fetch('/api/completion') проверял только HTTP-ответ генерации (EditorModal.tsx:8078-8091), а не расписку об успешной записи в документ.
Починка: сравнение и хеш legacy HTML через projectBlockForGrapheme(..., 'plain-text@1', 'legacy-html') (gated-writer.ts:583-603,856-906); UI различает session-disabled и legacy-fallback и показывает toast.error вместо успеха (:4508-4511,4521-4542,8103-8155).
⚠️ ОТКРЫТЫЙ РИСК для приёмки: сравнение по плоскому тексту отбрасывает разметку — сторож может ослепнуть к НАСТОЯЩЕЙ порче (потеря блока, ссылки, схлопывание абзацев). Это главный вопрос ACCPOLISH.
⚠️ ВТОРОЙ ДЕФЕКТ признан автором: sessionDisables не восстанавливается — boolean ставится один раз (gated-writer.ts:694-699), после чего open отклоняет даже новый документ (:1146-1148); помогает только перезагрузка страницы.
[29.08 ACCPOLISH_VERDICT=NO-GO — починка ослепила сторожа, круг 2 заряжен (очередь 448)]
ПРИНЯТО: корень (Chromium канонизирует CSS в атрибуте style) и работа по ложному «Done!» — UI различает session-disabled и legacy-fallback, отдаёт rejected, показывает ошибку; негативный контроль автора прошёл.
БЛОКЕР: сравнение через projectBlockForGrapheme(..., 'plain-text@1', 'legacy-html') выбрасывает разметку целиком — теги, порядок блоков и атрибуты в projection не входят. Приёмка показала, что теперь как «clean match» проходит НАСТОЯЩАЯ порча: потеря ссылки (<p><a href>New</a></p> -> <p>New</p>), потеря кодового блока (<pre> -> <p>), схлопывание двух блоков в один, изменение href/value. До починки все четыре давали content-divergence. Мы починили ложное срабатывание ценой слепоты к реальной порче.
НАПРАВЛЕНИЕ КРУГА 2: сравнивать НОРМАЛИЗОВАННУЮ разметку, а не голый текст — гасить ровно то, что канонизирует браузер (сериализация CSS в style, порядок/регистр свойств, кавычки и пробелы в значениях), сохраняя в сравнении теги, вложенность, порядок блоков и значения атрибутов. Плюс в этом же круге решить sessionDisables (не восстанавливается, ИИ-правка мертва до перезагрузки).
[29.08 КРУГ 2 (9f5e40c0) — ACCPOLISH2_VERDICT=NO-GO, круг 3 заряжен (очередь 462)]
ПРИНЯТО: отклонение грубых структурных порч, восстановление сессии при открытии нового документа, корректный пропуск браузерной пересериализации (margin:0 vs margin: 0px; регистр и порядок в margin:0;color:red vs COLOR: red; MARGIN: 0px; кавычки и пробелы синтаксиса атрибута).
БЛОКЕРЫ:
1. ЛОЖНЫЙ ОТКАЗ: перестановка семантически незначимых HTML-атрибутов отклоняется как content-divergence — порядок атрибутов ничего не значит, это вернёт мёртвую ступень E.
2. ЛОЖНЫЙ ПРОПУСК: перестановка неповторяющихся CSS-свойств пропускается даже когда МЕНЯЕТ КАСКАД — пара margin / margin-top зависит от порядка.
3. ⚠️ ЛОЖНЫЙ ПРОПУСК, самый опасный: изменение принадлежности текста ссылке пропускается — signature хранит только последовательность ТЕГОВ, а не ГРАНИЦЫ ТЕКСТОВЫХ УЗЛОВ. Текст можно вынести из ссылки или втянуть внутрь, и сторож не заметит.
Круг 3: правило каскада формулировать общим принципом («сокращённое свойство перекрывает частное, если стоит после него»), а не списком пар; в подпись включить границы текстовых узлов.
[29.08 ACCPOLISH3_VERDICT=NO-GO — два false-clean, круг 4 заряжен (очередь 474)]
ПРИНЯТО: все обязательные сценарии круга 2 и заявленные регрессии проходят — границы текстовых узлов в подписи, отклонение потери ссылки/кодового блока/склейки блоков/подмены href.
БЛОКЕР 1 — правило каскада НЕ общее, а список пар. src/lib/document-session/textProjection.ts, hasCascadeSensitivePropertyOrder проверяет только CSS_CASCADE_SHORTHANDS плюс inset. Приёмка сама придумала пары вне списка, все четыре прошли как чистые вместо отказа: place-items/align-items в обе стороны и padding/padding-top в обе стороны. place-items после align-items сбрасывает его значением из сокращённого свойства.
БЛОКЕР 2 — подмена НЕВИДИМОГО символа проходит как чистая: engine <p>New</p> (ZERO WIDTH SPACE) против shadow <p>New</p> (WORD JOINER). Видимый plain-text@1 в обоих равен «New», подпись даёт только text-node:4, структурные подписи совпадают — writer возвращает engine, сессию не отключает.
КРУГ 4: настоящее общее правило пересечения сокращённого свойства и частных случаев (не рукописный список — приёмка каждый раз придумывает пару, которой в нём нет) + решение по невидимым символам с обоснованием по каждому классу.
[29.08 ПРИНЯТО — ACCPOLISH4_VERDICT=GO, коммит 9ad4de810971b69ca4b003c9584a49923ea486e0, четвёртый круг]
Ступень E закрыта. Путь линии: круг 1 нашёл корень (Chromium канонизирует CSS: margin:0 -> margin: 0px) и убрал ложное «Done!», но ослепил сторожа сравнением по плоскому тексту; круг 2 вернул структурное сравнение, но правило каскада оказалось списком пар и подпись не хранила границ текстовых узлов; круг 3 добавил границы, но каскад всё ещё был списком и пропускал подмену невидимых символов; круг 4 принят.
Каждый круг заворачивала приёмка, которая САМА придумывала пары свойств вне списка (place-items/align-items, padding/padding-top) и свои невидимые символы (ZWSP против WORD JOINER) — именно так находились пропуски.
Кандидат в линию после l69.
[29.08 ЖИВАЯ ПРОБА НА l70 — СТУПЕНЬ E РАБОТАЕТ, «Done!» больше не врёт]
Выделена строка «this sentence is written badly and it have many mistake in it.», нажат Polish. Стало: «This sentence is written badly and it has many mistakes in it.» — три реальные правки (регистр, have->has, mistake->mistakes). Тост «Done!» на этот раз ЧЕСТНЫЙ: текст действительно изменился.
Правка пережила ДВЕ полные перезагрузки: после navigate на / и повторного открытия документа она на месте; после ручной дописки маркера MANUALEDITMARKER70 и ещё одной перезагрузки видны обе правки. Сторож ничего не откатил, сессия записи не погасла.
⚠️ НАХОДКА ДЛЯ ОТДЕЛЬНОГО РАЗБОРА: два хранилища расходятся. По одному id cmtedwlj3008510s2m834i9ij сразу после Polish Document.content содержал отполированный текст, а Source.content — исходный; после ручной правки они поменялись местами: Source.content стал актуальным (полировка + маркер), а Document.content откатился к исходному доpolish-тексту. Пользователю видно правильное (редактор читает Source), но Document.content держит протухшую копию. Кто ещё её читает — из браузера не выяснить, нужен разбор по коду.
Связи
- связан → WEB-283 — ЭПИК: движок редактора Gate C (docsession) — леги 1-9 + продуктовые хвосты · ступень лестницы движка
- связан → WEB-325 — Движок Gate E: mutating AI — AI-фичи пишут в документ через движок · ступень лестницы движка
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-324","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-29T13:10:21.558Z