Как Ghostchars решает, что удалять
Один проход по тексту, одно решение на символ и понятная причина у каждого решения. Ни модели, ни эвристик, ни оценок.
Четыре прохода
Очистка идёт в жёстком порядке. На отдельные символы смотрит только третий проход; первые два нужны, чтобы третий получил предсказуемую строку.
Нормализация (по желанию)
Если вы попросили, текст сначала приводится к NFC или NFKC. Именно это побеждает стеганографию на формах нормализации, где сообщение закодировано выбором между «é» одной кодовой позицией и «e» плюс комбинируемый акцент. NFKC идёт дальше и сводит стилизованные математические буквы, лигатуры и полноширинные формы; это потеря информации, поэтому по умолчанию выключено.
Сведение концов строк и пробельных серий (по желанию)
CRLF и CR становятся LF; полоса цитаты из терминала в начале строки уходит вместе с пробелом после неё; длинное тире с пробелами становится дефисом с пробелами; пробелы и табуляции в конце каждой строки срезаются; серии пробелов внутри строки схлопываются в один. Это переписывание по шаблону для всей строки, а не решения по каждому символу, и каждое вынесено в свой параметр, потому что каждое меняет видимый текст.
Классификация каждого символа
Каждый символ ищется в таблицах движка в фиксированном порядке, и побеждает первое совпадение. Результатом становятся категория, управляющий параметр и то, чем символ следует заменить: пустая строка для «удалить», обычный пробел для экзотического пробела, латинская буква для двойника. Обычные символы не совпадают ни с чем и возвращаются нетронутыми.
Сборка с учётом контекста
Результат собирается символ за символом. Символ, чей управляющий параметр выключен, копируется как есть и попадает в отчёт как найденный, но не тронутый. Символ с включённым параметром удаляется или заменяется. Всё это время движок помнит последний символ, который он действительно СОХРАНИЛ, а не последний увиденный, потому что именно к нему прикрепляется соединитель или селектор вариантов, а склейка основу не сдвигает.
Все четыре прохода про символы, ни один из них не читает предложение. Повторяет ли сам текст привычки, которые бросаются в глаза, то есть одинаковую длину фраз, дежурные связки, частые тире, это отдельное измерение: отчёт о стиле считает их и выдаёт числа, без баллов и без вероятностей.
Порядок принятия решений
Категории проверяются в этом порядке, и решает первое совпадение. Порядок важен: пробел нулевой ширины проверяется раньше общего правила для форматирующих символов, поэтому он попадает в отчёт как символ нулевой ширины, а не как безымянный «Cf».
| # | По категориям | Управляющий параметр |
|---|---|---|
| 1 | Нулевая ширина и невидимые | Базовый проход (всегда включено) |
| 2 | Переопределения направления | Базовый проход (всегда включено) |
| 3 | Метки направления | Bidi-метки --bidi |
| 4 | Соединители, селекторы и заполнители | Агрессивно -a |
| 5 | Любой другой форматирующий символ | Базовый проход (всегда включено) |
| 6 | Символы частного использования | Символы частного использования --pua |
| 7 | Экзотические пробелы | Пробелы -s |
| 8 | Разделители строк | Концы строк -n |
| 9 | Управляющие символы | Управляющие символы --controls |
| 10 | Типографская пунктуация | Пунктуация --punct |
| 11 | Полосы цитат из терминала | Полосы цитат --gutters |
| 12 | Буквы-двойники | Буквы-двойники --confusables |
Внутри этого порядка есть два исключения. В арабском, сирийском и кайтхи есть форматирующие символы, которые являются обычной орфографией (арабские знаки числа, знак числа кайтхи), и они сопоставляются раньше общего правила и остаются нетронутыми. Табуляция, перевод строки и возврат каретки исключены из группы управляющих символов, потому что их удаление склеило бы строки.
Почему решает контекст
Четыре семейства символов невидимы И несут нагрузку: соединители, селекторы вариантов, заполнители отдельных письменностей и метки направления. Одна и та же кодовая позиция в одном месте оказывается контрабандой, а в другом правильным написанием, поэтому движок сначала смотрит на соседей. Именно здесь ошибается большинство чистильщиков, причём в обе стороны: они либо оставляют спрятанные символы, либо ломают настоящие.
Эмодзи-семья
Эмодзи-семья складывается из трёх или четырёх человек, сваренных вместе соединителями нулевой ширины. Уберите их, и одно эмодзи станет тремя. Ghostchars сохраняет соединитель, у которого с обеих сторон стоят базовые эмодзи, и удаляет ту же кодовую позицию, когда она оказывается между двумя латинскими буквами: там она ничего не соединяет и хорошо прячется.
Персидское слово
В персидском продолженные формы и множественное число пишутся через разъединитель нулевой ширины: это не украшение, это орфография. Ghostchars сохраняет соединитель или разъединитель, если символы с обеих сторон принадлежат одной соединяющей письменности (арабской, персидской, деванагари, малаялам), и удаляет его во всех остальных случаях. То же правило защищает кхмерские гласные, монгольские селекторы и заполнители чамо хангыля рядом с их собственными письменностями.
Флаг Шотландии
Флаг региона состоит из чёрного флага, за которым идут теговые символы с кодом региона и завершающий тег. Те же теговые символы, стоящие сами по себе, дают атаку ASCII smuggling: невидимые инструкции, которые прочитает языковая модель. Ghostchars разбирает всю последовательность: теговые символы внутри полного флага сохраняются, а каждый теговый символ вне флага удаляется.
Предложение на иврите
Метками «слева направо» и «справа налево» текст со смешанным направлением удерживает пунктуацию на правильной стороне. Ghostchars сохраняет bidi-метку, у которой в соседях стоит символ письма справа налево, и удаляет ту, что затерялась в английском абзаце, где ей остаётся единственная функция: нести бит. Пять символов переопределения и встраивания такого доверия не получают: они способны переставить текст, который писали не вы, поэтому удаляются всегда.
Каждое такое решение попадает в отчёт. Сохранённый символ показан как «оставлен: несёт нагрузку» вместе с параметром, который всё-таки его удалит, так что ничего не решается молча и ничего от вас не скрывается.
Проверено на одиннадцати опубликованных схемах
В инструменте командной строки есть состязательный тест, который реализует одиннадцать документированных техник сокрытия текста парами «кодировщик и декодировщик». Для каждой он прячет секрет, очищает текст и проверяет две вещи: что декодировщик больше не может достать секрет и что видимый текст-обёртка остался побайтово тем же.
| Схема | Чем побеждается |
|---|---|
| Zero-width binary (ZWSP / ZWNJ / ZWJ) | базовый проход |
| Word-joiner and invisible-maths binary | базовый проход |
| Tag-block ASCII smuggling | базовый проход |
| Variation-selector byte smuggling | базовый проход |
| Bidi mark binary (LRM / RLM) | базовый проход |
| SNOW trailing whitespace | Пробелы в конце строк --trailing |
| Cyrillic homoglyph binary | Буквы-двойники --confusables |
| NFC / NFD normalisation-form binary | Нормализация (NFC) --nfc |
| NBSP / space homoglyph binary | Пробелы -s |
| Single / double space binary | Схлопывание пробелов --collapse |
| Curly / straight apostrophe binary | Пунктуация --punct |
Пять из одиннадцати падают от одного базового прохода. Остальным шести нужен параметр, потому что носителем работает видимый символ: пробел, апостроф, буква-двойник. Удалять такое без спроса значило бы менять текст.
Один движок в двух местах
Браузерный движок повторяет инструмент командной строки на Python строка в строку, а не переосмысливает его.
Генератор прогоняет реализацию на Python по корпусу фикстур (каждая перечисленная кодовая позиция, каждый контекстный случай, все одиннадцать стеганографических нагрузок, реалистичная смешанная проза) в четырнадцати сочетаниях параметров и записывает очищенный вывод и каждую находку. Движок на TypeScript тестируется против этой записи. Любое расхождение между ними роняет сборку, поэтому «сайт делает то же, что и CLI» остаётся результатом теста, а не обещанием.
Это важно по практической причине: вы можете проверить файл в pre-commit-хуке через CLI и получить ровно тот результат, который дал бы сайт.
Тот же движок работает и в третьем месте. Отчёт по документу распаковывает .docx или .odt и читает XML внутри, а не вставленную строку, поэтому символ получает в файле тот же вердикт, что и в поле на главной странице, а скрытые фрагменты, исправления и метаданные вокруг него попадают в отчёт, но не удаляются.
Чего это не умеет
Всё вышесказанное говорит о символах. Эти четыре вещи к символам не относятся, и никакая очистка их не касается.
- Статистические водяные знаки: Схемы вроде SynthID-Text и метода «зелёного списка» Кирхенбауэра смещают то, какие СЛОВА выбирает модель. Сигнал живёт в выборе слов на протяжении целого фрагмента, а не в отдельном символе. Изменить его может только переписывание текста, а этот инструмент текст не переписывает.
- Стилометрическое определение: Детекторы вроде GPTZero оценивают, насколько предсказуемо каждое слово и насколько сильно эта предсказуемость колеблется. Удаление пробела нулевой ширины не сдвинет эту оценку ни на балл. Отчёт о привычках письма измеряет стоящие за ней привычки, которые можно править (разброс длин предложений, слова-приметы, дежурные связки), и всё равно отказывается выдавать вероятность.
- Сокрытие на уровне формата: Белый текст по белому, скрытые фрагменты, комментарии HTML и метаданные PDF прячутся ВНЕ текста. Инструмент для документов сообщает о них (скрытые фрагменты, исправления, примечания, идентификаторы сеансов правки, метаданные об авторстве), но намеренно их не удаляет, потому что удаление меняет документ, а это должно быть вашим решением.
- Каналы меньше бита на эмодзи: Цепочка соединителей между эмодзи и селектор вариантов после эмодзи сохраняются по умолчанию, потому что так эмодзи и устроены. Настойчивый отправитель всё же может спрятать там немного данных. Включите «Агрессивно», если они должны исчезнуть, и примите, что ваши эмодзи развалятся.
Ghostchars не выдаёт ни вероятности, ни оценки, ни вердикта о том, кто написал текст. Он сообщает, какие символы есть в вашей строке, где они стоят и что происходит с каждым из них.
Вопросы
Почему просто не удалить все невидимые символы?
Потому что некоторые из них несут нагрузку. Удаление всех соединителей нулевой ширины превращает эмодзи-семью в трёх отдельных людей и портит написание персидских слов. Удаление всех bidi-меток ломает вёрстку смешанного текста на иврите и английском. Чистильщик, который делает это молча, хуже, чем никакого: вы не видите, что он забрал.
Браузерная версия совпадает с инструментом командной строки?
Да, и это проверяется тестами. Эталонные фикстуры, сгенерированные реализацией на Python, привязывают движок на TypeScript к побайтово одинаковому выводу для каждой фикстуры и каждого сочетания параметров; сборка падает при любом различии.
Он может сказать, написан ли текст ИИ?
Нет, и никогда не станет этого утверждать. Скрытые символы говорят, что текст прошёл через какую-то программу. Они не говорят, через какую, и не говорят, кто написал слова. Любой инструмент, превращающий число символов в проценты уверенности, это число выдумывает.
Мой текст покидает браузер?
Нет. Никакой обработки на стороне сервера нет, и отправлять некуда: движок написан на JavaScript и работает на вашем устройстве, а сайт остаётся набором статических файлов. Отключитесь от сети, и всё продолжит работать.