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.
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.