Skip to content

release: 0.11.1 - #522

Merged
raeq merged 1 commit into
mainfrom
release/0.11.1
Jul 13, 2026
Merged

release: 0.11.1#522
raeq merged 1 commit into
mainfrom
release/0.11.1

Conversation

@raeq

@raeq raeq commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Patch release cutting the backlog since 0.11.0 — lockstep across all four registries (crates.io, PyPI, npm, RubyGems).

What's in it

Version bump

0.11.0 → 0.11.1 in the four full-version files (Cargo.toml, pyproject.toml, bindings/node/package.json, bindings/ruby/lib/disarm/version.rb). The binding glue crates stay at 0.0.0 with the minor-only disarm_core = "0.11" pin — unchanged for a patch, per the 0.11.0 precedent.

Pre-release gate (local, all green)

  • cargo fmt --check, clippy (core + extension-module), perf_lint.sh, pyo3-leak guard — clean
  • tier-1 Rust tests pass; pytest 3529 passed / 0 failed
  • maturin build produces disarm-0.11.1 (abi3); version resolves to 0.11.1 across Rust/Python/Node/Ruby

Publishing is not triggered by this PR — it fires on the GitHub Release (v0.11.1), which will be created separately after merge.

Assisted-by: Claude Code:claude-opus-4-8

Copilot AI review requested due to automatic review settings July 13, 2026 19:13
@raeq
raeq enabled auto-merge (squash) July 13, 2026 19:13
@raeq
raeq disabled auto-merge July 13, 2026 19:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR cuts the 0.11.1 patch release for disarm by updating the published-version metadata across the Rust crate and language bindings, and by recording the release notes in the changelog.

Changes:

  • Bump release version from 0.11.00.11.1 in Rust, Python, Node, and Ruby version sources.
  • Add 0.11.1 release notes to CHANGELOG.md.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pyproject.toml Bumps the Python package version to 0.11.1.
CHANGELOG.md Adds the 0.11.1 release entry describing fixes/docs/internal changes since 0.11.0.
Cargo.toml Bumps the Rust crate version to 0.11.1.
bindings/ruby/lib/disarm/version.rb Bumps the Ruby binding version constant to 0.11.1.
bindings/node/package.json Bumps the Node binding package version to 0.11.1.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Cargo.toml
github-actions Bot added a commit that referenced this pull request Jul 13, 2026
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
raeq added a commit that referenced this pull request Jul 13, 2026
…plete)

`normalize_confusables` was not idempotent on some confusable-base + combining-mark
inputs. The `normalize_confusables_idempotent` proptest found it only
non-deterministically — it took an unlucky CI seed (`"¥\u{340}"`) on #522 to hit
one. It is a latent bug on `main`.

Root cause: confusable folding and canonical composition interact **both** ways,
so a single pass is not a fixed point.
  * A fold can expose a composition: `¥`+◌̀ composes to `¥`+U+0300 (yen has no
    precomposed grave), the fold maps `¥`→`Y`, and the emitted `Y`+U+0300 then
    composes to `Ỳ` (U+1EF2).
  * A composition can expose a *new* fold: `Ҫ`+◌̧ composes to `Ç` (U+00C7), which
    is itself a confusable that folds to `C` — so `Ç` in the output also violates
    the completeness invariant (`is_confusable` stays true).

Fix: re-run the fold/compose pass until the output stops changing. This is
idempotent by construction and complete by construction (the loop can only exit
once nothing folds). It converges fast — every fold moves toward the ASCII-ish
target script and composition only shrinks length, so no cycle is possible;
observed maximum is 3 internal passes, guarded by `MAX_PASSES = 8` +
`debug_assert`. The no-op fast path (borrow on first pass) is preserved, so
already-clean input still costs a single pass. The pipeline-internal
`normalize_confusables_into` is unaffected (presets canonicalize upstream).

Testing — this is the crux. A random `\PC*` proptest can only *stumble* on the
2-code-point adjacency that triggers this, which is why 1000-case (and even
100k-case) runs missed 61 further failures that a one-shot recompose left behind.
The bug class is local and bounded, so it is deterministically enumerable:

  * Add `exhaustive_fold_compose_idempotent_and_complete` (#[ignore], Tier 3):
    every confusable source code point × every combining mark (~9M pairs, ~6s in
    release), asserting idempotency AND completeness. This is the real gate.
  * Wire ignored lib-level exhaustive tests into tier3.yml (`--lib -- --ignored`),
    which also picks up the previously-orphaned `presets` fast-path sweep.
  * Replace the flawed `fold_never_drops_chars` proptest: iterating to a fixed
    point can legitimately shorten output (`Ҫ`+◌̧ → `C`, the mark absorbed then
    discarded when `Ç` folds), so its char-count proxy is false. The guarantee it
    stood for — no table value is empty — is now asserted directly and
    deterministically by `confusable_table_values_are_non_empty`.
  * Add an explicit regression unit test covering both interaction directions and
    pin the proptest counterexample seed.

Verified: full Rust suite, clippy (core + ext), perf_lint, pytest (3529), the
~9M-pair exhaustive gate (0 counterexamples), and a 50k-case debug proptest
(no debug_assert) all pass.

Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
Assisted-by: Claude Code:claude-opus-4-8
raeq added a commit that referenced this pull request Jul 13, 2026
…plete) (#523)

`normalize_confusables` was not idempotent on some confusable-base + combining-mark
inputs. The `normalize_confusables_idempotent` proptest found it only
non-deterministically — it took an unlucky CI seed (`"¥\u{340}"`) on #522 to hit
one. It is a latent bug on `main`.

Root cause: confusable folding and canonical composition interact **both** ways,
so a single pass is not a fixed point.
  * A fold can expose a composition: `¥`+◌̀ composes to `¥`+U+0300 (yen has no
    precomposed grave), the fold maps `¥`→`Y`, and the emitted `Y`+U+0300 then
    composes to `Ỳ` (U+1EF2).
  * A composition can expose a *new* fold: `Ҫ`+◌̧ composes to `Ç` (U+00C7), which
    is itself a confusable that folds to `C` — so `Ç` in the output also violates
    the completeness invariant (`is_confusable` stays true).

Fix: re-run the fold/compose pass until the output stops changing. This is
idempotent by construction and complete by construction (the loop can only exit
once nothing folds). It converges fast — every fold moves toward the ASCII-ish
target script and composition only shrinks length, so no cycle is possible;
observed maximum is 3 internal passes, guarded by `MAX_PASSES = 8` +
`debug_assert`. The no-op fast path (borrow on first pass) is preserved, so
already-clean input still costs a single pass. The pipeline-internal
`normalize_confusables_into` is unaffected (presets canonicalize upstream).

Testing — this is the crux. A random `\PC*` proptest can only *stumble* on the
2-code-point adjacency that triggers this, which is why 1000-case (and even
100k-case) runs missed 61 further failures that a one-shot recompose left behind.
The bug class is local and bounded, so it is deterministically enumerable:

  * Add `exhaustive_fold_compose_idempotent_and_complete` (#[ignore], Tier 3):
    every confusable source code point × every combining mark (~9M pairs, ~6s in
    release), asserting idempotency AND completeness. This is the real gate.
  * Wire ignored lib-level exhaustive tests into tier3.yml (`--lib -- --ignored`),
    which also picks up the previously-orphaned `presets` fast-path sweep.
  * Replace the flawed `fold_never_drops_chars` proptest: iterating to a fixed
    point can legitimately shorten output (`Ҫ`+◌̧ → `C`, the mark absorbed then
    discarded when `Ç` folds), so its char-count proxy is false. The guarantee it
    stood for — no table value is empty — is now asserted directly and
    deterministically by `confusable_table_values_are_non_empty`.
  * Add an explicit regression unit test covering both interaction directions and
    pin the proptest counterexample seed.

Verified: full Rust suite, clippy (core + ext), perf_lint, pytest (3529), the
~9M-pair exhaustive gate (0 counterexamples), and a 50k-case debug proptest
(no debug_assert) all pass.


Assisted-by: Claude Code:claude-opus-4-8

Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
@raeq
raeq force-pushed the release/0.11.1 branch from 7b10dd6 to f156b46 Compare July 13, 2026 21:01
github-actions Bot added a commit that referenced this pull request Jul 13, 2026
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@raeq

raeq commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

Good catch — fixed. Bumped CITATION.cff to 0.11.1 and regenerated uv.lock (disarm editable entry 0.11.0 → 0.11.1, no other dep churn). Swept the tree for any other stray 0.11.0: only the #[deprecated(since = "0.11.0")] markers remain, which correctly record the deprecation version and must not change.

@raeq
raeq force-pushed the release/0.11.1 branch from f156b46 to 80b4d2b Compare July 13, 2026 21:36
github-actions Bot added a commit that referenced this pull request Jul 13, 2026
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Patch release cutting the backlog since 0.11.0, lockstep across all four
registries (crates.io, PyPI, npm, RubyGems).

- Fixed: ml_normalize idempotent on NFKD-exposed symbol bases (#498)
- Fixed: normalize_confusables idempotent + complete on confusable+mark input,
  now iterated to a fixed point with an exhaustive Tier-3 gate (#523)
- Docs: unidecode() Cyrillic soft/hard-sign collision note (#511)
- Internal: binding publishers gated on the published core (#500); phf held
  at 0.13 to preserve MSRV 1.81 (#510)

Bump the version to 0.11.1 in every place it is recorded: Cargo.toml,
pyproject.toml, bindings/node/package.json, bindings/ruby/lib/disarm/version.rb,
CITATION.cff (citation metadata), and the disarm editable entry in uv.lock. The
binding glue crates stay at 0.0.0 with the minor-only `disarm_core = "0.11"` pin
— unchanged for a patch.

Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
Assisted-by: Claude Code:claude-opus-4-8
@raeq
raeq force-pushed the release/0.11.1 branch from 80b4d2b to 8f5884f Compare July 13, 2026 22:02
@raeq
raeq merged commit a0f7778 into main Jul 13, 2026
28 checks passed
@raeq
raeq deleted the release/0.11.1 branch July 13, 2026 22:07
github-actions Bot added a commit that referenced this pull request Jul 13, 2026
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants