judge_rails 0.0.1
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 +7 -0
- data/ADVANCED.md +136 -0
- data/BENCHMARK.md +885 -0
- data/CHANGELOG.md +163 -0
- data/LICENSE.txt +21 -0
- data/README.md +588 -0
- data/lib/generators/judge/attribute_generator.rb +149 -0
- data/lib/generators/judge/install_generator.rb +36 -0
- data/lib/generators/judge/templates/initializer.rb.tt +18 -0
- data/lib/generators/judge/templates/migration.rb.tt +9 -0
- data/lib/judge/adapter.rb +69 -0
- data/lib/judge/client.rb +253 -0
- data/lib/judge/configuration.rb +103 -0
- data/lib/judge/decision.rb +24 -0
- data/lib/judge/errors.rb +36 -0
- data/lib/judge/facade.rb +75 -0
- data/lib/judge/pool.rb +97 -0
- data/lib/judge/question/choice.rb +36 -0
- data/lib/judge/question/noul.rb +22 -0
- data/lib/judge/question/score.rb +36 -0
- data/lib/judge/question.rb +93 -0
- data/lib/judge/rails/attributes.rb +161 -0
- data/lib/judge/rails/definition.rb +156 -0
- data/lib/judge/rails/jobs.rb +156 -0
- data/lib/judge/rails/locale/en.yml +6 -0
- data/lib/judge/rails/migration.rb +115 -0
- data/lib/judge/rails/refresh.rb +37 -0
- data/lib/judge/rails/refresh_job.rb +15 -0
- data/lib/judge/rails/registry.rb +57 -0
- data/lib/judge/rails/relation.rb +132 -0
- data/lib/judge/rails/scopes.rb +124 -0
- data/lib/judge/rails/storage.rb +104 -0
- data/lib/judge/rails/validator.rb +246 -0
- data/lib/judge/rails.rb +45 -0
- data/lib/judge/result.rb +173 -0
- data/lib/judge/result_set.rb +75 -0
- data/lib/judge/version.rb +5 -0
- data/lib/judge.rb +41 -0
- data/lib/judge_rails.rb +3 -0
- metadata +126 -0
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1 (2026-09-25)
|
|
4
|
+
|
|
5
|
+
The first version under this name. It ships as 0.0.1 rather than 1.0.0 on purpose: the surface is
|
|
6
|
+
expected to move, and nothing here has a second user yet.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Structured criteria.** A Choice option or a Noul `true`/`false` entry can be a Hash or an Array,
|
|
11
|
+
in the `{ what:, not_for:, examples: }` form TypeSafe documents. Keys become strings and keep their
|
|
12
|
+
order, nested numbers and booleans keep their JSON type, nil entries are dropped, and two keys that
|
|
13
|
+
collide once stringified raise. String criteria serialise and fingerprint exactly as before, so no
|
|
14
|
+
existing judgment goes stale.
|
|
15
|
+
- **An adapter seam.** Everything now goes through one method, `call(state:, questions:, model:)
|
|
16
|
+
-> Judge::ResultSet`. An adapter builds its answers with `Judge::Result.from_values`, which takes
|
|
17
|
+
typed values, so no adapter reads or writes a wire format. `Judge::Adapter.register(:name) { ... }`
|
|
18
|
+
adds one, `Judge.config.adapter` or `JUDGE_ADAPTER` picks it, and `adapter:` overrides it per call.
|
|
19
|
+
The default stays `:jev`.
|
|
20
|
+
- `Judge::Result.from_values`, the typed entry point an adapter answers with.
|
|
21
|
+
- `Judge::Pool`, a standard-library thread pool. `judge_filter`, `judge_map` and `judge_sort` fan out across
|
|
22
|
+
records through it, default 8, set with `concurrency:` or `Judge.config.concurrency`. Measured
|
|
23
|
+
against the live API: 6.9x at 8 threads, 18.4x at 32, with the request unchanged and no judgment
|
|
24
|
+
traded for the speed.
|
|
25
|
+
- `request.judge` instrumentation through `ActiveSupport::Notifications` when it is loaded. Carries
|
|
26
|
+
model, question count, request size, latency and token counts. Never the state, never the key.
|
|
27
|
+
- `after_judge_refresh`, a model callback that runs once a refresh has stored new judgments.
|
|
28
|
+
- `option:` on `judge_filter` and `judge_sort` for a choice question, and `at_least:` on `judge_filter`
|
|
29
|
+
for a score question.
|
|
30
|
+
- `config.max_retry_wait` (10 s, `nil` for no cap), and an adapter object accepted directly by
|
|
31
|
+
`config.adapter`.
|
|
32
|
+
- `change_table` support for `t.judge_attribute`, and `--database` on `judge:attribute`.
|
|
33
|
+
- `ADVANCED.md` for `sync: true`, `on_error`, `if_condition`, `callbacks: false`, backfill and the
|
|
34
|
+
injectable enqueuer, so the README keeps one path.
|
|
35
|
+
- `activerecord` and `activesupport` declared as runtime dependencies. They were required by
|
|
36
|
+
`judge/rails` and declared nowhere, so bundler could not resolve them.
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
|
|
40
|
+
- `callbacks: :queue` and `Judge::Rails::Jobs.batch`. They coalesced job dispatch, never requests: one
|
|
41
|
+
demonstrated caller in the whole workspace, and it was a test. `judge_refresh_all` covers the same
|
|
42
|
+
ground with no setup.
|
|
43
|
+
- `Judge::Rails::BulkRefreshJob` and the `kind` field on the job payload, which only that mode reached.
|
|
44
|
+
- `config.batch_rows`, which nothing read.
|
|
45
|
+
- The GIN index on a PostgreSQL sidecar. Nothing queried inside it, and it tripled the cost of a
|
|
46
|
+
refresh write.
|
|
47
|
+
- `null: false` on the migration helper. A value column is empty until judged, so it now raises.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Renamed from `jev-in-rails` to `judge_rails`**, before publishing rather than after. The macro
|
|
52
|
+
prefix is `judge_`, the module is `Judge`, the sidecar column is `<name>_judge`, the generators are
|
|
53
|
+
`judge:install` and `judge:attribute`, and the validator key is `judge:`. TypeSafe Jev keeps its
|
|
54
|
+
name everywhere it is the subject: it is the default adapter, `:jev`, and the model string is still
|
|
55
|
+
`jev-latest`, because a gem named for one provider while carrying adapters for others is harder to
|
|
56
|
+
rename later than now.
|
|
57
|
+
- `client:` is `adapter:` everywhere, and `Judge.client` is `Judge.adapter`. One word for one concept.
|
|
58
|
+
- `required_ruby_version` is `>= 3.2.0`, which is what CI actually tests. It claimed 3.1 and never
|
|
59
|
+
ran it.
|
|
60
|
+
- `judge_refresh!` and `judge_refresh_all` store judgments with `update_columns`. Validations and save
|
|
61
|
+
callbacks no longer run, `updated_at` moves in the same statement so view caches see the change, and
|
|
62
|
+
other unsaved edits stay unsaved. A row deleted meanwhile raises `ActiveRecord::RecordNotFound`. A refresh
|
|
63
|
+
no longer re-bills judge validations, and a row that fails validation still gets its judgment.
|
|
64
|
+
- `judge_filter` and `judge_sort` with a choice or score question raise without a target, before any
|
|
65
|
+
call. They used to keep any row whose winning option was confident, whatever it was.
|
|
66
|
+
- Judge validation errors have types (`:judge_refuted`, `:judge_unmatched`, `:judge_unavailable`) and
|
|
67
|
+
I18n messages, honour `strict:` and `except_on:`, and evaluate `if:` lambdas the way Rails does.
|
|
68
|
+
- A read timeout is not re-sent by the client: the server may already have billed it.
|
|
69
|
+
`RefreshJob` still retries it.
|
|
70
|
+
- An async attribute with neither ActiveJob nor a custom enqueuer raises `Judge::ConfigurationError`
|
|
71
|
+
on the first save instead of skipping the judgment.
|
|
72
|
+
- `Result#true?` on a non-noul answer raises `ArgumentError`, a caller error, instead of
|
|
73
|
+
`InvalidResponseError`.
|
|
74
|
+
- Scopes are defined when the attribute is declared, not on first call.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- `gem "judge_rails"` now loads the gem. There was no `lib/judge_rails.rb`, so `Bundler.require` loaded
|
|
79
|
+
nothing and the generated initializer crashed on boot.
|
|
80
|
+
- `judge:attribute` no longer crashes on every run. Its `create_migration` step shadowed the method
|
|
81
|
+
`migration_template` calls.
|
|
82
|
+
- The generated initializer keeps a key already read from `JEV_API_KEY` or `TYPESAFE_API_KEY`.
|
|
83
|
+
- `judge_filter`, `judge_map` and `judge_sort` never raise a `limit` the relation already set, and skip
|
|
84
|
+
records with blank text.
|
|
85
|
+
- Judge validations remember a judgment per text on the record, so a save that leaves the text alone
|
|
86
|
+
costs nothing, and the prefetch runs after `before_validation` normalizers. A failed call is retried
|
|
87
|
+
on the next pass. A plain `ActiveModel` object can be validated.
|
|
88
|
+
- TLS and protocol failures are `Judge::TransportError`, so `on_error: :pass` covers them.
|
|
89
|
+
- A `Retry-After` above `config.max_retry_wait` (10 s, `nil` for no cap) raises `RateLimitError`
|
|
90
|
+
instead of sleeping.
|
|
91
|
+
- `RefreshJob` hands a 429, a 5xx, a transport error or a database deadlock back to ActiveJob, for up
|
|
92
|
+
to five attempts with backoff. It used to report success and leave the record unjudged. It is
|
|
93
|
+
defined once ActiveJob loads, so `queue_name_prefix` applies.
|
|
94
|
+
- `Judge::Pool` stops starting requests when the caller is interrupted and returns at once, closes
|
|
95
|
+
each worker's HTTP connections, returns any database connection a worker leased, and gives workers
|
|
96
|
+
the caller's log tags. Any exception, not only a `StandardError`, stops the queue.
|
|
97
|
+
- Blank source text is never sent. An automatic attribute clears on save, so it no longer enqueues a
|
|
98
|
+
job on every save.
|
|
99
|
+
- A zero-argument `if_condition` lambda runs against the record instead of raising.
|
|
100
|
+
- `model:` on `judge_attribute` is sent. A pin change makes old judgments stale, and so does changing
|
|
101
|
+
`config.model` for unpinned attributes.
|
|
102
|
+
- Changing `config.adapter` after the first call takes effect.
|
|
103
|
+
- A refresh never saves the record, so a source that changes on every save cannot feed a loop, and a
|
|
104
|
+
second save inside one transaction is still enqueued.
|
|
105
|
+
- A keep-alive connection opened before a fork is never reused by the child, which used to read
|
|
106
|
+
another process's answers.
|
|
107
|
+
- An answer missing its value, of the wrong type, or naming an option or level the question does not
|
|
108
|
+
have raises `InvalidResponseError` inside `Judge.ask`, so `on_error:` and `rescue Judge::Error`
|
|
109
|
+
cover it.
|
|
110
|
+
- An error body that is JSON but not an object (`null`, `[]`, `502`) maps to the right `APIError`, and a
|
|
111
|
+
413 is `PayloadTooLargeError`.
|
|
112
|
+
- `judge_refresh_all` judges each STI row with its own class's attributes.
|
|
113
|
+
- A refresh that fails part way stores the judgments it already paid for, never asks again for the
|
|
114
|
+
one that failed, and always raises the original error, even when storing fails too. A record never
|
|
115
|
+
saved is created from what was computed.
|
|
116
|
+
- An adapter factory may resolve another adapter without deadlocking.
|
|
117
|
+
- A hand-written scope keeps its name whether it comes from the model, a concern or a parent class.
|
|
118
|
+
- The generator never builds `judge_source` from string columns, so it cannot send a password hash or a
|
|
119
|
+
reset token; it lands in the right class of a file holding two, handles namespaced models, and
|
|
120
|
+
`destroy` removes the `judge_source` it added.
|
|
121
|
+
- Validation messages fall back to English in a locale that lacks them.
|
|
122
|
+
- A pool worker returns database connections from every pool, not only the primary one.
|
|
123
|
+
- A save that changes nothing relevant evaluates no async source after commit.
|
|
124
|
+
- `BENCHMARK.md`, which the README cites, ships with the gem.
|
|
125
|
+
- Saving a record loaded with a partial `select` skips the judgments it cannot read.
|
|
126
|
+
- A skipped judge validation no longer reads its attribute, `dup` gets its own judgment cache, and a
|
|
127
|
+
frozen form object can be validated.
|
|
128
|
+
- `inspect` on a configuration or a client hides the API key, and a key read with surrounding
|
|
129
|
+
whitespace is trimmed.
|
|
130
|
+
- On numeric score levels, an Integer passed to `_at_least`, `_at_most` or `_level` is the label.
|
|
131
|
+
- The sidecar migration works on MySQL, which rejects a default on a JSON column.
|
|
132
|
+
|
|
133
|
+
### Not added, on purpose
|
|
134
|
+
|
|
135
|
+
Subject batching, packing several records into one request the way `pg_judge` does. It was built,
|
|
136
|
+
measured against 250 committed judgments, and rejected: it costs 9 to 14 points of decision
|
|
137
|
+
agreement at every batch size, because Jev scores each question against the whole state.
|
|
138
|
+
`BENCHMARK.md` carries the protocol, which was written before the runs, and the numbers.
|
|
139
|
+
|
|
140
|
+
### What 0.0.1 contains
|
|
141
|
+
|
|
142
|
+
#### Core (plain Ruby, no dependencies outside the standard library)
|
|
143
|
+
|
|
144
|
+
- `Judge::Question::Noul`, `Choice` and `Score` value objects. Frozen, comparable, serialisable to the
|
|
145
|
+
wire format. Each fingerprints itself with a digest over its type, instructions and criteria.
|
|
146
|
+
- `Judge::Result` and `Judge::ResultSet`: typed answers carrying value, calibrated probability, confidence,
|
|
147
|
+
the full distribution, the score legend, model version, token usage and latency.
|
|
148
|
+
- `Judge::Client`: `Net::HTTP` with a per-fiber persistent connection, bearer auth, jittered exponential
|
|
149
|
+
backoff on 429, 5xx and connection failures, `Retry-After` support, and a full error taxonomy.
|
|
150
|
+
- `Judge.ask` facade: a Question, a String, an Array or a Hash in; a `Result` or a `ResultSet` out.
|
|
151
|
+
Many questions travel in one request.
|
|
152
|
+
|
|
153
|
+
#### ActiveRecord layer (`require "judge/rails"`)
|
|
154
|
+
|
|
155
|
+
- `judge_attribute` declares a judgment as an ordinary column plus a `<name>_judge` provenance sidecar.
|
|
156
|
+
- `judge_source` sets the text once per model, so every attribute sharing it costs one API call per record.
|
|
157
|
+
- Automatic invalidation on either the source text or the question wording changing.
|
|
158
|
+
- Compute timing: `sync: true` inline, `:async` after_commit (default), or `false`.
|
|
159
|
+
- `judge_refresh_all` resumable backfill with a per-run summary.
|
|
160
|
+
- Generated scopes per attribute, plus `judge_filter` / `judge_map` / `judge_sort` for undeclared questions,
|
|
161
|
+
with a mandatory `limit:`.
|
|
162
|
+
- `validates :body, judge: { refute: "..." }` with `on_error: :pass | :fail | :raise`.
|
|
163
|
+
- `judge:install` and `judge:attribute` generators, and a `judge_attribute` migration helper.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hugo V
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|