Шаблон прошёл проверку. Превью выглядело нормально. А рендер вернулся такой: Games: , slots and poker., или The lobby is kept compact.Game categories load on demand., или When someone asks "Is it audited? ", the figure…. Этих строк никто не писал: так движок обошёлся с шаблоном, который ему дали.

Все три случая приходят с одного этапа. Выбрав варианты, движок прогоняет результат через постобработку: пробел после точки, заглавная в начале предложения и щит, который убирает с дороги ссылки, адреса почты и домены. Постобработка — единственная часть движка spintax, которая правит текст, о правке которого её не просили, и ошибается она в две стороны. Либо не исправляет огрех автора: склеенное со следующим предложение, пустой вариант, оставивший после себя запятую. Либо исправляет то, что было верно: читает пример.рф как границу предложения и печатает Пример. Рф, ставит пробел между вопросительным знаком и кавычкой, которая его закрывает.

Увидите вы их или нет, зависит от того, как часто нужная им форма попадается в вашем тексте и сколько текстов вы смотрите. Это арифметика, и она не на вашей стороне.

Что успевает показать превью из десяти текстов

Дефект, который встречается в доле p текстов, попадётся в превью из n штук с вероятностью 1 − (1 − p)ⁿ. Две строки ниже — не круглые числа, а счёт по нашему контенту: 30 из 15 132 рендеров сдвинула правка классов символов, когда в issue движка заново прогнали шаблоны платформы (spintax-js#81), а 369 из 8 891 сохранённой ячейки — столько раз в готовом тексте сидела форма со склеенным предложением (#79).

Как часто встречается дефект В превью из 5 из 10 из 50 На прогон в 10 000
1 на 1 000 0.5% 1.0% 4.9% 10
30 из 15 132 (#81) 1.0% 2.0% 9.4% 20
1 на 100 4.9% 9.6% 39.5% 100
369 из 8 891 (#79) 19.1% 34.6% 88.0% 415

Вторая строка — рабочий случай. Дефект, который приходится на один текст из пятисот, переживает превью из десяти текстов в 98 случаях из 100 и встречается примерно двадцать раз в каталоге на десять тысяч страниц.

Четырнадцать форм, шестнадцать версий

Все выпущенные версии @spintax/core лежат в npm: двадцать четыре версии с 8 июля по 19 сентября 2026 года, шестнадцать из них — начиная с 0.3.0, с которой стартует карта: там #set стал макросом, и более ранние версии читают тот же шаблон иначе. Значит, формы можно прогнать через шестнадцать движков сразу. В тест-карте четырнадцать входов, которые какая-нибудь версия портила, три контрольных, которые меняться не должны (example.com, ASP.NET, info@Example.COM), и одна принятая цена. Фиксированный вход сверяется с точным выводом, вход со случайным выбором — с формой вывода на 100 сидах.

Вход Старый движок 0.10.0
The lobby is kept compact.Game categories load on demand. …compact.Game categories… …compact. Game categories…
Слоты, покер, рулетка и т.д. и т.п. всё в одном лобби. …и т. Д. И т. П. Всё… …и т.д. и т.п. всё…
Открой пример.рф сегодня. Открой пример. Рф сегодня. Открой пример.рф сегодня.
Providers: [<minsize=2;maxsize=3;sep=", ";lastsep=" and ">%list%]. Providers: Alpha|Beta|Gamma|Delta|Epsilon. Providers: Epsilon, Beta and Delta.
Games: [<sep=", ";lastsep=" and ">slots|{live casino|}|poker]. Games: , poker and slots. Games: poker and slots.
When someone asks "Is it audited?", the answer is yes. …asks "Is it audited? ", the answer… …asks "Is it audited?", the answer…

На лестнице версий карта раскладывается на пять ступеней:

Версии Форм верно Что появилось
0.3.0–0.6.1 (двенадцать версий) 1 из 14 —
0.7.0 2 из 14 список в переменной становится списком, а не одной строкой
0.8.0 8 из 14 сокращения, нелатинские домены и адреса почты, неразрывные пробелы, пустой вариант и условие в перечислении
0.9.0 10 из 14 склеенное предложение получает пробел обратно, на обеих письменностях
0.10.0 14 из 14 нет пробела между знаком и кавычкой или скобкой, которая его закрывает

Контрольные входы целы во всех шестнадцати версиях. Единственная форма, верная в самых старых движках, — русское склеенное предложение, и верна она случайно: граница слова была там ASCII и после кириллицы не срабатывала. Когда 0.8.0 привела классы символов в согласие с PHP-движком, случайность кончилась и предложение склеилось — ровно на одну версию, пока 0.9.0 не починила форму осознанно. Правка, которая чинит одну форму и ломает другую, — это и есть довод за то, чтобы гонять весь корпус после релиза, а не только до него.

Откуда берутся эти формы

Трекер движка открыт: сорок issue, с #41 по #85, из них двадцать три — баги. Восемь из них меняли то, что движок пишет. Четыре нашлись в контенте, который рендерит наша собственная платформа:

  • #78: пресет со списком из 57 имён отрендерился как Alpha|Beta|Gamma…, вместе с сырыми палками, в «131 published rows across 15 tenants».
  • #79: щит доменов считал доменом любое слово.Слово, и склеенные предложения оставались склеенными. В 8 891 сохранённой ячейке у 83 тенантов форма встретилась 369 раз, у 56 тенантов, с 49 разными «TLD» — The, This, From, All. Ни одного домена среди них.
  • #41: адрес mailto: разрезало пополам на странице контактов. Это поймали в прежнем встроенном движке платформы, где был такой же щит; этот движок делил с ним баг и починил его в 0.1.4.
  • #85: наша система лендингов стала цитировать ключи-вопросы, и лид When someone asks "%KeywordCap%?", the figure… вышел как "Is it audited? ",. До опубликованной страницы это не дошло: поймали в сборке, в тот же час закрыли заплаткой, а заплатку сняли, когда закрепили починенный движок.

Остальные четыре пришли от порта, который повторял эталонный движок, от ревью кода, и два не говорят, где всплыли, — про один из них, #81, changelog пишет, что нашли порты. Этот расклад стоит назвать прямо, потому что он не самый лестный: объём находит баги в тексте, а ревью и сравнение движков между собой — баги в коде. Падения и провалы по скорости в том трекере — шаблон в 62 символа, съедавший память, пятьдесят #include, превращавших 690 байт в 57 МБ, шаблон в 3,9 КБ, бросавший исключение там, где спецификация обещает снисходительность, — из второй группы: ни в одном из этих issue не описана страница, которая выглядела не так.

И объём здесь не означает тысячу рендеров одного шаблона. #85 — это один лендинг. #41 — одна страница контактов. Сайты дают не повторение, а разнообразие: тенанты, у которых ключ приходит в форме вопроса, список провайдеров приезжает из базы, а проза русская, с «и т.д.» внутри. Ни в одном файле с тестами этих форм не было, когда они всплыли: никому не приходит в голову записать предложение, которое он ещё не видел сломанным.

Шаблоны рендерятся
на наших собственных сайтах

Форма, которой не было в тестах:
ключ в виде вопроса,
список из базы

Issue: точный вход
и что задето

Правка и фикстура
в общем корпусе

Набор тестов каждого движка
читает тот же корпус

Релиз — и он доедет,
когда проект поднимет свою версию

Как форма, которой не было ни в одном тесте, становится правкой в пяти движках

Когда цикл работает, он быстрый: #85 завели, починили, выпустили как 0.10.0 и закрепили обратно в системе лендингов за одно утро. Медленный он, когда форма лежит в сохранённом тексте, который никто не перечитывает: в #79 из 371 склеенной ячейки 336 появились ещё до перехода платформы на этот движок — у прежнего был такой же щит и такая же слепая зона.

Меняет ли обновление то, что уже опубликовано

Этот сайт публикует скрипт замера к каждой статье, и в четырёх из них рендерится spintax. Это 14 252 вызова рендера, 8 800 из них дают разные тексты: 700 рендеров страниц по городам, 7 502 холодных письма, 1 000 писем аутрича, 5 050 описаний товаров. Прогон всех четырёх на каждой из шестнадцати версий со сверкой каждого текста против 0.6.1 — версии, на которой они были опубликованы, — даёт один и тот же ответ в каждом столбце: не отличается ни один текст. Ни байта не сдвинулось за четыре релиза, каждый из которых менял вывод.

Это один корпус, а не гарантия, — но он показывает размер риска. Каждый из релизов называет формы, которые двигает, и на четырёх шаблонах, где ни одной из них нет, шестнадцать версий выдали те же байты. Ваши шаблоны — не эти шаблоны, для чего и нужны три шага:

  1. Закрепить версию движка, чтобы рендер воспроизводился.
  2. Перед тем как поднять версию, прогнать свой корпус на обеих и сравнить. Скрипты ниже делают ровно это: загрузчик подменяет движок под замером.
  3. Читать в changelog раздел «Changed, visibly». Каждый релиз, который двигает вывод, называет формы, которые двигает.

Как поискать это в своём выводе

Дефекты рендера ищутся в готовом пуле обычным грепом: у большинства форм есть выражение. Вот те, что использует замер; каждое проверено и на старом движке, и на новом:

Что находит Выражение
склеенное предложение /\p{Ll}\.\p{Lu}\p{Ll}/u
разорванное сокращение /(?<!\p{L})[тТ]\. [ДПЕК]\./u
разорванный домен /\p{L}\. (?:рф|рус|com|net|org|中国)(?!\p{L})/iu
пробел перед закрывающим знаком /[.,;:!?…] ["'»”’)\]](?=$|[\s.,;:!?…)\]])/u
сырая палка в готовом тексте /|/u
список, оставшийся от пустого элемента /[:(]\s*,|,\s*,|\s(?:and|и)\s*[.!?]/u
двойной пробел, включая неразрывные /[ \u00a0]{2,}/u

На старом движке они ловят все формы карты, кроме двух. На 8 800 разных текстах статей этого сайта они не срабатывают ни разу — а это и есть главная проверка: выражение, которое ругается на хороший текст, выключают в тот же день.

Где ломается

У двух форм выражения нет. Перечисление, вернувшее три элемента там, где шаблон просил один (Pick: gamma, alpha, beta вместо Pick: gamma), — обычный текст, его находит только подсчёт элементов. А poker, and slots — пустой элемент, проглоченный между разделителем и последним разделителем, — не отличить от оксфордской запятой. Из рендеров, которые старый движок портит в этих двух случаях, выражения ловят 30 из 50 и 65 из 100.

А выражение про склеенное предложение останавливается на аббревиатуре. После заглавной оно требует строчную, поэтому word.API проходит мимо — и та же оговорка не даёт ему сработать на info@Example.COM. Расширишь — сработает на адресе; это форма выражения, а не его дефект.

Выражение про палку шумит намеренно. Законная | в таблице или в имени файла его поднимет. На отрендеренном spintax его всё равно стоит держать: сырая палка в готовой прозе почти всегда означает список, который не разделился.

У правила про склеенные предложения есть цена, и она записана в changelog движка: слово с заглавной после точки теперь начинает предложение, поэтому Yandex.Money выходит как Yandex. Money, а info@example.Com больше не опознаётся как адрес почты. Названиям такого вида нужна переменная или щит.

Карта — наша карта. Её входы взяты из форм, которые описаны в changelog самого движка, то есть она проверяет то, что уже нашли, а не то, что ещё прячется. Её ценность — в лестнице: на конкретной версии конкретная форма либо выходит правильно, либо нет.

Семья движков не в одном шаге. Все пять движков держатся на одном наборе фикстур, но выпускаются по своим расписаниям, а проект работает на той версии, которую закрепил. Сверено по реестрам 20 сентября 2026 года:

Что ставится Версия Счёт по карте
@spintax/core (npm) 0.10.0 14 из 14
spintax/core (Packagist) 0.10.0 14 из 14
spintax-core (PyPI) 0.5.0 10 из 14
Spintax.Core (NuGet) 0.1.3 10 из 14
Spintax для WordPress 3.1.0 8 из 14, на уровне движка, который в нём едет
n8n-nodes-spintax 0.3.3 8 из 14: внутри вшит движок 0.8.0
@spintax/mcp 0.3.2 8 из 14: диапазон зависимости держит его на линии 0.8.0

Строки npm и Packagist, Python и .NET — прогон. Строка WordPress — это версия движка, которую называет changelog плагина, прогнанная отдельно; самому плагину нужен WordPress вокруг. Ещё два участника семьи — модуль для OpenCart и фасад для ZennoPoster — не менялись с августа, и делать вид, что это не так, здесь неуместно.

Где кончается ручной путь

Замер лежит в /measurements/engines-keep-changing/: card.json держит формы, versions.mjs — список движков, measure.mjs прогоняет карту через каждую установленную версию и печатает таблицы выше, detectors.mjs — выражения, а articles.mjs вместе с swap.mjs заново рендерит замеры остальных статей сайта на каждой версии. Та же карта идёт на другие движки через family.py, family.php и проект .NET в dotnet/.

Пять файлов .mjs и card.json кладутся в одну папку, а рядом ставится каждая версия, которую карта считает, под своим алиасом. В шапке measure.mjs строка установки выписана со всеми шестнадцатью; вот её вид, и тот же цикл её печатает:

npm init -y && npm pkg set type=module
npm i $(node -e "import('./versions.mjs').then(m => console.log(m.VERSIONS.map(v => 'c' + v.replaceAll('.','') + '@npm:@spintax/core@' + v).join(' ')))")
node measure.mjs

Проверять свой пул и проверять свой шаблон — разные работы, и у движка есть проход на каждую: починка spintax на spintax.net — про дефекты, которые живут в сочетании выборов: два слота взяли одно слово, местоимение не согласовалось с существительным, рядом с которым оказалось; операция линта в ноде n8n читает рендеры, а не шаблон. Страница движков перечисляет, что ставится в каждой среде и как общий корпус заставляет их говорить одно и то же. Какой из них закрепить — вопрос менее важный, чем закрепить хоть какой-то, прогнать свой корпус на обеих версиях перед переходом и держать выражения выше в том, что проверяет ваш вывод.