feat(client): intraline diff в блоке предложения-правки (#331) #336

Merged
vvzvlad merged 2 commits from fix/331-intraline-diff into develop 2026-07-04 17:45:11 +03:00
Collaborator

Summary

Intraline diff в блоке предложения-правки (#315): вместо «вся старая строка красным зачёркнута / вся новая зелёным» подсвечиваются только ИЗМЕНЁННЫЕ фрагменты внутри строки (git-style). Правка одной буквы (заведем→заведём) теперь видна сразу. closes #331.

Чисто клиентское, только рендер — БД/бэкенд/MCP/IComment/мутации/Apply/Badge не тронуты.

Как

  • Новая ЧИСТАЯ computeSuggestionDiff(old, new) => { old: Segment[], new: Segment[] } (Segment = {text, changed}) в suggestion.ts. Гибрид word+char: diffWordsWithSpace — словный каркас, затем diffChars внутри смежной пары removed+added (замена слова), так что подсвечиваются только различающиеся буквы, а не слово целиком; одиночная вставка/удаление — целиком changed; equal — common с обеих сторон. Конкатенация каждой стороны воспроизводит вход (lossless). useMemo по [selection, suggestedText].
  • comment-list-item.tsx: посегментный рендер <span> вместо двух цельных <Text>; changed-сегменты получают .suggestionChanged (усиленный currentColor-тинт + bold, БЕЗ text-decoration — line-through старого блока сохраняется на изменённых буквах; вся старая строка всё ещё читается как removed).
  • diff@8.0.3 (jsdiff, уже в корневом package.json) добавлен в apps/client/package.json (+ lockfile, аддитивно).

How verified

  • tsc --noEmit — чисто.
  • client vitest run892 passed | 1 expected-fail (97 файлов).
  • Новый suggestion.test.ts: заведем→заведём (ровно е/ё changed), замена слова (общее мир остаётся common, без побуквенного шума), вставка/удаление слова, идентичные строки. Ассерты по сегментам (текст + флаги), non-vacuous.

Ограничение честно: визуального тулинга в репо нет — пиксельный вид тинта/жирности на изменённых токенах и на узком (390px) comment-панели нужен человеческий проход. Логика diff и рендер-компиляция проверены (юнит + tsc + полный suite).

Checklist

  • intraline (word+char) подсветка только изменённых фрагментов
  • чисто клиентское, backend/MCP/round-trip не тронуты
  • diff-функция чистая + юнит-тесты
  • визуальный проход (нужен человек — нет тулинга)

🤖 Generated with Claude Code

## Summary Intraline diff в блоке предложения-правки (#315): вместо «вся старая строка красным зачёркнута / вся новая зелёным» подсвечиваются только ИЗМЕНЁННЫЕ фрагменты внутри строки (git-style). Правка одной буквы (`заведем→заведём`) теперь видна сразу. `closes #331`. Чисто клиентское, только рендер — БД/бэкенд/MCP/IComment/мутации/Apply/Badge не тронуты. ## Как - Новая ЧИСТАЯ `computeSuggestionDiff(old, new) => { old: Segment[], new: Segment[] }` (`Segment = {text, changed}`) в `suggestion.ts`. Гибрид word+char: `diffWordsWithSpace` — словный каркас, затем `diffChars` внутри смежной пары removed+added (замена слова), так что подсвечиваются только различающиеся буквы, а не слово целиком; одиночная вставка/удаление — целиком changed; equal — common с обеих сторон. Конкатенация каждой стороны воспроизводит вход (lossless). `useMemo` по `[selection, suggestedText]`. - `comment-list-item.tsx`: посегментный рендер `<span>` вместо двух цельных `<Text>`; changed-сегменты получают `.suggestionChanged` (усиленный currentColor-тинт + bold, БЕЗ text-decoration — line-through старого блока сохраняется на изменённых буквах; вся старая строка всё ещё читается как removed). - `diff@8.0.3` (jsdiff, уже в корневом package.json) добавлен в `apps/client/package.json` (+ lockfile, аддитивно). ## How verified - `tsc --noEmit` — чисто. - client `vitest run` — **892 passed | 1 expected-fail** (97 файлов). - Новый `suggestion.test.ts`: `заведем→заведём` (ровно `е`/`ё` changed), замена слова (общее `мир` остаётся common, без побуквенного шума), вставка/удаление слова, идентичные строки. Ассерты по сегментам (текст + флаги), non-vacuous. **Ограничение честно:** визуального тулинга в репо нет — пиксельный вид тинта/жирности на изменённых токенах и на узком (390px) comment-панели нужен человеческий проход. Логика diff и рендер-компиляция проверены (юнит + tsc + полный suite). ## Checklist - [x] intraline (word+char) подсветка только изменённых фрагментов - [x] чисто клиентское, backend/MCP/round-trip не тронуты - [x] diff-функция чистая + юнит-тесты - [ ] визуальный проход (нужен человек — нет тулинга) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
agent_coder added 1 commit 2026-07-04 15:17:54 +03:00
The suggestion block (#315) struck the whole `selection` red and showed the whole
`suggestedText` green, so a one-letter edit (заведем→заведём) highlighted the
entire line. Now only the CHANGED fragments are emphasized intraline, git-style.

Pure, render-only — nothing changes in the DB/backend/MCP/IComment/mutations/
Apply/Badge. New pure `computeSuggestionDiff(old, new) => { old: Segment[], new:
Segment[] }` (Segment = {text, changed}) in suggestion.ts: hybrid word+char —
`diffWordsWithSpace` for the word skeleton, then `diffChars` inside an adjacent
removed+added pair so only the differing letters (not the whole word) are
flagged; a lone insertion/deletion is wholly changed; equal parts are common on
both sides. Concatenating each side reproduces the input (lossless). Wrapped in
`useMemo` on [selection, suggestedText].

comment-list-item.tsx renders per-segment spans instead of two whole <Text>;
changed segments get `.suggestionChanged` (a stronger currentColor tint + bold,
NO text-decoration so the old block's inherited line-through survives on the
changed letters — the whole old line still reads removed, new as added).

`diff@8.0.3` (jsdiff, already in the root package.json) added to
apps/client/package.json (+ lockfile, additive) so the workspace resolves it;
it bundles its own types.

Tests: new suggestion.test.ts (one-letter ё/е; word replacement keeping the
shared word common with no per-letter noise; word insertion/deletion; identical)
— asserts segment text + changed flags, non-vacuous. Two pre-existing
comment-list-item.test assertions switched from getByText (a single text node)
to container.textContent (the new line is now multiple spans) — adapts to the
intended DOM change, not a weakening.

Verified: tsc --noEmit clean; client vitest 892 passed | 1 expected-fail.
Visual/pixel check of the tint at the 390px comment panel needs a human (no
screenshot tooling in-repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-04 15:17:54 +03:00
Collaborator

Ревью — #336 (intraline diff в блоке предложения-правки, #331), round 1, head f13105333, base develop f5d19f97

Вердикт: CHANGES — фича чистая, корректная и хорошо покрыта; правка одна и микроскопическая: комментарий к CSS-правилу .suggestionChanged описывает подчёркивание, которого в правиле нет (и сам себе противоречит двумя строками ниже). Почини F1 — и PASS.

Объективка запущена мной (head f13105333, client): tsc --noEmit0; vitest на suggestion + comment-list-item20 passed (2 файла). Зелёная. (Кодер: полный client-suite 892 passed.)

Веер 7 аспектов (security/stability/test-coverage/conventions/simplification/regressions/coherence):

  • stabilitycomputeSuggestionDiff проверен эмпирически на реальном diff@8.0.3: 200k+ рандом-кейсов lossless, 0 потерь, ни одного throw (пустые строки, whole-line replace, whitespace, unicode ё/decomposed, emoji, смежные замены); useMemo-deps корректны, рендер без raw-HTML.
  • security — сегменты рендерятся как React-children (эскейпятся), dangerouslySetInnerHTML нет; diff@8.0.3 — аддитивный пин уже-корневого jsdiff; секретов нет.
  • test-coverage — тесты не вакуозны: заведем→заведём ассертит, что changed ТОЛЬКО е/ё (не всё слово) — падал бы на whole-line-baseline; замена слова / вставка / удаление / identical покрыты; comment-list-item.test.tsx адаптирован (getByTexttextContent.toContain из-за разбивки на span'ы), существующие ассерты не ослаблены.
  • simplification — word+char-гибрид оправдан (обе стадии load-bearing: только-diffWordsWithSpace красит слово целиком, только-diffChars местами хуже на whitespace-кейсах), dep уместен, мёртвого кода нет.
  • regressions/coherence/conventions — контракт Segment[] producer↔consumer совпадает; line-through старой строки сохраняется (правило намеренно без text-decoration); CSS-класс новый, коллизий нет; scope-claim верен (только apps/client, backend/MCP/мутации не тронуты).

Do — почини, потом ставь review/needs

  1. F1 [documentation] Комментарий к .suggestionChanged обещает подчёркивание, которого в правиле нет — поправь описаниеapps/client/src/features/comment/components/comment.module.css:56-60.
    Блок-комментарий (:58) говорит «adds a stronger tint plus an underline so the eye lands on the changed letters». Но само правило .suggestionChanged (:61-69) подчёркивания НЕ добавляет — только background (color-mix тинт) + font-weight:700, и внутренний комментарий (:64-65) прямо пишет «No text-decoration here on purpose». Итог: заголовочный комментарий противоречит и коду, и соседнему комментарию — вводит в заблуждение (будущий редактор решит, что подчёркивание потеряли, и «вернёт» его). Внесено этим PR.
    Fix: убери «plus an underline» из :58 (стиль различает changed-фрагмент тинтом + жирностью, без подчёркивания — чтобы наследованный line-through старого блока сохранялся), либо приведи текст в соответствие с фактическим правилом.

DROP — кодеру НЕ делать · калибровочный лог (для оператора)

  • [below-threshold] suggestion/high [regressions] при пустом comment.selection но заданном suggestedText вся новая строка становится lone-insertion → целиком changed (жирная/тинт), а не просто зелёная. Косметика, читается как «всё новое», автор вправе принять — не дефект.
  • [speculative] low/high [simplification] в isReplacementPair ветка (part.added && next.removed) (added-before-removed) не исполняется — jsdiff всегда эмитит removed-before-added (0/300k). Order-agnostic graceful degradation, остаётся lossless; удаление — не явный выигрыш, автор вправе оставить.
## Ревью — #336 (intraline diff в блоке предложения-правки, #331), round 1, head `f13105333`, base develop `f5d19f97` **Вердикт: CHANGES** — фича чистая, корректная и хорошо покрыта; правка одна и микроскопическая: комментарий к CSS-правилу `.suggestionChanged` описывает подчёркивание, которого в правиле нет (и сам себе противоречит двумя строками ниже). Почини F1 — и PASS. **Объективка запущена мной** (head `f13105333`, client): `tsc --noEmit` → **0**; vitest на `suggestion` + `comment-list-item` → **20 passed (2 файла)**. Зелёная. (Кодер: полный client-suite 892 passed.) Веер 7 аспектов (security/stability/test-coverage/conventions/simplification/regressions/coherence): - **stability** — `computeSuggestionDiff` проверен эмпирически на реальном `diff@8.0.3`: **200k+ рандом-кейсов lossless, 0 потерь, ни одного throw** (пустые строки, whole-line replace, whitespace, unicode `ё`/decomposed, emoji, смежные замены); `useMemo`-deps корректны, рендер без raw-HTML. - **security** — сегменты рендерятся как React-children (эскейпятся), `dangerouslySetInnerHTML` нет; `diff@8.0.3` — аддитивный пин уже-корневого jsdiff; секретов нет. - **test-coverage** — тесты не вакуозны: `заведем→заведём` ассертит, что changed ТОЛЬКО `е`/`ё` (не всё слово) — падал бы на whole-line-baseline; замена слова / вставка / удаление / identical покрыты; `comment-list-item.test.tsx` адаптирован (`getByText`→`textContent.toContain` из-за разбивки на span'ы), существующие ассерты не ослаблены. - **simplification** — word+char-гибрид оправдан (обе стадии load-bearing: только-`diffWordsWithSpace` красит слово целиком, только-`diffChars` местами хуже на whitespace-кейсах), dep уместен, мёртвого кода нет. - **regressions/coherence/conventions** — контракт `Segment[]` producer↔consumer совпадает; line-through старой строки сохраняется (правило намеренно без text-decoration); CSS-класс новый, коллизий нет; scope-claim верен (только `apps/client`, backend/MCP/мутации не тронуты). ### Do — почини, потом ставь `review/needs` 1. **F1 [documentation] Комментарий к `.suggestionChanged` обещает подчёркивание, которого в правиле нет — поправь описание** — `apps/client/src/features/comment/components/comment.module.css:56-60`. Блок-комментарий (:58) говорит «adds a stronger tint plus **an underline** so the eye lands on the changed letters». Но само правило `.suggestionChanged` (:61-69) подчёркивания НЕ добавляет — только `background` (color-mix тинт) + `font-weight:700`, и внутренний комментарий (:64-65) прямо пишет «**No `text-decoration` here on purpose**». Итог: заголовочный комментарий противоречит и коду, и соседнему комментарию — вводит в заблуждение (будущий редактор решит, что подчёркивание потеряли, и «вернёт» его). Внесено этим PR. Fix: убери «plus an underline» из :58 (стиль различает changed-фрагмент тинтом + жирностью, без подчёркивания — чтобы наследованный line-through старого блока сохранялся), либо приведи текст в соответствие с фактическим правилом. --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора) - `[below-threshold]` `suggestion/high` **[regressions]** при пустом `comment.selection` но заданном `suggestedText` вся новая строка становится lone-insertion → целиком changed (жирная/тинт), а не просто зелёная. Косметика, читается как «всё новое», автор вправе принять — не дефект. - `[speculative]` `low/high` **[simplification]** в `isReplacementPair` ветка `(part.added && next.removed)` (added-before-removed) не исполняется — jsdiff всегда эмитит removed-before-added (0/300k). Order-agnostic graceful degradation, остаётся lossless; удаление — не явный выигрыш, автор вправе оставить. <!-- state:review reviewed_head=f13105333abb287f7d32b3e4305f20854cc4c160 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-04 15:46:58 +03:00
Author
Collaborator

F1: fixed — поправил заголовочный комментарий к .suggestionChanged (comment.module.css): было «adds ... an underline», стало «adds a stronger tint plus bold weight». Правило действительно даёт только color-mix тинт + font-weight:700, без text-decoration (что и подтверждает внутренний комментарий). Только текст комментария, поведение/тесты не тронуты. review/needs.

F1: fixed — поправил заголовочный комментарий к `.suggestionChanged` (comment.module.css): было «adds ... an underline», стало «adds a stronger tint plus bold weight». Правило действительно даёт только color-mix тинт + font-weight:700, без text-decoration (что и подтверждает внутренний комментарий). Только текст комментария, поведение/тесты не тронуты. review/needs.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-04 16:21:42 +03:00
agent_coder added 1 commit 2026-07-04 16:21:43 +03:00
The header comment claimed the rule adds 'an underline'; it does not — it adds a
color-mix tint + font-weight:700, and the inner comment already notes text-
decoration is omitted on purpose. Aligned the header comment with the rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ре-ревью — #336 (intraline diff в блоке предложения-правки, #331), round 2, head 94f60cf0, base develop

Вердикт: PASS — F1 закрыт, готово к мержу. Delta r1→r2 = ровно одна строка (комментарий CSS), scope-creep нет; остальной PR байт-идентичен полностью отвеерянному round-1 состоянию (все 7 аспектов там LGTM).

F1 fixed (сверено по коду): заголовочный комментарий .suggestionChanged (comment.module.css:56-60) теперь «adds a stronger tint plus bold weight» — соответствует правилу (color-mix тинт + font-weight:700, без text-decoration) и больше не противоречит внутреннему комментарию «No text-decoration here on purpose». Ложное упоминание подчёркивания убрано.

Объективка перезапущена мной (head 94f60cf0): client tsc --noEmit0; vitest suggestion + comment-list-item20 passed. Зелёная. (Изменение — только текст комментария, поведение/тесты не тронуты.)

## Ре-ревью — #336 (intraline diff в блоке предложения-правки, #331), round 2, head `94f60cf0`, base develop **Вердикт: PASS** — F1 закрыт, готово к мержу. Delta r1→r2 = ровно одна строка (комментарий CSS), scope-creep нет; остальной PR байт-идентичен полностью отвеерянному round-1 состоянию (все 7 аспектов там LGTM). **F1 fixed (сверено по коду):** заголовочный комментарий `.suggestionChanged` (`comment.module.css:56-60`) теперь «adds a stronger tint plus **bold weight**» — соответствует правилу (color-mix тинт + `font-weight:700`, без text-decoration) и больше не противоречит внутреннему комментарию «No `text-decoration` here on purpose». Ложное упоминание подчёркивания убрано. **Объективка перезапущена мной** (head `94f60cf0`): client `tsc --noEmit` → **0**; vitest `suggestion` + `comment-list-item` → **20 passed**. Зелёная. (Изменение — только текст комментария, поведение/тесты не тронуты.) <!-- state:review reviewed_head=94f60cf0ec4fd21cf0758e12a4332f115f127c13 round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-04 16:40:08 +03:00
vvzvlad merged commit 20248b8c95 into develop 2026-07-04 17:45:11 +03:00
Sign in to join this conversation.