lda-ruby 0.3.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -13
- data/CHANGELOG.md +16 -0
- data/Gemfile +9 -0
- data/README.md +126 -3
- data/VERSION.yml +3 -3
- data/docs/modernization-handoff.md +233 -0
- data/docs/porting-strategy.md +148 -0
- data/docs/precompiled-platform-policy.md +81 -0
- data/docs/precompiled-target-evaluation.md +67 -0
- data/docs/release-runbook.md +192 -0
- data/docs/rust-orchestration-guardrails.md +50 -0
- data/ext/lda-ruby/cokus.c +10 -11
- data/ext/lda-ruby/cokus.h +3 -3
- data/ext/lda-ruby/extconf.rb +10 -6
- data/ext/lda-ruby/lda-inference.c +23 -7
- data/ext/lda-ruby/utils.c +8 -0
- data/ext/lda-ruby-rust/Cargo.toml +12 -0
- data/ext/lda-ruby-rust/README.md +73 -0
- data/ext/lda-ruby-rust/extconf.rb +135 -0
- data/ext/lda-ruby-rust/include/strings.h +35 -0
- data/ext/lda-ruby-rust/src/lib.rs +1263 -0
- data/lda-ruby.gemspec +0 -0
- data/lib/lda-ruby/backends/base.rb +133 -0
- data/lib/lda-ruby/backends/native.rb +158 -0
- data/lib/lda-ruby/backends/pure_ruby.rb +675 -0
- data/lib/lda-ruby/backends/rust.rb +607 -0
- data/lib/lda-ruby/backends.rb +58 -0
- data/lib/lda-ruby/corpus/corpus.rb +17 -15
- data/lib/lda-ruby/corpus/data_corpus.rb +2 -2
- data/lib/lda-ruby/corpus/directory_corpus.rb +2 -2
- data/lib/lda-ruby/corpus/text_corpus.rb +2 -2
- data/lib/lda-ruby/document/document.rb +6 -6
- data/lib/lda-ruby/document/text_document.rb +5 -4
- data/lib/lda-ruby/rust_build_policy.rb +21 -0
- data/lib/lda-ruby/version.rb +5 -0
- data/lib/lda-ruby.rb +293 -48
- data/test/backend_compatibility_test.rb +146 -0
- data/test/backends_selection_test.rb +100 -0
- data/test/benchmark_scripts_test.rb +23 -0
- data/test/gemspec_test.rb +27 -0
- data/test/lda_ruby_test.rb +49 -11
- data/test/packaged_gem_smoke_test.rb +33 -0
- data/test/pure_ruby_orchestration_test.rb +109 -0
- data/test/release_scripts_test.rb +93 -0
- data/test/rust_build_policy_test.rb +23 -0
- data/test/rust_orchestration_test.rb +911 -0
- data/test/simple_pipeline_test.rb +22 -0
- data/test/simple_yaml.rb +1 -7
- data/test/test_helper.rb +5 -6
- metadata +54 -38
- data/Rakefile +0 -61
- data/ext/lda-ruby/Makefile +0 -181
- data/test/data/.gitignore +0 -2
- data/test/simple_test.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
|
|
5
|
-
data.tar.gz: !binary |-
|
|
6
|
-
OTNjZjE5MGNmOGI2YzY3YzhlNDRiYTBlNDM5NmUwYmY4Mjc2ZmNkNQ==
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a2470a73c1ba2c8807574f34a606a429adbd20d94457990667b59d241b521171
|
|
4
|
+
data.tar.gz: 8c7aa9b952901e15b48b974d68b339ab20e41edfcc07fa391266591b219b5771
|
|
7
5
|
SHA512:
|
|
8
|
-
metadata.gz:
|
|
9
|
-
|
|
10
|
-
ZmFjZDZiZWIwZmRiMDJlYjNjYTg5YWQ0N2RlOTY1MTg0YzZjZTc0NGQ0YmI1
|
|
11
|
-
ZDljMjljNTA3ODdlZjBjNjNjMTc0MGRlMjBhODQzZTg3YWM5OWE=
|
|
12
|
-
data.tar.gz: !binary |-
|
|
13
|
-
OGFiMzVhMzY4OTFmZTkzMmM3YjQxMzNkM2JlMjVjOTRjM2ZhNTc1YmUxZTRj
|
|
14
|
-
Y2M3YzZiNzNiYmVkNDI1ZTUxODhkMjhlYTQ4MmRhZjVkZjQxMDhjOTk5Njc4
|
|
15
|
-
OTZhOTM5ZTQ3OTFhY2U1YjRhODQyYzBkZWNlYzhjNzBjMWFlNGU=
|
|
6
|
+
metadata.gz: 3fbe0fe85517e82b63bb8f28358178fd2490e1c4162342efa62864b7a5d83cc81488c83b75443bc8a539319717c588b37a27504722947c361a08636884ab4133
|
|
7
|
+
data.tar.gz: c240216720322fb4a539a63cff5c85a37544fbbad6281bb2b5c89787016f9dab817ab6db8295083e7c218454b23065ec06836e454b24d47e85111696e2e5dd1c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
version 0.5.0
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
- Expanded Rust-side EM orchestration with cached corpus snapshots, managed corpus sessions, and start-aware fallback paths.
|
|
5
|
+
- Added Rust benchmark guardrails and deeper pure/Rust orchestration parity coverage.
|
|
6
|
+
- Added release-blocking precompiled gem targets for Windows (`x64-mingw-ucrt`) and Linux musl (`x86_64-linux-musl`).
|
|
7
|
+
- Hardened precompiled release workflows with full-matrix CI, post-publish verification, and release failure alerts.
|
|
8
|
+
|
|
9
|
+
version 0.4.0
|
|
10
|
+
=============
|
|
11
|
+
|
|
12
|
+
- Ruby 3.2+/3.3+ modernization release.
|
|
13
|
+
- Added Rust/native/pure backend selection and packaged-gem install policy validation.
|
|
14
|
+
- Added tag-driven release automation and release runbook.
|
|
15
|
+
- Added precompiled platform gem build/publish pipeline (Linux + macOS) and compatibility policy documentation.
|
|
16
|
+
|
|
1
17
|
version 0.3.9
|
|
2
18
|
=============
|
|
3
19
|
|
data/Gemfile
ADDED
data/README.md
CHANGED
|
@@ -17,6 +17,119 @@ The original C code relied on files for the input and output. We felt it was nec
|
|
|
17
17
|
|
|
18
18
|
If you have general questions about Latent Dirichlet Allocation, I urge you to use the [topic models mailing list][topic-models], since the people who monitor that are very knowledgeable. If you encounter bugs specific to lda-ruby, please post an issue on the Github project.
|
|
19
19
|
|
|
20
|
+
## Development
|
|
21
|
+
|
|
22
|
+
### Local (Ruby 3.2+)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
bundle exec rake test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Docker (recommended for isolated setup)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
./bin/docker-test
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Rust backend runtime checks in Docker:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
./bin/docker-test-rust
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Install policy matrix checks in Docker:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
./bin/docker-test-install-policies
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For an interactive shell inside the dev container:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
./bin/docker-shell
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For an interactive shell with Rust toolchain + bindgen dependencies:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
./bin/docker-shell-rust
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Build tasks
|
|
60
|
+
|
|
61
|
+
- `bundle exec rake compile` builds the native extension.
|
|
62
|
+
- `bundle exec rake compile_rust` builds the experimental Rust extension and stages a Ruby-loadable artifact (`lda_ruby_rust.<dlext>`).
|
|
63
|
+
- On macOS, build tasks automatically add the Rust linker flag for Ruby extension `dynamic_lookup`.
|
|
64
|
+
- `bundle exec rake test` rebuilds the extension, then runs tests.
|
|
65
|
+
- `bundle exec rake build` builds the gem package.
|
|
66
|
+
- `bundle exec ruby -Ilib:test test/backend_compatibility_test.rb` runs backend compatibility fixtures.
|
|
67
|
+
- `LDA_RUBY_BACKEND=rust bundle exec ruby -Ilib:test test/backend_compatibility_test.rb` runs parity checks in rust mode.
|
|
68
|
+
- `./bin/benchmark-backends` benchmarks available backends (`pure`, `native`, `rust`) and prints JSON.
|
|
69
|
+
- `./bin/check-rust-benchmark` enforces the Rust/pure benchmark ratio guardrail (configurable via env vars).
|
|
70
|
+
- `./bin/docker-test-install-policies` verifies packaged-gem install behavior for `LDA_RUBY_RUST_BUILD=auto|always|never`, including runtime EM smoke checks.
|
|
71
|
+
- `./bin/test-packaged-gem-fallback` verifies packaged-gem fallback behavior without Cargo (auto/never succeed, always fails) plus runtime smoke checks.
|
|
72
|
+
- `./bin/test-packaged-gem-rust-enabled` verifies packaged-gem behavior with Cargo available (auto/always enable Rust, never disables Rust) plus runtime smoke checks.
|
|
73
|
+
- `./bin/test-packaged-gem-manifest` verifies packaged-gem contents/metadata and rejects leaked build artifacts.
|
|
74
|
+
- `./bin/release-preflight` runs unit tests + packaged-gem validation stack; set `SKIP_DOCKER=1` to skip Docker matrix checks.
|
|
75
|
+
- `./bin/check-version-sync` verifies version parity between `VERSION.yml`, `lib/lda-ruby/version.rb`, and expected release tag.
|
|
76
|
+
- `./bin/verify-rubygems-api-key` validates that your RubyGems API key can push non-interactively (required for CI release publishes).
|
|
77
|
+
- `./bin/verify-release-publish --tag v0.4.0` verifies published RubyGems + GitHub release assets for a release tag.
|
|
78
|
+
- `./bin/release-prepare 0.4.0` updates version/changelog files for a new release version.
|
|
79
|
+
- `./bin/release-artifacts --tag v0.4.0` runs release checks, builds the source gem, and writes SHA256 checksums.
|
|
80
|
+
- `./bin/release-precompiled-artifacts --tag v0.4.0 --platform x86_64-linux --skip-preflight` builds a precompiled platform gem and verifies install/runtime smoke checks.
|
|
81
|
+
- The `--platform` value must match the current host platform.
|
|
82
|
+
|
|
83
|
+
Benchmark environment variables:
|
|
84
|
+
- `BENCH_RUNS` (default: `3`)
|
|
85
|
+
- `BENCH_START` (default: `seeded`)
|
|
86
|
+
- `BENCH_TOPICS` (default: `8`)
|
|
87
|
+
- `BENCH_MAX_ITER` (default: `20`)
|
|
88
|
+
- `BENCH_EM_MAX_ITER` (default: `40`)
|
|
89
|
+
|
|
90
|
+
### Install-time Rust build policy
|
|
91
|
+
|
|
92
|
+
Source installs now run both extension setup scripts (`ext/lda-ruby/extconf.rb` and `ext/lda-ruby-rust/extconf.rb`).
|
|
93
|
+
|
|
94
|
+
Rust build policy is controlled by `LDA_RUBY_RUST_BUILD`:
|
|
95
|
+
- `auto` (default): build Rust extension if `cargo` is available, otherwise skip.
|
|
96
|
+
- `always`: require Rust extension build and fail install if unavailable.
|
|
97
|
+
- `never`: skip Rust extension build.
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
- `LDA_RUBY_RUST_BUILD=always gem install lda-ruby`
|
|
101
|
+
- `LDA_RUBY_RUST_BUILD=never bundle exec rake compile`
|
|
102
|
+
|
|
103
|
+
### Precompiled platform gems
|
|
104
|
+
|
|
105
|
+
Releases publish a source gem plus precompiled platform gems for:
|
|
106
|
+
- `x86_64-linux`
|
|
107
|
+
- `x86_64-darwin`
|
|
108
|
+
- `arm64-darwin`
|
|
109
|
+
|
|
110
|
+
On these platforms, installation should not require local C/Rust toolchains.
|
|
111
|
+
Other platforms install from source gem and use the existing install-time fallback policy.
|
|
112
|
+
|
|
113
|
+
For artifact strategy, compatibility targets, and rollout/deprecation rules, see `docs/precompiled-platform-policy.md`.
|
|
114
|
+
|
|
115
|
+
### Backend selection
|
|
116
|
+
|
|
117
|
+
- Default mode is `auto`: Rust backend when available, otherwise native extension, otherwise pure Ruby.
|
|
118
|
+
- Force pure Ruby backend:
|
|
119
|
+
- `Lda::Lda.new(corpus, backend: :pure)`
|
|
120
|
+
- or `LDA_RUBY_BACKEND=pure`
|
|
121
|
+
- Force native backend:
|
|
122
|
+
- `Lda::Lda.new(corpus, backend: :native)`
|
|
123
|
+
- Force Rust backend (when extension is available):
|
|
124
|
+
- `Lda::Lda.new(corpus, backend: :rust)`
|
|
125
|
+
- or `LDA_RUBY_BACKEND=rust`
|
|
126
|
+
|
|
127
|
+
`em("seeded")` is supported by both native and pure backends for deterministic fixture-oriented runs.
|
|
128
|
+
|
|
129
|
+
Rust status: the extension hook layer is scaffolded in `ext/lda-ruby-rust`. Current Rust kernels include batched per-iteration corpus inference, batched per-document inference, topic-weights-per-word, topic-term-count accumulation, topic-term normalization/log-beta finalization, gamma-shift convergence reduction, topic-document average log-probability computation, seeded topic-term initialization, random topic-term initialization, and Rust-side EM orchestration paths (`run_em`, `run_em_with_start`, `run_em_with_start_seed`, session-based `run_em_on_session_with_start_seed`, settings-aware session `run_em_on_session_start`, and unified session-settings orchestration `run_em_on_session`) when `backend: :rust` is active. Session orchestration now uses shared Rust-side corpus storage and borrowed execution paths so EM session runs do not deep-clone corpus arrays per call, and the Ruby adapter now auto-recreates missing Rust sessions before EM to stay on the session path. The Rust backend still keeps the pure Ruby implementation as a compatibility fallback path if Rust orchestration is unavailable or returns invalid output. CI runs dedicated rust-runtime checks and numeric parity fixtures against the pure backend.
|
|
130
|
+
`compile_rust` and `LDA_RUBY_RUST_BUILD=always` require a Rust toolchain plus Ruby development headers and `libclang`.
|
|
131
|
+
Gem packaging excludes local Rust build artifacts (`ext/lda-ruby-rust/target/**`) so local cargo outputs do not leak into published gems.
|
|
132
|
+
|
|
20
133
|
## Resources
|
|
21
134
|
|
|
22
135
|
+ [Blog post about LDA-Ruby][lda-ruby]
|
|
@@ -29,9 +142,19 @@ If you have general questions about Latent Dirichlet Allocation, I urge you to u
|
|
|
29
142
|
Blei, David M., Ng, Andrew Y., and Jordan, Michael I. 2003. Latent dirichlet allocation. Journal of Machine Learning Research. 3 (Mar. 2003), 993-1022 [[pdf][pdf]].
|
|
30
143
|
|
|
31
144
|
[svmlight]: http://svmlight.joachims.org
|
|
32
|
-
[lda-ruby]: http://mendicantbug.com/2008/11/17/lda-in-ruby/
|
|
33
|
-
[blei]: http://www.cs.princeton.edu/~blei/lda-c/
|
|
145
|
+
[lda-ruby]: http://web.archive.org/web/20120616115448/http://mendicantbug.com/2008/11/17/lda-in-ruby/
|
|
146
|
+
[blei]: http://web.archive.org/web/20161126004857/http://www.cs.princeton.edu/~blei/lda-c/
|
|
34
147
|
[wikipedia]: http://en.wikipedia.org/wiki/Latent_Dirichlet_allocation
|
|
35
|
-
[ap-data]: http://www.cs.princeton.edu/~blei/lda-c/ap.tgz
|
|
148
|
+
[ap-data]: http://web.archive.org/web/20160507090044/http://www.cs.princeton.edu/~blei/lda-c/ap.tgz
|
|
36
149
|
[pdf]: http://www.cs.princeton.edu/picasso/mats/BleiNgJordan2003_blei.pdf
|
|
37
150
|
[topic-models]: https://lists.cs.princeton.edu/mailman/listinfo/topic-models
|
|
151
|
+
|
|
152
|
+
## Modernization
|
|
153
|
+
|
|
154
|
+
For a Ruby 3.2+/3.3+ porting proposal, see `docs/porting-strategy.md`.
|
|
155
|
+
|
|
156
|
+
For the latest implementation status and exact resume instructions, see `docs/modernization-handoff.md`.
|
|
157
|
+
|
|
158
|
+
For release steps and rollback guidance, see `docs/release-runbook.md`.
|
|
159
|
+
|
|
160
|
+
For precompiled gem strategy and compatibility policy, see `docs/precompiled-platform-policy.md`.
|
data/VERSION.yml
CHANGED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Modernization Handoff (Resume Guide)
|
|
2
|
+
|
|
3
|
+
This document is the canonical handoff state for continuing the Ruby 3.2+/3.3+ modernization in a new conversation.
|
|
4
|
+
|
|
5
|
+
## Snapshot
|
|
6
|
+
|
|
7
|
+
- Snapshot date: 2026-03-02
|
|
8
|
+
- Active branch: `codex/rust-orchestration-phase8`
|
|
9
|
+
- Repo status at snapshot start: clean working tree on `codex/rust-orchestration-phase8` (ahead of `master`)
|
|
10
|
+
- Modernization branch: `codex/modernization` merged into `master`
|
|
11
|
+
- Merge commit for modernization PR: `bc11269` (`Merge pull request #18 from ealdent/codex/modernization`)
|
|
12
|
+
- Latest release dry-run validation (GitHub Actions):
|
|
13
|
+
- date: 2026-03-02
|
|
14
|
+
- workflow run: `release.yml` run `22556487788` (`workflow_dispatch`, `publish=false`)
|
|
15
|
+
- result: success (`validate release candidate`, `build release artifacts`, and all `build precompiled artifacts` matrix targets)
|
|
16
|
+
- publish jobs skipped by design (`publish=false`)
|
|
17
|
+
- Modernization pull request:
|
|
18
|
+
- PR `#18`: `https://github.com/ealdent/lda-ruby/pull/18`
|
|
19
|
+
- state: merged (2026-02-25)
|
|
20
|
+
- Release publish attempts (`v0.4.0`):
|
|
21
|
+
- `release.yml` run `22383716372`: failed at RubyGems publish (`No such API key`)
|
|
22
|
+
- `release.yml` run `22383849236`: failed at RubyGems publish (`OTP code required`)
|
|
23
|
+
- rerun attempt (`22383849236`, attempt 2): failed at RubyGems publish (`OTP code required`)
|
|
24
|
+
- rerun attempt (`22383849236`, attempt 3): success (RubyGems publish + GitHub release publish complete)
|
|
25
|
+
- Release status:
|
|
26
|
+
- `v0.4.0` published to RubyGems (source + precompiled Linux/macOS gems)
|
|
27
|
+
- GitHub release `v0.4.0` published with gem + `.sha256` assets
|
|
28
|
+
- next release dry-run matrix is validated for Linux, Linux musl, macOS Intel, macOS Apple Silicon, and Windows targets (`22556487788`)
|
|
29
|
+
|
|
30
|
+
## Project Goal
|
|
31
|
+
|
|
32
|
+
Modernize `lda-ruby` for Ruby 3.2+/3.3+ with:
|
|
33
|
+
|
|
34
|
+
- stable Ruby API compatibility
|
|
35
|
+
- high-performance Rust backend for hot paths
|
|
36
|
+
- pure Ruby fallback for portability
|
|
37
|
+
- reliable packaging and release validation
|
|
38
|
+
|
|
39
|
+
## Current Backend Behavior
|
|
40
|
+
|
|
41
|
+
- `auto` selection order: `rust` -> `native` -> `pure_ruby`
|
|
42
|
+
- `LDA_RUBY_BACKEND` override supported for `rust`, `native`, and `pure`
|
|
43
|
+
- Rust build policy for source installs: `LDA_RUBY_RUST_BUILD=auto|always|never`
|
|
44
|
+
|
|
45
|
+
## Phase Status
|
|
46
|
+
|
|
47
|
+
### Phase 1 (API/tests stabilization)
|
|
48
|
+
|
|
49
|
+
Status: complete.
|
|
50
|
+
|
|
51
|
+
Delivered:
|
|
52
|
+
|
|
53
|
+
- expanded compatibility fixtures
|
|
54
|
+
- CI test coverage for multiple backend modes
|
|
55
|
+
|
|
56
|
+
### Phase 2 (backend boundary extraction)
|
|
57
|
+
|
|
58
|
+
Status: complete.
|
|
59
|
+
|
|
60
|
+
Delivered:
|
|
61
|
+
|
|
62
|
+
- `Lda::Lda` delegates through backend adapters
|
|
63
|
+
- backend selection normalized through `Lda::Backends.build`
|
|
64
|
+
|
|
65
|
+
### Phase 3 (pure Ruby reference backend)
|
|
66
|
+
|
|
67
|
+
Status: complete.
|
|
68
|
+
|
|
69
|
+
Delivered:
|
|
70
|
+
|
|
71
|
+
- full pure Ruby backend path with EM/model outputs
|
|
72
|
+
- pure backend compatibility in tests
|
|
73
|
+
|
|
74
|
+
### Phase 4 (Rust native backend)
|
|
75
|
+
|
|
76
|
+
Status: mostly complete.
|
|
77
|
+
|
|
78
|
+
Delivered:
|
|
79
|
+
|
|
80
|
+
- Rust extension scaffold with magnus/rb_sys
|
|
81
|
+
- Rust kernels for the main hot loops:
|
|
82
|
+
- corpus iteration
|
|
83
|
+
- document inference
|
|
84
|
+
- topic weights
|
|
85
|
+
- topic-term accumulation
|
|
86
|
+
- topic-term finalization (`beta`/`log(beta)`)
|
|
87
|
+
- gamma shift reduction
|
|
88
|
+
- topic-document probability
|
|
89
|
+
- seeded initialization
|
|
90
|
+
- trusted kernel-output fast path enabled in rust mode
|
|
91
|
+
- Rust-side EM orchestration path (`Lda::RustBackend.run_em`) retained as a compatibility fallback for precomputed beta-input execution
|
|
92
|
+
- Rust-side start-aware deterministic orchestration path (`Lda::RustBackend.run_em_with_start`) for `seeded`/`deterministic` EM starts
|
|
93
|
+
- Rust-side random-start orchestration path (`Lda::RustBackend.run_em_with_start_seed`) using explicit seed-controlled random initialization (`Lda::RustBackend.random_topic_term_probabilities`)
|
|
94
|
+
- Rust-managed corpus session lifecycle (`Lda::RustBackend.create_corpus_session` / `drop_corpus_session`) with session-based start-aware EM orchestration (`run_em_on_session_with_start_seed`) wired in `Lda::Backends::Rust`
|
|
95
|
+
- Rust-managed session settings lifecycle (`configure_corpus_session`) with settings-aware orchestration (`run_em_on_session_start`) wired in `Lda::Backends::Rust`
|
|
96
|
+
- Rust session execution refactor to shared session corpus storage + borrowed orchestration helpers, eliminating deep corpus clone overhead on each session EM run
|
|
97
|
+
- unified Rust session orchestration API (`run_em_on_session`) that applies settings + runs EM in one call inside Rust session orchestration
|
|
98
|
+
- `Lda::Backends::Rust` now routes cached-corpus EM through managed Rust orchestration (`run_em_on_session_with_corpus`), leaving session reuse/recovery decisions in Rust and preferring that managed path even when no active session id is cached locally; when the managed API is unavailable it still prefers direct Rust non-session orchestration (`run_em_with_start_seed`) before legacy Ruby-side beta-input fallback (`run_em`)
|
|
99
|
+
- direct non-session Rust orchestration now reuses the backend's cached Rust corpus snapshot instead of rebuilding corpus arrays from `@corpus` on each fallback invocation
|
|
100
|
+
- legacy Rust beta-input compatibility fallback now also reuses the backend's cached Rust corpus snapshot, only asking the pure-Ruby backend to synthesize the initial beta matrix
|
|
101
|
+
- Rust managed-session orchestration API (`run_em_on_session_with_corpus`) added to recreate missing sessions and run EM in one Rust call, and now directly falls back to start-aware array execution inside Rust if session-backed execution cannot be used
|
|
102
|
+
- Rust session lifecycle replacement API (`replace_corpus_session`) added so corpus reassignment can update existing Rust sessions in place (config reset + corpus swap) instead of Ruby-side drop/recreate
|
|
103
|
+
- `Lda::Backends::Rust` now keeps session-based orchestration on the managed Rust path (`run_em_on_session_with_corpus`) even when sessions are dropped externally
|
|
104
|
+
- parity/compatibility test coverage and rust runtime CI
|
|
105
|
+
|
|
106
|
+
Open in Phase 4:
|
|
107
|
+
|
|
108
|
+
- optional deeper Rust ownership beyond current unified session orchestration (for example additional control-plane logic and lifecycle APIs)
|
|
109
|
+
|
|
110
|
+
### Phase 5 (packaging/release)
|
|
111
|
+
|
|
112
|
+
Status: Phase 5A complete (source-gem release automation), Phase 5B complete for Linux/macOS/Windows/musl precompiled release matrix.
|
|
113
|
+
|
|
114
|
+
Delivered:
|
|
115
|
+
|
|
116
|
+
- clean gem file filtering (no local cargo/native artifacts)
|
|
117
|
+
- Docker install-policy matrix checks
|
|
118
|
+
- packaged gem runtime checks without Cargo (`bin/test-packaged-gem-fallback`)
|
|
119
|
+
- packaged gem runtime checks with Cargo (`bin/test-packaged-gem-rust-enabled`)
|
|
120
|
+
- packaged gem manifest/metadata gate (`bin/test-packaged-gem-manifest`)
|
|
121
|
+
- single-command local gate (`bin/release-preflight`)
|
|
122
|
+
- version/tag parity guard (`bin/check-version-sync`)
|
|
123
|
+
- RubyGems CI credential preflight helper (`bin/verify-rubygems-api-key`)
|
|
124
|
+
- post-publish artifact verification helper (`bin/verify-release-publish`)
|
|
125
|
+
- deterministic release preparation helper (`bin/release-prepare`)
|
|
126
|
+
- release artifact builder with checksum output (`bin/release-artifacts`)
|
|
127
|
+
- precompiled artifact builder + runtime validator (`bin/release-precompiled-artifacts`)
|
|
128
|
+
- gemspec precompiled variant support (`LDA_RUBY_GEM_VARIANT=precompiled`)
|
|
129
|
+
- precompiled platform compatibility/publish policy (`docs/precompiled-platform-policy.md`)
|
|
130
|
+
- precompiled target expansion tracker (`docs/precompiled-target-evaluation.md`)
|
|
131
|
+
- macOS Rust build linker guardrail (`dynamic_lookup`) for precompiled packaging paths
|
|
132
|
+
- tag-driven release workflow (`.github/workflows/release.yml`)
|
|
133
|
+
- release failure alert workflow (`.github/workflows/release-failure-alert.yml`)
|
|
134
|
+
- tuned to create issues only for failed tag-triggered release runs, with failed job links in alert body
|
|
135
|
+
- now auto-closes matching alert issues when the same release run later reports success
|
|
136
|
+
- maintainer release runbook (`docs/release-runbook.md`)
|
|
137
|
+
- manual precompiled candidate workflow now validated on both lanes (`precompiled-candidate-evaluation.yml` run `22556129503`)
|
|
138
|
+
- manual precompiled candidate workflow now validated with runtime checks on both lanes (`precompiled-candidate-evaluation.yml` run `22556206925`)
|
|
139
|
+
- Windows candidate portability hardening across native + Rust build paths:
|
|
140
|
+
- C portability fixes (`cokus` macros, `time_t`, `_mkdir`)
|
|
141
|
+
- Rust toolchain alignment to GNU target for RubyInstaller (`x64-mingw-ucrt`)
|
|
142
|
+
- Rust bindgen header/sysroot compatibility wiring for Windows runners
|
|
143
|
+
- dual Rust DLL artifact-name staging support (`lda_ruby_rust.dll` and `liblda_ruby_rust.dll`)
|
|
144
|
+
- CI jobs for packaged-gem fallback, rust-enabled checks, and manifest checks
|
|
145
|
+
- CI precompiled gem build guardrail job (`precompiled-gem-build`) aligned to the full release-blocking precompiled matrix
|
|
146
|
+
- macOS precompiled CI/release lanes now pin Homebrew `llvm@18` (with fallback to `llvm`) and export `LIBCLANG_PATH` from the selected prefix to avoid bindgen breakage from Homebrew LLVM drift
|
|
147
|
+
- release workflow matrix for precompiled gems:
|
|
148
|
+
- `x86_64-linux`
|
|
149
|
+
- `x86_64-linux-musl`
|
|
150
|
+
- `x86_64-darwin`
|
|
151
|
+
- `arm64-darwin`
|
|
152
|
+
- `x64-mingw-ucrt`
|
|
153
|
+
- release dry-run validation for expanded precompiled matrix (`release.yml` run `22556487788`)
|
|
154
|
+
|
|
155
|
+
Open in Phase 5:
|
|
156
|
+
|
|
157
|
+
- optional expansion of precompiled targets beyond current Linux/macOS/Windows/musl set
|
|
158
|
+
|
|
159
|
+
## Validation Commands
|
|
160
|
+
|
|
161
|
+
Core:
|
|
162
|
+
|
|
163
|
+
- `./bin/docker-test`
|
|
164
|
+
- `./bin/docker-test-rust`
|
|
165
|
+
|
|
166
|
+
Packaging/release checks:
|
|
167
|
+
|
|
168
|
+
- `./bin/check-version-sync`
|
|
169
|
+
- `./bin/verify-rubygems-api-key`
|
|
170
|
+
- `./bin/verify-release-publish --tag v0.4.0`
|
|
171
|
+
- `./bin/test-packaged-gem-manifest`
|
|
172
|
+
- `./bin/test-packaged-gem-fallback`
|
|
173
|
+
- `./bin/test-packaged-gem-rust-enabled`
|
|
174
|
+
- `SKIP_DOCKER=1 ./bin/release-preflight`
|
|
175
|
+
- `./bin/release-artifacts --tag v0.4.0`
|
|
176
|
+
- `./bin/release-precompiled-artifacts --tag v0.4.0 --skip-preflight`
|
|
177
|
+
|
|
178
|
+
Optional full Docker matrix:
|
|
179
|
+
|
|
180
|
+
- `./bin/docker-test-install-policies`
|
|
181
|
+
|
|
182
|
+
Performance tracking:
|
|
183
|
+
|
|
184
|
+
- `./bin/benchmark-backends`
|
|
185
|
+
- `./bin/check-rust-benchmark`
|
|
186
|
+
- `docs/rust-orchestration-guardrails.md`
|
|
187
|
+
- CI currently enforces `BENCH_RUST_TO_PURE_MAX_RATIO=0.045` in `benchmark-guardrail`
|
|
188
|
+
|
|
189
|
+
## CI Jobs Expected
|
|
190
|
+
|
|
191
|
+
- native tests (`test-native`)
|
|
192
|
+
- pure backend tests (`test-pure`)
|
|
193
|
+
- rust runtime tests (`rust-runtime`)
|
|
194
|
+
- Docker install policy matrix (`install-policy-matrix`)
|
|
195
|
+
- packaged gem fallback checks (`packaged-gem-fallback`)
|
|
196
|
+
- packaged gem rust-enabled checks (`packaged-gem-rust-enabled`)
|
|
197
|
+
- packaged gem manifest checks (`packaged-gem-manifest`)
|
|
198
|
+
- precompiled gem build checks (`precompiled-gem-build`)
|
|
199
|
+
- rust scaffold check (`rust-scaffold`)
|
|
200
|
+
- benchmark guardrail check (`benchmark-guardrail`)
|
|
201
|
+
- release validation/build/publish pipeline on `v*` tags (`release.yml`)
|
|
202
|
+
- post-publish artifact verification (`verify_published_artifacts` in `release.yml`)
|
|
203
|
+
- release-failure issue alerting (`release-failure-alert`)
|
|
204
|
+
|
|
205
|
+
## Remaining Work Queue
|
|
206
|
+
|
|
207
|
+
Priority 1:
|
|
208
|
+
|
|
209
|
+
- continue periodic Rust-orchestration benchmark guardrail tightening (`docs/rust-orchestration-guardrails.md`) as performance data remains stable
|
|
210
|
+
- keep hybrid backend compatibility guarantees (Rust + native + pure fallback) while extending Rust orchestration only behind parity/guardrail checks
|
|
211
|
+
|
|
212
|
+
Priority 2:
|
|
213
|
+
|
|
214
|
+
- monitor expanded precompiled release lanes (Windows + musl Linux) now that release dry-run matrix validation is green in run `22556487788`
|
|
215
|
+
- evaluate any additional precompiled targets beyond current release-blocking set using `docs/precompiled-target-evaluation.md`
|
|
216
|
+
|
|
217
|
+
Priority 3:
|
|
218
|
+
|
|
219
|
+
- monitor release-failure auto-alert and auto-close behavior (`.github/workflows/release-failure-alert.yml`) and adjust signal/noise as release cadence grows
|
|
220
|
+
|
|
221
|
+
## Resume Instructions For A New Conversation
|
|
222
|
+
|
|
223
|
+
1. Check out `master`.
|
|
224
|
+
2. Open this file first: `docs/modernization-handoff.md`.
|
|
225
|
+
3. Run `SKIP_DOCKER=1 ./bin/release-preflight`.
|
|
226
|
+
4. Review `docs/release-runbook.md` for release flow/rollback details.
|
|
227
|
+
5. Validate precompiled packaging locally for your host:
|
|
228
|
+
- `./bin/release-precompiled-artifacts --tag "$(./bin/check-version-sync --print-tag)" --skip-preflight`
|
|
229
|
+
6. Continue with remaining modernization queue (`Priority 1` then `Priority 2/3`).
|
|
230
|
+
|
|
231
|
+
If you want the next assistant to continue immediately, use:
|
|
232
|
+
|
|
233
|
+
"Open `docs/modernization-handoff.md`, validate with `SKIP_DOCKER=1 ./bin/release-preflight`, run `./bin/release-precompiled-artifacts --skip-preflight`, and continue the `Priority 1/2/3` modernization queue."
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Ruby 3.2+/3.3+ Porting Strategy (Experimental)
|
|
2
|
+
|
|
3
|
+
## Recommendation
|
|
4
|
+
|
|
5
|
+
For this gem, the best long-term path is:
|
|
6
|
+
|
|
7
|
+
1. **Keep the public Ruby API.**
|
|
8
|
+
2. **Replace the handwritten C extension bindings with Rust + magnus** (Ruby native extension bindings for modern CRuby).
|
|
9
|
+
3. **Ship a pure-Ruby fallback backend** for portability and easier debugging.
|
|
10
|
+
|
|
11
|
+
This gives a practical balance between maintenance and speed:
|
|
12
|
+
|
|
13
|
+
- Pure Ruby only: easiest to maintain, but likely much slower for training.
|
|
14
|
+
- Modern C extension rewrite: fast, but still painful to maintain against Ruby internals.
|
|
15
|
+
- Rust extension: native speed with safer memory management and cleaner FFI layer.
|
|
16
|
+
|
|
17
|
+
## Why this path fits this project
|
|
18
|
+
|
|
19
|
+
- The current project already has a stable Ruby-facing object model (`Lda::Lda`, corpus/document classes).
|
|
20
|
+
- The expensive parts are numeric loops in inference/training, which benefit from native code.
|
|
21
|
+
- Ruby 3.2+ compatibility is much easier to preserve with a modern binding layer than with legacy C wrapper patterns.
|
|
22
|
+
|
|
23
|
+
## Proposed architecture
|
|
24
|
+
|
|
25
|
+
- `Lda::Backends::Rust` (preferred in `auto` mode when extension loads)
|
|
26
|
+
- `Lda::Backends::Native` (fallback in `auto` mode when Rust is unavailable)
|
|
27
|
+
- `Lda::Backends::PureRuby` (always available)
|
|
28
|
+
- `Lda::Lda` delegates heavy operations to the selected backend.
|
|
29
|
+
|
|
30
|
+
Suggested backend selection:
|
|
31
|
+
|
|
32
|
+
- `ENV['LDA_RUBY_BACKEND']=pure_ruby` → force Ruby backend.
|
|
33
|
+
- default (`auto`) → try Rust backend, then native backend, then pure Ruby.
|
|
34
|
+
|
|
35
|
+
## Migration plan
|
|
36
|
+
|
|
37
|
+
### Current status
|
|
38
|
+
|
|
39
|
+
Completed in `codex/experiment-ruby3-modernization`:
|
|
40
|
+
|
|
41
|
+
- Phase 1 baseline test capture expanded with backend compatibility fixtures.
|
|
42
|
+
- Phase 2 backend boundary extraction (`Lda::Lda` now delegates through backend adapters).
|
|
43
|
+
- Phase 3 pure Ruby backend implementation (available as `backend: :pure` or `LDA_RUBY_BACKEND=pure`).
|
|
44
|
+
- CI matrix added for Ruby 3.2/3.3 with native and pure backend jobs.
|
|
45
|
+
- Phase 4 started with Rust extension scaffolding (`ext/lda-ruby-rust`) and backend mode wiring (`backend: :rust` when extension is available).
|
|
46
|
+
- Rust kernels ported so far:
|
|
47
|
+
- batched per-iteration corpus inference
|
|
48
|
+
- batched per-document inference loop (EM inner updates)
|
|
49
|
+
- per-word topic-weight computation
|
|
50
|
+
- topic-term accumulation from per-document `phi`
|
|
51
|
+
- topic-term normalization and log-beta finalization in EM
|
|
52
|
+
- gamma convergence shift reduction between EM iterations
|
|
53
|
+
- topic-document average log-probability computation
|
|
54
|
+
- seeded topic-term initialization
|
|
55
|
+
- Rust runtime CI job added (compile + execute rust backend tests).
|
|
56
|
+
- Rust/Pure numeric parity fixtures added for deterministic seeded runs.
|
|
57
|
+
- `compile_rust` now stages a Ruby-loadable extension artifact to avoid `Init_` symbol mismatch from Cargo's `lib*` output naming.
|
|
58
|
+
- Rust-side EM orchestration path added (`Lda::RustBackend.run_em`) and retained as legacy compatibility fallback for precomputed beta-input execution.
|
|
59
|
+
- Rust-side deterministic-start orchestration path added (`Lda::RustBackend.run_em_with_start`) so `seeded`/`deterministic` startup can stay in Rust.
|
|
60
|
+
- Rust-side seed-controlled random-start orchestration path added (`Lda::RustBackend.run_em_with_start_seed` + `random_topic_term_probabilities`) so random initialization can stay in Rust while preserving deterministic replay from an explicit seed.
|
|
61
|
+
- Rust-side corpus session lifecycle added (`create_corpus_session`/`drop_corpus_session`) and `Lda::Backends::Rust` now prefers session-based EM orchestration (`run_em_on_session_with_start_seed`) before array-based fallback paths.
|
|
62
|
+
- Rust-side session settings lifecycle added (`configure_corpus_session`) and `Lda::Backends::Rust` now prefers settings-aware session orchestration (`run_em_on_session_start`) before parameter-heavy session and array-based fallbacks.
|
|
63
|
+
- Rust session orchestration now runs on shared Rust-side corpus session data via borrowed execution helpers, avoiding deep corpus array cloning on each session EM call.
|
|
64
|
+
- Unified Rust session API added (`run_em_on_session`) to apply settings and execute EM in one call inside Rust session orchestration.
|
|
65
|
+
- `Lda::Backends::Rust` now prefers direct Rust non-session orchestration (`run_em_with_start_seed`) before legacy `run_em(initial_beta, ...)` compatibility fallback when a session path is unavailable.
|
|
66
|
+
- Direct non-session Rust orchestration now reuses the backend's cached Rust corpus snapshot instead of rebuilding corpus arrays from `@corpus` on each fallback invocation.
|
|
67
|
+
- Rust managed-session orchestration API added (`run_em_on_session_with_corpus`) to recreate missing sessions and execute EM in one Rust call.
|
|
68
|
+
- Rust session lifecycle replacement API added (`replace_corpus_session`) so corpus reassignment can update existing Rust sessions in place (config reset + corpus swap) instead of Ruby-side drop/recreate.
|
|
69
|
+
- `Lda::Backends::Rust` now routes cached-corpus EM through `run_em_on_session_with_corpus`, leaving session reuse/recovery decisions in Rust, preferring that managed path even when no active session id is cached locally, and reducing Ruby-side fallback branching when sessions are externally dropped.
|
|
70
|
+
- `run_em_on_session_with_corpus` now acts as a unified Rust managed-corpus entrypoint: it attempts session-backed execution first, then falls back to direct start-aware array execution inside Rust when a managed session cannot be used.
|
|
71
|
+
- Legacy `run_em(initial_beta, ...)` compatibility fallback now reuses the Rust backend's cached corpus snapshot and only relies on the pure-Ruby backend to synthesize the initial beta matrix.
|
|
72
|
+
- Dockerized rust runtime workflow added for local parity with CI (`Dockerfile.rust`, `bin/docker-test-rust`).
|
|
73
|
+
- Gem packaging now excludes local Rust cargo build artifacts (`target/**`) for clean release builds.
|
|
74
|
+
- Backend benchmark driver added (`bin/benchmark-backends`) to track pure/native/rust runtime deltas.
|
|
75
|
+
- Rust orchestration guardrail policy documented (`docs/rust-orchestration-guardrails.md`) with benchmark threshold checker (`bin/check-rust-benchmark`).
|
|
76
|
+
- CI benchmark guardrail job added (`benchmark-guardrail`) to enforce Rust/pure runtime ratio on Ubuntu (currently `BENCH_RUST_TO_PURE_MAX_RATIO=0.045`).
|
|
77
|
+
- Source install path now has explicit Rust build policy via `LDA_RUBY_RUST_BUILD=auto|always|never`.
|
|
78
|
+
- Docker install-policy matrix script added (`bin/docker-test-install-policies`) to verify source install behavior across environments.
|
|
79
|
+
- CI now runs install-policy matrix checks on Ubuntu.
|
|
80
|
+
- Install-policy matrix now runs packaged-gem runtime smoke checks (auto/pure/native/rust mode selection + EM pipeline) to validate release-time fallback behavior.
|
|
81
|
+
- Cross-OS packaged-gem fallback CI job added (`bin/test-packaged-gem-fallback`) to validate auto/never/always install policy semantics without Cargo.
|
|
82
|
+
- Packaged-gem Rust-enabled CI job added (`bin/test-packaged-gem-rust-enabled`) to validate auto/never/always install policy semantics with Cargo available.
|
|
83
|
+
- Packaged-gem manifest CI job added (`bin/test-packaged-gem-manifest`) to enforce release artifact contents and metadata.
|
|
84
|
+
- Local release preflight command added (`bin/release-preflight`) to run unit + packaged-gem validation checks in one pass.
|
|
85
|
+
- Version/tag sync guard added (`bin/check-version-sync`) to enforce parity between `VERSION.yml`, `lib/lda-ruby/version.rb`, and release tags.
|
|
86
|
+
- Release preparation helper added (`bin/release-prepare`) for deterministic version/changelog updates.
|
|
87
|
+
- Release artifact helper added (`bin/release-artifacts`) to build source gem artifacts with SHA256 checksums.
|
|
88
|
+
- Precompiled platform artifact helper added (`bin/release-precompiled-artifacts`) to build + validate native gems.
|
|
89
|
+
- Tag-driven release workflow added (`.github/workflows/release.yml`) with dry-run support and environment-gated publish jobs.
|
|
90
|
+
- RubyGems credential preflight helper added (`bin/verify-rubygems-api-key`) for CI-safe publish key validation.
|
|
91
|
+
- Post-publish verification helper added (`bin/verify-release-publish`) to validate RubyGems + GitHub release artifacts by tag.
|
|
92
|
+
- CI precompiled guardrail job added (`precompiled-gem-build`) for full release-blocking platform packaging checks (Linux, Linux musl, macOS Intel, macOS Apple Silicon, Windows).
|
|
93
|
+
- macOS precompiled CI/release lanes now pin Homebrew `llvm@18` (with fallback to `llvm`) and export `LIBCLANG_PATH` from the selected prefix to avoid bindgen breakage from Homebrew LLVM drift.
|
|
94
|
+
- Release workflow post-publish verification job added (`verify_published_artifacts`).
|
|
95
|
+
- Release failure alert workflow added (`.github/workflows/release-failure-alert.yml`) to open issue alerts for failed tag-triggered `release.yml` runs and auto-close matching alerts when reruns succeed.
|
|
96
|
+
- Maintainer release runbook added (`docs/release-runbook.md`) with publish and rollback/yank procedures.
|
|
97
|
+
- Precompiled platform support policy added (`docs/precompiled-platform-policy.md`).
|
|
98
|
+
|
|
99
|
+
For an up-to-date resume snapshot (phase status + exact remaining queue), see `docs/modernization-handoff.md`.
|
|
100
|
+
|
|
101
|
+
### Phase 1: Stabilize API and tests
|
|
102
|
+
|
|
103
|
+
- Capture current behavior with golden tests around:
|
|
104
|
+
- corpus loading
|
|
105
|
+
- EM convergence hooks
|
|
106
|
+
- topic-word output format
|
|
107
|
+
- `top_words`, `top_word_indices`, and `phi` shape
|
|
108
|
+
- Add CI matrix for Ruby 3.2, 3.3, and latest.
|
|
109
|
+
|
|
110
|
+
### Phase 2: Extract backend boundary
|
|
111
|
+
|
|
112
|
+
- Introduce backend interface in Ruby.
|
|
113
|
+
- Keep all existing high-level classes and output methods.
|
|
114
|
+
- Route existing calls through one backend object.
|
|
115
|
+
|
|
116
|
+
### Phase 3: Add pure Ruby reference backend
|
|
117
|
+
|
|
118
|
+
- Implement a simple, correct (not necessarily fast) Gibbs or variational inference path.
|
|
119
|
+
- Use this backend in tests as the compatibility baseline.
|
|
120
|
+
|
|
121
|
+
### Phase 4: Add Rust native backend
|
|
122
|
+
|
|
123
|
+
- Implement performance-critical loops in Rust.
|
|
124
|
+
- Expose only minimal Ruby-facing methods.
|
|
125
|
+
- Verify parity against pure Ruby backend on deterministic fixtures.
|
|
126
|
+
|
|
127
|
+
### Phase 5: Packaging and release
|
|
128
|
+
|
|
129
|
+
- Phase 5A (source-gem release automation): complete.
|
|
130
|
+
- Keep source build path available.
|
|
131
|
+
- Phase 5B (precompiled/native gem publishing): complete for Linux/macOS/Windows/musl release matrix targets via `bin/release-precompiled-artifacts` and release workflow matrix builds.
|
|
132
|
+
|
|
133
|
+
## Tooling suggestions
|
|
134
|
+
|
|
135
|
+
- Ruby test framework: Minitest (already present).
|
|
136
|
+
- Native extension: `magnus` + `rb_sys`.
|
|
137
|
+
- Performance checks: benchmark script comparing legacy behavior vs pure Ruby vs Rust.
|
|
138
|
+
- CI: GitHub Actions with matrix (`ubuntu`, `macos`) and Ruby 3.2/3.3/latest.
|
|
139
|
+
|
|
140
|
+
## What not to do first
|
|
141
|
+
|
|
142
|
+
- Do not start by rewriting all algorithms at once.
|
|
143
|
+
- Do not couple file parsing and inference internals.
|
|
144
|
+
- Do not rely on old Ruby C API macros that changed across versions.
|
|
145
|
+
|
|
146
|
+
## Decision summary
|
|
147
|
+
|
|
148
|
+
If the goal is a future-proof gem with acceptable speed and much lower maintenance pain, **use a hybrid model (Rust native backend + pure Ruby fallback)** instead of a full pure-Ruby rewrite or another large handwritten C extension.
|