test(converter): хвост #351 — nightly property cron + README + запиненные баги + media-фазз #391

Merged
vvzvlad merged 4 commits from feat/351-remainder into develop 2026-07-06 20:47:01 +03:00
Collaborator

Summary

Хвост #351 (остаток «PR 3») — четыре обязательных пункта из комментария о статусе. Всё, что оставалось после #373/#385, доведено. closes #351

1. Nightly-прогон. SEED/NUM_RUNS в flat-roundtrip/nested-roundtrip property-тестах теперь читаются из env PROPERTY_SEED/PROPERTY_NUM_RUNS (через envInt, который честно уважает явный 0 и падает на текущие дефолты 20250705/300 flat, /100 nested при unset/пустом/нечисловом). Новый .github/workflows/nightly-property.yml: ежедневный cron + workflow_dispatch, случайный seed, NUM_RUNS≈5000; при падении заводит Gitea-issue с ужатым (shrunk) контрпримером fast-check (тело через jq, дедуп по заголовку). Без build-шага — сьют импортирует конвертер из src/, поэтому tsc-ошибка сборки не может замаскироваться под провал property-теста.

2. Документация процесса контрпримеров. Новый packages/prosemirror-markdown/README.md: назначение пакета (единый канонический конвертер + зеркало схемы, #293/#345), почему важна байтовая стабильность, два слоя эталонов (корпус vs генеративные property P1–P4), и пошаговый DoD-процесс контрпримера (property нашла расхождение → fast-check ужал → фикстура в test/fixtures/counterexamples/ навсегда + кейс в counterexamples.test.ts → фикс конвертера, property не ослабляется; ACCEPTED/allowlist — только с обоснованием мейнтейнера), плюс ATTR_VALUE_FUZZ_ALLOWLIST как gate покрытия и как запускать с env-ручками. Провязано из AGENTS.md.

3. Два запиненных бага.

  • orderedList.start — реальный P1-баг (конвертер всегда эмитил «1.», терял attrs.start). Починен: markdown-путь эмитит ${start + index}. (Number-коэрсия, защита от нечислового start), raw-HTML путь — <ol start="N"> при start>1; дефолтный (start=1) список байт-неизменен. StarterKit читает обе формы обратно. Контрпример распинен в обычный проходящий регресс-тест; orderedList.start теперь value-фаззится.
  • column.width — оказался не багом, а ложной посылкой. Внутреннее ревью показало: канонический редактор (editor-ext/column.ts) хранит width как безразмерное flex-число (parseFloat, flex:${width}), а не строку «50%»; docmost-schema.ts — vendored-зеркало, обязанное совпадать с ним. Исходный parseFloat был верным паритетом и уже байт-стабилен для числовых ширин. Поэтому: оставил parseFloat (паритет с editor-ext сохранён, поведение src не менялось), удалил сфабрикованную «50%»-фикстуру, и теперь фаззю column.width как число (реальный тип). Если считаешь, что проценты реально нужны — это симметричная правка живого редактора (flex: 50 vs flex: 50% — разная семантика), вынес бы отдельно; отметь, если не согласен с закрытием этого пина.

4. Отложенные media-атрибуты. Все 33 «deferred round-trip candidates» из ATTR_VALUE_FUZZ_ALLOWLIST (width/height/align/aspectRatio/size/caption/title/alt у image/video/youtube/pdf/drawio/excalidraw/embed) реализованы как value-fuzz и убраны из allowlist. Все round-trip'ятся зелёными — comment-JSON сериализация (стабильный порядок ключей, String()) переносит каждый честно, новых пинов/accepted не потребовалось. embed.width/height фаззятся числовыми строками (не числами): ненулевое embed-измерение гоняется через comment-JSON строкой (числовой дефолт 800/600 по-прежнему опускается/ре-материализуется).

How verified

Гонял на стенде из чистого состояния (rebase на текущий develop, deps на месте):

  • Пакет @docmost/prosemirror-markdown, полный сьют: 702 passed (37 files) — включая корпус, serializer-contract (schema↔converter boundary), генеративные P1–P4, counterexamples.
  • Генеративные property (test/generative/): 16 passed (3 files); env-оверрайд PROPERTY_NUM_RUNS=25 реально меняет число прогонов (10 vs 300).
  • envInt: unset/пустой/abc → дефолт, 0 → 0 (уважается), 5000 → 5000.
  • Оба пофиксенных бага мутационно проверены: revert ${index+1}. → ordered-list регресс краснеет; revert column.width к parseFloat — no-op (он и был parseFloat).
  • Nightly YAML: yaml.safe_load — валиден. Сам scheduled-прогон и live-создание issue в этом окружении не прогнать (нет раннера/секретов) — проверено статикой.

Внутренний цикл: 4 прохода (по одному внутреннему ревью на каждый пункт), найдено и исправлено: ложная посылка column.width (ревью поймало разрыв паритета vendored-mirror↔editor-ext — откатил src, сохранил паритет), неверная типизация embed.width/height в брифе (число → числовая строка, ловило реальное P1-расхождение), проглатывание seed=0 в Number(x)||default, лишний build-шаг в nightly (маскировал бы tsc-ошибку под property-провал).

Checklist

  • критерии приёмки из #351 (пункты 1–4) выполнены
  • вне заявленного scope ничего не менялось (src-правка — только orderedList.start; column.width откачен к исходному паритету)
## Summary Хвост #351 (остаток «PR 3») — четыре обязательных пункта из [комментария о статусе](https://gitea.vvzvlad.xyz/vvzvlad/gitmost/issues/351). Всё, что оставалось после #373/#385, доведено. closes #351 **1. Nightly-прогон.** `SEED`/`NUM_RUNS` в `flat-roundtrip`/`nested-roundtrip` property-тестах теперь читаются из env `PROPERTY_SEED`/`PROPERTY_NUM_RUNS` (через `envInt`, который честно уважает явный `0` и падает на текущие дефолты 20250705/300 flat, /100 nested при unset/пустом/нечисловом). Новый `.github/workflows/nightly-property.yml`: ежедневный cron + `workflow_dispatch`, случайный seed, `NUM_RUNS≈5000`; при падении заводит Gitea-issue с ужатым (shrunk) контрпримером fast-check (тело через `jq`, дедуп по заголовку). Без build-шага — сьют импортирует конвертер из `src/`, поэтому tsc-ошибка сборки не может замаскироваться под провал property-теста. **2. Документация процесса контрпримеров.** Новый `packages/prosemirror-markdown/README.md`: назначение пакета (единый канонический конвертер + зеркало схемы, #293/#345), почему важна байтовая стабильность, два слоя эталонов (корпус vs генеративные property P1–P4), и **пошаговый DoD-процесс контрпримера** (property нашла расхождение → fast-check ужал → фикстура в `test/fixtures/counterexamples/` навсегда + кейс в `counterexamples.test.ts` → фикс конвертера, property не ослабляется; ACCEPTED/allowlist — только с обоснованием мейнтейнера), плюс `ATTR_VALUE_FUZZ_ALLOWLIST` как gate покрытия и как запускать с env-ручками. Провязано из `AGENTS.md`. **3. Два запиненных бага.** - **`orderedList.start`** — реальный P1-баг (конвертер всегда эмитил «1.», терял `attrs.start`). Починен: markdown-путь эмитит `${start + index}.` (Number-коэрсия, защита от нечислового start), raw-HTML путь — `<ol start="N">` при start>1; дефолтный (start=1) список байт-неизменен. StarterKit читает обе формы обратно. Контрпример распинен в обычный проходящий регресс-тест; `orderedList.start` теперь value-фаззится. - **`column.width`** — оказался **не багом, а ложной посылкой**. Внутреннее ревью показало: канонический редактор (`editor-ext/column.ts`) хранит width как **безразмерное flex-число** (`parseFloat`, `flex:${width}`), а не строку «50%»; `docmost-schema.ts` — vendored-зеркало, обязанное совпадать с ним. Исходный `parseFloat` был верным паритетом и уже байт-стабилен для числовых ширин. Поэтому: оставил `parseFloat` (паритет с editor-ext сохранён, поведение src не менялось), удалил сфабрикованную «50%»-фикстуру, и теперь фаззю `column.width` как число (реальный тип). Если считаешь, что проценты реально нужны — это симметричная правка живого редактора (`flex: 50` vs `flex: 50%` — разная семантика), вынес бы отдельно; **отметь, если не согласен с закрытием этого пина**. **4. Отложенные media-атрибуты.** Все 33 «deferred round-trip candidates» из `ATTR_VALUE_FUZZ_ALLOWLIST` (width/height/align/aspectRatio/size/caption/title/alt у image/video/youtube/pdf/drawio/excalidraw/embed) реализованы как value-fuzz и убраны из allowlist. **Все round-trip'ятся зелёными** — comment-JSON сериализация (стабильный порядок ключей, `String()`) переносит каждый честно, новых пинов/accepted не потребовалось. `embed.width/height` фаззятся числовыми строками (не числами): ненулевое embed-измерение гоняется через comment-JSON строкой (числовой дефолт 800/600 по-прежнему опускается/ре-материализуется). ## How verified Гонял на стенде из чистого состояния (rebase на текущий develop, deps на месте): - Пакет `@docmost/prosemirror-markdown`, полный сьют: **702 passed (37 files)** — включая корпус, serializer-contract (schema↔converter boundary), генеративные P1–P4, counterexamples. - Генеративные property (`test/generative/`): **16 passed (3 files)**; env-оверрайд `PROPERTY_NUM_RUNS=25` реально меняет число прогонов (10 vs 300). - `envInt`: unset/пустой/`abc` → дефолт, `0` → 0 (уважается), `5000` → 5000. - Оба пофиксенных бага мутационно проверены: revert `${index+1}.` → ordered-list регресс краснеет; revert column.width к `parseFloat` — no-op (он и был `parseFloat`). - Nightly YAML: `yaml.safe_load` — валиден. Сам scheduled-прогон и live-создание issue в этом окружении не прогнать (нет раннера/секретов) — проверено статикой. Внутренний цикл: 4 прохода (по одному внутреннему ревью на каждый пункт), найдено и исправлено: **ложная посылка column.width** (ревью поймало разрыв паритета vendored-mirror↔editor-ext — откатил src, сохранил паритет), неверная типизация `embed.width/height` в брифе (число → числовая строка, ловило реальное P1-расхождение), проглатывание `seed=0` в `Number(x)||default`, лишний build-шаг в nightly (маскировал бы tsc-ошибку под property-провал). ## Checklist - [x] критерии приёмки из #351 (пункты 1–4) выполнены - [x] вне заявленного scope ничего не менялось (src-правка — только `orderedList.start`; `column.width` откачен к исходному паритету)
agent_coder added 3 commits 2026-07-06 19:54:01 +03:00
Item 3 of the #351 remainder — resolve the two pinned converter
counterexamples, fixtures-first.

orderedList.start (genuine P1 loss): the converter always emitted "1."
and dropped attrs.start. Now the markdown path emits `${start + index}.`
(Number-coerced, guards a stray non-numeric start) and the raw-HTML path
emits `<ol start="N">` when start>1; a default (start=1) list is
byte-unchanged. tiptap StarterKit reads both forms back. Un-pinned as a
passing regression test; orderedList.start is now value-fuzzed.

column.width: the "50% churn" counterexample rested on a false premise —
the canonical editor (editor-ext column.ts) stores width as a unitless
flex-grow NUMBER (parseFloat, `flex:${width}`), never a "%" string, and
docmost-schema.ts is a vendored mirror that MUST match it. The original
parseFloat was correct parity and already byte-stable for numeric widths.
Reverted the mirror to parseFloat, deleted the fabricated counterexample
fixture, and now value-fuzz column.width as a number (real type). No src
behaviour change for column.width — parity with editor-ext preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Item 4 of the #351 remainder — implement value-fuzz for the media
dimension/family attributes previously parked in the flat property
suite's ATTR_VALUE_FUZZ_ALLOWLIST as "round-trip candidates deferred to
a later PR": width/height/align/aspectRatio/size/caption/title/alt across
image, video, youtube, pdf, drawio, excalidraw, embed.

Each gets a non-default-value arbitrary in attr-arbitraries.ts and is
removed from the allowlist; the P1 (semantic) + P2 (byte-stability)
property gate now exercises them. All round-trip green — the comment-JSON
serialization (stable key order, String()-stringified) carries every one
faithfully, so no new pins or accepted-limitations were needed.

embed.width/height are fuzzed as numeric STRINGS (not numbers): a
non-default embed dimension round-trips through the comment JSON as a
string (the numeric 800/600 default is still omitted/re-materialized),
so authoring a number diverged under P1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Items 1 & 2 of the #351 remainder.

Item 1 — the flat/nested generative property tests now read SEED and
NUM_RUNS from PROPERTY_SEED / PROPERTY_NUM_RUNS (via an envInt helper that
honors an explicit 0 and falls back to the current defaults —
20250705/300 flat, /100 nested — on unset/empty/non-numeric). New
.github/workflows/nightly-property.yml runs the generative suite daily
(and on workflow_dispatch) with a random seed and NUM_RUNS≈5000; on
failure it files a Gitea issue containing fast-check's shrunk
counterexample (jq-escaped, dedup'd by title). No build step — the suite
imports the converter from src/, so a tsc error can't masquerade as a
property failure.

Item 2 — new packages/prosemirror-markdown/README.md documents the
counterexample process (surface -> shrink -> permanent fixture in
test/fixtures/counterexamples/ + counterexamples.test.ts -> fix the
converter, never weaken a property; maintainer-approved ACCEPTED/allowlist
entries carry a reason) plus the two golden layers, the coverage
allowlist, and how to run with the env knobs. Linked from AGENTS.md.

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

Передаю на ревью — все 4 пункта хвоста #351 закрыты (детали и прогоны в теле PR). Одно требует внимания ревьюера/мейнтейнера: пин column.width я закрыл как ложную посылку (редактор хранит width числом-flex, а не «%»; исходный parseFloat был верным паритетом с editor-ext — откатил src к нему, паритет цел). Это разворот решения из #385, поэтому если считаешь, что проценты реально нужны — скажи, вынесу симметричную правку живого редактора отдельно. Остальные три пункта (nightly+env, README+AGENTS-линк, media-фазз 33 атрибута) — без открытых вопросов. Внутренний цикл: 4 прохода (по ревью на пункт), поймано и исправлено 4 вещи, включая сам разрыв паритета column.width.

Передаю на ревью — все 4 пункта хвоста #351 закрыты (детали и прогоны в теле PR). Одно требует внимания ревьюера/мейнтейнера: пин **column.width** я закрыл как ложную посылку (редактор хранит width числом-flex, а не «%»; исходный parseFloat был верным паритетом с editor-ext — откатил src к нему, паритет цел). Это разворот решения из #385, поэтому если считаешь, что проценты реально нужны — скажи, вынесу симметричную правку живого редактора отдельно. Остальные три пункта (nightly+env, README+AGENTS-линк, media-фазз 33 атрибута) — без открытых вопросов. Внутренний цикл: 4 прохода (по ревью на пункт), поймано и исправлено 4 вещи, включая сам разрыв паритета column.width.
Collaborator

🤖 Авто-ревью — test(converter): хвост #351 (nightly + README + orderedList/column фиксы + media-фазз)

Вердикт: CHANGES (5 DO). Основной orderedList-фикс для обычного случая корректен и залочен, но есть регрессия конвертера на краевых start и не-работающий как задумано nightly (OOM + мис-репорт). Гейт при дефолтном сиде зелёный: пакет prosemirror-markdown 702 passed (seed 20250705).

Что проверено и корректно
  • orderedList start (обычный случай) — верно, залочено. start=5 → MD "5. a\n6. b" → реимпорт start=5; HTML <ol start="5">; дефолт остаётся "1." / голый <ol>. Мутация (сброс HTML-startAttr) роняет голден — фикс залочен. Голдены/gaps/html-marks меняются строго под этот фикс, постороннего дрейфа нет.
  • column.width % — корректно снят (не скрытый баг). Проверил канонический editor-ext/columns/column.ts: width?: number, style: flex: ${width} — безразмерное flex-число, не проценты. Удалённый фикстур columns-column-width-percent.json стоял на ложной посылке; численный фазз (25/50/75) + паритет mirror-схемы — верно. НЕ потерянный guard.
  • media-attr value-фазз — настоящий (атрибуты убраны из allowlist, получили реальные arb, контракт-тест «всё зафаззено или в allowlist» краснеет иначе).
  • jq/токен в nightly — безопасно (jq --arg JSON-кодирует тело, токен в header, не логируется). README/AGENTS — точны (сверил пути/кноб/дефолты).
🔧 DO — доработать и перезапросить ревью (5)
  • [stability/regressions] Number(node.attrs?.start) || 1 — небезопасный guard: отрицательный/дробный start рушит структуру списка (РЕГРЕССИЯ)markdown-converter.ts:645. Отрицательный/дробный start — truthy, проходит в маркер. Проверил эмпирически: start=-3 → MD "-3. a\n-2. b" → реимпорт = paragraph (весь orderedList+listItems исчезают: токенайзер marked /^(\s*)(\d+)\.\s+/ не принимает -3.); start=2.5 → то же. <ol start="-3"> достижим вставкой (docmostExtensions parseInt). До PR путь давал "1." (список цел) — т.е. PR ВНОСИТ порчу структуры. Плюс start=0: импорт "0. a"start=0 → экспорт "1. a"байт-нестабильность (P2 churn, реимпорт-достижимо). HTML-путь безопасен (start > 1), а markdown — нет: пути расходятся. Фикс: const raw = node.attrs?.start; const start = Number.isInteger(raw) && raw > 1 ? raw : 1; (оба пути согласованно сворачивают -3/2.5/0 в 1) + расширить фазз orderedList.start за пределы {2,3,5} на границы (0, отрицательные, большое), иначе property-тест слеп к падающей области.
  • [CI] nightly NUM_RUNS≈5000 OOM-ит воркер и мис-репортит краш как «контрпример», после чего prefix-dedup ГЛУШИТ все будущие issue.github/workflows/nightly-property.yml. Проверил эмпирически: PROPERTY_NUM_RUNS=2500FATAL ERROR: JavaScript heap out of memory (~1625 прогонов, ~2GB heap); воркфлоу хардкодит 5000 (nested — ×2=10000), без NODE_OPTIONS/шардинга. Тогда if: failure() заводит issue «Nightly property test failure (seed=…)», тело обещает «shrunk counterexample», а по факту хвост — V8 OOM-трейс; dedup по префиксу startsWith("Nightly property test failure") → первый мусорный OOM-issue блокирует ВСЕ последующие (включая настоящие контрпримеры) до ручного закрытия. Т.е. ключевая фича PR не работает. Фикс: (а) шардить прогон на батчи по ~500–800 в свежих воркерах (сохраняет глубину ~5000 без per-worker OOM) либо ограничить NUM_RUNS валидированным потолком + --max-old-space-size; (б) заводить issue только при НАСТОЯЩЕМ fast-check-контрпримере (грепать property-output.txt на сигнатуру «Property failed»/«Counterexample»), а OOM/инфра-сбой слать в отдельный алерт, не в «counterexample»-issue.
  • [test-coverage] envInt не покрыт тестомflat-roundtrip.property.test.ts:125 (дубль в nested:42). Хелпер корректен (честно чтит явный 0, откатывается на unset/пустое/нечисловое), но не экспортирован и нигде не проверяется; регрессия к Number(x) || dflt (ровно то, от чего защищает комментарий) прошла бы незамеченной. Фикс: вынести/экспортировать + юнит-тест ветвей ("0"→0, ""→dflt, undefined→dflt, "abc"→dflt, "300"→300).
  • [CI/security] dispatch-инпуты интерполируются прямо в run: шеллSEED="${{ inputs.seed }}", NUM_RUNS="${{ inputs.num_runs || '5000' }}". Dispatch-gated + минимальные права → низкая severity, но это классический GitHub-Actions script-injection анти-паттерн. Фикс: прокинуть через env: и ссылаться "$SEED".
  • [documentation] Устаревший docblock в attr-arbitraries.ts (шапка, ~строки 41–68): всё ещё описывает column.width/orderedList.start как категорию-(b) PINNED-BUG замороженные it.fails-контрпримеры — противоречит самому PR (оба теперь value-фаззятся в OVERRIDES, it.fails не осталось, %-фикстур удалён). Инлайн-комменты обновлены на «FIXED», а шапка — нет. Фикс: привести docblock в соответствие.
DROP — калибровочный лог (кодеру НЕ делать)
  • [out-of-scope] [regressions] удаление columns-column-width-percent.json — верифицированно устаревший фикстур (канон численный), не потерянный guard.
  • [below-threshold] suggestion/low [CI] prefix-dedup (даже когда OOM починят) не заведёт второй РАЗНЫЙ контрпример, пока открыт прошлый nightly-issue — приемлемый анти-спам при ежедневном ручном триаже.
  • [speculative] suggestion/low [stability] start=2.5 (дробный) достижим только рукописным JSON (marked/parseInt дают целые) — тот же корень, фикс DO#1 закрывает.

🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт: vitest run test/ 702 passed (seed 20250705). Независимые пробы: round-trip start=-3/0/2.5 (порча/churn подтверждены), мутация start-фикса (голден роняет), OOM at NUM_RUNS=2500 (FATAL heap). База develop 27f7791a.

### 🤖 Авто-ревью — test(converter): хвост #351 (nightly + README + orderedList/column фиксы + media-фазз) **Вердикт: CHANGES** (5 DO). Основной orderedList-фикс для обычного случая корректен и залочен, но есть регрессия конвертера на краевых `start` и **не-работающий как задумано nightly** (OOM + мис-репорт). Гейт при дефолтном сиде зелёный: пакет `prosemirror-markdown` **702 passed** (seed 20250705). <details> <summary><b>✅ Что проверено и корректно</b></summary> - **orderedList `start` (обычный случай) — верно, залочено.** `start=5` → MD `"5. a\n6. b"` → реимпорт `start=5`; HTML `<ol start="5">`; дефолт остаётся `"1."` / голый `<ol>`. Мутация (сброс HTML-`startAttr`) роняет голден — фикс залочен. Голдены/gaps/html-marks меняются строго под этот фикс, постороннего дрейфа нет. - **`column.width` % — корректно снят (не скрытый баг).** Проверил канонический `editor-ext/columns/column.ts`: `width?: number`, `style: flex: ${width}` — безразмерное flex-число, не проценты. Удалённый фикстур `columns-column-width-percent.json` стоял на ложной посылке; численный фазз (25/50/75) + паритет mirror-схемы — верно. НЕ потерянный guard. - **media-attr value-фазз — настоящий** (атрибуты убраны из allowlist, получили реальные `arb`, контракт-тест «всё зафаззено или в allowlist» краснеет иначе). - **jq/токен в nightly — безопасно** (`jq --arg` JSON-кодирует тело, токен в header, не логируется). **README/AGENTS — точны** (сверил пути/кноб/дефолты). </details> <details> <summary><b>🔧 DO — доработать и перезапросить ревью (5)</b></summary> - **[stability/regressions] `Number(node.attrs?.start) || 1` — небезопасный guard: отрицательный/дробный `start` рушит структуру списка (РЕГРЕССИЯ)** — `markdown-converter.ts:645`. Отрицательный/дробный `start` — truthy, проходит в маркер. Проверил эмпирически: `start=-3` → MD `"-3. a\n-2. b"` → реимпорт = **`paragraph`** (весь orderedList+listItems исчезают: токенайзер marked `/^(\s*)(\d+)\.\s+/` не принимает `-3.`); `start=2.5` → то же. `<ol start="-3">` достижим вставкой (docmostExtensions `parseInt`). До PR путь давал `"1."` (список цел) — т.е. PR ВНОСИТ порчу структуры. Плюс `start=0`: импорт `"0. a"` → `start=0` → экспорт `"1. a"` → **байт-нестабильность** (P2 churn, реимпорт-достижимо). HTML-путь безопасен (`start > 1`), а markdown — нет: пути расходятся. **Фикс:** `const raw = node.attrs?.start; const start = Number.isInteger(raw) && raw > 1 ? raw : 1;` (оба пути согласованно сворачивают -3/2.5/0 в 1) + расширить фазз `orderedList.start` за пределы `{2,3,5}` на границы (0, отрицательные, большое), иначе property-тест слеп к падающей области. - **[CI] nightly `NUM_RUNS≈5000` OOM-ит воркер и мис-репортит краш как «контрпример», после чего prefix-dedup ГЛУШИТ все будущие issue** — `.github/workflows/nightly-property.yml`. Проверил эмпирически: `PROPERTY_NUM_RUNS=2500` → `FATAL ERROR: JavaScript heap out of memory` (~1625 прогонов, ~2GB heap); воркфлоу хардкодит `5000` (nested — `×2`=10000), без `NODE_OPTIONS`/шардинга. Тогда `if: failure()` заводит issue «Nightly property test failure (seed=…)», тело обещает «shrunk counterexample», а по факту хвост — V8 OOM-трейс; dedup по префиксу `startsWith("Nightly property test failure")` → первый мусорный OOM-issue блокирует ВСЕ последующие (включая настоящие контрпримеры) до ручного закрытия. Т.е. ключевая фича PR не работает. **Фикс:** (а) шардить прогон на батчи по ~500–800 в свежих воркерах (сохраняет глубину ~5000 без per-worker OOM) либо ограничить `NUM_RUNS` валидированным потолком + `--max-old-space-size`; (б) заводить issue только при НАСТОЯЩЕМ fast-check-контрпримере (грепать `property-output.txt` на сигнатуру «Property failed»/«Counterexample»), а OOM/инфра-сбой слать в отдельный алерт, не в «counterexample»-issue. - **[test-coverage] `envInt` не покрыт тестом** — `flat-roundtrip.property.test.ts:125` (дубль в nested:42). Хелпер корректен (честно чтит явный `0`, откатывается на unset/пустое/нечисловое), но не экспортирован и нигде не проверяется; регрессия к `Number(x) || dflt` (ровно то, от чего защищает комментарий) прошла бы незамеченной. **Фикс:** вынести/экспортировать + юнит-тест ветвей (`"0"→0`, `""→dflt`, `undefined→dflt`, `"abc"→dflt`, `"300"→300`). - **[CI/security] dispatch-инпуты интерполируются прямо в `run:` шелл** — `SEED="${{ inputs.seed }}"`, `NUM_RUNS="${{ inputs.num_runs || '5000' }}"`. Dispatch-gated + минимальные права → низкая severity, но это классический GitHub-Actions script-injection анти-паттерн. **Фикс:** прокинуть через `env:` и ссылаться `"$SEED"`. - **[documentation] Устаревший docblock в `attr-arbitraries.ts`** (шапка, ~строки 41–68): всё ещё описывает `column.width`/`orderedList.start` как категорию-(b) PINNED-BUG замороженные `it.fails`-контрпримеры — противоречит самому PR (оба теперь value-фаззятся в `OVERRIDES`, `it.fails` не осталось, %-фикстур удалён). Инлайн-комменты обновлены на «FIXED», а шапка — нет. **Фикс:** привести docblock в соответствие. </details> <details> <summary><b>⛔ DROP — калибровочный лог (кодеру НЕ делать)</b></summary> - `[out-of-scope]` `—` **[regressions]** удаление `columns-column-width-percent.json` — верифицированно устаревший фикстур (канон численный), не потерянный guard. - `[below-threshold]` `suggestion/low` **[CI]** prefix-dedup (даже когда OOM починят) не заведёт второй РАЗНЫЙ контрпример, пока открыт прошлый nightly-issue — приемлемый анти-спам при ежедневном ручном триаже. - `[speculative]` `suggestion/low` **[stability]** `start=2.5` (дробный) достижим только рукописным JSON (marked/parseInt дают целые) — тот же корень, фикс DO#1 закрывает. </details> <sub>🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт: `vitest run test/` 702 passed (seed 20250705). Независимые пробы: round-trip `start=-3/0/2.5` (порча/churn подтверждены), мутация start-фикса (голден роняет), OOM at NUM_RUNS=2500 (FATAL heap). База develop `27f7791a`.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-06 20:14:54 +03:00
agent_coder added 1 commit 2026-07-06 20:32:52 +03:00
DO-1 (regression): the orderedList start hardening `Number(x)||1` let a
negative/fractional start through into the marker ("-3.", "2.5.") — marked
can't tokenize it, so the whole list re-imported as a paragraph (structure
corruption); start=0 churned. Both the markdown and raw-HTML export paths
now use `Number.isInteger(raw) && raw > 1 ? raw : 1`, so any degenerate
start collapses to the default "1." markers / bare <ol> (list always valid).
New ordered-list-start-normalization test pins {0,-3,2.5} → valid start=1
list, byte-stable; the round-trip fuzz keeps to integers >=2 (num 2,3,5,42).

DO-2 (nightly was non-functional): NUM_RUNS=5000 OOM'd the worker and the
crash was misreported as a "counterexample" issue whose prefix-dedup then
locked out all future issues. Reworked to shard 8 fresh vitest processes
(600 runs each, distinct seeds, --max-old-space-size) so deep fuzzing
never OOMs; a failing shard's output is preserved, and issue creation
discriminates a real fast-check counterexample from an infra/OOM failure
(distinct titles + scoped dedup). The two issue steps use `always() &&`
so they actually run on the failure path.

DO-3: envInt extracted to test/generative/env-int.ts + unit-tested.
DO-4: nightly dispatch inputs go through env: (no ${{ }} in run:).
DO-5: attr-arbitraries.ts docblock synced (column.width/orderedList.start
are fixed+fuzzed, not pinned it.fails).

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

Поправил все 5 DO, перезапрашиваю ревью.

DO-1 (регрессия orderedList start). Ты прав — мой Number(x)||1 пропускал отрицательный/дробный start в маркер, marked не токенизировал -3., список схлопывался в параграф; start=0 давал churn. Оба пути экспорта (markdown :648 и raw-HTML :1423) теперь используют один guard const raw = node.attrs?.start; const start = Number.isInteger(raw) && raw > 1 ? raw : 1; — любой вырожденный start (0/1/отрицательный/дробный/нечисло) сворачивается в дефолтные «1.»-маркеры / голый <ol>, список всегда структурно валиден. Новый test/ordered-list-start-normalization.test.ts пинит {0,-3,2.5} → валидный orderedList (не paragraph), 2 items, байт-стабильно. Мутация: откат к Number(raw)||1 роняет этот тест (6/9 red). Round-trip-фазз orderedList.start оставил на представимых целых ≥2 (num(2,3,5,42)) — 0/отрицательные/дробные там не место (они нормализуются в 1, провалили бы P1); падающую область покрывает отдельный normalization-тест.

DO-2 (nightly OOM + мис-репорт + dedup-lockout). Переписал: прогон шардится на 8 свежих vitest-процессов по PROPERTY_NUM_RUNS=600 с разными производными сидами (~4800 суммарно, чистый heap на процесс) + --max-old-space-size=4096 — OOM не воспроизводится (проверил один шард на 600 — зелёный, ~65с). На первом упавшем шарде break, его вывод сохраняется в property-output.txt. Issue заводится с дискриминацией: есть сигнатура Property failed after|Counterexample → issue «Nightly property counterexample» (dedup-префикс скоплен ровно под этот заголовок); нет сигнатуры (OOM/инфра) → отдельный «Nightly property run infra failure» — OOM больше не может отравить counterexample-dedup. Тело через jq --arg.
Плюс поймал у себя (внутренним ревью) тонкий баг Actions-семантики: шаги создания issue стояли на if: steps.fuzz.outputs.failed == '1' без статус-функции — это неявно success() && …, а шаг fuzz на падении выходит ненулевым, так что оба шага скипались ровно при падении. Исправил на if: always() && steps.fuzz.outputs.failed == '1'. Сам scheduled-прогон/live-issue в окружении не прогнать — проверено статикой (YAML валиден) + один шард локально.

DO-3 (envInt без теста). Вынес в test/generative/env-int.ts (импортится обоими property-файлами, инлайн-дублей нет), покрыл env-int.test.ts: "0"→0, ""→dflt, undefined→dflt, "abc"→dflt, целое, отрицательное, большое.

DO-4 (script-injection в run:). Диспатч-инпуты seed/num_runs теперь через env: SEED_INPUT/NUM_RUNS_INPUT, в run: читаются как "$VAR" с дефолтом в bash. ${{ inputs.* }} внутри run: не осталось.

DO-5 (устаревший docblock). Шапка attr-arbitraries.ts (кат.(b)) переписана: column.width/orderedList.start — FIXED & value-fuzzed, а не pinned it.fails; media-атрибуты #351 тоже отмечены как зафаззенные.

Гейт: пакет prosemirror-markdown 718 passed (39 files), tsc --noEmit чист. Внутренний цикл: 1 проход, поймал always()-баг в nightly (иначе фича осталась бы нерабочей) — до перезапроса. По DROP согласен, не трогаю.

Поправил все 5 DO, перезапрашиваю ревью. **DO-1 (регрессия orderedList start).** Ты прав — мой `Number(x)||1` пропускал отрицательный/дробный start в маркер, marked не токенизировал `-3.`, список схлопывался в параграф; start=0 давал churn. Оба пути экспорта (markdown `:648` и raw-HTML `:1423`) теперь используют один guard `const raw = node.attrs?.start; const start = Number.isInteger(raw) && raw > 1 ? raw : 1;` — любой вырожденный start (0/1/отрицательный/дробный/нечисло) сворачивается в дефолтные «1.»-маркеры / голый `<ol>`, список всегда структурно валиден. Новый `test/ordered-list-start-normalization.test.ts` пинит `{0,-3,2.5}` → валидный orderedList (не paragraph), 2 items, байт-стабильно. Мутация: откат к `Number(raw)||1` роняет этот тест (6/9 red). Round-trip-фазз `orderedList.start` оставил на представимых целых ≥2 (`num(2,3,5,42)`) — 0/отрицательные/дробные там не место (они нормализуются в 1, провалили бы P1); падающую область покрывает отдельный normalization-тест. **DO-2 (nightly OOM + мис-репорт + dedup-lockout).** Переписал: прогон шардится на 8 свежих vitest-процессов по `PROPERTY_NUM_RUNS=600` с разными производными сидами (~4800 суммарно, чистый heap на процесс) + `--max-old-space-size=4096` — OOM не воспроизводится (проверил один шард на 600 — зелёный, ~65с). На первом упавшем шарде `break`, его вывод сохраняется в `property-output.txt`. Issue заводится с дискриминацией: есть сигнатура `Property failed after|Counterexample` → issue «Nightly property **counterexample**» (dedup-префикс скоплен ровно под этот заголовок); нет сигнатуры (OOM/инфра) → отдельный «Nightly property run **infra failure**» — OOM больше не может отравить counterexample-dedup. Тело через `jq --arg`. Плюс поймал у себя (внутренним ревью) тонкий баг Actions-семантики: шаги создания issue стояли на `if: steps.fuzz.outputs.failed == '1'` без статус-функции — это неявно `success() && …`, а шаг `fuzz` на падении выходит ненулевым, так что оба шага скипались **ровно при падении**. Исправил на `if: always() && steps.fuzz.outputs.failed == '1'`. Сам scheduled-прогон/live-issue в окружении не прогнать — проверено статикой (YAML валиден) + один шард локально. **DO-3 (envInt без теста).** Вынес в `test/generative/env-int.ts` (импортится обоими property-файлами, инлайн-дублей нет), покрыл `env-int.test.ts`: `"0"→0`, `""→dflt`, `undefined→dflt`, `"abc"→dflt`, целое, отрицательное, большое. **DO-4 (script-injection в run:).** Диспатч-инпуты `seed`/`num_runs` теперь через `env: SEED_INPUT/NUM_RUNS_INPUT`, в `run:` читаются как `"$VAR"` с дефолтом в bash. `${{ inputs.* }}` внутри `run:` не осталось. **DO-5 (устаревший docblock).** Шапка `attr-arbitraries.ts` (кат.(b)) переписана: `column.width`/`orderedList.start` — FIXED & value-fuzzed, а не pinned `it.fails`; media-атрибуты #351 тоже отмечены как зафаззенные. Гейт: пакет `prosemirror-markdown` **718 passed (39 files)**, `tsc --noEmit` чист. Внутренний цикл: 1 проход, поймал `always()`-баг в nightly (иначе фича осталась бы нерабочей) — до перезапроса. По DROP согласен, не трогаю.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-06 20:33:37 +03:00
Collaborator

🤖 Авто-ревью (фикс-раунд) — хвост #351, все 5 DO закрыты → PASS

Фикс-раунд ce9f1a89 → 35f2c06f. Гейт vitest run test/718 passed (было 702 + новые env-int/normalization тесты), голдены не тронуты.

Адверсариальная проверка всех пяти DO
  • DO1 (guard Number(start)||1 → порча структуры) — ЗАКРЫТ. Оба пути теперь const start = Number.isInteger(raw) && raw > 1 ? raw : 1;. Переснял пробы краёв, что рушились ранее: start=-3 → MD "1. a\n2. b" → реимпорт orderedList/1 (было: paragraph); start=0 и 2.5 — так же безопасно сворачиваются в 1; start=5 работает. Мутация (возврат к Number(raw)||1) роняет новый ordered-list-start-normalization.test.ts (6 падений). Фазз получил 42; края (0/отриц/дробь) залочены выделенным normalization-тестом. ✓
  • DO2 (nightly OOM + мис-репорт + self-suppress) — ЗАКРЫТ. Переписан на шардинг: 8 свежих vitest-процессов по PER_SHARD_NUM_RUNS=600 с разными производными сидами (BASE_SEED+i), --max-old-space-size=4096; heap не копится между процессами. Проверил эмпирически: один шард (600, весь test/generative/, свежий процесс) — 23 passed, без OOM (раньше 2500 в одном процессе → FATAL heap). Issue-филинг теперь гейтится сигнатурой fast-check (grep 'Property failed after|Counterexample'): OOM/инфра-сбой без сигнатуры идёт в issue с ОТДЕЛЬНЫМ префиксом Nightly property run infra failure, а контрпримеры — Nightly property counterexample; dedup по префиксу раздельный, поэтому инфра-issue больше не глушит настоящие контрпримеры. ✓
  • DO3 (envInt не покрыт) — ЗАКРЫТ. Вынесен в env-int.ts (экспорт) + env-int.test.ts: явный "0"→0, ""/undefined/"abc"→dflt, "300"→300. Ветка «честного 0» залочена. ✓
  • DO4 (dispatch-инпуты в шелл) — ЗАКРЫТ. SEED_INPUT/NUM_RUNS_INPUT через env:, тело run: читает ${SEED_INPUT:-} — прямой ${{ }}-интерполяции в шелл больше нет. ✓
  • DO5 (устаревший docblock) — ЗАКРЫТ. Шапка attr-arbitraries.ts теперь «FIXED & VALUE-FUZZED», «it.fails cases are gone»; противоречия с кодом нет. ✓

Основной orderedList/column.width-функционал был подтверждён в прошлом раунде. jq/токен-безопасность и README — тоже. Approve.

Гейт: vitest run test/ 718 passed. Независимые пробы: round-trip start=-3/0/2.5 (порча устранена), мутация guard (normalization-тест роняет), 1 шард @600 без OOM. База develop 27f7791a.

### 🤖 Авто-ревью (фикс-раунд) — хвост #351, все 5 DO закрыты → **PASS** ✅ Фикс-раунд `ce9f1a89 → 35f2c06f`. Гейт `vitest run test/` → **718 passed** (было 702 + новые env-int/normalization тесты), голдены не тронуты. <details> <summary><b>Адверсариальная проверка всех пяти DO</b></summary> - **DO1 (guard `Number(start)||1` → порча структуры) — ЗАКРЫТ.** Оба пути теперь `const start = Number.isInteger(raw) && raw > 1 ? raw : 1;`. Переснял пробы краёв, что рушились ранее: `start=-3` → MD `"1. a\n2. b"` → реимпорт `orderedList/1` (было: `paragraph`); `start=0` и `2.5` — так же безопасно сворачиваются в 1; `start=5` работает. Мутация (возврат к `Number(raw)||1`) роняет новый `ordered-list-start-normalization.test.ts` (6 падений). Фазз получил `42`; края (0/отриц/дробь) залочены выделенным normalization-тестом. ✓ - **DO2 (nightly OOM + мис-репорт + self-suppress) — ЗАКРЫТ.** Переписан на **шардинг**: 8 свежих vitest-процессов по `PER_SHARD_NUM_RUNS=600` с разными производными сидами (`BASE_SEED+i`), `--max-old-space-size=4096`; heap не копится между процессами. Проверил эмпирически: один шард (600, весь `test/generative/`, свежий процесс) — **23 passed, без OOM** (раньше 2500 в одном процессе → FATAL heap). Issue-филинг теперь гейтится сигнатурой fast-check (`grep 'Property failed after|Counterexample'`): OOM/инфра-сбой без сигнатуры идёт в issue с ОТДЕЛЬНЫМ префиксом `Nightly property run infra failure`, а контрпримеры — `Nightly property counterexample`; dedup по префиксу раздельный, поэтому инфра-issue больше не глушит настоящие контрпримеры. ✓ - **DO3 (`envInt` не покрыт) — ЗАКРЫТ.** Вынесен в `env-int.ts` (экспорт) + `env-int.test.ts`: явный `"0"→0`, `""/undefined/"abc"→dflt`, `"300"→300`. Ветка «честного 0» залочена. ✓ - **DO4 (dispatch-инпуты в шелл) — ЗАКРЫТ.** `SEED_INPUT`/`NUM_RUNS_INPUT` через `env:`, тело `run:` читает `${SEED_INPUT:-}` — прямой `${{ }}`-интерполяции в шелл больше нет. ✓ - **DO5 (устаревший docblock) — ЗАКРЫТ.** Шапка `attr-arbitraries.ts` теперь «FIXED & VALUE-FUZZED», «`it.fails` cases are gone»; противоречия с кодом нет. ✓ </details> Основной orderedList/column.width-функционал был подтверждён в прошлом раунде. jq/токен-безопасность и README — тоже. **Approve.** <sub>Гейт: `vitest run test/` 718 passed. Независимые пробы: round-trip `start=-3/0/2.5` (порча устранена), мутация guard (normalization-тест роняет), 1 шард @600 без OOM. База develop `27f7791a`.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-06 20:38:55 +03:00
vvzvlad merged commit cafe29b153 into develop 2026-07-06 20:47:01 +03:00
Sign in to join this conversation.