Шаблон прошёл проверку. Превью выглядело нормально. А рендер вернулся такой: 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 — одна страница контактов. Сайты дают не повторение, а разнообразие: тенанты, у которых ключ приходит в форме вопроса, список провайдеров приезжает из базы, а проза русская, с «и т.д.» внутри. Ни в одном файле с тестами этих форм не было, когда они всплыли: никому не приходит в голову записать предложение, которое он ещё не видел сломанным.
Когда цикл работает, он быстрый: #85 завели, починили, выпустили как 0.10.0 и закрепили обратно в системе лендингов за одно утро. Медленный он, когда форма лежит в сохранённом тексте, который никто не перечитывает: в #79 из 371 склеенной ячейки 336 появились ещё до перехода платформы на этот движок — у прежнего был такой же щит и такая же слепая зона.
Меняет ли обновление то, что уже опубликовано
Этот сайт публикует скрипт замера к каждой статье, и в четырёх из них рендерится spintax. Это 14 252 вызова рендера, 8 800 из них дают разные тексты: 700 рендеров страниц по городам, 7 502 холодных письма, 1 000 писем аутрича, 5 050 описаний товаров. Прогон всех четырёх на каждой из шестнадцати версий со сверкой каждого текста против 0.6.1 — версии, на которой они были опубликованы, — даёт один и тот же ответ в каждом столбце: не отличается ни один текст. Ни байта не сдвинулось за четыре релиза, каждый из которых менял вывод.
Это один корпус, а не гарантия, — но он показывает размер риска. Каждый из релизов называет формы, которые двигает, и на четырёх шаблонах, где ни одной из них нет, шестнадцать версий выдали те же байты. Ваши шаблоны — не эти шаблоны, для чего и нужны три шага:
- Закрепить версию движка, чтобы рендер воспроизводился.
- Перед тем как поднять версию, прогнать свой корпус на обеих и сравнить. Скрипты ниже делают ровно это: загрузчик подменяет движок под замером.
- Читать в 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 читает рендеры, а не шаблон. Страница движков перечисляет, что ставится в каждой среде и как общий корпус заставляет их говорить одно и то же. Какой из них закрепить — вопрос менее важный, чем закрепить хоть какой-то, прогнать свой корпус на обеих версиях перед переходом и держать выражения выше в том, что проверяет ваш вывод.