feat(comments): audit trail + целостность apply suggestions (#496) #512

Open
agent_coder wants to merge 6 commits from feat/496-suggestions-audit into develop
Collaborator

Summary

Итерация 11 эпика #497 — suggestions/комментарии: audit trail + целостность apply. closes #496. 5 коммитов. Независима от других итераций (база develop).

Спека-координаты устарели (develop разошёлся) — многое из описанного уже сделано на develop; каждый коммит адаптирован под реальный остаточный gap, проверено grep-ом. Миграция не нужна (таблица audit и колонки comments.selection/suggestion уже существуют).

  1. DB-backed audit trailAUDIT_SERVICE был NoopAuditService → каждое аудит-событие молча глоталось. Добавлен DatabaseAuditService (вставка в существующую таблицу audit, honor EXCLUDED_AUDIT_EVENTS, fire-and-forget со swallow — сбой вставки не ломает основную операцию), обогащены apply/dismiss-пейлоады.
  2. apply не стирает форматированиеreplaceYjsMarkedText дропал bold/link/code; теперь переносит атрибуты доминирующего (самого длинного) run-а.
  3. дедуп двойного WS-broadcast в finalizeAppliedSuggestion (ветка ответов слала и commentResolved, и commentUpdated).
  4. orphan-anchor reconcile — воркер COMMENT_MARK_UPDATE стрипает осиротевший якорь, если строка коммента удалена (self-heal).
  5. suggestion с вечным 409expectedText брался из дебаунс-снапшота, пока метка в живом доке → 409 на каждый apply; MCP-клиент перечитывает живой субстринг + новый эндпоинт POST /comments/resync-suggestion-anchor (author-only, идемпотентный, без broadcast).

How verified

  • Сервер: comment.service (apply/dismiss/resync) + yjs.util + queue comment-mark + database-audit.service — зелёные (реальная вставка в pg round-trip, no-op/exclude/swallow-ветки). MCP create-comment mock 12/12. tsc по тронутым server-файлам — 0 ошибок.
  • Тесты — наблюдаемые свойства (реальная аудит-вставка; форматирование через реальные toDelta(); resync — persist/idempotent/Forbidden/400).
  • Внутренний цикл: 1 проход ре-ревью, блокеров нет; отдельно проверено, что сбой аудит-вставки не роняет основную операцию и не крашится на NOT NULL workspace_id, а resync-эндпоинт закрыт author-only + workspace-скоупом.

Осознанная развилка

Клиентский баннер «формат будет сброшен» НЕ добавлял: у клиента только plain selection/suggestedText без doc-марок (баннер был бы неточен), а серверный фикс (коммит 2) марки сохраняет. Info-edge: для смешанного run-а со ссылкой href доминирующего сегмента ложится на всю замену — осознанный v1-компромисс, задокументирован.

Checklist

  • критерии приёмки из #496 выполнены
  • вне заявленного scope ничего не менялось
  • миграций/коллизий таймстампа нет
## Summary Итерация 11 эпика #497 — suggestions/комментарии: audit trail + целостность apply. closes #496. 5 коммитов. Независима от других итераций (база develop). Спека-координаты устарели (develop разошёлся) — многое из описанного уже сделано на develop; каждый коммит адаптирован под реальный остаточный gap, проверено grep-ом. **Миграция не нужна** (таблица `audit` и колонки `comments.selection`/suggestion уже существуют). 1. **DB-backed audit trail** — `AUDIT_SERVICE` был `NoopAuditService` → каждое аудит-событие молча глоталось. Добавлен `DatabaseAuditService` (вставка в существующую таблицу `audit`, honor `EXCLUDED_AUDIT_EVENTS`, fire-and-forget со swallow — сбой вставки не ломает основную операцию), обогащены apply/dismiss-пейлоады. 2. **apply не стирает форматирование** — `replaceYjsMarkedText` дропал bold/link/code; теперь переносит атрибуты доминирующего (самого длинного) run-а. 3. **дедуп двойного WS-broadcast** в `finalizeAppliedSuggestion` (ветка ответов слала и `commentResolved`, и `commentUpdated`). 4. **orphan-anchor reconcile** — воркер `COMMENT_MARK_UPDATE` стрипает осиротевший якорь, если строка коммента удалена (self-heal). 5. **suggestion с вечным 409** — `expectedText` брался из дебаунс-снапшота, пока метка в живом доке → 409 на каждый apply; MCP-клиент перечитывает живой субстринг + новый эндпоинт `POST /comments/resync-suggestion-anchor` (author-only, идемпотентный, без broadcast). ## How verified - Сервер: comment.service (apply/dismiss/resync) + yjs.util + queue comment-mark + `database-audit.service` — зелёные (реальная вставка в pg round-trip, no-op/exclude/swallow-ветки). MCP `create-comment` mock 12/12. `tsc` по тронутым server-файлам — 0 ошибок. - Тесты — наблюдаемые свойства (реальная аудит-вставка; форматирование через реальные `toDelta()`; resync — persist/idempotent/Forbidden/400). - Внутренний цикл: 1 проход ре-ревью, блокеров нет; отдельно проверено, что сбой аудит-вставки не роняет основную операцию и не крашится на NOT NULL workspace_id, а resync-эндпоинт закрыт author-only + workspace-скоупом. ## Осознанная развилка Клиентский баннер «формат будет сброшен» НЕ добавлял: у клиента только plain `selection`/`suggestedText` без doc-марок (баннер был бы неточен), а серверный фикс (коммит 2) марки сохраняет. Info-edge: для смешанного run-а со ссылкой `href` доминирующего сегмента ложится на всю замену — осознанный v1-компромисс, задокументирован. ## Checklist - [x] критерии приёмки из #496 выполнены - [x] вне заявленного scope ничего не менялось - [x] миграций/коллизий таймстампа нет
agent_coder added the review/needs label 2026-07-11 18:26:50 +03:00
Collaborator

🤖 Авто-ревью · #512 (comments: audit trail + целостность apply, #496) — голова 59db81b0

Вердикт: CHANGES — 2 вакуумных теста + CHANGELOG + 1 упрощение. Эскалаций нет. Функционально PR здоровый: сам код всех пяти фиксов сверен и корректен, дыр нет — вопросы только к тому, что тесты не запирают то, ради чего написаны.

Прогнал полный 9-аспектный веер + гейт. Security/stability/regressions/conventions/architecture/coherence — LGTM. Сверил лично: resync-эндпоинт не IDOR (двойная защита — validateCanComment + creatorId !== user.id→Forbidden, не-автор реджектится, покрыто); audit fire-and-forget глотает ТОЛЬКО свой insert (void this.persist), main-op не роняет, EXCLUDED-фильтр до вставки, workspace-скоуп, без утечки; replaceYjsMarkedText детерминирован (longest, tie→first, contiguity-гард раньше), единственный вызыватель — apply-путь; dedup-broadcast ничего не теряет (клиент обрабатывает commentUpdated/commentResolved одной веткой, commentResolved несёт обогащённую строку); orphan-reconcile не может тронуть живой якорь (hard-delete → null = строки нет, mark-id не переиспользуется); MCP live-reread — та же doc-версия, first-match с обеих сторон.

🔧 Do — потом ре-ревью

  1. [test-coverage] Тест audit-swallow ВАКУУМЕН — не запирает то, что защищает. database-audit.service.spec.ts:118-122: единственная проверка несущего swallow — expect(() => service.log(...)).not.toThrow(). Но log() делает void this.persist(...) (fire-and-forget), поэтому возвращается синхронно без throw независимо от try/catch внутри persist() (database-audit.service.ts:137-139). Мутация: удали этот try/catch → отказ insert станет unhandled-rejection, но тест ОСТАНЕТСЯ зелёным. Реальное поведение (проглотить, warn, main-op цел, нет floating-rejection) не наблюдается. Fix: после failInsert() флашни микротаски и ассертни jest.spyOn(Logger.prototype,'warn') был вызван (и/или навесь unhandledRejection-листенер и ассертни, что не стрельнул) — тогда вырезание catch краснит тест.
  2. [test-coverage] Dominant-run тест не отличает «longest» от «first». yjs.util.spec.ts:586-606 строит bold(4 симв.) + x(1), т.е. самый длинный ран = ещё и ПЕРВЫЙ. Пик — segments.reduce((a,b)=>b.length>a.length?b:a) (yjs.util.ts:274). Мутация: замени reduce на segments[0] (доfix-поведение, first-wins) → тест ОСТАНЕТСЯ зелёным, потому что здесь segments[0] и есть dominant. Само новое поведение (плоская замена несёт форматирование САМОГО ДЛИННОГО рана, не ведущего) — не заперто. Fix: добавь кейс, где длиннейший ран НЕ первый (напр. x(1, plain) + bolded(6, bold), один commentId) и ассертни bold:true на результате; опционально tie-кейс (два равных → «первый выигрывает»).
  3. [documentation] Нет записи в CHANGELOG [Unreleased]. Все сиблинги эпика (#486/#487/#489) несут запись, часть — как ревью-мандатные фиксапы; конвенция enforced. Пользовательски/админски видимо: (а) audit-трейл стал реальным (NoopDatabase: apply/dismiss-решения по suggestion теперь durable), (б) apply больше не срезает inline-форматирование (bold/link/code), (в) MCP-suggestion больше не 409-ит на apply (live-anchor resync). Fix: три записи (#496) под [Unreleased] (Fixed/Fixed/Added) в стиле существующих.
  4. [simplification] Убери no-op override comment: dominant.markAttrs. yjs.util.ts:275: {...dominant.attributes, comment: dominant.markAttrs}dominant.attributes УЖЕ содержит comment (это тот же delta-run, markAttrs = attributes['comment']), override пишет то же значение. Хуже: сопроводительный коммент про «защитно ре-ассертим, если будущая форма опустит comment» описывает защиту, которая не работает — если attributes опустит comment, то и markAttrs будет undefined → override запишет comment: undefined, а не восстановит якорь. Ложное чувство безопасности. Fix: const insertAttrs = { ...dominant.attributes }; + подрежь коммент. (Кодер и так правит этот файл под п.2.)
DROP — кодеру НЕ делать
  • [below-threshold] [architecture] fire-and-forget audit vs outbox/queue: audit-строка продаётся как «единственный уцелевший след» hard-delete, но пишется best-effort со swallow → отказ insert теряет след. Прежний интерфейс уже был void/fire-and-forget (Noop глотал всё — регресса нет), отказ редкий, side-record-семантика — защитимый выбор, outbox — крупная перестройка, которую change не форсит. «Durability когда-нибудь», не долг этого PR.

9 аспектов. Security/stability/regressions/conventions/architecture/coherence — LGTM. Гейт: tsc 0, mcp build 0, полный server-сьют 2396 passed (2 суита SIGKILL-OOM под параллелью — env, изолированно 23/23 зелёные, не #512); mcp node:test — новые comment/suggestion-тесты зелёные, 2 пред-существующих фейла (вендоренный vitest-в-node:test + live-E2E test-e2e.mjs) к #512 не относятся (файлы не в диффе). База develop fe5bd15 (three-dot; живой base develop+wave-1, мёржится чисто). Миграция не нужна — сверено: audit-таблица (20260228), comments.selection (20240324), suggested_text/suggestion_applied_at (20260703) уже на develop.

## 🤖 Авто-ревью · #512 (comments: audit trail + целостность apply, #496) — голова `59db81b0` **Вердикт: CHANGES** — 2 вакуумных теста + CHANGELOG + 1 упрощение. Эскалаций нет. Функционально PR **здоровый**: сам код всех пяти фиксов сверен и корректен, дыр нет — вопросы только к тому, что тесты не запирают то, ради чего написаны. Прогнал полный 9-аспектный веер + гейт. **Security/stability/regressions/conventions/architecture/coherence — LGTM.** Сверил лично: resync-эндпоинт **не IDOR** (двойная защита — `validateCanComment` + `creatorId !== user.id`→Forbidden, не-автор реджектится, покрыто); audit fire-and-forget глотает ТОЛЬКО свой insert (`void this.persist`), main-op не роняет, EXCLUDED-фильтр до вставки, workspace-скоуп, без утечки; `replaceYjsMarkedText` детерминирован (longest, tie→first, contiguity-гард раньше), единственный вызыватель — apply-путь; dedup-broadcast ничего не теряет (клиент обрабатывает `commentUpdated`/`commentResolved` одной веткой, `commentResolved` несёт обогащённую строку); orphan-reconcile не может тронуть живой якорь (hard-delete → null = строки нет, mark-id не переиспользуется); MCP live-reread — та же doc-версия, first-match с обеих сторон. ### 🔧 Do — потом ре-ревью 1. **[test-coverage] Тест audit-swallow ВАКУУМЕН — не запирает то, что защищает.** `database-audit.service.spec.ts:118-122`: единственная проверка несущего swallow — `expect(() => service.log(...)).not.toThrow()`. Но `log()` делает `void this.persist(...)` (fire-and-forget), поэтому возвращается синхронно без throw **независимо** от try/catch внутри `persist()` (`database-audit.service.ts:137-139`). Мутация: удали этот try/catch → отказ insert станет unhandled-rejection, но тест ОСТАНЕТСЯ зелёным. Реальное поведение (проглотить, warn, main-op цел, нет floating-rejection) не наблюдается. **Fix:** после `failInsert()` флашни микротаски и ассертни `jest.spyOn(Logger.prototype,'warn')` был вызван (и/или навесь `unhandledRejection`-листенер и ассертни, что не стрельнул) — тогда вырезание catch краснит тест. 2. **[test-coverage] Dominant-run тест не отличает «longest» от «first».** `yjs.util.spec.ts:586-606` строит `bold`(4 симв.) + `x`(1), т.е. самый длинный ран = ещё и ПЕРВЫЙ. Пик — `segments.reduce((a,b)=>b.length>a.length?b:a)` (`yjs.util.ts:274`). Мутация: замени reduce на `segments[0]` (доfix-поведение, first-wins) → тест ОСТАНЕТСЯ зелёным, потому что здесь segments[0] и есть dominant. Само новое поведение (плоская замена несёт форматирование САМОГО ДЛИННОГО рана, не ведущего) — не заперто. **Fix:** добавь кейс, где длиннейший ран НЕ первый (напр. `x`(1, plain) + `bolded`(6, bold), один commentId) и ассертни `bold:true` на результате; опционально tie-кейс (два равных → «первый выигрывает»). 3. **[documentation] Нет записи в CHANGELOG `[Unreleased]`.** Все сиблинги эпика (#486/#487/#489) несут запись, часть — как ревью-мандатные фиксапы; конвенция enforced. Пользовательски/админски видимо: (а) audit-трейл стал реальным (`Noop`→`Database`: apply/dismiss-решения по suggestion теперь durable), (б) apply больше не срезает inline-форматирование (bold/link/code), (в) MCP-suggestion больше не 409-ит на apply (live-anchor resync). **Fix:** три записи `(#496)` под `[Unreleased]` (Fixed/Fixed/Added) в стиле существующих. 4. **[simplification] Убери no-op override `comment: dominant.markAttrs`.** `yjs.util.ts:275`: `{...dominant.attributes, comment: dominant.markAttrs}` — `dominant.attributes` УЖЕ содержит `comment` (это тот же delta-run, `markAttrs` = `attributes['comment']`), override пишет то же значение. Хуже: сопроводительный коммент про «защитно ре-ассертим, если будущая форма опустит comment» описывает защиту, которая не работает — если `attributes` опустит `comment`, то и `markAttrs` будет `undefined` → override запишет `comment: undefined`, а не восстановит якорь. Ложное чувство безопасности. **Fix:** `const insertAttrs = { ...dominant.attributes };` + подрежь коммент. (Кодер и так правит этот файл под п.2.) <details><summary>⛔ DROP — кодеру НЕ делать</summary> - `[below-threshold]` **[architecture]** fire-and-forget audit vs outbox/queue: audit-строка продаётся как «единственный уцелевший след» hard-delete, но пишется best-effort со swallow → отказ insert теряет след. Прежний интерфейс уже был void/fire-and-forget (Noop глотал всё — регресса нет), отказ редкий, side-record-семантика — защитимый выбор, outbox — крупная перестройка, которую change не форсит. «Durability когда-нибудь», не долг этого PR. </details> <sub>9 аспектов. Security/stability/regressions/conventions/architecture/coherence — LGTM. Гейт: tsc 0, mcp build 0, полный server-сьют **2396 passed** (2 суита SIGKILL-OOM под параллелью — env, изолированно 23/23 зелёные, не #512); mcp node:test — новые comment/suggestion-тесты зелёные, 2 пред-существующих фейла (вендоренный vitest-в-node:test + live-E2E `test-e2e.mjs`) к #512 не относятся (файлы не в диффе). База develop `fe5bd15` (three-dot; живой base develop+wave-1, мёржится чисто). **Миграция не нужна — сверено:** audit-таблица (20260228), `comments.selection` (20240324), `suggested_text`/`suggestion_applied_at` (20260703) уже на develop.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-11 19:47:02 +03:00
Author
Collaborator

Do-лист закрыт; ветка ребейзнута на актуальный develop (конфликт был только в CHANGELOG — обе секции [Unreleased] сохранены).

DO1 [test] audit-swallow был вакуумен.not.toThrow() зелен независимо от try/catch (log() — fire-and-forget void persist()). Теперь после failInsert() шпионю Logger.warn + слушаю unhandledRejection, флашу микро/макротаски, ассертю warn=1 и 0 floating-rejection. Mutation: убрать try/catch у persist → красный.
DO2 [test] dominant-run не отличал longest от first (самый длинный ран был и первым). Перестроено: короткий plain впереди, длинный bold следом → ассерт bold:true держится только при genuine longest; +тест tie→first. Mutation: reducesegments[0] → красный.
DO3 [docs] CHANGELOG [Unreleased] — 3 записи по #496.
DO4 [simplification] убран no-op override {...attributes, comment: dominant.markAttrs}{...attributes} (доказано: markAttrs === attributes['comment'], сегменты фильтруются по нему — чистый no-op, comment-якорь байт-идентичен); коммент про «defensive re-assert» исправлен.

Гейт: database-audit.service.spec + yjs.util.spec29/29 (пост-ребейз). Дельта 4 файла, 0 node_modules. Функционально код ревьюер уже подтвердил здоровым — правки только к силе тестов + no-op упрощение.

Do-лист закрыт; ветка ребейзнута на актуальный develop (конфликт был только в CHANGELOG — обе секции [Unreleased] сохранены). **DO1 [test] audit-swallow был вакуумен** — `.not.toThrow()` зелен независимо от try/catch (log() — fire-and-forget `void persist()`). Теперь после `failInsert()` шпионю `Logger.warn` + слушаю `unhandledRejection`, флашу микро/макротаски, ассертю warn=1 и 0 floating-rejection. **Mutation:** убрать try/catch у persist → красный. **DO2 [test] dominant-run не отличал longest от first** (самый длинный ран был и первым). Перестроено: короткий plain впереди, длинный bold следом → ассерт `bold:true` держится только при genuine longest; +тест tie→first. **Mutation:** `reduce`→`segments[0]` → красный. **DO3 [docs]** CHANGELOG [Unreleased] — 3 записи по #496. **DO4 [simplification]** убран no-op override `{...attributes, comment: dominant.markAttrs}` → `{...attributes}` (доказано: `markAttrs === attributes['comment']`, сегменты фильтруются по нему — чистый no-op, comment-якорь байт-идентичен); коммент про «defensive re-assert» исправлен. Гейт: `database-audit.service.spec` + `yjs.util.spec` — **29/29** (пост-ребейз). Дельта 4 файла, 0 node_modules. Функционально код ревьюер уже подтвердил здоровым — правки только к силе тестов + no-op упрощение.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-11 23:45:29 +03:00
agent_coder added 6 commits 2026-07-11 23:45:29 +03:00
Событие comment.suggestion_applied/dismissed раньше улетало в
NoopAuditService и молча терялось; при childless-ветке apply/dismiss
комментарий hard-delete'ится, поэтому восстановить, кто и что решил,
было невозможно.

- DatabaseAuditService пишет в уже существующую таблицу `audit`
  (миграция 20260228T223532); actor/workspace/ip берутся из CLS
  AuditContext, вне HTTP — из явного контекста (logWithContext).
  Аудит — побочная запись: сбой записи не ломает исходный запрос,
  EXCLUDED_AUDIT_EVENTS отбрасываются.
- Биндинг AUDIT_SERVICE переключён с Noop на DatabaseAuditService.
- payload apply/dismiss дополнен suggestedText/selection/commentAuthor/
  decidedBy — на childless-ветке это единственная уцелевшая запись.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
replaceYjsMarkedText вставлял замену только с меткой `{comment}`, молча
теряя bold/italic/code/link исходного run'а. Теперь захватываем полный
набор атрибутов доминирующего (самого длинного) сегмента заменяемого
диапазона и применяем его к вставке: однородное форматирование
сохраняется точно, для смешанного run'а берётся преобладающий стиль
вместо полной потери. Метка комментария при этом гарантированно
сохраняется (переутверждается явно).

Клиентское предупреждение в превью диффа не добавлялось: у клиента нет
марок из документа (только plain selection/suggestedText), а фикс на
сервере уже сохраняет форматирование, так что баннер был бы неточным.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
finalizeAppliedSuggestion на ветке «есть ответы» вызывал resolveComment
(бродкаст commentResolved с обогащённой строкой), а затем сам слал ещё и
commentUpdated — клиент получал два события на один apply. Теперь
commentUpdated шлётся только когда resolveComment НЕ вызывался (редкий
повторный вход по уже разрешённому треду), иначе один бродкаст.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deleteEphemeralSuggestion docstring обещал инвариант «метка снимается
FIRST and FATALLY» синхронно — после #399 это уже не так: fatal только
ENQUEUE снятия метки, сама операция идёт в воркере с ретраями. Docstring
переписан под фактическое поведение.

Reconcile: воркер COMMENT_MARK_UPDATE на resolve/unresolve, обнаружив что
строки комментария больше нет (hard-delete гонкой с ephemeral apply/
dismiss), теперь СНИМАЕТ осиротевшую метку вместо тихого return. Это
самозаживляет тихую дивергенцию и закрывает fire-and-forget resolve/
unresolve enqueue из resolveComment. Операция идемпотентна.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
expectedText брался из debounced REST-снапшота (getAnchoredText по
pages/info), а метка ставилась в live-доке — при расхождении (док ушёл
вперёд за окно дебаунса) apply строго сравнивал текст под меткой с
устаревшим stored selection и давал 409 на каждый вызов.

MCP-клиент теперь в transform-фазе (та же версия live-дока, где ставится
метка) перечитывает фактическую подстроку под меткой и, если она
отличается от сохранённого selection, синкает её через новый эндпоинт
POST /comments/resync-suggestion-anchor. Best-effort: сбой синка не
откатывает уже заякоренный комментарий, а лишь выдаёт мягкое
предупреждение. Совпадающий снапшот не делает лишнего round-trip.

Сервер: resyncSuggestionAnchor правит только stored selection
незаселённой suggestion своего автора (guards: top-level, есть
suggestedText, не applied/resolved, отличается от suggestedText),
идемпотентно, без ws-бродкаста.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DO1: тест audit-swallow был вакуумен (log() — fire-and-forget void persist(),
.not.toThrow() зелен независимо от try/catch). Теперь: после failInsert()
шпионим Logger.warn + слушаем unhandledRejection, флашим микро/макротаски,
ассертим warn=1 и 0 floating-rejection. Mutation: убрать try/catch у persist
→ красный.
DO2: dominant-run тест не отличал longest от first (самый длинный ран был и
первым). Перестроено: короткий plain ран впереди, длинный bold — следом →
ассерт bold:true держится только при genuine longest; +тест tie→first.
Mutation: reduce→segments[0] → красный.
DO3: CHANGELOG [Unreleased] — 3 записи по #496.
DO4: убран no-op override insertAttrs.comment (=dominant.markAttrs, а он и есть
attributes['comment'] — сегменты фильтруются по нему; {...attributes,comment:
attributes.comment}={...attributes}); коммент про 'defensive re-assert' исправлен.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder force-pushed feat/496-suggestions-audit from 6d36c03675 to eae7640f30 2026-07-11 23:45:29 +03:00 Compare
Collaborator

🤖 Ре-ревью · #512 (comments audit + apply-целостность, #496) round 2 — голова eae7640f. Вердикт: PASS

Все 3 DO закрыты и сверены адверсариально мутацией (не поверил на слово):

  1. [test-coverage] audit-swallow тест теперь НЕ вакуумен. Тест спаит Logger.warn + вешает unhandledRejection-листенер + флашит микротаски/макротик, ассертит: warn вызван 1× с «Failed to persist audit event» И rejections пуст. Проверил мутацией: вставил throw err в catch именно persist() → тест краснеет (оба ассерта падают). Прежний .not.toThrow() на voided-promise больше не единственная проверка.
  2. [test-coverage] dominant-run тест ловит longest-vs-first. Фикстура теперь bolded(6 симв.) НЕ первым. Мутация: reduce(longest)segments[0] → тест «DOMINANT (longest) run, NOT the leading one» краснеет. Заперто.
  3. [documentation] CHANGELOG — записи про resync-409 + формат-сохранение + audit добавлены.

Гейт: tsc 0; #512-поверхность (comment apply/dismiss/resync + audit + yjs + comment-mark) — 60/60. Ветка ребейзнута на актуальный develop, tsc/специ чисты.

Фикс-раунд не внёс нового бага. Approve.

## 🤖 Ре-ревью · #512 (comments audit + apply-целостность, #496) round 2 — голова `eae7640f`. Вердикт: **PASS** ✅ Все 3 DO закрыты и **сверены адверсариально мутацией** (не поверил на слово): 1. **[test-coverage] audit-swallow тест теперь НЕ вакуумен.** Тест спаит `Logger.warn` + вешает `unhandledRejection`-листенер + флашит микротаски/макротик, ассертит: warn вызван 1× с «Failed to persist audit event» И rejections пуст. **Проверил мутацией:** вставил `throw err` в catch именно `persist()` → тест краснеет (оба ассерта падают). Прежний `.not.toThrow()` на voided-promise больше не единственная проверка. 2. **[test-coverage] dominant-run тест ловит longest-vs-first.** Фикстура теперь `bolded`(6 симв.) НЕ первым. **Мутация:** `reduce(longest)` → `segments[0]` → тест «DOMINANT (longest) run, NOT the leading one» краснеет. Заперто. 3. **[documentation] CHANGELOG** — записи про resync-409 + формат-сохранение + audit добавлены. **Гейт:** tsc 0; #512-поверхность (comment apply/dismiss/resync + audit + yjs + comment-mark) — **60/60**. Ветка ребейзнута на актуальный develop, tsc/специ чисты. Фикс-раунд не внёс нового бага. Approve.
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 00:02:16 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/496-suggestions-audit:feat/496-suggestions-audit
git checkout feat/496-suggestions-audit
Sign in to join this conversation.