Changelog: 0.10.x¶
Archived verbatim from CHANGELOG.md. Newer releases are in the changelog; the archive index lists every older series.
[0.10.0] — 2026-06-15¶
The multi-language milestone (epic #326): disarm becomes a publishable,
pyo3-free Rust crate with a first-class idiomatic Rust API, gains a Ruby
binding, and adds opt-in diagnostic logging — all over a single shared
pure-Rust core. The Python package is unchanged for callers (same import disarm
surface); the work is the core extraction and the new non-Python surfaces.
Added¶
- Pure-Rust core, published to crates.io (#38, #42). The default build is now
the pyo3-free core (
default = []); the Python extension is the opt-inextension-modulefeature, socargo add disarmpulls a clean Rust library with no libpython in its dependency tree (enforced by a CI gate: the defaultcargo tree -e no-devtree must contain nopyo3, matched case-insensitively). The codebase is organized in three layers: Layer-1pub(crate)algorithm cores, Layer-2 the publicdisarm::api, and Layer-3b the feature-gated pyo3 shims — all consuming one implementation. - Idiomatic Rust API (
disarm::api) (#352, #361, #362). The semver-governed crates.io surface: typed enums (TargetScript,Scheme,NormalizationForm,UrlComponent,Platform,ReverseLang) that each round-trip viaas_str/Display/FromStr; theTransliteratebuilder withScheme/OnUnknown(which carries its replacement in theReplace(String)variant); an opaqueErrorwith a stableErrorKind/code();Cow<'_, str>borrow-on-no-op returns; agraphemes()iterator; theSlugConfigbuilder; theDisarmStrextension trait for method-call syntax; named#[non_exhaustive]struct returns (EncodingDetection,DecodedText,HostnameAnalysis,Untranslatable— no anonymous tuples); and a guarded process-global registration API (register_lang/register_replacements/remove_replacement/clear_replacements/seal_registrations) that enforces the registration cap and the one-way seal latch. Two contract tests fail CI if apub fnever returns a tuple or a token enum loses its round-trip. - Ruby bindings — the
disarmRubyGem (#45, #357). A magnus-based native extension wrapping the pure-Rust core (no Python), with an idiomatic Ruby surface: keyword arguments with defaults, symbol tokens (:latin,:strict_iso9, …), a singletransliterate(text, scheme:), and aDisarm::Error < StandardErrorhierarchy. Precompiled platform gems (Linux x86_64/aarch64, macOS x86_64/arm64, Windows) install with no local Rust toolchain. - Opt-in, binding-neutral diagnostic logging (#208, #358). Behind the
log/log-contentfeatures (off by default), the core emits structured records at API boundaries via thelogfacade — zero cost when off (the macros compile to nothing) and never inside a per-codepoint hot loop (enforced by a source-scan test). Default-level records carry metadata only (lengths, counts, flags, durations, error codes — never input or output content, enforced by a redaction sentinel test); thelog-contentTRACE escape hatch routes its truncated samples through disarm's ownstrip_log_injection(dogfooding) so a log line can never forge a record.
Changed¶
- Native module renamed
disarm._disarm→disarm._core(#42). The public Python API is unchanged — callersimport disarm. The native module name is an implementation detail the public surface doesn't require; the package's own internals (and the type-stub drift checks) referencedisarm._coredirectly, so any consumer reaching into it should update the path.
Fixed¶
- Confusables: cross-script ASCII folds and additive Greek/Cyrillic pairs (#341, #342, #343), plus the halfwidth vertical form U+FFE8 residue (#245).
- Terminal width: corrected the additivity-across-space precondition (#279).
Security¶
- HAI-SDLC hardening pass over the Rust core (#360): a deep multi-pass review
(0 critical / 0 high) actioned into 21 fixes — tightened a hostname IPv6-literal
zone-id check, added limit-rejection logging, a unique-slug truncation-error fix,
and an allocation-free
is_normalized, among others.
Internal¶
- Wired Tier 3 (exhaustive + formal) into the release/publish gate (#159, epic #326).
publish.ymlnow runs atier3job on the release/publish trigger that executes the exhaustive Rust domain tests (cargo test --no-default-features --test exhaustive_transliterate -- --ignored) and the Python formal invariants (pytest -m formal, against a freshly built wheel). Every wheel/sdist build job and thepublishjobneeds:it, so a Tier-3 failure blocks the upload to PyPI — closing the gap where these tiers were a manual pre-release step. They remain excluded from fast PR CI; the#[ignore]/@pytest.mark.formalmarkers are untouched. - Split the 1,200-line
src/api.rsinto cohesive submodules (api/{safety,text,transliterate,presets}.rs) re-exported fromapi/mod.rs, with theDisarmStrtrait in the hub (#361). No public-path change. translit-rs0.8.2 redirect shim published so the old PyPI name points users atdisarm(#264 follow-up).