dexkit 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +97 -5
- data/guides/llm/EVENT.md +74 -10
- data/guides/llm/FORM.md +1 -1
- data/guides/llm/OPERATION.md +352 -51
- data/guides/llm/QUERY.md +1 -1
- data/lib/dex/context_setup.rb +64 -0
- data/lib/dex/event/export.rb +56 -0
- data/lib/dex/event/handler.rb +33 -0
- data/lib/dex/event.rb +28 -0
- data/lib/dex/operation/async_proxy.rb +18 -2
- data/lib/dex/operation/explain.rb +204 -0
- data/lib/dex/operation/export.rb +144 -0
- data/lib/dex/operation/guard_wrapper.rb +149 -0
- data/lib/dex/operation/jobs.rb +18 -11
- data/lib/dex/operation/once_wrapper.rb +240 -0
- data/lib/dex/operation/record_backend.rb +87 -0
- data/lib/dex/operation/record_wrapper.rb +87 -20
- data/lib/dex/operation.rb +62 -4
- data/lib/dex/props_setup.rb +25 -2
- data/lib/dex/railtie.rb +84 -0
- data/lib/dex/registry.rb +63 -0
- data/lib/dex/test_helpers/assertions.rb +23 -0
- data/lib/dex/tool.rb +115 -0
- data/lib/dex/type_serializer.rb +132 -0
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +21 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1009624f8d508e4d6b61d76c8d54f32fc04a11f445163915c027ebc1bdd30b5e
|
|
4
|
+
data.tar.gz: 1b5a0755af0be468d67c3c5447f1322deff584364a493fb7665687d1417189b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08552d04b1e9ddf5991f1454f9491fcabe80047d9606f0432be411f09d7b632a797435fabf55d8e9b190ddbc1a70daa5e73c19aa08c2bde88540559c3d35275e'
|
|
7
|
+
data.tar.gz: 33d58dd5b421a1e7f41d3ab133c1800787d2d1f6c22327e7db7faf9053222d62d087c4a563105fb39075fbefc1a73f04984b45511595a0e3976eda9833de0cdf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-03-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Registry** — `Dex::Operation.registry`, `Dex::Event.registry`, and `Dex::Event::Handler.registry` return frozen Sets of all named subclasses. Populated automatically via `inherited`; anonymous and stale (unreachable after code reload) classes are excluded. `deregister(klass)` removes entries. `clear!` empties the registry. Zeitwerk-compatible — registries reflect loaded classes; eager-load to get the full list
|
|
8
|
+
- **Description & prop descriptions** — `description "text"` class-level DSL for operations and events. `desc:` keyword on `prop`/`prop?` for per-property descriptions (validated as String). Both appear in `contract.to_h`, `to_json_schema`, and `explain` output. Optional — no error or warning when omitted
|
|
9
|
+
- **`contract.to_h` export** — serializes the full operation contract to a plain Ruby Hash: `name`, `description`, `params` (with typed strings and `desc`), `success`, `errors`, `guards`, `context`, `pipeline`, `settings`. Types are human-readable strings (`"String"`, `"Integer(1..)"`, `"Ref(Product)"`, `"Nilable(String)"`). Omits nil/empty fields
|
|
10
|
+
- **`contract.to_json_schema` export** — generates JSON Schema (Draft 2020-12) from the operation contract. Default section is `:params` (input schema for LLM tools, form generation, API validation). Also supports `:success`, `:errors`, and `:full` sections
|
|
11
|
+
- **Event export** — `Event.to_h` and `Event.to_json_schema` class methods for serializing event definitions. Same type serialization as operations
|
|
12
|
+
- **Handler export** — `Handler.to_h` returns name, events (array), retries, transaction, and pipeline metadata. `handled_events` returns all subscribed event classes
|
|
13
|
+
- **Bulk export** — `Dex::Operation.export(format: :hash|:json_schema)`, `Dex::Event.export(format: :hash|:json_schema)`, `Dex::Event::Handler.export(format: :hash)`. Returns arrays sorted by name — directly serializable with `JSON.generate`
|
|
14
|
+
- **`Dex::Tool` — ruby-llm integration** — bridges dexkit operations to [ruby-llm](https://rubyllm.com/) tools. `Dex::Tool.from(Op)` generates a `RubyLLM::Tool` from an operation's contract. `Dex::Tool.all` converts all registered operations. `Dex::Tool.from_namespace("Order")` filters by namespace. `Dex::Tool.explain_tool` provides a built-in preflight check tool. Lazy-loaded — ruby-llm is only required when you call `Dex::Tool`
|
|
15
|
+
- **`Dex::TypeSerializer`** — converts Literal types to human-readable strings and JSON Schema. Handles `String`, `Integer`, `Float`, `Boolean`, `Symbol`, `Hash`, `Date`, `Time`, `DateTime`, `BigDecimal`, `_Nilable`, `_Array`, `_Union`, `_Ref`, and range-constrained types (`_Integer(1..)`)
|
|
16
|
+
- **Rake task `dex:export`** — `rake dex:export` with `FORMAT=hash|json_schema`, `SECTION=operations|events|handlers`, `FILE=path` environment variables. Auto-loaded via Railtie in Rails apps
|
|
17
|
+
- **Rake task `dex:guides`** — `rake dex:guides` installs LLM-optimized guides as `AGENTS.md` files in app directories (`app/operations/`, `app/events/`, `app/event_handlers/`, `app/forms/`, `app/queries/`). Only writes to directories that exist. Stamps each file with the installed dexkit version. The event guide is installed to both `app/events/` and `app/event_handlers/` when either exists. Existing hand-written `AGENTS.md` files are detected and skipped (`FORCE=1` to overwrite). Override paths with `OPERATIONS_PATH`, `EVENTS_PATH`, `EVENT_HANDLERS_PATH`, `FORMS_PATH`, `QUERIES_PATH` environment variables
|
|
18
|
+
- **`explain` includes `description`** — `explain` output now contains `:description` when set on the operation
|
|
19
|
+
- **`explain` class method for operations** — `MyOp.explain(**kwargs)` returns a frozen Hash with the full preflight state: resolved props, context source tracking (`:explicit`/`:ambient`/`:default`), per-guard pass/fail results with messages, once key and status (`:fresh`/`:exists`/`:expired`/`:pending`/`:invalid`/`:misconfigured`/`:unavailable`), advisory lock key, record/transaction/rescue/callback settings, pipeline steps, and overall `callable` verdict (accounts for both guard failures and once blocking statuses). No side effects — `perform` is never called. Gracefully handles invalid props — returns partial results with `error` key instead of raising, class-level information always available. Respects pipeline customization — removed steps report inactive. Custom middleware can contribute via `_name_explain` class methods
|
|
20
|
+
|
|
21
|
+
### Breaking
|
|
22
|
+
|
|
23
|
+
- **`contract.to_h` returns rich format** — `contract.to_h` now returns a comprehensive serialized Hash with string-typed params, description, context, pipeline, and settings instead of the raw `Data#to_h` shape. Before: `contract.to_h[:success]` returned `String` (the class). After: it returns `"String"` (a string). Code doing type comparisons like `contract.to_h[:success] == String` must update to use `contract.success` (which still returns raw types) or compare against `"String"`. The raw Ruby types remain accessible via `contract.params`, `contract.success`, `contract.errors`, `contract.guards`
|
|
24
|
+
- **`_Ref` JSON Schema type changed from `"integer"` to `"string"`** — `_Ref(Model)` now serializes as `{ type: "string" }` in JSON Schema. IDs are treated as opaque strings to support Mongoid BSON::ObjectId, UUIDs, and other non-integer primary key formats. Code that relied on `type: "integer"` for Ref params must update
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`Handler.deregister` now unsubscribes from Bus** — `Dex::Event::Handler.deregister(klass)` removes the handler from both the registry and the event Bus. Previously, deregistered handlers remained subscribed and would still fire on published events
|
|
29
|
+
- **Registry prunes stale entries** — `registry` now removes unreachable class references from the backing Set during each call, preventing memory leaks from code reload cycles
|
|
30
|
+
- **`description(false)` and `desc: false` now raise `ArgumentError`** — previously accepted as "missing" values due to falsey evaluation. Both DSL methods now validate with `!text.nil?` / `!desc.nil?` to enforce the String requirement, matching the library's fail-fast convention
|
|
31
|
+
- **`prop_descriptions` no longer leaks parent descriptions for redeclared props** — when a child class redefines a prop without `desc:`, the parent's description is cleared instead of being inherited. Providing a new `desc:` on the child works as before
|
|
32
|
+
- **Rake task validates handler format** — `rake dex:export SECTION=handlers FORMAT=json_schema` now raises a clear error instead of hitting `Handler.export`'s `ArgumentError`
|
|
33
|
+
|
|
34
|
+
## [0.7.0] - 2026-03-08
|
|
35
|
+
|
|
36
|
+
### Breaking
|
|
37
|
+
|
|
38
|
+
- **Operation record schema refactored** — the `response` column is renamed to `result`, the `error` column is split into `error_code`, `error_message`, and `error_details`, `params` no longer has `default: {}` (nil means "not captured"), and `status` is now `null: false`. The `record response: false` DSL option is now `record result: false`. Status value `done` is renamed to `completed`, and a new `error` status represents business errors via `error!`
|
|
39
|
+
- **All outcomes now recorded** — previously, only successful operations were recorded in the sync path. Now business errors (`error!`) record with status `error` and populate `error_code`/`error_message`/`error_details`, and unhandled exceptions record with status `failed`
|
|
40
|
+
- **Recording moved outside transaction** — operation records are now persisted outside the database transaction, so error and failure records survive rollbacks. Previously, records were created inside the transaction and would be rolled back alongside the operation's side effects
|
|
41
|
+
- **Pipeline order changed** — `RecordWrapper` now runs before `TransactionWrapper` (was after). The pipeline order is now: result → once → lock → record → transaction → rescue → guard → callback
|
|
42
|
+
- **`Operation.contract` shape changed** — `Contract` gains a fourth field `:guards`. Code using positional destructuring (`in Contract[params, success, errors]`) must be updated to include the new field. Keyword-based access (`.params`, `.errors`, etc.) is unaffected
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Ambient context** — `Dex.with_context(current_user: user) { ... }` sets fiber-local ambient state. The `context` DSL on operations and events maps props to ambient keys, auto-filling them when not passed explicitly. Explicit kwargs always win. Works with guards (`callable?`), events (captured at publish time), and nested operations. Introspection via `context_mappings`
|
|
47
|
+
- **`guard` DSL for precondition checks** — named, inline preconditions that detect threats (conditions under which the operation should not proceed). Guards auto-declare error codes, support dependencies (`requires:`), collect all independent failures, and skip dependent guards when a dependency fails. `callable?` and `callable` class methods check guards without running `perform` – useful for UI show/hide, disabled buttons with reasons, and API pre-validation. Contract introspection via `contract.guards`. Test helpers: `assert_callable`, `refute_callable`
|
|
48
|
+
- **`once` DSL for operation idempotency** — ensures an operation executes at most once for a given key, replaying stored results on subsequent calls. Supports prop-based keys (`once :order_id`), composite keys, block-based custom keys, call-site keys (`.once("key")`), optional expiry (`expires_in:`), and `clear_once!` for key management. Business errors are replayed; exceptions release the key for retry. Works with `.safe.call` and `.async.call`
|
|
49
|
+
- **`error_code`, `error_message`, `error_details` columns** — structured error recording replaces the single `error` string column
|
|
50
|
+
- **Recommended indexes** — `name`, `status`, `[:name, :status]` composite index, and unique partial index on `once_key` in the migration schema
|
|
51
|
+
|
|
3
52
|
## [0.6.0] - 2026-03-07
|
|
4
53
|
|
|
5
54
|
### Added
|
data/README.md
CHANGED
|
@@ -69,6 +69,93 @@ end
|
|
|
69
69
|
Order::Fulfill.new(order_id: 123).async(queue: "fulfillment").call
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
**Idempotency** with `once` — run an operation at most once for a given key. Results are replayed on duplicates:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Payment::Charge < Dex::Operation
|
|
76
|
+
prop :order_id, Integer
|
|
77
|
+
prop :amount, Integer
|
|
78
|
+
|
|
79
|
+
once :order_id # key from prop
|
|
80
|
+
# once :order_id, :merchant_id # composite key
|
|
81
|
+
# once # all props as key
|
|
82
|
+
# once { "custom-#{order_id}" } # block-based key
|
|
83
|
+
# once :order_id, expires_in: 24.hours # expiring key
|
|
84
|
+
|
|
85
|
+
def perform
|
|
86
|
+
Gateway.charge!(order_id, amount)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Call-site key (overrides class-level declaration)
|
|
91
|
+
Payment::Charge.new(order_id: 1, amount: 500).once("ext-key-123").call
|
|
92
|
+
|
|
93
|
+
# Bypass once guard for a single call
|
|
94
|
+
Payment::Charge.new(order_id: 1, amount: 500).once(nil).call
|
|
95
|
+
|
|
96
|
+
# Clear a stored key to allow re-execution
|
|
97
|
+
Payment::Charge.clear_once!(order_id: 1)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Business errors are replayed; exceptions release the key so the operation can be retried. Requires the record backend (recording is enabled by default when `record_class` is configured).
|
|
101
|
+
|
|
102
|
+
**Guards** – inline precondition checks with introspection. Ask "can this operation run?" from views and controllers:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
guard :out_of_stock, "Product must be in stock" do
|
|
106
|
+
!product.in_stock?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# In a view or controller:
|
|
110
|
+
Order::Place.callable?(customer: customer, product: product, quantity: 1)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Ambient context** – declare which props come from ambient state. Set once in a controller, auto-fill everywhere:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
class Order::Place < Dex::Operation
|
|
117
|
+
prop :product, _Ref(Product)
|
|
118
|
+
prop :customer, _Ref(Customer)
|
|
119
|
+
context customer: :current_customer # filled from Dex.context[:current_customer]
|
|
120
|
+
|
|
121
|
+
def perform
|
|
122
|
+
Order.create!(product: product, customer: customer)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Controller
|
|
127
|
+
Dex.with_context(current_customer: current_customer) do
|
|
128
|
+
Order::Place.call(product: product) # customer auto-filled
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Tests – just pass it explicitly
|
|
132
|
+
Order::Place.call(product: product, customer: customer)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Explain** – full preflight check in one call. Context, guards, idempotency, locks, settings – everything the operation would do, without doing it:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
info = Order::Place.explain(product: product, customer: customer, quantity: 2)
|
|
139
|
+
info[:callable] # => true (all guards pass)
|
|
140
|
+
info[:once][:status] # => :fresh (would execute, not replay)
|
|
141
|
+
info[:context][:source] # => { customer: :ambient }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Registry & Export** — list all operations, export contracts as JSON or JSON Schema, and bridge to LLM function-calling via [ruby-llm](https://rubyllm.com/):
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# List all operations
|
|
148
|
+
Dex::Operation.registry # => #<Set: {Order::Place, Order::Cancel, ...}>
|
|
149
|
+
|
|
150
|
+
# Export contracts
|
|
151
|
+
Dex::Operation.export(format: :json_schema)
|
|
152
|
+
|
|
153
|
+
# LLM tools (requires ruby-llm gem)
|
|
154
|
+
chat = RubyLLM.chat
|
|
155
|
+
chat.with_tools(*Dex::Tool.all)
|
|
156
|
+
chat.ask("Place an order for 2 units of product #42")
|
|
157
|
+
```
|
|
158
|
+
|
|
72
159
|
**Transactions** on by default, **advisory locking**, **recording** to database, **callbacks**, and a customizable **pipeline** – all composable, all optional.
|
|
73
160
|
|
|
74
161
|
### Testing
|
|
@@ -263,13 +350,18 @@ Full documentation at **[dex.razorjack.net](https://dex.razorjack.net)**.
|
|
|
263
350
|
|
|
264
351
|
## AI Coding Assistant Setup
|
|
265
352
|
|
|
266
|
-
dexkit ships LLM-optimized guides.
|
|
353
|
+
dexkit ships LLM-optimized guides. Install them as `AGENTS.md` files in your app directories so AI coding agents automatically know the API:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
rake dex:guides
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
This copies guides into directories that exist (`app/operations/`, `app/events/`, `app/event_handlers/`, `app/forms/`, `app/queries/`), stamped with the installed dexkit version. Re-run after upgrading dexkit to sync. Existing hand-written `AGENTS.md` files are never overwritten (use `FORCE=1` to override).
|
|
360
|
+
|
|
361
|
+
Override paths for non-standard directory names:
|
|
267
362
|
|
|
268
363
|
```bash
|
|
269
|
-
|
|
270
|
-
cp $(bundle show dexkit)/guides/llm/EVENT.md app/event_handlers/CLAUDE.md
|
|
271
|
-
cp $(bundle show dexkit)/guides/llm/FORM.md app/forms/CLAUDE.md
|
|
272
|
-
cp $(bundle show dexkit)/guides/llm/QUERY.md app/queries/CLAUDE.md
|
|
364
|
+
rake dex:guides OPERATIONS_PATH=app/services
|
|
273
365
|
```
|
|
274
366
|
|
|
275
367
|
## License
|
data/guides/llm/EVENT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dex::Event — LLM Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Install with `rake dex:guides` or copy manually to `app/events/AGENTS.md`.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -235,21 +235,43 @@ Persistence failures are silently rescued — they never halt event publishing.
|
|
|
235
235
|
|
|
236
236
|
---
|
|
237
237
|
|
|
238
|
-
## Context
|
|
238
|
+
## Ambient Context
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
Events use the same `context` DSL as operations. Context-mapped props are captured at **publish time** and stored as regular props on the event — handlers don't need ambient context, they read from the event.
|
|
241
241
|
|
|
242
242
|
```ruby
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
Current.tenant = ctx["tenant"]
|
|
248
|
-
}
|
|
243
|
+
class Order::Placed < Dex::Event
|
|
244
|
+
prop :order_id, Integer
|
|
245
|
+
prop :customer, _Ref(Customer)
|
|
246
|
+
context customer: :current_customer # resolved at publish time
|
|
249
247
|
end
|
|
248
|
+
|
|
249
|
+
# In a controller with Dex.with_context(current_customer: customer):
|
|
250
|
+
Order::Placed.publish(order_id: 1) # customer auto-filled from context
|
|
251
|
+
|
|
252
|
+
# Or pass explicitly:
|
|
253
|
+
Order::Placed.publish(order_id: 1, customer: customer)
|
|
250
254
|
```
|
|
251
255
|
|
|
252
|
-
|
|
256
|
+
Handlers receive the event with everything already set — no `context` needed on handlers:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
class AuditTrail < Dex::Event::Handler
|
|
260
|
+
on Order::Placed
|
|
261
|
+
|
|
262
|
+
def perform
|
|
263
|
+
AuditLog.create!(customer: event.customer, action: "placed", order_id: event.order_id)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Resolution order:** explicit kwarg → ambient context → prop default → TypeError.
|
|
269
|
+
|
|
270
|
+
**Introspection:** `MyEvent.context_mappings` returns the mapping hash.
|
|
271
|
+
|
|
272
|
+
### Legacy Context (Metadata)
|
|
273
|
+
|
|
274
|
+
The older `event_context` / `restore_event_context` configuration captures arbitrary metadata at publish time and restores it before async handler execution. Both mechanisms coexist.
|
|
253
275
|
|
|
254
276
|
---
|
|
255
277
|
|
|
@@ -361,4 +383,46 @@ end
|
|
|
361
383
|
|
|
362
384
|
---
|
|
363
385
|
|
|
386
|
+
## Registry, Export & Description
|
|
387
|
+
|
|
388
|
+
### Description
|
|
389
|
+
|
|
390
|
+
Events can declare a human-readable description. Props can include `desc:`:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
class Order::Placed < Dex::Event
|
|
394
|
+
description "Emitted after an order is successfully placed"
|
|
395
|
+
|
|
396
|
+
prop :order_id, Integer, desc: "The placed order"
|
|
397
|
+
prop :total, BigDecimal, desc: "Order total"
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Registry
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
Dex::Event.registry # => #<Set: {Order::Placed, Order::Cancelled, ...}>
|
|
405
|
+
Dex::Event::Handler.registry # => #<Set: {NotifyWarehouse, SendConfirmation, ...}>
|
|
406
|
+
Dex::Event.deregister(klass)
|
|
407
|
+
Dex::Event::Handler.deregister(klass)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Export
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
Order::Placed.to_h
|
|
414
|
+
# => { name: "Order::Placed", description: "...", props: { order_id: { type: "Integer", ... } } }
|
|
415
|
+
|
|
416
|
+
Order::Placed.to_json_schema # JSON Schema (Draft 2020-12)
|
|
417
|
+
|
|
418
|
+
NotifyWarehouse.to_h
|
|
419
|
+
# => { name: "NotifyWarehouse", events: ["Order::Placed"], retries: 3, ... }
|
|
420
|
+
|
|
421
|
+
Dex::Event.export # all events as hashes
|
|
422
|
+
Dex::Event.export(format: :json_schema) # all as JSON Schema
|
|
423
|
+
Dex::Event::Handler.export # all handlers as hashes
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
364
428
|
**End of reference.**
|
data/guides/llm/FORM.md
CHANGED