hegeltest 0.1.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +264 -0
  6. data/Rakefile +19 -0
  7. data/docs/README.md +25 -0
  8. data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
  9. data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
  10. data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
  11. data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
  12. data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
  13. data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
  14. data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
  15. data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
  16. data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
  17. data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
  18. data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
  19. data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
  20. data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
  21. data/docs/architecture.md +182 -0
  22. data/lib/hegel/draw_name.rb +109 -0
  23. data/lib/hegel/errors.rb +47 -0
  24. data/lib/hegel/generator.rb +98 -0
  25. data/lib/hegel/generators.rb +865 -0
  26. data/lib/hegel/lib_hegel/real.rb +1149 -0
  27. data/lib/hegel/lib_hegel.rb +269 -0
  28. data/lib/hegel/libhegel_version.rb +9 -0
  29. data/lib/hegel/locate.rb +188 -0
  30. data/lib/hegel/report.rb +87 -0
  31. data/lib/hegel/runner.rb +464 -0
  32. data/lib/hegel/settings.rb +164 -0
  33. data/lib/hegel/state_machine.rb +89 -0
  34. data/lib/hegel/stateful/pool.rb +111 -0
  35. data/lib/hegel/stateful.rb +120 -0
  36. data/lib/hegel/syntax/methods.rb +173 -0
  37. data/lib/hegel/test_case.rb +523 -0
  38. data/lib/hegel/version.rb +5 -0
  39. data/lib/hegel.rb +92 -0
  40. data/lib/hegeltest.rb +7 -0
  41. data/lib/tasks/libhegel.rake +112 -0
  42. data/lib/tasks/platform_gems.rake +111 -0
  43. data/sig/hegel.rbs +563 -0
  44. data/skills/hegel-ruby/SKILL.md +30 -0
  45. data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
  46. metadata +113 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: facbe6e366b74782ab68e979295856ef15c8da56ea9d9fe9291764cbf1629eb3
4
+ data.tar.gz: e78e0364232dc02974f7ca81135a34325a76287c425d59f1d384ef2f2acfb2aa
5
+ SHA512:
6
+ metadata.gz: c977d4fd78f3439b18b731ae14c6772968aff53271fe1f44a8712f044a6dc0bcdcbd0ece18da677a7e3599a831651806a979c6985d5b942e6d89534c5c6eae14
7
+ data.tar.gz: 429fb8e7ce09f224c32368ad0d34bc69d1020f56891bdb2616da02acb86fa0ef5c174f340f27fc5a0cdf4918e8a4f82075781128850656b571e0b4232143abf5
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-08-20
4
+
5
+ The first release.
6
+
7
+ `Hegel.test` runs a property against libhegel, shrinks a failure to its
8
+ smallest counterexample, reports the values it drew, and re-raises the
9
+ exception the test body itself raised.
10
+
11
+ Twenty-five generators, composing through `map` and `filter`. A test case can
12
+ discard itself with `assume` and `reject`, leave messages with `note`, and
13
+ steer generation with `target`. A run can be shaped with `phases`,
14
+ `suppress_health_check`, `report_multiple_failures`, `stateful_step_count`,
15
+ and libhegel's example database. Stateful testing runs a `Hegel::StateMachine`
16
+ and shrinks a failing sequence of rules.
17
+
18
+ The engine is called through the `ffi` gem. Each platform gem carries the
19
+ matching `libhegel` 0.32.5 build. The platform-independent gem carries none,
20
+ so a run on it needs `HEGEL_LIBHEGEL_PATH` pointing at a local build.
21
+
22
+ [0.1.0]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.1.0
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "hegeltest" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at [meganemura@users.noreply.github.com](mailto:meganemura@users.noreply.github.com).
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 meganemura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # Hegel for Ruby
2
+
3
+ > [!IMPORTANT]
4
+ > **This is an unofficial, third-party implementation.** The Hegel project and
5
+ > the `libhegel` engine belong to Antithesis, LLC. This repository has no
6
+ > affiliation with Antithesis or with the `hegeldev` organization, and nobody
7
+ > there reviews or endorses it.
8
+ >
9
+ > The official implementations are
10
+ > [Rust](https://github.com/hegeldev/hegel-rust),
11
+ > [Go](https://github.com/hegeldev/hegel-go),
12
+ > [C++](https://github.com/hegeldev/hegel-cpp),
13
+ > [TypeScript](https://github.com/hegeldev/hegel-typescript),
14
+ > [Java](https://github.com/hegeldev/hegel-java), and
15
+ > [OCaml](https://github.com/hegeldev/hegel-ocaml). Report problems with this
16
+ > gem here, not to them.
17
+
18
+ > [!NOTE]
19
+ > Hegel itself is in beta, and its maintainers expect to make breaking changes.
20
+ > See <https://hegel.dev/compatibility>.
21
+
22
+ Hegel is a property-based testing framework based on
23
+ [Hypothesis](https://github.com/hypothesisworks/hypothesis). Instead of writing
24
+ tests with hand-picked inputs, you state a property that must hold for every
25
+ input. Hegel generates inputs, tries to falsify the property, and shrinks any
26
+ failure to a minimal counterexample.
27
+
28
+ This gem drives [libhegel](https://hegel.dev/reference/libhegel), the native
29
+ engine that the official implementations also drive. The engine runs in the
30
+ same process. There is no server and no Python dependency.
31
+
32
+ ## Status
33
+
34
+ **The first release is prepared, and the version is `0.1.0`.** Everything
35
+ described below runs: Hegel finds a counterexample, shrinks it,
36
+ names the values it drew, and re-raises your own exception.
37
+
38
+ Work ran in three stages, and all three are done:
39
+
40
+ 1. **The walking skeleton**: the libhegel binding, the run loop, and failure
41
+ reports.
42
+ 2. **The full generator set**: twenty-five generators, composing through
43
+ `map` and `filter`.
44
+ 3. **The advanced features**: the example database, targeted testing,
45
+ stateful testing, phases, and health checks.
46
+
47
+ **Nothing is published to RubyGems yet.** The packaging is built:
48
+ `rake platform_gems:build` produces one gem per platform, each carrying the
49
+ matching `libhegel` build. The arm64 macOS gem is verified to find that
50
+ engine inside itself and run a property with no `HEGEL_LIBHEGEL_PATH` set.
51
+ A `v*` tag publishes all six gems from CI. The Hegel maintainers permit a
52
+ third-party project to redistribute their release binaries
53
+ ([hegeldev/hegel-rust#411]).
54
+
55
+ So running this today means cloning the repository:
56
+
57
+ ```bash
58
+ bin/setup
59
+ bundle exec rake libhegel:fetch # downloads the pinned build, checked against its SHA-256
60
+ bundle exec rake
61
+ ```
62
+
63
+ Every push to main runs the suite on Ruby 3.3, 3.4, and 4.0, across Linux x64
64
+ and arm64, macOS arm64, and Windows x64 and arm64. Windows arm64 starts at
65
+ Ruby 3.4, the oldest Ruby that RubyInstaller publishes an arm64 build for. A
66
+ pull request runs Linux x64 alone.
67
+
68
+ [hegeldev/hegel-rust#411]: https://github.com/hegeldev/hegel-rust/issues/411
69
+
70
+ ## Design
71
+
72
+ | Decision | Choice |
73
+ | --- | --- |
74
+ | Gem name | `hegeltest` |
75
+ | Require path | `require "hegel"` (`require "hegeltest"` also works) |
76
+ | Namespace | `Hegel` |
77
+ | Binding | the `ffi` gem, which publishes a prebuilt binary for every platform above |
78
+ | Ruby | 3.3, 3.4, and 4.0 |
79
+ | Platforms | Linux amd64/arm64, macOS arm64, Windows amd64/arm64 |
80
+ | Engine delivery | One prebuilt `libhegel` per platform-specific gem, built by `rake platform_gems:build` |
81
+
82
+ `HEGEL_LIBHEGEL_PATH` will override the bundled engine with a local build.
83
+
84
+ The gem name follows the Rust implementation, whose published crate is
85
+ `hegeltest` and whose library is `hegel`. The name `hegel` on RubyGems.org
86
+ stays free for whoever publishes an official Ruby implementation.
87
+
88
+ macOS on Intel has no published `libhegel` artifact, so that platform will need
89
+ `HEGEL_LIBHEGEL_PATH` and a local build.
90
+
91
+ ## Quickstart
92
+
93
+ Everything below runs today.
94
+
95
+ ```ruby
96
+ # spec/spec_helper.rb
97
+ require "hegel"
98
+
99
+ RSpec.configure do |config|
100
+ config.include Hegel::Syntax::Methods
101
+ end
102
+ ```
103
+
104
+ ```ruby
105
+ # spec/my_sort_spec.rb
106
+ def my_sort(ls) = ls.sort.uniq # oops: uniq drops duplicates
107
+
108
+ RSpec.describe "my_sort" do
109
+ it "matches the builtin sort" do
110
+ Hegel.test do |tc|
111
+ xs = tc.draw(arrays(integers))
112
+ expect(my_sort(xs)).to eq(xs.sort)
113
+ end
114
+ end
115
+ end
116
+ ```
117
+
118
+ That test fails, and Hegel shrinks the failure to the smallest input that
119
+ still shows the bug:
120
+
121
+ ```
122
+ Falsified after 11 test cases (0 discarded):
123
+
124
+ xs = [0, 0]
125
+
126
+ To reproduce this failure, pass the blob below to Hegel.test:
127
+ reproduce_failure: "AXicY2VgYGBkZOBiZEBhMAAAAd8AIQ=="
128
+ ```
129
+
130
+ Two duplicates are all it takes. Hegel names the value `xs` by reading the
131
+ line the draw was written on, then re-raises RSpec's own expectation failure,
132
+ so the framework reports it as its own.
133
+
134
+ Including `Hegel::Syntax::Methods` makes the generators available without a
135
+ prefix, the way FactoryBot makes `create` available. The same generators stay
136
+ reachable as `Hegel::Generators.arrays(...)` without the include.
137
+
138
+ The generators are `arrays`, `binary`, `booleans`, `characters`, `composite`,
139
+ `dates`, `datetimes`, `deferred`, `domains`, `emails`, `floats`, `from_regex`,
140
+ `hashes`, `integers`, `ip_addresses`, `just`, `one_of`, `optional`,
141
+ `sampled_from`, `sets`, `text`, `times`, `tuples`, `urls`, and `uuids`. Each
142
+ one composes through `map` and `filter`.
143
+
144
+ ### Minitest
145
+
146
+ Hegel needs nothing from your test framework. Install the generator methods
147
+ once, and write the property inside an ordinary test:
148
+
149
+ ```ruby
150
+ # test/test_helper.rb
151
+ require "hegel"
152
+
153
+ class Minitest::Test
154
+ include Hegel::Syntax::Methods
155
+ end
156
+ ```
157
+
158
+ ```ruby
159
+ # test/my_sort_test.rb
160
+ class MySortTest < Minitest::Test
161
+ def test_matches_the_builtin_sort
162
+ Hegel.test do |tc|
163
+ xs = tc.draw(arrays(integers))
164
+ assert_equal xs.sort, my_sort(xs)
165
+ end
166
+ end
167
+ end
168
+ ```
169
+
170
+ Minitest reports the shrunk case as its own assertion failure, pointing at
171
+ your line:
172
+
173
+ ```
174
+ 1) Failure:
175
+ MySortTest#test_matches_the_builtin_sort [test/my_sort_test.rb:5]:
176
+ Expected: [0, 0]
177
+ Actual: [0]
178
+ ```
179
+
180
+ ### Stateful testing
181
+
182
+ Some bugs need a sequence of operations rather than one input. Declare the
183
+ operations as rules on a `Hegel::StateMachine`, and Hegel picks which runs
184
+ next, checks your invariants after each one, and shrinks a failure to the
185
+ shortest sequence that still shows it:
186
+
187
+ ```ruby
188
+ class StackMachine < Hegel::StateMachine
189
+ def initialize
190
+ @stack = BoundedStack.new(3)
191
+ @model = []
192
+ end
193
+
194
+ rule :push do |tc|
195
+ x = tc.draw(integers(min_value: 0, max_value: 9))
196
+ @stack.push(x)
197
+ @model.push(x) if @model.size < 3
198
+ end
199
+
200
+ rule :pop do |tc|
201
+ tc.assume(!@model.empty?)
202
+ raise "pop disagreed" unless @stack.pop == @model.pop
203
+ end
204
+
205
+ invariant :size_agrees do
206
+ raise "size disagreed" unless @stack.size == @model.size
207
+ end
208
+ end
209
+
210
+ Hegel.test { |tc| Hegel::Stateful.run(StackMachine.new, tc) }
211
+ ```
212
+
213
+ `tc.assume` inside a rule rejects that rule and lets Hegel choose another,
214
+ rather than throwing the whole test case away. For a rule that has to act on
215
+ something an earlier rule produced, such as freeing a handle that some
216
+ `alloc` actually returned, put the value in a `Hegel::Stateful::Pool` and
217
+ draw it back out.
218
+
219
+ ### Shaping a run
220
+
221
+ `Hegel.test` takes keywords for the rest: `test_cases`, `seed`,
222
+ `derandomize`, `verbosity`, `phases`, `suppress_health_check`,
223
+ `report_multiple_failures`, and `stateful_step_count`. Each one left unset
224
+ means the engine's own default.
225
+
226
+ Two more turn on libhegel's example database, which stores a failing case and
227
+ replays it first next time. `database_key` is the switch and `database`
228
+ chooses the directory:
229
+
230
+ ```ruby
231
+ Hegel.test(database_key: "my_sort matches the builtin sort") { |tc| ... }
232
+ ```
233
+
234
+ Give each property its own key. See
235
+ [ADR 0009](docs/adr/0009-turn-the-example-database-on-with-a-key.md) for why
236
+ the key, rather than the directory, is what turns it on.
237
+
238
+ Inside a test, `tc.note` records a message the failure report prints, and
239
+ `tc.target` tells Hegel which inputs to search toward.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ bin/setup # install dependencies
245
+ bundle exec rake # run the tests and the linter
246
+ bin/console # open an interactive prompt
247
+ ```
248
+
249
+ `just` recipes call the same Rake tasks, so `just test` and `just lint` work
250
+ for anyone who already uses `just` with the other Hegel implementations.
251
+
252
+ ## Contributing
253
+
254
+ Report bugs and open pull requests at
255
+ <https://github.com/meganemura/hegel-ruby>. Contributors follow the
256
+ [code of conduct](CODE_OF_CONDUCT.md).
257
+
258
+ ## License
259
+
260
+ This gem is available under the [MIT License](LICENSE.txt).
261
+
262
+ Released gems also carry `libhegel`'s own license, which is MIT and copyright
263
+ Antithesis, LLC. Each platform gem ships `NOTICE-libhegel.txt` next to the
264
+ engine, and that file holds the text.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ load File.expand_path("lib/tasks/libhegel.rake", __dir__)
9
+ load File.expand_path("lib/tasks/platform_gems.rake", __dir__)
10
+
11
+ require "standard/rake"
12
+
13
+ desc "Run the test suite with coverage measurement enforced at 100%"
14
+ task :coverage do
15
+ ENV["COVERAGE"] = "1"
16
+ Rake::Task["test"].invoke
17
+ end
18
+
19
+ task default: %i[coverage standard]
data/docs/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Documentation
2
+
3
+ An index of the design records for `hegel-ruby`.
4
+
5
+ - [architecture.md](architecture.md): the layers between Ruby and
6
+ libhegel, and the boundary each one draws.
7
+
8
+ ## Architecture Decision Records
9
+
10
+ - [0001: Bind libhegel through Fiddle](adr/0001-bind-libhegel-through-fiddle.md)
11
+ - [0002: Ship one prebuilt engine per platform-specific gem](adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md)
12
+ - [0003: Publish as hegeltest, require as hegel](adr/0003-publish-as-hegeltest-require-as-hegel.md)
13
+ - [0004: Expose generators through a mixin, with keyword options](adr/0004-expose-generators-through-a-mixin-with-keyword-options.md)
14
+ - [0005: Name drawn values from the caller's source with Prism](adr/0005-name-drawn-values-from-the-callers-source-with-prism.md)
15
+ - [0006: Verify the binding in seven layers, with full coverage](adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md)
16
+ - [0007: Ship a thin Ruby skill, shaped for donation](adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md)
17
+ - [0008: Revisit the binding after milestone C, on measurement](adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md)
18
+ - [0009: Turn the example database on with a key](adr/0009-turn-the-example-database-on-with-a-key.md)
19
+ - [0010: Declare stateful rules with a class macro](adr/0010-declare-stateful-rules-with-a-class-macro.md)
20
+ - [0011: Let the test case own every pool drawn from it](adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md)
21
+ - [0012: Build a failure origin from the caller's own frame](adr/0012-build-a-failure-origin-from-the-callers-own-frame.md)
22
+ - [0013: Bind libhegel through the ffi gem](adr/0013-bind-libhegel-through-the-ffi-gem.md)
23
+
24
+ A new decision gets a new record. A changed decision supersedes the old
25
+ record instead of editing it, so the history stays readable.
@@ -0,0 +1,54 @@
1
+ # 0001: Bind libhegel through Fiddle
2
+
3
+ ## Status
4
+
5
+ Superseded by [ADR 0013](0013-bind-libhegel-through-the-ffi-gem.md),
6
+ which binds libhegel through the `ffi` gem instead, on the measurement
7
+ [ADR 0008](0008-revisit-the-binding-after-milestone-c-on-measurement.md)
8
+ scheduled.
9
+
10
+ ## Context
11
+
12
+ libhegel is hegel-rust's native engine, exposed as a C ABI in
13
+ `hegel-c/include/hegel.h`. It runs in the same process as its caller.
14
+ Nearly every function takes a `hegel_context_t*` as its first argument and
15
+ returns a `hegel_result_t` status code.
16
+
17
+ CRuby needs a way to call that ABI. A C extension can call it directly, but
18
+ building one needs a compiler at gem install time. Fiddle is a library
19
+ bundled with Ruby. It wraps libffi and `dlopen`, so it calls a C function
20
+ by address without compiling anything.
21
+
22
+ The other implementations bind the same ABI under different constraints.
23
+ The Java implementation drives it through the Foreign Function and Memory
24
+ API. It caches one `MethodHandle` per C symbol, behind a `Libhegel`
25
+ interface; `RealLibhegel` implements that interface in production, and a
26
+ fake implements it in tests. The Go implementation opens the library with
27
+ `dlopen` directly, giving each C symbol a Go func-typed struct field. The
28
+ OCaml implementation binds through `ctypes`, declaring each function with
29
+ `Foreign.foreign` over a handle from `Dl.dlopen`. The C++ implementation
30
+ links the library at compile time and calls its functions directly, with
31
+ no runtime loading step.
32
+
33
+ ## Decision
34
+
35
+ Bind libhegel through Fiddle (`~> 1.1`). Confine every raw Fiddle call to
36
+ one module, `Hegel::LibHegel`. The rest of the library will call that
37
+ module's wrappers. This matches the way the Java implementation's
38
+ `Libhegel` interface sits between its runner and the raw FFI calls.
39
+
40
+ ## Consequences
41
+
42
+ Installing the gem needs no compiler and no link step against `hegel.h`.
43
+ The Go binding shares this property: it opens the library at run time
44
+ instead of linking against it.
45
+
46
+ `Hegel::LibHegel` needs a substitute implementation for testing error-code
47
+ translation without a real engine loaded. The Java implementation's fake
48
+ binding plays that role in its own test suite.
49
+
50
+ The gem can use only the Fiddle API that ships in Ruby 3.3's bundled
51
+ fiddle, 1.1.2, since that Ruby is the oldest one supported.
52
+
53
+ Whether `Fiddle::Closure` behaves the same from fiddle 1.1.2 through 1.1.8
54
+ has not been checked yet.
@@ -0,0 +1,48 @@
1
+ # 0002: Ship one prebuilt engine per platform-specific gem
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ hegel-rust publishes libhegel as GitHub release assets. Release v0.32.5
10
+ carries ten assets: one shared library and one sha256 checksum, for each of
11
+ five platforms (`darwin-arm64`, `linux-amd64`, `linux-arm64`,
12
+ `windows-amd64`, `windows-arm64`).
13
+
14
+ The other implementations distribute these prebuilt libraries in different
15
+ shapes. The TypeScript implementation publishes one npm package per
16
+ platform, such as `@hegeldev/hegel-linux-x64`, listed as an
17
+ `optionalDependencies` entry so npm installs only the matching one. The Go
18
+ implementation embeds all five binaries into one module, behind
19
+ per-platform `go:embed` build tags. A build for one target links in only
20
+ its own binary. The OCaml implementation's loader can also download the
21
+ matching binary at install or first run, through a `dune` site that a
22
+ release tarball prefills.
23
+
24
+ RubyGems can build one physical gem per `platform` value declared in a
25
+ gemspec. Bundler and RubyGems then install and require the gem matching the
26
+ local platform automatically.
27
+
28
+ ## Decision
29
+
30
+ Publish `hegeltest` as five platform-specific gems, one prebuilt libhegel
31
+ bundled in each, following the model the TypeScript implementation's
32
+ per-platform npm packages set.
33
+
34
+ libhegel resolves in two steps: `HEGEL_LIBHEGEL_PATH`, naming a file or a
35
+ directory that contains one, checked first; the platform gem's bundled
36
+ copy otherwise. The Go, TypeScript, Java, and OCaml implementations all
37
+ check `HEGEL_LIBHEGEL_PATH` before falling back to their own bundled or
38
+ downloaded engine.
39
+
40
+ ## Consequences
41
+
42
+ No Intel-Mac gem can ship, since v0.32.5's release assets cover five
43
+ platforms and Intel Mac is not one of them. That platform needs
44
+ `HEGEL_LIBHEGEL_PATH` and a local build.
45
+
46
+ Only the resolution step, `HEGEL_LIBHEGEL_PATH` then the bundled copy,
47
+ depends on how libhegel reaches the gem. The rest of the library calls
48
+ `Hegel::LibHegel` the same way, regardless of where the file came from.
@@ -0,0 +1,39 @@
1
+ # 0003: Publish as hegeltest, require as hegel
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ hegel-rust's `Cargo.toml` names the package `hegeltest` (`[package] name =
10
+ "hegeltest"`) and the library `hegel` (`[lib] name = "hegel"`). `cargo add
11
+ --dev hegeltest` installs it; source calls `use hegel::...`.
12
+
13
+ The Go module is `hegel.dev/go/hegel`, imported as package `hegel`. The
14
+ npm package is `@hegeldev/hegel`. The Java artifact is `dev.hegel:hegel`.
15
+ The OCaml package is `hegel`. In each of these four, the published name
16
+ and the code's own name match.
17
+
18
+ As of 2026-08, neither `hegel` nor `hegeltest` is registered on
19
+ RubyGems.org.
20
+
21
+ ## Decision
22
+
23
+ Publish the gem as `hegeltest`, matching hegel-rust's crate name. Keep the
24
+ require path and the namespace `hegel` and `Hegel`, matching hegel-rust's
25
+ library name. `lib/hegeltest.rb` exists only to satisfy Bundler's automatic
26
+ require, which is derived from the gem name; it requires `hegel.rb` and
27
+ defines nothing of its own.
28
+
29
+ ## Consequences
30
+
31
+ `require "hegel"` is the documented form. `require "hegeltest"` also
32
+ works, because Bundler requires a gem automatically under its own name
33
+ when the gem is listed in a Gemfile.
34
+
35
+ The name `hegel` stays free on RubyGems.org for whoever publishes an
36
+ official Ruby implementation.
37
+
38
+ A reader who already knows the Rust implementation's naming pattern can
39
+ guess this gem's install name and require path correctly.
@@ -0,0 +1,42 @@
1
+ # 0004: Expose generators through a mixin, with keyword options
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ FactoryBot, a widely used Ruby test-data library, defines a module,
10
+ `FactoryBot::Syntax::Methods`, holding methods such as `build` and
11
+ `create`. Including it in a test makes those methods callable bare, with
12
+ no `FactoryBot.` prefix. The same methods stay reachable through the
13
+ module's own namespace for a caller who does not include it.
14
+
15
+ hegel-rust and the Go implementation expose generators as builder-chain
16
+ calls: `gs::integers::<i32>()` in Rust and `hegel.Integers[int](min, max)`
17
+ in Go. Each is followed by chained methods, such as `min_size`, for
18
+ further options. Neither Rust nor Go has native optional keyword
19
+ arguments; Ruby does.
20
+
21
+ ## Decision
22
+
23
+ Define `Hegel::Syntax::Methods`, a module of generator-constructing
24
+ methods (`integers`, `text`, `arrays`, and so on), matching
25
+ `FactoryBot::Syntax::Methods` in shape. A caller includes it, in an RSpec
26
+ `config.include` or a Minitest test class, to call `integers` and `text`
27
+ bare. The same generators stay reachable as `Hegel::Generators.integers`
28
+ without the include.
29
+
30
+ Generator options are keyword arguments, not a builder chain:
31
+ `text(min_size: 1, alphabet: "abc")`.
32
+
33
+ ## Consequences
34
+
35
+ A caller who already uses FactoryBot recognizes the include-a-methods-
36
+ module pattern.
37
+
38
+ A generator with several options reads as one call, such as
39
+ `text(min_size: 1, max_size: 20)`, instead of a chain of setter calls.
40
+
41
+ `Hegel::Syntax::Methods`, `Hegel::Generators`, and every generator remain
42
+ to be written; only the naming decision is settled.
@@ -0,0 +1,40 @@
1
+ # 0005: Name drawn values from the caller's source with Prism
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ A failure report is more useful when each drawn value has a name, instead
10
+ of a position number. hegel-rust's `#[hegel::test]` macro rewrites, at
11
+ compile time, `let x = tc.draw(gen)` inside a test function into `let x =
12
+ tc.__draw_named(gen, "x", repeatable)`, so the failure report can print
13
+ `let x = <value>;`. The Go implementation resolves the caller's source
14
+ position at runtime with `runtime.Caller`, then parses that file with
15
+ `go/ast` to recover the enclosing statement's text.
16
+
17
+ Ruby has no macros. Prism, Ruby's parser, ships as a default gem starting
18
+ with Ruby 3.3, and can parse a source file into a node tree with source
19
+ locations. Parsing `n = tc.draw(gs.integers)` with Prism yields a
20
+ `Prism::LocalVariableWriteNode` whose `name` is `:n`.
21
+
22
+ ## Decision
23
+
24
+ Recover a drawn value's name at runtime, by parsing the caller's source
25
+ file with Prism and reading the assignment target at the `draw` call's
26
+ location. This mirrors the Go implementation's runtime-parse approach, in
27
+ a language without Rust's compile-time macros.
28
+
29
+ A `label:` argument to `draw` overrides the recovered name. When no name
30
+ can be recovered, the value gets a number instead.
31
+
32
+ ## Consequences
33
+
34
+ Ruby 3.3, the oldest supported Ruby, is also the oldest Ruby that ships
35
+ Prism by default. The two floors were chosen together.
36
+
37
+ No `draw` call, name recovery, or failure report exists in code yet.
38
+
39
+ How far this recovery reaches into a heredoc, several `draw` calls on one
40
+ line, or a method chain has not been checked.
@@ -0,0 +1,51 @@
1
+ # 0006: Verify the binding in seven layers, with full coverage
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ hegel-rust's own `tests/` directory separates concerns.
10
+ `tests/test_shrink_quality/` asserts an exact minimal counterexample for a
11
+ given predicate; for example, shrinking a byte pair under a length
12
+ constraint to `vec![0u8; 7]` and `vec![0u8; 8]`. This kind of assertion
13
+ depends on how the engine's shrinker responds to span structure. A missing
14
+ or misplaced span shows up there, as a counterexample larger than the
15
+ minimal one. hegel-rust's own contributor documentation states that every
16
+ invalid combination of builder values must be caught at draw time. It
17
+ also states that a validation message is public API, asserted against by
18
+ tests as a stable substring.
19
+
20
+ The Java implementation enforces 100% instruction and branch coverage
21
+ through its `jacoco-maven-plugin` configuration, `<minimum>1.00</minimum>`
22
+ for both counters. Its `Libhegel` interface exists so the run loop can be
23
+ tested against `FakeLibhegel`, without a real engine, exercising every
24
+ error code libhegel can return.
25
+
26
+ ## Decision
27
+
28
+ Verify the binding in seven layers:
29
+
30
+ 1. FFI, against a fake engine.
31
+ 2. Library resolution.
32
+ 3. Conformance, running every generator against the real engine.
33
+ 4. Shrink quality, ported from hegel-rust's `tests/test_shrink_quality`
34
+ result assertions.
35
+ 5. Generation quality, catching option-translation mistakes.
36
+ 6. Failure reports: Prism naming, blob replay, output format.
37
+ 7. Integration, running the Minitest and RSpec suites end to end.
38
+
39
+ Enforce 100% line and branch coverage in CI. Ruby's coverage tooling
40
+ counts lines where jacoco counts instructions, so the line counter is
41
+ this project's nearest equivalent to the instruction bar quoted above.
42
+ An excluded file carries a comment stating why.
43
+
44
+ ## Consequences
45
+
46
+ A missing or misplaced span costs nothing at generation time. It shows up
47
+ as a larger-than-minimal counterexample, the property the shrink-quality
48
+ layer asserts against.
49
+
50
+ Layer two, library resolution, is written and covered. The other six
51
+ follow the code they test, so they arrive with it.