test(converter): хвост #351 — nightly property cron + README + запиненные баги + media-фазз #391
Reference in New Issue
Block a user
Delete Branch "feat/351-remainder"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Хвост #351 (остаток «PR 3») — четыре обязательных пункта из комментария о статусе. Всё, что оставалось после #373/#385, доведено. closes #351
1. Nightly-прогон.
SEED/NUM_RUNSвflat-roundtrip/nested-roundtripproperty-тестах теперь читаются из envPROPERTY_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: 50vsflex: 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.test/generative/): 16 passed (3 files); env-оверрайдPROPERTY_NUM_RUNS=25реально меняет число прогонов (10 vs 300).envInt: unset/пустой/abc→ дефолт,0→ 0 (уважается),5000→ 5000.${index+1}.→ ordered-list регресс краснеет; revert column.width кparseFloat— no-op (он и былparseFloat).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
orderedList.start;column.widthоткачен к исходному паритету)Передаю на ревью — все 4 пункта хвоста #351 закрыты (детали и прогоны в теле PR). Одно требует внимания ревьюера/мейнтейнера: пин column.width я закрыл как ложную посылку (редактор хранит width числом-flex, а не «%»; исходный parseFloat был верным паритетом с editor-ext — откатил src к нему, паритет цел). Это разворот решения из #385, поэтому если считаешь, что проценты реально нужны — скажи, вынесу симметричную правку живого редактора отдельно. Остальные три пункта (nightly+env, README+AGENTS-линк, media-фазз 33 атрибута) — без открытых вопросов. Внутренний цикл: 4 прохода (по ревью на пункт), поймано и исправлено 4 вещи, включая сам разрыв паритета column.width.
🤖 Авто-ревью — test(converter): хвост #351 (nightly + README + orderedList/column фиксы + media-фазз)
Вердикт: CHANGES (5 DO). Основной orderedList-фикс для обычного случая корректен и залочен, но есть регрессия конвертера на краевых
startи не-работающий как задумано nightly (OOM + мис-репорт). Гейт при дефолтном сиде зелёный: пакетprosemirror-markdown702 passed (seed 20250705).✅ Что проверено и корректно
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.arb, контракт-тест «всё зафаззено или в allowlist» краснеет иначе).jq --argJSON-кодирует тело, токен в header, не логируется). README/AGENTS — точны (сверил пути/кноб/дефолты).🔧 DO — доработать и перезапросить ревью (5)
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">достижим вставкой (docmostExtensionsparseInt). До 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-тест слеп к падающей области.NUM_RUNS≈5000OOM-ит воркер и мис-репортит краш как «контрпример», после чего 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.envIntне покрыт тестом —flat-roundtrip.property.test.ts:125(дубль в nested:42). Хелпер корректен (честно чтит явный0, откатывается на unset/пустое/нечисловое), но не экспортирован и нигде не проверяется; регрессия кNumber(x) || dflt(ровно то, от чего защищает комментарий) прошла бы незамеченной. Фикс: вынести/экспортировать + юнит-тест ветвей ("0"→0,""→dflt,undefined→dflt,"abc"→dflt,"300"→300).run:шелл —SEED="${{ inputs.seed }}",NUM_RUNS="${{ inputs.num_runs || '5000' }}". Dispatch-gated + минимальные права → низкая severity, но это классический GitHub-Actions script-injection анти-паттерн. Фикс: прокинуть черезenv:и ссылаться"$SEED".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-tripstart=-3/0/2.5(порча/churn подтверждены), мутация start-фикса (голден роняет), OOM at NUM_RUNS=2500 (FATAL heap). База develop27f7791a.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>Поправил все 5 DO, перезапрашиваю ревью.
DO-1 (регрессия orderedList start). Ты прав — мой
Number(x)||1пропускал отрицательный/дробный start в маркер, marked не токенизировал-3., список схлопывался в параграф; start=0 давал churn. Оба пути экспорта (markdown:648и raw-HTML:1423) теперь используют один guardconst 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, а не pinnedit.fails; media-атрибуты #351 тоже отмечены как зафаззенные.Гейт: пакет
prosemirror-markdown718 passed (39 files),tsc --noEmitчист. Внутренний цикл: 1 проход, поймалalways()-баг в nightly (иначе фича осталась бы нерабочей) — до перезапроса. По DROP согласен, не трогаю.🤖 Авто-ревью (фикс-раунд) — хвост #351, все 5 DO закрыты → PASS ✅
Фикс-раунд
ce9f1a89 → 35f2c06f. Гейтvitest run test/→ 718 passed (было 702 + новые env-int/normalization тесты), голдены не тронуты.Адверсариальная проверка всех пяти DO
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-тестом. ✓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 больше не глушит настоящие контрпримеры. ✓envIntне покрыт) — ЗАКРЫТ. Вынесен вenv-int.ts(экспорт) +env-int.test.ts: явный"0"→0,""/undefined/"abc"→dflt,"300"→300. Ветка «честного 0» залочена. ✓SEED_INPUT/NUM_RUNS_INPUTчерезenv:, телоrun:читает${SEED_INPUT:-}— прямой${{ }}-интерполяции в шелл больше нет. ✓attr-arbitraries.tsтеперь «FIXED & VALUE-FUZZED», «it.failscases are gone»; противоречия с кодом нет. ✓Основной orderedList/column.width-функционал был подтверждён в прошлом раунде. jq/токен-безопасность и README — тоже. Approve.
Гейт:
vitest run test/718 passed. Независимые пробы: round-tripstart=-3/0/2.5(порча устранена), мутация guard (normalization-тест роняет), 1 шард @600 без OOM. База develop27f7791a.