woods 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +175 -2
- data/exe/woods-console-mcp +4 -0
- data/exe/woods-mcp +4 -0
- data/lib/woods/extractors/model_extractor.rb +4 -1
- data/lib/woods/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec72d151147ef4b43df866b6ebee48f52a26915efdb44df7bed08c24646cfb7a
|
|
4
|
+
data.tar.gz: f03a813c3f525eeba7a95269770ee80ee4c4fd91017561529800745a241cfd9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75608caf708f2f4ef913653af543e983ac68abb9ab0028f9e967bda13af4e5b0c47f73a1d440696b453fb08afc69ab56cf2d70d366aa29949676e29abb6cdc85
|
|
7
|
+
data.tar.gz: a0f0e086045967cee236f4d98bd6131e5ca8c99daf7a150aa395c60dc56665bb3f4cc5a615d101480c1f8da752fd38a35893662ebf4cda02f49f008aeb6bc081
|
data/README.md
CHANGED
|
@@ -65,6 +65,40 @@ Woods boots your Rails app, introspects everything using runtime APIs, and write
|
|
|
65
65
|
|
|
66
66
|
Your `User` model includes `Auditable`, `Searchable`, and `SoftDeletable`. An AI tool reading `app/models/user.rb` sees 40 lines. Woods inlines all three concerns directly into the extracted unit — the AI sees the full 200-line behavioral surface area in one block.
|
|
67
67
|
|
|
68
|
+
```ruby
|
|
69
|
+
# What your AI sees (app/models/user.rb) — 4 lines:
|
|
70
|
+
class User < ApplicationRecord
|
|
71
|
+
include Auditable
|
|
72
|
+
include Searchable
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# What Woods produces — full source with schema + inlined concerns:
|
|
76
|
+
# == Schema Information
|
|
77
|
+
# email :string not null
|
|
78
|
+
# name :string
|
|
79
|
+
#
|
|
80
|
+
# class User < ApplicationRecord
|
|
81
|
+
# include Auditable
|
|
82
|
+
# include Searchable
|
|
83
|
+
# validates :email, presence: true, uniqueness: true
|
|
84
|
+
# ...
|
|
85
|
+
# end
|
|
86
|
+
#
|
|
87
|
+
# ┌─────────────────────────────────────────────────────────────────────┐
|
|
88
|
+
# │ Included from: Auditable │
|
|
89
|
+
# └─────────────────────────────────────────────────────────────────────┘
|
|
90
|
+
# def audit_trail ...
|
|
91
|
+
# ─────────────────────────── End Auditable ───────────────────────────
|
|
92
|
+
#
|
|
93
|
+
# ┌─────────────────────────────────────────────────────────────────────┐
|
|
94
|
+
# │ Included from: Searchable │
|
|
95
|
+
# └─────────────────────────────────────────────────────────────────────┘
|
|
96
|
+
# scope :search, ->(q) { where("name ILIKE ?", "%#{q}%") }
|
|
97
|
+
# ─────────────────────────── End Searchable ───────────────────────────
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The `metadata[:inlined_concerns]` array lists which concerns were resolved, so retrieval can filter by concern inclusion.
|
|
101
|
+
|
|
68
102
|
### Schema Prepending
|
|
69
103
|
|
|
70
104
|
Model source gets a header with actual column types, indexes, and foreign keys pulled from the live database. No more guessing whether `name` is a `string` or `text`, or whether there's an index on `email`.
|
|
@@ -83,6 +117,122 @@ Controller source gets a route map prepended showing the real HTTP verb + path +
|
|
|
83
117
|
|
|
84
118
|
---
|
|
85
119
|
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
### Extracted Model with Schema and Associations
|
|
123
|
+
|
|
124
|
+
After extraction, each model is a self-contained JSON file with schema, associations, validations, and inlined concern source:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"type": "model",
|
|
129
|
+
"identifier": "Order",
|
|
130
|
+
"file_path": "app/models/order.rb",
|
|
131
|
+
"source_code": "# == Schema Information\n# id :bigint not null, pk\n# user_id :bigint not null, fk\n# status :string default(\"pending\")\n# total_cents :integer\n#\nclass Order < ApplicationRecord\n belongs_to :user\n has_many :line_items\n validates :status, inclusion: { in: %w[pending paid shipped] }\n ...\nend\n\n# ┌───────────────────────────────────────────────────────────────────┐\n# │ Included from: Auditable │\n# └───────────────────────────────────────────────────────────────────┘\n# module Auditable\n# ...\n# end\n# ──────────────────────── End Auditable ────────────────────────────",
|
|
132
|
+
"metadata": {
|
|
133
|
+
"associations": [
|
|
134
|
+
{ "type": "belongs_to", "name": "user", "target": "User" },
|
|
135
|
+
{ "type": "has_many", "name": "line_items", "target": "LineItem" }
|
|
136
|
+
],
|
|
137
|
+
"validations": [
|
|
138
|
+
{ "attribute": "status", "type": "inclusion", "options": { "in": ["pending", "paid", "shipped"] } }
|
|
139
|
+
],
|
|
140
|
+
"enums": { "status": { "pending": 0, "active": 1, "shipped": 2 } },
|
|
141
|
+
"scopes": [{ "name": "active", "source": "-> { where(status: :active) }" }],
|
|
142
|
+
"inlined_concerns": ["Auditable"]
|
|
143
|
+
},
|
|
144
|
+
"dependencies": [
|
|
145
|
+
{ "type": "model", "target": "User", "via": "belongs_to" },
|
|
146
|
+
{ "type": "model", "target": "LineItem", "via": "has_many" }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Callback Chain with Side-Effects
|
|
152
|
+
|
|
153
|
+
Woods resolves the full callback chain in execution order and detects side-effects — which columns get written, which jobs get enqueued, which mailers fire:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
"callbacks": [
|
|
157
|
+
{ "type": "before_validation", "filter": "normalize_email", "kind": "before", "conditions": {} },
|
|
158
|
+
{ "type": "before_save", "filter": "set_slug", "kind": "before", "conditions": {},
|
|
159
|
+
"side_effects": { "columns_written": ["slug"], "jobs_enqueued": [], "services_called": [], "mailers_triggered": [], "database_reads": [], "operations": [] } },
|
|
160
|
+
{ "type": "after_commit", "filter": "send_welcome", "kind": "after", "conditions": {},
|
|
161
|
+
"side_effects": { "columns_written": [], "jobs_enqueued": ["WelcomeEmailJob"], "services_called": [], "mailers_triggered": ["UserMailer"], "database_reads": [], "operations": [] } }
|
|
162
|
+
]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Side-effects are detected by `CallbackAnalyzer`, which scans callback method bodies for patterns like `self.col =` (column writes), `perform_later` (job enqueues), and `deliver_later` (mailer triggers). This is the #1 thing AI tools get wrong about Rails models.
|
|
166
|
+
|
|
167
|
+
### Route-to-Controller Lookup
|
|
168
|
+
|
|
169
|
+
Every route becomes its own `ExtractedUnit` with the controller and action bound from the live routing table:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"type": "route",
|
|
174
|
+
"identifier": "POST /checkout",
|
|
175
|
+
"metadata": {
|
|
176
|
+
"controller": "orders",
|
|
177
|
+
"action": "create",
|
|
178
|
+
"route_name": "checkout"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
To find which controller handles a URL, use the MCP `search` tool:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{ "tool": "search", "params": { "query": "/checkout", "types": ["route"] } }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This returns all matching route units with their controller and action — no guessing about custom routes, nested resources, or engine mount points.
|
|
190
|
+
|
|
191
|
+
### Looking Up a Model's Full Structure
|
|
192
|
+
|
|
193
|
+
Use the MCP `lookup` tool to get a model's complete JSON representation — schema, associations, validations, callbacks, and inlined concerns in one call:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{ "tool": "lookup", "params": { "identifier": "Order", "include_source": true } }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Returns the full `ExtractedUnit` JSON shown in the example above, including `source_code` (with schema header and inlined concerns), `metadata` (associations, callbacks, validations, enums, scopes), `dependencies`, and `dependents`.
|
|
200
|
+
|
|
201
|
+
To get just the structured metadata without source code:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{ "tool": "lookup", "params": { "identifier": "Order", "include_source": false, "sections": ["metadata"] } }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Finding Jobs Enqueued by a Service
|
|
208
|
+
|
|
209
|
+
Use the MCP `dependencies` tool to trace what a service triggers:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{ "tool": "dependencies", "params": { "identifier": "CheckoutService", "depth": 2, "types": ["job"] } }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Returns all job units reachable from `CheckoutService` within 2 hops — including jobs triggered indirectly via model callbacks (e.g., `CheckoutService` → `Order` → `OrderConfirmationJob`).
|
|
216
|
+
|
|
217
|
+
### Runtime-Generated Method Detection
|
|
218
|
+
|
|
219
|
+
Because Woods runs inside the booted Rails process, it captures every method Rails generates dynamically — enum predicates, association builders, attribute accessors, and scope methods that static analysis tools cannot see:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"identifier": "Order",
|
|
224
|
+
"metadata": {
|
|
225
|
+
"enums": { "status": { "pending": 0, "active": 1, "shipped": 2 } },
|
|
226
|
+
"scopes": [{ "name": "active", "source": "-> { where(status: :active) }" }],
|
|
227
|
+
"associations": [{ "type": "has_many", "name": "line_items", "target": "LineItem" }]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Static tools miss `status_active?`, `status_pending?`, `build_line_item`, `create_line_item!`, and dynamically registered scopes. Woods captures all of these because it queries the runtime class via `instance_methods(false)` after Rails has processed every DSL declaration.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
86
236
|
## Connect to Your AI Tool
|
|
87
237
|
|
|
88
238
|
Woods ships two MCP servers. Most users only need the **Index Server**.
|
|
@@ -223,6 +373,26 @@ Woods is backend-agnostic. Your app database, vector store, embedding provider,
|
|
|
223
373
|
|
|
224
374
|
See [Backend Matrix](docs/BACKEND_MATRIX.md) for supported combinations and [Configuration Reference](docs/CONFIGURATION_REFERENCE.md) for every option with defaults.
|
|
225
375
|
|
|
376
|
+
### Environment-Specific Configuration
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
Woods.configure do |config|
|
|
380
|
+
config.output_dir = Rails.root.join('tmp/woods')
|
|
381
|
+
|
|
382
|
+
# CI: only extract models and controllers for faster builds
|
|
383
|
+
config.extractors = %i[models controllers] if ENV['CI']
|
|
384
|
+
|
|
385
|
+
# Environment-conditional embedding provider
|
|
386
|
+
if ENV['OPENAI_API_KEY']
|
|
387
|
+
config.embedding_provider = :openai
|
|
388
|
+
config.embedding_options = { api_key: ENV['OPENAI_API_KEY'] }
|
|
389
|
+
else
|
|
390
|
+
config.embedding_provider = :ollama
|
|
391
|
+
config.embedding_options = { base_url: 'http://localhost:11434' }
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
226
396
|
---
|
|
227
397
|
|
|
228
398
|
## Keeping the Index Current
|
|
@@ -289,12 +459,15 @@ Everything flows through `ExtractedUnit` — the universal data structure. Each
|
|
|
289
459
|
|-------|-----------------|
|
|
290
460
|
| `identifier` | Class name or descriptive key (`"User"`, `"POST /orders"`) |
|
|
291
461
|
| `type` | Category (`:model`, `:controller`, `:service`, `:job`, etc.) |
|
|
462
|
+
| `file_path` | Source file location relative to Rails root |
|
|
463
|
+
| `namespace` | Module namespace (`"Admin"`, `nil` for top-level) |
|
|
292
464
|
| `source_code` | Annotated source with inlined concerns and schema |
|
|
293
465
|
| `metadata` | Structured data — associations, callbacks, routes, fields |
|
|
294
466
|
| `dependencies` | What this unit depends on (forward edges) |
|
|
295
467
|
| `dependents` | What depends on this unit (reverse edges) |
|
|
296
468
|
| `chunks` | Semantic sub-sections for large units |
|
|
297
|
-
| `
|
|
469
|
+
| `extracted_at` | ISO 8601 timestamp of extraction |
|
|
470
|
+
| `source_hash` | SHA-256 digest for change detection |
|
|
298
471
|
|
|
299
472
|
### Output Structure
|
|
300
473
|
|
|
@@ -323,7 +496,7 @@ tmp/woods/
|
|
|
323
496
|
│ │
|
|
324
497
|
│ ┌────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
|
|
325
498
|
│ │ Extract │───>│ Resolve │───>│ Write JSON │ │
|
|
326
|
-
│ │
|
|
499
|
+
│ │ 33 types │ │ graph + │ │ per unit │ │
|
|
327
500
|
│ │ │ │ git data │ │ │ │
|
|
328
501
|
│ └────────────┘ └─────────────┘ └──────────────────────┘ │
|
|
329
502
|
└──────────────────────────────────────────────────────────────────┘
|
data/exe/woods-console-mcp
CHANGED
|
@@ -17,6 +17,10 @@ require_relative '../lib/woods/console/server'
|
|
|
17
17
|
config_path = ENV.fetch('WOODS_CONSOLE_CONFIG', File.expand_path('~/.woods/console.yml'))
|
|
18
18
|
config = File.exist?(config_path) ? YAML.safe_load_file(config_path) : {}
|
|
19
19
|
|
|
20
|
+
# Suppress json-schema MultiJSON deprecation notice that would pollute stderr
|
|
21
|
+
# during MCP stdio transport. The notice fires when multi_json is in the bundle.
|
|
22
|
+
JSON::Validator.use_multi_json = false if defined?(JSON::Validator) && JSON::Validator.respond_to?(:use_multi_json=)
|
|
23
|
+
|
|
20
24
|
server = Woods::Console::Server.build(config: config)
|
|
21
25
|
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
22
26
|
transport.open
|
data/exe/woods-mcp
CHANGED
|
@@ -19,6 +19,10 @@ require_relative '../lib/woods/mcp/bootstrapper'
|
|
|
19
19
|
require_relative '../lib/woods/embedding/text_preparer'
|
|
20
20
|
require_relative '../lib/woods/embedding/indexer'
|
|
21
21
|
|
|
22
|
+
# Suppress json-schema MultiJSON deprecation notice that would pollute stderr
|
|
23
|
+
# during MCP stdio transport. The notice fires when multi_json is in the bundle.
|
|
24
|
+
JSON::Validator.use_multi_json = false if defined?(JSON::Validator) && JSON::Validator.respond_to?(:use_multi_json=)
|
|
25
|
+
|
|
22
26
|
index_dir = Woods::MCP::Bootstrapper.resolve_index_dir(ARGV)
|
|
23
27
|
retriever = Woods::MCP::Bootstrapper.build_retriever
|
|
24
28
|
snapshot_store = Woods::MCP::Bootstrapper.build_snapshot_store(index_dir)
|
|
@@ -327,6 +327,9 @@ module Woods
|
|
|
327
327
|
callbacks: extract_callbacks(model),
|
|
328
328
|
scopes: extract_scopes(model, source),
|
|
329
329
|
enums: extract_enums(model),
|
|
330
|
+
inlined_concerns: extract_included_modules(model)
|
|
331
|
+
.select { |mod| mod.name && concern_source(mod) }
|
|
332
|
+
.map { |mod| mod.name.demodulize },
|
|
330
333
|
|
|
331
334
|
# API surface
|
|
332
335
|
class_methods: model.methods(false).sort,
|
|
@@ -611,7 +614,7 @@ module Woods
|
|
|
611
614
|
def extract_dependencies(model, source = nil)
|
|
612
615
|
# Associations point to other models
|
|
613
616
|
deps = model.reflect_on_all_associations.filter_map do |assoc|
|
|
614
|
-
{ type: :model, target: assoc.class_name, via:
|
|
617
|
+
{ type: :model, target: assoc.class_name, via: assoc.macro }
|
|
615
618
|
rescue NameError => e
|
|
616
619
|
@warnings << "[#{model.name}] Skipping broken association dep #{assoc.name}: #{e.message}"
|
|
617
620
|
nil
|
data/lib/woods/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: woods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leah Armstrong
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mcp
|