You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
strip_accents treats U+0338 COMBINING LONG SOLIDUS OVERLAY and U+20D2 COMBINING LONG VERTICAL LINE OVERLAY as diacritics and drops them. They are not diacritics — they are the negation. Every surface that runs the step therefore emits the un-negated operator, and its output asserts the opposite of its input.
One primitive, one character, no preset needed, on 0.14.1:
>>>importdisarm>>>disarm.strip_accents("≠") # NOT EQUAL TO'='>>>disarm.has_anomalies("≠")
False
45 assigned code points decompose to base + negation slash under NFD. Measured across every preset, profile and key builder — invert means the output is byte-identical to that surface's output for the positive base:
surface
invert
erase
preserve
example
search_key
45
0
0
↚ → ←
catalog_key
45
0
0
↚ → ←
strip_obfuscation
45
0
0
↚ → left-pointing arrow
profile llm_guardrail
45
0
0
↚ → left-pointing arrow
ml_normalize
32
0
13
∄ → there exists
profile ml_corpus_normalize
15
0
30
≭ → ≍
profile search_index
3
42
0
≠ → =
profile rag_ingest
3
42
0
≠ → =
sort_key
0
0
45
—
canonicalize
0
0
45
—
canonicalize_strict
0
0
45
—
strip_format
0
0
45
—
At sentence level, through the profile docs/policy-templates.md:79 presents as the corpus-normalization recipe:
>>>p=get_pipeline("ml_corpus_normalize")
>>>p("p ∤ q") # p does not divide q'p ∣ q'# p divides q>>>p("∄ x . P(x)") # there does not exist x such that P(x)'∃ x . p(x)'# there exists x such that P(x)>>>p("A ⊈ B")
'a ⊆ b'
ml_normalize is worse, because the exposed base is then rendered as English that states the opposite:
>>>disarm.ml_normalize("∄ x . P(x)")
'there exists x . p(x)'>>>disarm.ml_normalize("p ∤ q")
'p divides q'>>>disarm.ml_normalize("A ⊈ B")
'a subset equal b'
Mechanism
strip_accents (src/transliterate.rs:1521) is NFD → drop combining marks → NFC. The negation overlays are Mn, so they go with the accents.
STEP_ORDER (src/pipeline.rs:47) runs STRIP_ACCENTS (:51) before every step that could have named the negated form, so nothing downstream can tell that a negation was present.
The four clean surfaces are clean for one reason: they have no strip_accents step. canonicalize uses strip_zalgo, whose mark threshold leaves a single combining mark alone.
This is the half of #467/#498 that was never asked about
Both issues were filed as idempotence defects and closed as idempotence defects. Neither asks whether the fixed point they converge on is the right one.
The #498 fix is a second Demojize after StripAccents (src/presets.rs:910), and the comment above it states the mechanism precisely:
src/presets.rs:899-908 — "the strip-accents step above (NFD decompose → drop combining marks → NFC) then drops the overlay and exposes the bare base (≅), which IS named."
So the exposure was understood. The fix made the pipeline reach "approximately equal" for ≇ NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO in one pass instead of two. It converged the pipeline on the inverted reading, deterministically.
#467 has the same shape for catalog_key: ∤ → ∣ → l, closed by making the fold reach l in one pass.
Why it matters beyond mathematics
rag_ingest and search_index map ≠ onto =. A retrieval index built with either returns documents asserting the opposite of the query, and an LLM stack reading from that index is given the negation of what it asked for. ml_corpus_normalize is documented for "NLP/ML text preprocessing, corpus normalization, embedding preparation" (docs/policy-templates.md:81); a corpus normalized through it has its negated statements silently flipped before any model sees them.
Nothing reports this. has_anomalies, inspect_anomalies and is_zalgo are all clean on every one of the 45.
Scope
Exempt U+0338 and U+20D2 from the mark set strip_accents removes, or from the Mn sweep generally where the mark carries the negation rather than a pronunciation.
Decide the contract for the eight affected surfaces and write it down. Either the negation survives, or the surface erases the operator rather than inverting it — rag_ingest's [?]/empty result for 42 of the 45 is already the safer of the two behaviours it exhibits.
Pin the 45 code points as a regression fixture asserting the verdict, not just f(f(x)) == f(x) — the current proptests pass on the inverted fixed point.
Summary
strip_accentstreats U+0338 COMBINING LONG SOLIDUS OVERLAY and U+20D2 COMBINING LONG VERTICAL LINE OVERLAY as diacritics and drops them. They are not diacritics — they are the negation. Every surface that runs the step therefore emits the un-negated operator, and its output asserts the opposite of its input.One primitive, one character, no preset needed, on 0.14.1:
45 assigned code points decompose to
base + negation slashunder NFD. Measured across every preset, profile and key builder —invertmeans the output is byte-identical to that surface's output for the positive base:search_key↚→←catalog_key↚→←strip_obfuscation↚→left-pointing arrowllm_guardrail↚→left-pointing arrowml_normalize∄→there existsml_corpus_normalize≭→≍search_index≠→=rag_ingest≠→=sort_keycanonicalizecanonicalize_strictstrip_formatAt sentence level, through the profile
docs/policy-templates.md:79presents as the corpus-normalization recipe:ml_normalizeis worse, because the exposed base is then rendered as English that states the opposite:Mechanism
strip_accents(src/transliterate.rs:1521) is NFD → drop combining marks → NFC. The negation overlays areMn, so they go with the accents.STEP_ORDER(src/pipeline.rs:47) runsSTRIP_ACCENTS(:51) before every step that could have named the negated form, so nothing downstream can tell that a negation was present.The four clean surfaces are clean for one reason: they have no
strip_accentsstep.canonicalizeusesstrip_zalgo, whose mark threshold leaves a single combining mark alone.This is the half of #467/#498 that was never asked about
Both issues were filed as idempotence defects and closed as idempotence defects. Neither asks whether the fixed point they converge on is the right one.
The #498 fix is a second
DemojizeafterStripAccents(src/presets.rs:910), and the comment above it states the mechanism precisely:So the exposure was understood. The fix made the pipeline reach
"approximately equal"for≇NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO in one pass instead of two. It converged the pipeline on the inverted reading, deterministically.#467 has the same shape for
catalog_key:∤→∣→l, closed by making the fold reachlin one pass.Why it matters beyond mathematics
rag_ingestandsearch_indexmap≠onto=. A retrieval index built with either returns documents asserting the opposite of the query, and an LLM stack reading from that index is given the negation of what it asked for.ml_corpus_normalizeis documented for "NLP/ML text preprocessing, corpus normalization, embedding preparation" (docs/policy-templates.md:81); a corpus normalized through it has its negated statements silently flipped before any model sees them.Nothing reports this.
has_anomalies,inspect_anomaliesandis_zalgoare all clean on every one of the 45.Scope
strip_accentsremoves, or from theMnsweep generally where the mark carries the negation rather than a pronunciation.rag_ingest's[?]/empty result for 42 of the 45 is already the safer of the two behaviours it exhibits.f(f(x)) == f(x)— the current proptests pass on the inverted fixed point.docs/limitations.md, which does not mention it today.Reproduction script and the 540-row per-cell dataset are in a comment below.
Refs #467, #498, #723