graph_weaver 0.5.0 → 0.6.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +537 -0
  3. data/Gemfile.lock +19 -19
  4. data/README.md +74 -53
  5. data/docs/cassettes.md +29 -4
  6. data/docs/editors.md +3 -1
  7. data/docs/errors.md +75 -16
  8. data/docs/federation.md +206 -155
  9. data/docs/generated_modules.md +223 -166
  10. data/docs/getting_started.md +106 -82
  11. data/docs/logging.md +35 -5
  12. data/docs/scalars.md +119 -24
  13. data/docs/testing.md +196 -155
  14. data/docs/transports.md +47 -19
  15. data/docs/upgrading.md +243 -22
  16. data/graph_weaver.gemspec +16 -2
  17. data/lib/generators/graph_weaver/install_generator.rb +31 -16
  18. data/lib/graph_weaver/client.rb +52 -15
  19. data/lib/graph_weaver/codegen/aliases.rb +15 -8
  20. data/lib/graph_weaver/codegen/emit.rb +107 -42
  21. data/lib/graph_weaver/codegen/enum_type.rb +4 -3
  22. data/lib/graph_weaver/codegen/nodes.rb +42 -21
  23. data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
  24. data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
  25. data/lib/graph_weaver/codegen.rb +382 -105
  26. data/lib/graph_weaver/coerce.rb +113 -0
  27. data/lib/graph_weaver/errors.rb +57 -13
  28. data/lib/graph_weaver/federation.rb +10 -22
  29. data/lib/graph_weaver/hints.rb +76 -2
  30. data/lib/graph_weaver/in_process.rb +11 -8
  31. data/lib/graph_weaver/inflect.rb +2 -0
  32. data/lib/graph_weaver/input_struct.rb +115 -12
  33. data/lib/graph_weaver/internal/overrides.rb +101 -0
  34. data/lib/graph_weaver/internal/planner.rb +868 -0
  35. data/lib/graph_weaver/internal/schemas.rb +50 -0
  36. data/lib/graph_weaver/internal/selection.rb +127 -0
  37. data/lib/graph_weaver/{testing → internal}/subgraphs.rb +45 -43
  38. data/lib/graph_weaver/internal/values.rb +181 -0
  39. data/lib/graph_weaver/internal.rb +206 -0
  40. data/lib/graph_weaver/logging.rb +108 -20
  41. data/lib/graph_weaver/parsing.rb +6 -13
  42. data/lib/graph_weaver/query_module.rb +2 -0
  43. data/lib/graph_weaver/railtie.rb +113 -14
  44. data/lib/graph_weaver/representation.rb +30 -2
  45. data/lib/graph_weaver/response.rb +15 -0
  46. data/lib/graph_weaver/retry.rb +54 -22
  47. data/lib/graph_weaver/rspec.rb +63 -18
  48. data/lib/graph_weaver/schema_diff.rb +293 -0
  49. data/lib/graph_weaver/schema_loader.rb +126 -35
  50. data/lib/graph_weaver/tasks.rb +88 -36
  51. data/lib/graph_weaver/testing/cassette.rb +131 -78
  52. data/lib/graph_weaver/testing/coverage.rb +11 -15
  53. data/lib/graph_weaver/testing/failure.rb +14 -8
  54. data/lib/graph_weaver/testing/fake_client.rb +253 -60
  55. data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
  56. data/lib/graph_weaver/testing/router.rb +147 -840
  57. data/lib/graph_weaver/testing.rb +40 -83
  58. data/lib/graph_weaver/transport/faraday.rb +1 -1
  59. data/lib/graph_weaver/transport/http.rb +29 -12
  60. data/lib/graph_weaver/transport.rb +11 -34
  61. data/lib/graph_weaver/version.rb +1 -1
  62. data/lib/graph_weaver.rb +221 -118
  63. metadata +17 -13
  64. data/CLAUDE.md +0 -161
  65. data/DECISIONS.md +0 -309
  66. data/Makefile +0 -23
  67. data/NOTES.md +0 -182
  68. data/PLAN.md +0 -115
  69. data/REVIEW.md +0 -946
  70. data/lib/graph_weaver/schemas.rb +0 -46
  71. data/lib/graph_weaver/selection.rb +0 -120
  72. data/lib/graph_weaver/testing/values.rb +0 -98
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f9913a2f1dba64e814682cd54d4f6f3da0696071e1f20f0045aaf585eb8fe4b
4
- data.tar.gz: 57f2f2b701f09c52235dd0acedee77891b8ed0a570f28a086e140ef3c7a7c57b
3
+ metadata.gz: 3847d3890b566828d7bfe496a6523b1032036d6aad4df0959f0b940bddb14d9b
4
+ data.tar.gz: d2a38677d13f8a0f2a92c67c9082a1a9523add9429bf4388419dbabc8959302f
5
5
  SHA512:
6
- metadata.gz: 469bff9b186698995c51ed7997957ecccb613c70da29ea0a9788e9597cb9c4cb4a679ceb95cd23aade6475159dbbd47fa7f13c5073ee5a28b5f3f2b0e4dc3483
7
- data.tar.gz: 0144261b27419fd180a96193c0cfc1a6bc2d023fbb9d2cbb55e45f9608118385b91d4e2ee5a453a21c0022a0804aa88e598981c925322b3519144bfd246d0c45
6
+ metadata.gz: 118114092cf2cd436e8189fb79dd5be733aa25e34d0aa727da9247e87bc5e309b2b3692248ffa9e40832bf743d672859db444c3fcffc4fc1cbb263b6c395b7e4
7
+ data.tar.gz: c0846cca1819d0e2b75d98f4b8d6a74af2f29113686998bd9e616a7d7d1e9a3a70edbe985412c4750a8ec63f0f0c4389e6363c1bc9d58b251a10b2f652d10e0b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,540 @@
1
+ ### v0.6.0 (2026-09-09)
2
+ - **One rule for relative path settings: they resolve against
3
+ `GraphWeaver.root`** — `Rails.root` in a Rails app, the working directory
4
+ otherwise. There were two: cassettes resolved against `Rails.root` while
5
+ the schema dump, queries and generated directories resolved against the
6
+ process's working directory, and the railtie patched three of the four
7
+ places that mattered. A Rails app started anywhere but its own root — a
8
+ `bin/rails server` or an `rspec` run from a subdirectory — loaded no
9
+ generated modules at all. An absolute setting is untouched, and the settings
10
+ keep returning what you configured, so `graphql.config.yml` stays portable.
11
+ Set `GraphWeaver.root` to override. The rule runs both ways: a path is
12
+ resolved against the root on access and reported relative to it on output,
13
+ so `generate!`'s return value, `changed_files`, `check_queries`' keys, the
14
+ rake tasks' `wrote`/`pruned` lines and the file an error names all come back
15
+ in the short form you configured. `cache:` also accepts a `Pathname` now,
16
+ as `schema:` and `query:` always have.
17
+ - **One spelling for the generated module's name: `name:`.** `GraphWeaver.parse`
18
+ took `name:` while `Codegen.generate` took `module_name:`. **Breaking:**
19
+ `module_name:` now raises naming `name:`. A parsed module also names itself
20
+ in errors — `GraphWeaver.parse::PersonQuery::Result::Person` instead of a
21
+ hex address — and assigning it to a constant upgrades every nested struct
22
+ to the real path.
23
+ - **`generate!` leaves a file alone when its content hasn't changed**, so the
24
+ rake task prints `wrote` only for files it wrote and `N already up to date`
25
+ for the rest, and a watching dev server reloads only what moved.
26
+ `GraphWeaver.changed_files` says which. A generated file now says which
27
+ release wrote it (`# Generated by GraphWeaver 0.6.0 — do not edit.`), so the
28
+ first `verify` after an upgrade reports the tree as stale. **Regenerate.**
29
+ - **A fake refuses an option it doesn't take**, naming the accepted ones and a
30
+ did-you-mean, at every door (`FakeClient.new`, `graphql_fake`,
31
+ `Router.new(fake:)`, `graphql_router(fake:)`) — it was a raw
32
+ `unknown keyword` from inside the fabricator. A scalar the fake can't
33
+ fabricate is now reported at its path (`at reader.orders.0.total`) with the
34
+ coordinate to override (`"Order.total"`).
35
+ - **A supergraph using federation 2.8's `@context`/`@fromContext` is refused
36
+ per query rather than routed with the argument unset.** `@join__field` was
37
+ on the routing table's known list, so its `contextArguments:` was read and
38
+ dropped: `Testing::Router` planned a fetch for the contextual field with the
39
+ argument empty. Apollo composes such a graph even when the context-setting
40
+ type and the contextual field sit in different subgraphs, so nothing
41
+ upstream caught it. The refusal fires only where the router would plan that
42
+ fetch itself; a subtree one subgraph answers whole still runs. New
43
+ `Unplannable` category `:context_argument`.
44
+ - **The local router makes one entity fetch where it made two** when an entity
45
+ has two `@requires` fields crossing into the same subgraph on the same
46
+ `@key`. Both sent the identical representation; Apollo makes one. Specs
47
+ asserting on `#trace` for such a query see one fewer entry. A missing `@key`
48
+ on a representation now names the entity type in `InputError#struct`, as a
49
+ coercion failure already did.
50
+ - **Three rake and generator polish items.** `rake -T` no longer prints a
51
+ queries path it can't know — a task description is baked before
52
+ `:environment`, so it presented the default as your setting; it now names
53
+ the defaults as defaults. `cassettes:anonymize` prints `no recordings in
54
+ <dir>` instead of nothing. `rails g graph_weaver:install` scaffolds
55
+ `app/graphql/fragments/.keep`, the directory its editor config already
56
+ globbed; the getting-started note names `rails g graphql:install
57
+ --skip-query-logs` for an app without Active Record.
58
+ - **`filter_parameters` scrubs error messages, not just the debug log.** A
59
+ rejected value reached the log twice: on the `variables=` line at debug, and
60
+ in the message every error writes at `warn` — above the gate, as free text.
61
+ One rule now covers both: a message the library composes about a value you
62
+ supplied names it only when the key it arrived under isn't filtered. So
63
+ `$password of Login: [FILTERED]`, while `$count of Compute: expected an Int,
64
+ got "lots"` is unchanged — the value is usually the whole diagnosis. Applies
65
+ to execute kwargs, input fields at any depth, federation `@key` fields, and
66
+ the missing-cassette report. A wrong-typed input field with no coercer now
67
+ says which field (`cents: expected BigDecimal, got 5`) instead of sorbet's
68
+ sentence, and the local federation router no longer logs variables
69
+ unscrubbed.
70
+ - **The public surface is what the docs name, what generated code calls, and
71
+ the `execute` slot — 421 names, down from about 700.** Everything else moved
72
+ under `GraphWeaver::Internal`, whose one rule is that nothing in it is API,
73
+ or went `private`. `spec/public_surface_spec.rb` diffs what's reachable from
74
+ `GraphWeaver` against a checked-in list, so the next accidental promotion
75
+ fails CI instead of shipping. **Breaking** only for code reaching past the
76
+ documented door: the federation query planner and its IR are
77
+ `Internal::Planner`; the fake-value engine is `Internal::Values`; the
78
+ selection walk is `Internal::Selection` (so `FakeClient` no longer answers
79
+ to `each_field` or `gather`); the cassette matching rules are
80
+ `Internal::RequestKey`; `GraphWeaver.log`/`.instrument`/`.filter_variables`
81
+ are `Internal::Log` (`logger=`, `instrumenter=`, `filter_parameters=` are
82
+ unchanged); `Transport.operation_name`/`.mutation?`/`.log_tag` left the
83
+ class you subclass for `Internal::Wire`. `SchemaDiff::Change`,
84
+ `Cassette::Check`, `Coverage::Result` and `InputStruct::Field` are `Data`,
85
+ not `Struct`, so they no longer hand out writers. `Codegen::ScalarType` is
86
+ private from load rather than from the first `reset_scalars!` — the
87
+ `private_constant` had landed inside the method body. Generated modules also
88
+ stop exposing their plumbing: `DEFAULT_CLIENT`, `FIELDS` and `ONE_OF` are
89
+ emitted as `private_constant`. **Regenerate**; nothing you call changes.
90
+ - **The retry options are flat.** `retries:` is how many attempts follow the
91
+ first; every other `Retry` option now sits beside it on the client —
92
+ `GraphWeaver.new(url, retries: 5, backoff: :linear, retry_codes:
93
+ ["THROTTLED"])`. **Breaking:** `retries:` no longer takes a Hash (`retries:
94
+ { retries: 5 }` read as a key nested in itself), and three options are
95
+ spelled out now that they sit next to a count: `on:` is `retry_on:`,
96
+ `base:`/`max:` are `base_delay:`/`max_delay:` — `retries: 5, max: 30`
97
+ invited reading `max:` as a second attempt count. The Hash form names its
98
+ flat replacement, and a retry option passed without `retries:` says so.
99
+ - **A representation's key fields are coerced, like an `execute` kwarg.**
100
+ `Representations.user(id: params[:id])` typechecked statically and then
101
+ raised sorbet-runtime's unbranded `TypeError` at runtime. The generated
102
+ builders now carry `.checked(:never)` and run each key field through the
103
+ registered scalar's own conversion; a value that converts to nothing raises
104
+ `GraphWeaver::InputError` naming the representation and the field.
105
+ **Regenerate.**
106
+ - **The library's internals are private.** Everything not documented and not
107
+ named by generated code is now `private` / `private_class_method` /
108
+ `private_constant`: codegen's IR and walk, the schema loader's detection
109
+ tables, the transport's log patterns, `Retry`'s default predicate, the
110
+ file-naming helpers. **Breaking:** `GraphWeaver::Selection`'s methods are
111
+ private in every class that mixes it in, so `Testing::FakeClient` no longer
112
+ answers to `each_field`, `gather` or `load_operation` — never a supported
113
+ door. The runtime that emitted source calls into (`cast_data`,
114
+ `check_envelope!`, `Coerce`, `Hints`, `InputStruct`, `QueryModule`,
115
+ `Representation`) stays public and says in its comments that it is
116
+ generated code's, not yours.
117
+ - **A registration another schema owns warns instead of failing generation.**
118
+ One registry serves a whole graph — federation composes scalars and types
119
+ by name — but validation ran against the single schema in hand, so the
120
+ natural multi-subgraph pattern (register once, generate each query against
121
+ its own subgraph) hard-failed. Generation now fails only on what the schema
122
+ can *disprove*: a name it declares as something else, or a coordinate whose
123
+ field it declares as a composite. Everything else warns, naming the schema
124
+ and suggesting a corrected name. `rake graph_weaver:generate` and `verify`
125
+ print that list once per run after the files;
126
+ `GraphWeaver.unmatched_registrations` is the same list for a Rakefile.
127
+ **Breaking**: a typo'd registration that used to stop generation is now a
128
+ printed warning. `docs/federation.md` gains "Generating for a federated
129
+ graph": through the gateway, generate against the supergraph; calling
130
+ subgraphs directly, one client each with `reset_registrations!` between.
131
+ - **Variables coerce inside `execute` now, and the sig stays as narrow as the
132
+ schema.** `execute(first: params[:first])` works — the String becomes an
133
+ Integer, an ISO-8601 string a `Date`, a model's Integer primary key an `ID` —
134
+ while `execute(first: "20")` written literally in a typed file is still an
135
+ `srb tc` error. The only way to accept loose input used to be `coerce: true`
136
+ / `GraphWeaver.auto_coerce`, which bought it by *widening* the emitted kwarg
137
+ and so switched off the static check at every call site. The generated sigs
138
+ are now `.checked(:never)` so an untyped value reaches the body; coercion is
139
+ the check that replaces sorbet-runtime's, and it refuses more than sorbet
140
+ did. Bad input raises `GraphWeaver::InputError` naming the variable, the
141
+ operation and the value — the same 422 rescue point as an input-object
142
+ field. **Breaking:** `GraphWeaver.auto_coerce` and `register_scalar(coerce:)`
143
+ are gone; a scalar that had `coerce: true` keeps its behaviour but its kwarg
144
+ narrows, so a call site passing a literal String is now an `srb tc` error.
145
+ `cast:` is what a loose value converts through. **Regenerate.**
146
+ - **Numeric strings are read as a wire format, not as Ruby source.** `"010"`
147
+ is ten, not eight, and `"0x1f"` and `"1_0"` are refused, in variables and
148
+ responses alike — `Kernel#Integer` accepted all three, which made a
149
+ zero-padded form field silently mean something else. An `Int` refuses a
150
+ `Float` it can't hold losslessly (`2.5`), and a `Boolean` refuses a String:
151
+ Ruby has no `Kernel#Boolean`, so every rule for `"0"`/`"off"` is a
152
+ convention the library declines to pick — convert at the call site.
153
+ - **A faked subgraph takes the options every other fake takes, and
154
+ `graphql_router` says them per example.** `subgraphs: { "reviews" => :fake }`
155
+ built its fake with nothing, so `overrides:`, `list_size:` and `null_chance:`
156
+ had nowhere to go. `fake:` carries them, on `Router.new`, on
157
+ `Testing.config.router`, and per example through `graphql_router(fake:)`,
158
+ which mirrors `graphql_fake`. One `fake:` covers every faked subgraph —
159
+ coordinate-keyed overrides already say which type they mean. A `fail_at:`
160
+ inside an `_entities` fetch used to null the field and report no error; it
161
+ now comes back as an error on the caller's path.
162
+ - **A subgraph two loaded schema classes fit is refused by the query that
163
+ reaches it, not by `Router.new`.** In an app where several loaded schemas
164
+ each satisfy a foundational subgraph, building a router raised for
165
+ subgraphs the query under test never touched — and only when eager loading
166
+ happened to be on. The router now builds, every query that avoids the
167
+ subgraph runs, and one that reaches it raises naming the candidates, saying
168
+ they came from loaded schema classes, and showing the `subgraphs:` form that
169
+ pins one. A class named explicitly still fails at construction.
170
+ `router.ambiguous` lists them. `Testing::Subgraphs.resolve` now returns a
171
+ `Resolution` (`#served`, `#ambiguous`).
172
+ - **A `.graphql` edit reaches the next request in development.** The query
173
+ directories and the schema dump join Rails' own reloaders, and the
174
+ `to_prepare` that loads the generated modules now regenerates first — after
175
+ your registrations, so an `extend_type` in a `to_prepare` is in place before
176
+ codegen reads it. A query that doesn't compile is logged at `error` with its
177
+ file and position while the modules already loaded keep serving. Development
178
+ only, while the server is running; `config.graph_weaver.watch = false` turns
179
+ it off, and `rake graph_weaver:verify` still gates CI on the committed files.
180
+ `GraphWeaver.reload_generated!` does the same by hand after regenerating in
181
+ another terminal, and generated files are now written to a temp file and
182
+ renamed, so a `rake graph_weaver:generate` beside a watching dev server can't
183
+ hand the running app a prefix that no longer parses.
184
+ - **A variable passed `nil` now sends `null`; one left out is still left
185
+ out.** GraphQL tells an absent variable from an explicit null — `bio: null`
186
+ clears a bio, omitting it does nothing — and a Ruby kwarg with a nil default
187
+ cannot, so both spellings omitted the key and no generated mutation could
188
+ clear a field. An optional nullable variable now records whether the keyword
189
+ was supplied. Input objects get the same distinction where a Hash can express
190
+ it: `coerce({nickname: nil})` sends null, `coerce({})` omits; a struct built
191
+ with `.new` can't tell, so nil there still means omit. A non-null variable
192
+ can't carry null, so nil there also still omits and its schema default
193
+ applies. **Breaking**: `bio: params[:bio]` with a missing param now sends
194
+ `null` where it used to omit — pass the keyword only when you mean to.
195
+ **Regenerate.**
196
+ - **A response that won't cast says which field, and whose bug it is.** A
197
+ leaf's cast raises about the value alone — "invalid date" on a struct holding
198
+ four dates located nothing — so each casting leaf now carries its response
199
+ key. An `ID` the server sent unquoted is out of spec but read like a
200
+ graph_weaver bug in sorbet's words; the message now says which it is and how
201
+ to take it anyway. An enum value the generated enum doesn't hold now names
202
+ the legal values and says the likely cause is drift, matching the input
203
+ side. **Regenerate.**
204
+ - **`register_scalar` with a type the wire can't build is refused at
205
+ generation.** `register_scalar("Money", BigDecimal)` produced a `BigDecimal`
206
+ prop and no cast, so every response failed its prop check at runtime, far
207
+ from the initializer that caused it. Generation now refuses it where a query
208
+ reads that scalar back, naming the field and how to give it a `cast:`.
209
+ `docs/scalars.md` now tabulates what the wire carries in both directions.
210
+ - **A fake takes pins, keyed by a scalar type, an object type, or a field.**
211
+ A pin says what the fake uses instead of inventing a value: a wire value,
212
+ an object it reads the selected fields off — a FactoryBot build, a model, a
213
+ `Struct`; a field the object doesn't answer is still fabricated, and a Ruby
214
+ value goes on the wire the way its scalar registration serializes it — or a
215
+ proc handed the seeded `Random`. Pins lead and options follow, in one call;
216
+ suite-wide, the same hash is `Testing.config.overrides`, and a router's
217
+ `fake:` takes it for the subgraphs it fakes.
218
+
219
+ graphql_fake("Money" => "12.00", "Person" => build(:person), "Order.total" => "999.00")
220
+
221
+ A scalar registered as your own class — `register_scalar("Money", Money,
222
+ cast: :parse)` — told codegen how to read the wire value but left the
223
+ testing harness guessing what to write, so every fake response touching it
224
+ died inside `from_h` blaming `Money.parse` for a `"Money-1"` placeholder.
225
+ The type pin is the answer, from every door including cassette
226
+ anonymization; without one, fabrication refuses, naming the scalar, the
227
+ field, and the pin to add. Scalars registered as `Time`, `Date`, `Integer`,
228
+ `Float`, `String` or `T::Boolean` need nothing. Fakes also now honor a
229
+ per-field `register_scalar("User.birthday", Date)`, matching codegen's
230
+ resolution. `graphql_fake` and `graphql_router(fake:)` refuse a per-example
231
+ `seed:` — `rspec --seed` is the mechanism, and `config.seed` the suite-wide
232
+ one for a harness that isn't rspec.
233
+ - **`Testing.config.null_chance` and `Testing.config.mode` are gone; both
234
+ are per-fake now.** A suite-wide answer to a per-example question nils an
235
+ unrelated field one run in ten. **Move them:** `graphql_fake(null_chance:
236
+ 0.3)` and `graphql_fake(values: :literal)` (the per-fake `mode:` is renamed
237
+ `values:` so it can't be confused with `graphql: :fake` /
238
+ `config.default_mode`, which stay). The auto-detect — faker when the gem is
239
+ loaded, else literal — is the only suite-wide value style.
240
+ `Testing::MODES` is now `VALUE_STYLES`.
241
+ - **`retries:` takes the count you'd write, and means the same thing
242
+ everywhere.** `GraphWeaver.new(url, retries: 3)` raised and pointed at
243
+ `retries: { tries: 3 }` — a second word for the same number that disagreed
244
+ about whether it counted the first attempt. One word now: `retries:` is how
245
+ many attempts follow the first, on the client and on `Retry` alike, so
246
+ `retries: 0` is one attempt. **Rename** `Retry.new(tries: n)` to
247
+ `Retry.new(retries: n - 1)`; the default is unchanged in effect.
248
+ - **`Response#to_h`.** Every error class answered `#to_h`; the envelope didn't.
249
+ It returns `{"data" =>, "errors" =>, "extensions" =>}` with each error as its
250
+ JSON-ready hash. `data` stays the typed struct rather than re-serialized:
251
+ `T::Struct#serialize` gives snake_case keys, drops nulls, and leaves a
252
+ registered scalar as its Ruby object — output that would pass for the
253
+ server's response without being one.
254
+ - **Sensitive variables are scrubbed from the debug log.** A `login(password:)`
255
+ mutation's variables were one log-level switch away from the log. Matching
256
+ values are replaced with `[FILTERED]` at any depth, on the wire and
257
+ in-process paths. Rails apps configure nothing: the railtie adopts the app's
258
+ own `config.filter_parameters`. Elsewhere,
259
+ `GraphWeaver.filter_parameters = [:password, /token/]`; the default list is
260
+ `[:password, :token, :secret, :authorization]`, matched as case-insensitive
261
+ substrings, and `[]` turns filtering off.
262
+ - **`schema:diff` names what changed, not just that something did.** It
263
+ reported `schema.json is stale` and stopped, so learning what moved meant
264
+ refreshing and reading a `git diff` of a 3 MB dump. It now prints one line
265
+ per change at its schema coordinate, breaking ones first — types, fields,
266
+ arguments, nullability, enum values, union members, interfaces and
267
+ deprecations — and still exits non-zero on any drift. Breaking is judged from
268
+ the client's side, so nullability is directional: an output losing `!` breaks
269
+ a generated struct, an input gaining one breaks a query that omits it.
270
+ `SchemaLoader.stale?` is now `SchemaLoader.diff`, returning the summary;
271
+ `diff(path).empty?` is what `stale?` answered.
272
+ - **A Rails app whose generated code includes an autoloaded helper now
273
+ boots.** `extend_type("Pet", PetHelpers)` and `register_enum("Species",
274
+ PetKind)` name constants your app autoloads, so the library tells you to
275
+ register them from a `to_prepare` block — but the railtie required the
276
+ generated files before Zeitwerk was set up and before `to_prepare` ran, so
277
+ the `include PetHelpers` those registrations emit raised `NameError` at every
278
+ boot, in every environment. They now load from a `to_prepare` block of their
279
+ own: after the autoloader, after your registrations, before eager loading.
280
+ - **A fragment on the abstract type no longer narrows the field away.**
281
+ `named { __typename ... on Named { name } ... on Pet { species } }` read the
282
+ `Named` fragment as a type condition and narrowed to Pet, so a Person came
283
+ back as `nil` and the `name` the server sent went on the floor — while the
284
+ same selection spelled bare dispatched correctly. Two ways of writing one
285
+ query disagreed, and the fragment is how anyone shares interface fields.
286
+ **Regenerate** to pick it up.
287
+ - **A `Float` field reads the whole number a JSON encoder writes.** JSON has
288
+ one number type, so `1.0` reaches Ruby as an `Integer` from graphql-js and
289
+ Go's `encoding/json` alike, and `const :amount, Float` refused a response the
290
+ spec calls legal. Widened with `Kernel#Float`, which still refuses what
291
+ `.to_f` would quietly turn into `0.0`. **Regenerate** to pick it up.
292
+ - **A guarded `__typename` no longer makes a narrowing miss look like a
293
+ match.** Narrowing without a readable tag decides on emptiness, but a
294
+ `__typename` behind `@skip`/`@include` still arrives for the member narrowing
295
+ means to filter, so the wrong member was cast. That query now refuses at
296
+ generation, with the message that already names the fix.
297
+ - **Generation refuses what Ruby would misread.** A result key that would
298
+ shadow a constant the file uses (`date` beside a `Date` scalar nested a
299
+ `class Date < T::Struct` and typed the scalar prop as that struct) is refused
300
+ naming both keys; so is an enum value that camelizes to nothing (`_`), which
301
+ emitted a file that died at load with a syntax error.
302
+ - **A bad enum value in a variable says what the enum accepts**, as an
303
+ `InputError` — it raised a bare `KeyError` naming an anonymous module as a
304
+ top-level variable while the same mistake inside an input object was already
305
+ branded. A missing required input field now lists every missing key instead
306
+ of sorbet's symptom for the first prop it reached. **Regenerate** to pick up
307
+ the new emitted coercion.
308
+ - **A named fragment now crosses a subgraph boundary.** A stitched fetch is
309
+ built from selections alone, so a spread that survived into one named a
310
+ fragment the subgraph had never seen and the whole subtree came back
311
+ `Fragment X was used, but not defined`. Every spread a fetch carries is now
312
+ spelled as the inline fragment it is.
313
+ - **A fake's value for a custom scalar comes from what you registered it
314
+ as.** It was picked off the schema's *name* for the scalar, so a `Timestamp`
315
+ registered as `Time` got `"Timestamp-1"` and every fake response touching it
316
+ died inside the codec codegen had emitted for it. A scalar registered as
317
+ `Time` now gets iso8601, one registered as `Integer` an Integer; an
318
+ unregistered one keeps its placeholder. Cassette anonymization runs through
319
+ the same engine and had the same hole, which made `cassettes:check` fail on
320
+ a cassette the anonymizer had just written. **Re-run
321
+ `rake graph_weaver:cassettes:anonymize`** on a cassette holding a registered
322
+ custom scalar.
323
+ - **A mutation is no longer retried.** A read timeout, a 502 or a reset socket
324
+ does not say whether the server applied the request, and a second `charge`
325
+ is worse than a failed one — so `Retry` gives a mutation one attempt and says
326
+ so on the logger. Pass `retry_mutations: true` for an API whose mutations are
327
+ idempotent. Each retry now logs the wait and the attempt number.
328
+ - **A connection that dies mid-body is a dropped connection.** net/http's
329
+ `ignore_eof` default handed back the bytes that arrived when a socket closed
330
+ short of its `Content-Length`, so a half-sent response reached you as a 200
331
+ with a truncated body — a permanent `ServerError` that `Retry` would not
332
+ retry. It now raises the retriable `TransportError` it is.
333
+ - **An in-process query gets its own context.** graphql-ruby writes a
334
+ resolver's `context[...] =` into the hash it is handed, and one `InProcess`
335
+ is normally the whole app's client — so a flag set by one request was still
336
+ there for the next, and racing between them under Puma.
337
+ - **A response that carries neither `data` nor `errors` is refused.** A client
338
+ returning `nil`, one keying the envelope by symbol, one that typo'd `"dat"`:
339
+ each produced a `Response` reporting `success?` with `data` nil.
340
+ `from_response` is documented public API and symbolized keys are the
341
+ likeliest mistake at that seam, so it now brands, naming the keys it found;
342
+ a client answering with a String brands too, instead of dying on `#to_h`.
343
+ **Regenerate** to pick that up.
344
+ - **`@skip`/`@include` are evaluated by the fake against the variables you
345
+ passed**, declared defaults included — the way a server and
346
+ `Testing::Router` already did, so one query no longer carries a key under
347
+ `graphql: :fake` and not under `graphql: :router`. A faked subgraph does the
348
+ same for directives inside its `_entities` selection. A `first:`/`last:`
349
+ arriving as a variable caps the fabricated list like a literal, a cap below
350
+ zero reads as a page of none, and `null_chance` reaches list positions.
351
+ - **An alias spelling a response key the router carries a `@key` under is
352
+ refused.** The router injects the `@key` it crosses on under a reserved key
353
+ and strips it from the answer, so `_gw_weight: weight` came back without
354
+ `_gw_weight` at all. It joins the `shadowed_key` refusal, which already
355
+ covered Apollo's half of the same collision.
356
+ - **A `@requires` field set excluded by `@skip`/`@include` no longer
357
+ prefetches.** The router ran a subgraph fetch a real router never makes —
358
+ which matters for a test double, where a resolver that runs is one your
359
+ example can observe.
360
+ - **`verify_generated!` fails over zero query documents**, and `generate!`
361
+ warns. A mistyped `queries_paths` left `verify` returning true having
362
+ compared nothing, so a CI gate stayed green forever.
363
+ - **`rake graph_weaver:generate` says what it pruned, and where it looked when
364
+ there was nothing to generate.** Deleting a `.graphql` deletes the checked-in
365
+ file it produced, and the task printed nothing and exited 0; so did a run
366
+ with no queries, the state every install starts in.
367
+ - **`Failure.server` takes the headers a backoff branches on.**
368
+ `ServerError#retry_after` and `#throttled?` read `Retry-After`, and there was
369
+ no way to set one: `Failure.server(status: 429, headers: { "retry-after" =>
370
+ "2" })`. `Failure.transport`'s message now names the class it caught, the
371
+ way the bundled transports do.
372
+ - **Cassette recording is safe across threads.** `record` was
373
+ read-modify-write; two threads recording through one cassette each saved a
374
+ snapshot missing the other's entry.
375
+ - **A `.json` schema dump that isn't JSON says which file and what it holds.**
376
+ A truncated download or a login page saved over the dump raised a bare
377
+ `JSON::ParserError`, outside `GraphWeaver::Error` and naming neither.
378
+ `rake graph_weaver:schema:refresh` — the fix — no longer trips over it.
379
+ - **Setup mistakes say what to do.** A schema source that looks like a url
380
+ (`localhost:4000/graphql` included) gets the url advice first; a non-token
381
+ `auth:`, a `retries:` that is neither true nor a Hash, a url
382
+ `Transport::HTTP` cannot POST to, a `parse` path without a `.graphql`
383
+ extension, a cache directory that can't be written, `schema:refresh URL=`
384
+ with a file path, and `schema:diff` with no dump all name the fix. A
385
+ `ServerError` carries the advice its status implies: a 3xx names where the
386
+ server pointed (redirects are not followed) and a 401/403 names `auth:`. A
387
+ spec with a `graphql:` tag but no `graph_weaver/rspec` is told to require
388
+ it, a misspelled tag reports one failure instead of two, `InputError` no
389
+ longer prints sorbet's `Caller:` frame, a generated file whose registered
390
+ constant is gone names the registration, and `rails g graph_weaver:install`
391
+ names a retry that works when introspection fails.
392
+ - **The GitHub example's checked-in modules were regenerated**, and a spec now
393
+ keeps them in step with the emitter.
394
+ - **A cold process no longer introspects once per in-flight thread.** A
395
+ url-built client fetches its schema lazily, and Puma serves its first
396
+ requests concurrently — so eight threads arriving together meant eight full
397
+ introspection round trips, each of them also writing the schema cache. One
398
+ now, whoever asks first.
399
+ - **The schema cache and cassettes are written atomically.** `File.write`
400
+ truncates before it writes, so an interrupted run — or a second writer, a
401
+ rake task beside a running app — could leave a half-written file where a
402
+ committed artifact used to be. Both now write beside the target and rename,
403
+ which is atomic: a reader sees the old file or the new one.
404
+ - **The connection pool keeps its ceiling under an interrupt.** An async raise
405
+ landing between taking a permit and the `ensure` that returns it would have
406
+ leaked one for the life of the process, and `Rack::Timeout` raises exactly
407
+ that way; the gap is closed. Its saturation warning is also once now rather
408
+ than once per racing thread.
409
+
410
+ ### v0.5.1 (2026-09-07)
411
+ - **A union's catch-all struct keeps the fields an interface fragment asked
412
+ for.** `... on Named { name }` under a union types `name` on every member the
413
+ query named, but the catch-all — the member it didn't name, including one the
414
+ schema grows later — carried only `__typename`, so a `name` the server
415
+ actually sent was dropped on the floor. Those fields are `T.nilable`: the
416
+ member that arrives needn't implement the interface. **Regenerate** to pick
417
+ it up.
418
+ - **`schema_stale?` now recognises the drift a graphql-ruby server reports.**
419
+ It matched Apollo's one flat code plus a message pattern, but graphql-ruby —
420
+ which this library ships as its in-process client — names the rule that fired
421
+ in `extensions.code`. So a renamed type, a removed argument, an argument that
422
+ became required and four other drift shapes all raised without the one
423
+ sentence telling you to refresh the dump and regenerate, and `to_h` reported
424
+ `"schema_stale": false`.
425
+ - **A cast failure keeps the server's own explanation.** A field that came back
426
+ null *with a reason* — a permission rule, a partial outage — raised only
427
+ Sorbet's nil complaint, because the generated envelope built `data` before it
428
+ read `errors`. The reason is now in the message. **Regenerate** to pick it up.
429
+ - **Cassette anonymization missed `errors` and `extensions`.** It walked
430
+ `data` and nothing else, so a rejected request's error message — which
431
+ routinely quotes the input that caused it — and whatever the server hangs
432
+ off `extensions` went to disk verbatim, under a flag that says real data
433
+ never reaches it. **Re-run `rake graph_weaver:cassettes:anonymize` on any
434
+ cassette you've already committed**, or re-record it. Neither key has a
435
+ schema behind it, so both are now walked by shape: keys, nesting, nulls and
436
+ booleans survive and every string and number is replaced, with `path`,
437
+ `locations` and an error's `extensions.code` kept because they describe the
438
+ request rather than the data.
439
+ - **Recording flags a credential that lands in the file.** A cassette gets
440
+ committed as written, and its query and variables can't be anonymized —
441
+ they're the key replay matches on. So the bytes headed for disk are checked
442
+ for the shapes that are unmistakable (a JWT, `AKIA…`, `ghp_…`, `xox…`,
443
+ `sk_live_…`, a PEM block, a `Bearer` header) and named on stderr once per
444
+ cassette. Nothing is rewritten; a password like `hunter2` has no shape, so a
445
+ quiet run still isn't a clean bill of health.
446
+ - **An anonymized cassette no longer drops a key selected twice.** The
447
+ anonymizer kept the last occurrence's sub-selection where codegen merges
448
+ them, so `a { x } a { y }` came back holding only `y` — a recording that
449
+ then failed its own `cassettes:check`.
450
+ - **Three generated types disagreed with the schema.** `srb tc` proves the
451
+ generated code is self-consistent, not that it is true, so each of these was a
452
+ lie the typechecker endorsed. **Regenerate** to pick them up:
453
+ - A field reached only through a `@skip`/`@include`-guarded occurrence, where
454
+ the same key is also selected unguarded, was typed as guaranteed. The server
455
+ legitimately omits it, so `from_h` raised `key not found` on a valid
456
+ response — and a union whose `__typename` arrived that way crashed its own
457
+ dispatch. Those children are nilable now, and the `__typename` case refuses
458
+ at generation as it already did for the plainer spelling.
459
+ - A narrowed abstract inside a non-null list made the *list* nilable
460
+ (`[Thing!]!` with `... on Widget` → `T.nilable(T::Array[…])`). Only the
461
+ elements can be nil; the array is always there.
462
+ - An input field the schema gives a default is optional, but a non-null one
463
+ emitted `const :x, String, default: nil` — so `x.upcase` typechecked and
464
+ was a `NoMethodError`.
465
+ - **`rake graph_weaver:federation:coverage` no longer counts a subgraph as
466
+ served here when two loaded schemas fit it.** `Router.new` refuses that case
467
+ — picking either would be a coin flip — so the report was promising a run
468
+ the suite can't have. It now lands in the "nothing here serves what they
469
+ reach" list, whose advice (name a schema for those subgraphs) is the fix.
470
+ - **A supergraph that renamed the join spec is now refused instead of read as
471
+ empty.** `@link(url: ".../join/v0.3", as: "j")` spells every marker `j__`,
472
+ which the routing table doesn't follow — so it reported a composed graph with
473
+ *no subgraphs* and nothing unsupported, and `Testing::Router` built happily on
474
+ top of that. It now lands in `unsupported`, so the router and
475
+ `federation:coverage` refuse at construction and say why.
476
+ - **`rake graph_weaver:cassettes:anonymize` now finds an SDL schema dump.** It
477
+ opened `GraphWeaver.schema_path` directly instead of locating the dump the
478
+ way every sibling task does, so an app whose committed dump is
479
+ `schema.graphql` (what the generator writes for a supergraph) got a raw
480
+ `Errno::ENOENT`. A missing dump now says so.
481
+ - **`rake graph_weaver:queries:check` now authenticates with the token your
482
+ app uses.** It re-introspects the url the dump records, but read
483
+ `GRAPHWEAVER_AUTH` rather than the ENV var the dump named — so an app
484
+ installed with `--auth MY_TOKEN` reached its private API while the check
485
+ 401'd. Every re-introspection now goes through `SchemaLoader.source_transport`,
486
+ which is also now public.
487
+ - **A `graphql: :router` example now starts from the same fabricated data
488
+ whether it runs alone or after two hundred others.** The router is built once
489
+ for the suite, and a faked subgraph kept fabricating from wherever the last
490
+ example left its sequence — so `rspec spec/foo_spec.rb:12` disagreed with the
491
+ same example in a full run, which is the opposite of what `--seed` promises.
492
+ `Router#reset!` is the example boundary (`reset_trace` still clears only the
493
+ trace); the rspec tag calls it for you.
494
+ - **An override key given as a Symbol now pins.** `graphql_fake(overrides: { name: "Ada" })`
495
+ validated clean and silently fabricated a random value, because lookup is by
496
+ String — the exact silent-green failure the override validation exists to
497
+ prevent.
498
+ - **A helper no longer contradicts `config.default_mode`.** With
499
+ `default_mode = :fake`, an untagged example calling `graphql_in_process`
500
+ raised *"this example is tagged graphql: :fake"* — naming a tag that wasn't
501
+ there, and blocking the untagged form the docs recommend. Only an explicit
502
+ tag can disagree with a helper now.
503
+ - **A variable default that isn't a Boolean no longer reaches a subgraph.** An
504
+ enum or input-object default is a parser AST node; sending one put a
505
+ back-pointer to the lexer on the wire, and an input object raised
506
+ `NoMethodError` outside any rescue. Only `@skip`/`@include` read these, and
507
+ they read Booleans; a subgraph applies its own defaults from the variable
508
+ declaration it already receives.
509
+ - The local router asks the schema for one type by name rather than rebuilding
510
+ the whole type map, at four sites — one of them per response row. On a
511
+ 1200-type supergraph a stitched query over 200 rows was **5x** slower than it
512
+ needed to be; the cost is now flat in schema size.
513
+ - **`graphql: :in_process` ran the committed schema dump instead of your live
514
+ schema class.** A dump loads as an anonymous `GraphQL::Schema` subclass, which
515
+ looks like a runnable class and has no resolvers — so every app that followed
516
+ `getting_started.md` (the generator always commits a dump) got a graphql-ruby
517
+ 500 blaming its own resolver, and the error that was written to explain this
518
+ was unreachable. Nothing to do; if you worked around it with
519
+ `Testing.config.schema = MySchema`, that still works and still wins.
520
+ - **`GraphWeaver.parse(query:)` accepts a `Pathname`.** `schema:` already did,
521
+ so `GraphWeaver.parse(schema: Rails.root.join("schema.graphql"), query:
522
+ Rails.root.join("app/graphql/queries/person.graphql"))` died on the query
523
+ argument with `undefined method 'end_with?'`. Same for `client.parse(path)`.
524
+ - **`rake graph_weaver:cassettes:check` and `:anonymize` now find cassettes
525
+ from any working directory.** Both read `config.cassette_dir` raw, while
526
+ `Cassette.new` resolves it against `Rails.root` — so run from anywhere but
527
+ the app root, `check` aborted with "this checked nothing, so it proved
528
+ nothing" and `anonymize` silently did nothing, both while a spec run found
529
+ the same files fine. They now use the same resolution the recordings do.
530
+ - **An `extend_type(alias:)` name a struct instance already answers to now
531
+ refuses** instead of silently overriding it. `alias: { hash: "…" }` emitted
532
+ `def hash`, which breaks every `Hash` and `Set` holding that struct;
533
+ `inspect`, `to_s`, `method` and `class` were the same story. A *wire field*
534
+ by any of those names was already refused, so this is the same rule reaching
535
+ the same case from the other side. **If generation now refuses an alias you
536
+ had**, rename it — the accessor it generated was overriding a Ruby method.
537
+
1
538
  ### v0.5.0 (2026-09-07)
2
539
  - **`graphql_in_process(SomeSchema)`** runs one example against that schema
3
540
  class's real resolvers — the sibling of `graphql_fake`, and how a federated
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graph_weaver (0.5.0)
4
+ graph_weaver (0.6.0)
5
5
  graphql (>= 2.6.7)
6
6
  sorbet-runtime
7
7
 
@@ -99,16 +99,16 @@ GEM
99
99
  rubydex (0.2.7-arm64-darwin)
100
100
  rubydex (0.2.7-x86_64-darwin)
101
101
  rubydex (0.2.7-x86_64-linux)
102
- simplecov (1.1.1)
103
- sorbet (0.6.13454)
104
- sorbet-static (= 0.6.13454)
105
- sorbet-runtime (0.6.13454)
106
- sorbet-static (0.6.13454-aarch64-linux)
107
- sorbet-static (0.6.13454-universal-darwin)
108
- sorbet-static (0.6.13454-x86_64-linux)
109
- sorbet-static-and-runtime (0.6.13454)
110
- sorbet (= 0.6.13454)
111
- sorbet-runtime (= 0.6.13454)
102
+ simplecov (1.2.0)
103
+ sorbet (0.6.13485)
104
+ sorbet-static (= 0.6.13485)
105
+ sorbet-runtime (0.6.13485)
106
+ sorbet-static (0.6.13485-aarch64-linux)
107
+ sorbet-static (0.6.13485-universal-darwin)
108
+ sorbet-static (0.6.13485-x86_64-linux)
109
+ sorbet-static-and-runtime (0.6.13485)
110
+ sorbet (= 0.6.13485)
111
+ sorbet-runtime (= 0.6.13485)
112
112
  spoom (1.8.3)
113
113
  erubi (>= 1.10.0)
114
114
  prism (>= 0.28.0)
@@ -176,7 +176,7 @@ CHECKSUMS
176
176
  google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b
177
177
  google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee
178
178
  google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd
179
- graph_weaver (0.5.0)
179
+ graph_weaver (0.6.0)
180
180
  graphql (2.6.10) sha256=9b7c8633767f516ff9d48a8d6305b2a00a2101c82aa871b92e69086944f9f83e
181
181
  i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
182
182
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
@@ -206,13 +206,13 @@ CHECKSUMS
206
206
  rubydex (0.2.7-arm64-darwin) sha256=f0d28bbf4153568be79b671642424750053e0bea971b60ddf5cec19bf4563990
207
207
  rubydex (0.2.7-x86_64-darwin) sha256=b002b259d118ac69de44470eff1597143318402c45630c47371f9542631447dc
208
208
  rubydex (0.2.7-x86_64-linux) sha256=dacfade9fa42ce4469618da6dac07e69d5f3ac6a313b4caced5234c8f052419a
209
- simplecov (1.1.1) sha256=25825ef13f0b2e74694d769817dad6ab8e90131dabdaa666e522fea105521e78
210
- sorbet (0.6.13454) sha256=3273dda082b8aa5ecf79b7675e2b79127f202a45370d881124af3af19a3c2b35
211
- sorbet-runtime (0.6.13454) sha256=b9441ae4bd265f51861c54240e8c5888f6355e3284885b21c5154b184e791d80
212
- sorbet-static (0.6.13454-aarch64-linux) sha256=e09dd1e6cd7e63b3fb6c8c9c6785eeb55a6c4afecb0f932538ee4401b4552632
213
- sorbet-static (0.6.13454-universal-darwin) sha256=6ac0cf10ae2b4e0e9ecabb886f9be739046424dc35d100e7888c8538a223f93a
214
- sorbet-static (0.6.13454-x86_64-linux) sha256=569c52ea926e514ebfdc64825f65cd188b7aba5c1b480cc26e1870fd53d5e2c0
215
- sorbet-static-and-runtime (0.6.13454) sha256=defc00684a6f8bd498bb70a4541f293b9232938a776137946247cba4baed3d20
209
+ simplecov (1.2.0) sha256=ea6acd05eece5a41990e2a5171c57d15700d329326c7666c85ee8c6a0dd0977e
210
+ sorbet (0.6.13485) sha256=b885d6a2fcde57bc46c3cdd597b9014b82619dee0f811a6fdf36f57c8f82c380
211
+ sorbet-runtime (0.6.13485) sha256=2982504e662eb424e515b3aa1c8139070451a014b6c1af2302250ed82495b4cc
212
+ sorbet-static (0.6.13485-aarch64-linux) sha256=5d802793633363442f189615fdb6c6d1be726b402d78a3d63eab96ca74b98d40
213
+ sorbet-static (0.6.13485-universal-darwin) sha256=6c7c4551872d556b37bf1f1a3495509b9fd9479b4a080bd57f9c5db0e8116658
214
+ sorbet-static (0.6.13485-x86_64-linux) sha256=e22d88df0bba5a97e664a329a2212ea59e6a2020626597a162c60c5ddfb4f9b4
215
+ sorbet-static-and-runtime (0.6.13485) sha256=b893346f9e1a244133071ad3c3d06b8ddf70c1b8246bb594c9aaa5cfc778df5f
216
216
  spoom (1.8.3) sha256=32871fa189bbfa49cf557a50f819f23cc9a6ceefd0346caa7a6adc193becd5dd
217
217
  tapioca (0.19.2) sha256=938731b07811aee8d23871b1aee8861d464fbaf2cfffbf79a62b0c869a5120ec
218
218
  thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73