woods 1.6.1 → 2.0.0.beta2
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 +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
# Configuration Reference
|
|
2
|
+
|
|
3
|
+
All configuration is done via the `Woods.configure` block, typically in `config/initializers/woods.rb`.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Woods.configure do |config|
|
|
7
|
+
config.output_dir = Rails.root.join('tmp/woods')
|
|
8
|
+
config.max_context_tokens = 8000
|
|
9
|
+
# ...
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Options index
|
|
14
|
+
|
|
15
|
+
- [Common configuration patterns](#common-configuration-patterns)
|
|
16
|
+
- [CI-only extraction (skip framework sources)](#ci-only-extraction-skip-framework-sources)
|
|
17
|
+
- [Docker extraction with environment-based paths](#docker-extraction-with-environment-based-paths)
|
|
18
|
+
- [Environment-conditional embedding provider](#environment-conditional-embedding-provider)
|
|
19
|
+
- [Core options](#core-options)
|
|
20
|
+
- [Embedding options](#embedding-options)
|
|
21
|
+
- [OpenAI embeddings](#openai-embeddings)
|
|
22
|
+
- [Ollama embeddings](#ollama-embeddings)
|
|
23
|
+
- [Fake embeddings (CI / sandboxes / offline hosts)](#fake-embeddings-ci--sandboxes--offline-hosts)
|
|
24
|
+
- [Injecting a provider object](#injecting-a-provider-object)
|
|
25
|
+
- [Storage options](#storage-options)
|
|
26
|
+
- [pgvector (PostgreSQL)](#pgvector-postgresql)
|
|
27
|
+
- [Qdrant](#qdrant)
|
|
28
|
+
- [SQLite metadata](#sqlite-metadata)
|
|
29
|
+
- [In-memory metadata](#in-memory-metadata)
|
|
30
|
+
- [Retrieval cache options](#retrieval-cache-options)
|
|
31
|
+
- [Deployment shapes](#deployment-shapes)
|
|
32
|
+
- [Shape 2 setup (`:shared_filesystem`)](#shape-2-setup-shared_filesystem)
|
|
33
|
+
- [Presets](#presets)
|
|
34
|
+
- [Pipeline options](#pipeline-options)
|
|
35
|
+
- [Session tracer options](#session-tracer-options)
|
|
36
|
+
- [Gem indexing](#gem-indexing)
|
|
37
|
+
- [Extractors](#extractors)
|
|
38
|
+
- [Component directories](#component-directories)
|
|
39
|
+
- [Console MCP options](#console-mcp-options)
|
|
40
|
+
- [Environment variables](#environment-variables)
|
|
41
|
+
- [Index server (`woods-mcp` / `woods-mcp-http` / `woods-mcp-start`)](#index-server-woods-mcp--woods-mcp-http--woods-mcp-start)
|
|
42
|
+
- [Rake tasks](#rake-tasks)
|
|
43
|
+
- [HTTP transport (`woods-mcp-http`)](#http-transport-woods-mcp-http)
|
|
44
|
+
- [Console server (`woods-console-mcp`)](#console-server-woods-console-mcp)
|
|
45
|
+
- [Watch daemon (`woods:watch`)](#watch-daemon-woodswatch)
|
|
46
|
+
- [Extraction rake tasks](#extraction-rake-tasks)
|
|
47
|
+
- [Exporters](#exporters)
|
|
48
|
+
- [Database compatibility](#database-compatibility)
|
|
49
|
+
|
|
50
|
+
## Common configuration patterns
|
|
51
|
+
|
|
52
|
+
### CI-only extraction (skip framework sources)
|
|
53
|
+
|
|
54
|
+
`config.extractors` cannot select a subset of extractors, it's accepted for
|
|
55
|
+
forward compatibility only (see [Extractors](#extractors) below). To speed up
|
|
56
|
+
CI, skip the one extractor that's actually optional instead:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Woods.configure do |config|
|
|
60
|
+
config.output_dir = Rails.root.join('tmp/woods')
|
|
61
|
+
|
|
62
|
+
# In CI, skip Rails/gem framework source extraction for faster builds
|
|
63
|
+
config.include_framework_sources = false if ENV['CI']
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Docker extraction with environment-based paths
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Woods.configure do |config|
|
|
71
|
+
# Inside Docker, /app is the Rails root
|
|
72
|
+
config.output_dir = ENV.fetch('WOODS_OUTPUT', Rails.root.join('tmp/woods'))
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Environment-conditional embedding provider
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
Woods.configure do |config|
|
|
80
|
+
# Use OpenAI in production/CI where the API key is set,
|
|
81
|
+
# fall back to Ollama for local development (free, no API key needed)
|
|
82
|
+
if ENV['OPENAI_API_KEY']
|
|
83
|
+
config.embedding_provider = :openai
|
|
84
|
+
config.embedding_model = 'text-embedding-3-small'
|
|
85
|
+
config.embedding_options = { api_key: ENV['OPENAI_API_KEY'] }
|
|
86
|
+
else
|
|
87
|
+
config.embedding_provider = :ollama
|
|
88
|
+
config.embedding_options = {
|
|
89
|
+
model: 'nomic-embed-text',
|
|
90
|
+
host: ENV.fetch('OLLAMA_URL', 'http://localhost:11434')
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Core options
|
|
99
|
+
|
|
100
|
+
Columns:
|
|
101
|
+
|
|
102
|
+
- **User-settable**: a direct `Woods.configure { |c| c.<option> = ... }` writes the value verbatim.
|
|
103
|
+
- **Preset-derived**: set by `Builder.preset_config(:local | :shared_filesystem | :postgresql | :production)` as a group. You can override any preset value afterwards in the `configure` block, later writes win.
|
|
104
|
+
- **Computed**: derived from other options at read time (or at `build_*` time by `Woods::Builder`). Writing directly has no effect; change the inputs instead.
|
|
105
|
+
|
|
106
|
+
| Option | Type | Default | Role | Description |
|
|
107
|
+
|--------|------|---------|------|-------------|
|
|
108
|
+
| `output_dir` | Pathname/String | `Rails.root.join('tmp/woods')` | user-settable | Directory where extracted data is written |
|
|
109
|
+
| `extractors` | Array<Symbol> | `[:models, :controllers, :services, ...]` | accepted, not implemented | Does not select which extractors run. See [Extractors](#extractors) below. |
|
|
110
|
+
| `pretty_json` | Boolean | `true` | user-settable | Format extracted JSON with indentation |
|
|
111
|
+
| `max_context_tokens` | Integer | `8000` | user-settable | Maximum tokens for retrieval context windows |
|
|
112
|
+
| `similarity_threshold` | Float | `0.7` | user-settable | Minimum similarity score (0.0-1.0) for retrieval results |
|
|
113
|
+
| `context_format` | Symbol | `:markdown` | user-settable | Output format for retrieval: `:claude`, `:markdown`, `:plain`, `:json` |
|
|
114
|
+
| `include_framework_sources` | Boolean | `true` | user-settable | Extract Rails and gem source code |
|
|
115
|
+
| `concurrent_extraction` | Boolean | `false` | user-settable | Enable parallel extraction (experimental) |
|
|
116
|
+
| `vector_store` / `metadata_store` / `graph_store` / `embedding_provider` | Symbol | n/a | preset-derived | Adapter types. Set by presets; override individually to mix stacks. |
|
|
117
|
+
| chars-per-token ratio (used by ContextAssembler, TextPreparer, Builder, cost_model) | Float | `4.0` (OpenAI) / `1.5` (Ollama) | computed | Derived from the active embedding provider via `Woods::TokenUtils.chars_per_token_for(...)`. Not directly user-settable; change `embedding_provider` to change the ratio. |
|
|
118
|
+
|
|
119
|
+
## Embedding options
|
|
120
|
+
|
|
121
|
+
| Option | Type | Default | Description |
|
|
122
|
+
|--------|------|---------|-------------|
|
|
123
|
+
| `embedding_provider` | Symbol or Object | n/a | Embedding backend: `:openai`, `:ollama`, `:fake` (deterministic, offline, see below), or an already-constructed provider object responding to `#embed`/`#embed_batch` |
|
|
124
|
+
| `embedding_model` | String | `'text-embedding-3-small'` | Model name for the embedding provider |
|
|
125
|
+
| `embedding_options` | Hash | `nil` | Provider-specific options (see below) |
|
|
126
|
+
|
|
127
|
+
### OpenAI embeddings
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
config.embedding_provider = :openai
|
|
131
|
+
config.embedding_model = 'text-embedding-3-small'
|
|
132
|
+
config.embedding_options = {
|
|
133
|
+
api_key: ENV['OPENAI_API_KEY'],
|
|
134
|
+
dimensions: 1536
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Ollama embeddings
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
config.embedding_provider = :ollama
|
|
142
|
+
config.embedding_options = {
|
|
143
|
+
model: 'nomic-embed-text',
|
|
144
|
+
host: 'http://localhost:11434'
|
|
145
|
+
# num_ctx: 2048 # Optional override, see below
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The provider reads `model:`, `host:`, and `num_ctx:` from `embedding_options`. `num_ctx` is auto-selected from a per-model registry (`nomic-embed-text` → 2048, `bge-m3` → 8192, `mxbai-embed-large` → 512, `snowflake-arctic-embed` → 512, `snowflake-arctic-embed2` → 8192, `all-minilm` → 512). Unknown models fall back to 2048, matching Ollama's conservative embedding default. Set `num_ctx:` explicitly only when running a model with a known-larger native context that isn't in the registry yet.
|
|
150
|
+
|
|
151
|
+
**Why `num_ctx` is capped at the native context.** Ollama has an open regression ([ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)) where `options.num_ctx` does not lift the effective ceiling on `/api/embed` for models whose native context is smaller than the override. Woods advertises the native ceiling so the chunker sizes inputs to what Ollama will actually accept.
|
|
152
|
+
|
|
153
|
+
**Optional exact tokenization.** Install the [`tokenizers`](https://github.com/ankane/tokenizers-ruby) gem alongside Woods to get BERT WordPiece token counting. Without it, Woods falls back to a chars/token ratio, which under-counts dense Ruby source (CamelCase constants, callback DSLs) and can silently over-pack chunks. Recommended for any Ollama setup.
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# Gemfile (optional)
|
|
157
|
+
gem 'tokenizers', '~> 0.5'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See [EMBEDDING_MODELS.md](EMBEDDING_MODELS.md) for the full model comparison and the procedure for adding a new model to the registry.
|
|
161
|
+
|
|
162
|
+
### Fake embeddings (CI / sandboxes / offline hosts)
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
config.embedding_provider = :fake
|
|
166
|
+
config.embedding_options = { dims: 128 } # optional; default 128
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`:fake` wires `Woods::Embedding::Provider::Fake`, a deterministic bag-of-words hashing provider that needs no network endpoint, so `rake woods:embed` and `rake woods:retrieve` run in CI, sandboxes, and offline hosts. Vectors are L2-normalized, so cosine similarity stays mechanically meaningful (texts sharing vocabulary rank closer), but they are **not semantically meaningful embeddings**: use `:fake` for pipeline smoke tests, never for production retrieval quality. It pairs with any configured store stack: `:in_memory` everywhere for a self-contained smoke run, or the same pgvector/Qdrant + SQLite stores a real provider would use (MySQL-backed hosts pair with Qdrant exactly as in the [backend matrix](BACKEND_MATRIX.md); the provider itself never touches the database).
|
|
170
|
+
|
|
171
|
+
### Injecting a provider object
|
|
172
|
+
|
|
173
|
+
Anything responding to `#embed` and `#embed_batch` can be assigned directly, it is used as-is and wrapped in the same retry/circuit-breaker resilience stack as the built-in adapters:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
config.embedding_provider = MyCompany::CustomEmbedder.new(endpoint: internal_url)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Storage options
|
|
180
|
+
|
|
181
|
+
| Option | Type | Default | Description |
|
|
182
|
+
|--------|------|---------|-------------|
|
|
183
|
+
| `vector_store` | Symbol | n/a | Vector backend: `:in_memory`, `:pgvector`, `:qdrant` |
|
|
184
|
+
| `vector_store_options` | Hash | `nil` | Backend-specific connection options |
|
|
185
|
+
| `metadata_store` | Symbol | n/a | Metadata backend: `:in_memory`, `:sqlite` |
|
|
186
|
+
| `metadata_store_options` | Hash | `nil` | Backend-specific options |
|
|
187
|
+
| `graph_store` | Symbol | n/a | Graph backend: `:in_memory` |
|
|
188
|
+
|
|
189
|
+
### pgvector (PostgreSQL)
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
config.vector_store = :pgvector
|
|
193
|
+
config.vector_store_options = {
|
|
194
|
+
connection: ActiveRecord::Base.connection,
|
|
195
|
+
dimensions: 1536
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Requires the pgvector extension. Run the generator to create migrations:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bundle exec rails generate woods:pgvector
|
|
203
|
+
bundle exec rails db:migrate
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Qdrant
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
config.vector_store = :qdrant
|
|
210
|
+
config.vector_store_options = {
|
|
211
|
+
url: 'http://localhost:6333',
|
|
212
|
+
collection: 'woods',
|
|
213
|
+
dimensions: 1536,
|
|
214
|
+
allow_private_hosts: true # explicit opt-in for trusted localhost/private URLs
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### SQLite metadata
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
config.metadata_store = :sqlite
|
|
222
|
+
config.metadata_store_options = {
|
|
223
|
+
database: Rails.root.join('tmp/woods/metadata.sqlite3').to_s
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Requires the `sqlite3` gem in your host bundle. Rails apps backed by
|
|
228
|
+
MySQL or PostgreSQL won't have it by default, selecting `:sqlite`
|
|
229
|
+
without it raises `Woods::ConfigurationError` with install
|
|
230
|
+
instructions. For MySQL/Postgres-only hosts, use `:in_memory` (below)
|
|
231
|
+
unless cross-process metadata persistence matters.
|
|
232
|
+
|
|
233
|
+
### In-memory metadata
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
config.metadata_store = :in_memory
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Pure-Ruby hash-backed store. No external dependencies, no persistence, vectors and metadata both live in the building process and die with
|
|
240
|
+
it. The `_index.json` manifest under `output_dir` is the durable
|
|
241
|
+
metadata for the index MCP server, so this is a reasonable default
|
|
242
|
+
for hosts that don't bundle `sqlite3`.
|
|
243
|
+
|
|
244
|
+
## Retrieval cache options
|
|
245
|
+
|
|
246
|
+
The optional cache wraps both embedding-provider calls and assembled retrieval
|
|
247
|
+
contexts. It is disabled by default and is separate from the Index Server's
|
|
248
|
+
tool-result `_meta` cache hint.
|
|
249
|
+
|
|
250
|
+
| Option | Type | Default | Description |
|
|
251
|
+
|--------|------|---------|-------------|
|
|
252
|
+
| `cache_enabled` | Boolean | `false` | Enable embedding and retrieval-context caching in retrievers built by `Woods::Builder`. |
|
|
253
|
+
| `cache_store` | Symbol or `Woods::Cache::CacheStore` | `nil` | Cache backend: `:memory`, `:redis`, `:solid_cache`, or an already-constructed cache-store instance. Must be set when caching is enabled. |
|
|
254
|
+
| `cache_options` | Hash | `{}` | Backend constructor options and optional TTL overrides; see below. |
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
# Process-local bounded cache
|
|
258
|
+
config.cache_enabled = true
|
|
259
|
+
config.cache_store = :memory
|
|
260
|
+
config.cache_options = {
|
|
261
|
+
max_entries: 500,
|
|
262
|
+
ttl: { embeddings: 86_400, context: 900 }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Redis-backed cache
|
|
266
|
+
config.cache_store = :redis
|
|
267
|
+
config.cache_options = {
|
|
268
|
+
redis: Redis.new(url: ENV.fetch('REDIS_URL')),
|
|
269
|
+
default_ttl: 3_600,
|
|
270
|
+
ttl: { embeddings: 86_400, context: 900 }
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`:solid_cache` uses the same shape with `cache:` set to an
|
|
275
|
+
`ActiveSupport::Cache::Store`-compatible instance. `default_ttl` applies at the
|
|
276
|
+
Redis/Solid Cache store layer when a write supplies no TTL. The `ttl:` hash
|
|
277
|
+
overrides the wrapper defaults for `:embeddings` (24 hours) and `:context`
|
|
278
|
+
(15 minutes). `:memory` accepts `max_entries` (default 500); it ignores
|
|
279
|
+
`default_ttl` because each wrapper write supplies its domain TTL.
|
|
280
|
+
|
|
281
|
+
## Deployment shapes
|
|
282
|
+
|
|
283
|
+
Woods supports three deployment shapes, pick the preset that matches yours.
|
|
284
|
+
|
|
285
|
+
| Shape | When | Preset |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| **Single-process** | Embed + query in one Ruby VM (dev console, tests, `rails runner` scripts). Simplest. | `:local` |
|
|
288
|
+
| **Shared filesystem** | Rake task runs `woods:embed`, separate `woods-mcp` server reads the dump. Common with MCP sidecars. | `:shared_filesystem` |
|
|
289
|
+
| **Distributed** | Vectors live in an external service (pgvector / Qdrant) queried by both the embed process and the MCP server. Highest durability, highest ops cost. | `:postgresql` or `:production` |
|
|
290
|
+
|
|
291
|
+
### Shape 2 setup (`:shared_filesystem`)
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
Woods.configure_with_preset(:shared_filesystem) do |config|
|
|
295
|
+
config.output_dir = Rails.root.join('tmp/woods')
|
|
296
|
+
config.embedding_options = {
|
|
297
|
+
model: 'nomic-embed-text',
|
|
298
|
+
host: ENV.fetch('OLLAMA_HOST', 'http://localhost:11434') # your own variable; Woods reads none
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The embed run writes `woods.json` + `dumps/<ISO8601>/vectors.bin` + `metadata.msgpack` under `output_dir`. The MCP server reads them at boot, no sqlite3 gem required, no pgvector/Qdrant service needed. Dump retention defaults to the last 3 (configurable via `config.dump_retention_count`).
|
|
304
|
+
|
|
305
|
+
Requirements:
|
|
306
|
+
- `output_dir` must be set and readable by both the embed process and the MCP server.
|
|
307
|
+
- The MCP server must know the same `output_dir` (pass via `woods-mcp <DIR>` or set `WOODS_DIR`).
|
|
308
|
+
|
|
309
|
+
## Presets
|
|
310
|
+
|
|
311
|
+
For quick setup, use named presets that configure storage + embedding together:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# Local development: no cloud key; requires sqlite3 and a running Ollama service
|
|
315
|
+
Woods.configure_with_preset(:local)
|
|
316
|
+
# → in_memory vectors, SQLite metadata, in_memory graph, Ollama embeddings
|
|
317
|
+
|
|
318
|
+
# Shared filesystem: rake embed → separate MCP server reads the dump.
|
|
319
|
+
# No sqlite3 gem needed; works on MySQL/Postgres-only hosts.
|
|
320
|
+
Woods.configure_with_preset(:shared_filesystem)
|
|
321
|
+
# → in_memory everything + Snapshotter-based persistence via output_dir
|
|
322
|
+
|
|
323
|
+
# PostgreSQL: requires pgvector, sqlite3 gem, OpenAI key, and connection
|
|
324
|
+
Woods.configure_with_preset(:postgresql) do |config|
|
|
325
|
+
config.embedding_options = { api_key: ENV.fetch('OPENAI_API_KEY') }
|
|
326
|
+
config.vector_store_options = {
|
|
327
|
+
connection: ActiveRecord::Base.connection
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
# → pgvector vectors, SQLite metadata, in_memory graph, OpenAI embeddings
|
|
331
|
+
|
|
332
|
+
# Production: requires Qdrant, sqlite3 gem, and OpenAI API key
|
|
333
|
+
Woods.configure_with_preset(:production) do |config|
|
|
334
|
+
config.embedding_options = { api_key: ENV.fetch('OPENAI_API_KEY') }
|
|
335
|
+
config.vector_store_options = {
|
|
336
|
+
url: ENV.fetch('QDRANT_URL'),
|
|
337
|
+
collection: ENV.fetch('WOODS_QDRANT_COLLECTION', 'woods'),
|
|
338
|
+
allow_private_hosts: true # only when QDRANT_URL is deliberately private
|
|
339
|
+
}
|
|
340
|
+
end
|
|
341
|
+
# → Qdrant vectors, SQLite metadata, in_memory graph, OpenAI embeddings
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Presets can be overridden:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
Woods.configure_with_preset(:local) do |config|
|
|
348
|
+
config.max_context_tokens = 16000
|
|
349
|
+
config.embedding_model = 'mxbai-embed-large'
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Pipeline options
|
|
354
|
+
|
|
355
|
+
| Option | Type | Default | Description |
|
|
356
|
+
|--------|------|---------|-------------|
|
|
357
|
+
| `precompute_flows` | Boolean | `false` | Pre-compute per-action request flow maps during extraction |
|
|
358
|
+
| `extract_navigation_edges` | Boolean | `true` | Extract `link_to`, `redirect_to`, and `form_action` navigation edges from views and controllers |
|
|
359
|
+
| `enable_snapshots` | Boolean | `false` | Enable temporal snapshots. Woods automatically migrates its internal output-directory SQLite store; if SQLite is unavailable, it uses the JSON snapshot store. No Rails migration is required. |
|
|
360
|
+
| `volatile_dependency_ratio` | Float | `3.0` | A dependency whose commit count (last 365 days) exceeds the dependent's by this ratio appears in the `volatile_dependencies` report (top 20, ranked by PageRank). Must be greater than 1. Report only, never a gate. |
|
|
361
|
+
| `graph_cycle_limit` | Integer or `nil` | `500` | How many distinct cycles `GraphAnalyzer` enumerates before it stops. Cycle detection finds one cycle per DFS back-edge, so a dense graph has tens of thousands of them and enumerating every one is the largest single cost of the analysis that runs on every extraction. Set to `nil` for exhaustive enumeration. |
|
|
362
|
+
| `graph_cycle_max_length` | Integer or `nil` | `50` | The longest cycle recorded, in distinct nodes. A back-edge deep in the DFS closes a cycle as long as the path, which on a large graph is thousands of nodes: unreadable as a report and expensive to canonicalize. Set to `nil` to record a cycle of any length. |
|
|
363
|
+
|
|
364
|
+
| `incremental_blast_radius_depth` | Integer or `nil` | `nil` | How many reverse hops an incremental run walks from a changed file before it stops re-extracting dependents. `nil` keeps the unbounded transitive closure. See the note below before setting it. |
|
|
365
|
+
| `durable_payload_writes` | Boolean | `false` | Force an `fsync` on every payload file as it is written, on top of the single flush every publish already performs. See the note below before setting it. |
|
|
366
|
+
|
|
367
|
+
`incremental_blast_radius_depth` is unbounded by default because a unit two hops
|
|
368
|
+
out really can have content that depends on the changed file. An STI grandchild
|
|
369
|
+
(`SportsCar < Car < Vehicle`) inherits its grandparent's associations,
|
|
370
|
+
validations and callback chain, and a nested `has_many :through` resolves
|
|
371
|
+
through the same kind of chain, while the graph only records the one-hop
|
|
372
|
+
superclass reference each source file mentions. Set the key on a tree you know
|
|
373
|
+
has neither shape; the units it stops re-extracting still get their `dependents`
|
|
374
|
+
list refreshed, and `spec/integration/incremental_equivalence_spec.rb` holds a
|
|
375
|
+
depth of 1 to full-extraction equivalence.
|
|
376
|
+
|
|
377
|
+
`durable_payload_writes` is off by default, and off is not the weaker
|
|
378
|
+
guarantee. Readers resolve only through `generation.json`, so a payload file
|
|
379
|
+
has no reader until that pointer names it. Every publish flushes the whole
|
|
380
|
+
payload directory once (`syncfs`, else `sync -f`, else `sync`, else a per-file
|
|
381
|
+
`fsync` pass) *before* writing the pointer, which makes the contract: when
|
|
382
|
+
`generation.json` is durable, every file in the payload it names is durable.
|
|
383
|
+
A crash before that leaves an unreferenced partial payload the next run prunes.
|
|
384
|
+
|
|
385
|
+
Turning the key on buys exactly one thing: an individual payload file being
|
|
386
|
+
durable before the pointer exists. It costs two forced flushes per file, about
|
|
387
|
+
8.9ms each on btrfs, so 8000 units is roughly 71s of writing against 1s. It
|
|
388
|
+
cannot disable the publish flush, which has no opt-out.
|
|
389
|
+
|
|
390
|
+
When either cap fires, `graph_analysis.json` reports `stats.cycle_limit_reached: true`
|
|
391
|
+
alongside `stats.cycle_count`, so a reader of the `cycles` array can tell a truncated
|
|
392
|
+
view from a short one. Enumeration stops at the cap rather than counting past it, so
|
|
393
|
+
there is no full population count to publish. Both caps must be a positive Integer or
|
|
394
|
+
`nil`; anything else raises `Woods::ConfigurationError`.
|
|
395
|
+
|
|
396
|
+
## Session tracer options
|
|
397
|
+
|
|
398
|
+
| Option | Type | Default | Description |
|
|
399
|
+
|--------|------|---------|-------------|
|
|
400
|
+
| `session_tracer_enabled` | Boolean | `false` | Enable session tracing middleware |
|
|
401
|
+
| `session_tracer_allow_production` | Boolean | `false` | Explicitly allow session tracing in `Rails.env.production?`. Without this opt-in, the Railtie warns and leaves the tracer disabled even when `session_tracer_enabled` is true. Review trace contents, retention, and access controls before enabling it. |
|
|
402
|
+
| `session_store` | Object | `nil` | Store backend: `FileStore`, `RedisStore`, or `SolidCacheStore` |
|
|
403
|
+
| `session_id_proc` | Proc | `nil` | Custom proc to extract session ID from requests |
|
|
404
|
+
| `session_exclude_paths` | Array<String> | `[]` | Path patterns to exclude from tracing |
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
require 'woods/session_tracer/file_store' # the stores are not autoloaded
|
|
408
|
+
|
|
409
|
+
config.session_tracer_enabled = true
|
|
410
|
+
config.session_store = Woods::SessionTracer::FileStore.new(
|
|
411
|
+
Rails.root.join('tmp/session_traces')
|
|
412
|
+
)
|
|
413
|
+
config.session_exclude_paths = ['/health', '/metrics', '/assets']
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Gem indexing
|
|
417
|
+
|
|
418
|
+
`config.add_gem` is accepted for forward compatibility but **not implemented**: nothing in the
|
|
419
|
+
extraction path reads the registered gem configs, and calling it emits a warning.
|
|
420
|
+
`RailsSourceExtractor` indexes a fixed set of Rails framework paths only.
|
|
421
|
+
|
|
422
|
+
Priority levels (`:low`, `:medium`, `:high`) affect retrieval ranking when framework source is relevant to a query.
|
|
423
|
+
|
|
424
|
+
## Extractors
|
|
425
|
+
|
|
426
|
+
`config.extractors` accepts an array of symbols but **extractor selection is
|
|
427
|
+
not implemented**. All 35 extractors always run during a full extraction,
|
|
428
|
+
regardless of what this array holds, nothing in the extraction path reads
|
|
429
|
+
it (`Woods::Extractor::EXTRACTORS` is a frozen constant, not derived from
|
|
430
|
+
config). Setting `extractors` to anything other than its default value emits
|
|
431
|
+
a warning:
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
config.extractors = %i[models controllers services]
|
|
435
|
+
# => warns: "config.extractors is accepted for forward compatibility but
|
|
436
|
+
# extractor selection is not implemented; all extractors run."
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
The array exists for forward compatibility with a future selection knob.
|
|
440
|
+
Leave it at its default. The full list of what always runs:
|
|
441
|
+
|
|
442
|
+
| Symbol | Extractor | What it adds |
|
|
443
|
+
|--------|-----------|-------------|
|
|
444
|
+
| `:models` | ModelExtractor | ActiveRecord models, with concerns inlined and schema prepended |
|
|
445
|
+
| `:controllers` | ControllerExtractor | Controllers with route context and filter chains |
|
|
446
|
+
| `:services` | ServiceExtractor | Service/interactor/operation objects |
|
|
447
|
+
| `:components` | PhlexExtractor | Phlex components |
|
|
448
|
+
| `:view_components` | ViewComponentExtractor | ViewComponent classes |
|
|
449
|
+
| `:jobs` | JobExtractor | ActiveJob/Sidekiq workers |
|
|
450
|
+
| `:mailers` | MailerExtractor | ActionMailer classes |
|
|
451
|
+
| `:graphql` | GraphQLExtractor | GraphQL types, mutations, resolvers, queries |
|
|
452
|
+
| `:serializers` | SerializerExtractor | AMS/Blueprinter/Alba/Draper serializers |
|
|
453
|
+
| `:managers` | ManagerExtractor | SimpleDelegator wrapper classes |
|
|
454
|
+
| `:policies` | PolicyExtractor | Non-Pundit domain policy classes |
|
|
455
|
+
| `:validators` | ValidatorExtractor | Custom ActiveModel validators |
|
|
456
|
+
| `:concerns` | ConcernExtractor | ActiveSupport::Concern modules |
|
|
457
|
+
| `:routes` | RouteExtractor | Rails routes |
|
|
458
|
+
| `:middleware` | MiddlewareExtractor | Rack middleware stack |
|
|
459
|
+
| `:i18n` | I18nExtractor | Locale translation files |
|
|
460
|
+
| `:pundit_policies` | PunditExtractor | Pundit authorization policies |
|
|
461
|
+
| `:configurations` | ConfigurationExtractor | Rails initializers + behavioral profile |
|
|
462
|
+
| `:engines` | EngineExtractor | Mounted Rails engines |
|
|
463
|
+
| `:view_templates` | ViewTemplateExtractor | ERB view templates |
|
|
464
|
+
| `:migrations` | MigrationExtractor | ActiveRecord migrations |
|
|
465
|
+
| `:action_cable_channels` | ActionCableExtractor | ActionCable channels |
|
|
466
|
+
| `:scheduled_jobs` | ScheduledJobExtractor | Recurring/scheduled jobs |
|
|
467
|
+
| `:rake_tasks` | RakeTaskExtractor | Rake task definitions |
|
|
468
|
+
| `:state_machines` | StateMachineExtractor | AASM/Statesman state machines |
|
|
469
|
+
| `:events` | EventExtractor | Event publish/subscribe patterns |
|
|
470
|
+
| `:decorators` | DecoratorExtractor | Decorators, presenters, form objects |
|
|
471
|
+
| `:database_views` | DatabaseViewExtractor | SQL views (Scenic) |
|
|
472
|
+
| `:caching` | CachingExtractor | Cache usage patterns |
|
|
473
|
+
| `:factories` | FactoryExtractor | FactoryBot factory definitions |
|
|
474
|
+
| `:test_mappings` | TestMappingExtractor | Test file → subject class mapping |
|
|
475
|
+
| `:rails_source` | RailsSourceExtractor | Rails/gem framework source (toggle via `include_framework_sources`) |
|
|
476
|
+
| `:poros` | PoroExtractor | Plain Ruby objects in app/models |
|
|
477
|
+
| `:libs` | LibExtractor | Ruby files in lib/ |
|
|
478
|
+
|
|
479
|
+
See [EXTRACTOR_REFERENCE.md](EXTRACTOR_REFERENCE.md) for what each one captures in detail.
|
|
480
|
+
|
|
481
|
+
### Component directories
|
|
482
|
+
|
|
483
|
+
| Option | Type | Default | Description |
|
|
484
|
+
|--------|------|---------|-------------|
|
|
485
|
+
| `component_paths` | Array<String> | `["app/components", "app/views/components", "app/views"]` | Directories `PhlexExtractor` and `ViewComponentExtractor` walk before reading `descendants`, relative to `Rails.root`. |
|
|
486
|
+
|
|
487
|
+
Both component extractors discover their units from
|
|
488
|
+
`component_base.descendants`, which only knows classes something has already
|
|
489
|
+
loaded. Rails leaves `app/views` out of both `autoload_paths` and
|
|
490
|
+
`eager_load_paths`, so an app that keeps components beside their templates and
|
|
491
|
+
opts that subtree into autoloading has components that resolve by name and are
|
|
492
|
+
absent from `descendants` for the whole extraction. Woods asks the autoloader
|
|
493
|
+
for each file under these directories first.
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
Woods.configure do |config|
|
|
497
|
+
# Components live under app/ui/ in this app, nowhere else.
|
|
498
|
+
config.component_paths = %w[app/ui]
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
`nil` means the defaults above. An explicitly empty array means walk nothing,
|
|
503
|
+
which is how an app that keeps no components (or eager-loads all of them) opts
|
|
504
|
+
out of the walk entirely:
|
|
505
|
+
|
|
506
|
+
```ruby
|
|
507
|
+
Woods.configure { |config| config.component_paths = [] }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Nested entries collapse into their ancestor, so the default list walks
|
|
511
|
+
`app/components` and `app/views` and never hands a file under
|
|
512
|
+
`app/views/components` to the autoloader twice.
|
|
513
|
+
|
|
514
|
+
A file is loaded only when a Rails autoload path owns it, so the constant its
|
|
515
|
+
path implies is the one Zeitwerk manages. A directory that is not autoloaded is
|
|
516
|
+
walked and skipped; one `Rails.logger.debug` line per extraction says how many
|
|
517
|
+
files that was, so the misconfiguration is visible instead of looking like an
|
|
518
|
+
app with no components.
|
|
519
|
+
|
|
520
|
+
## Console MCP options
|
|
521
|
+
|
|
522
|
+
These options configure the Console MCP server (live database queries via
|
|
523
|
+
MCP). See [CONSOLE_MCP_SETUP.md](CONSOLE_MCP_SETUP.md) for the full
|
|
524
|
+
deployment guide including defense layers.
|
|
525
|
+
|
|
526
|
+
| Key | Type | Default | Description |
|
|
527
|
+
|---|---|---|---|
|
|
528
|
+
| `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, the Railtie does not mount the Console MCP middleware. |
|
|
529
|
+
| `console_mcp_token` | String | `ENV['WOODS_CONSOLE_MCP_TOKEN']` or `nil` | Bearer token required on every Console HTTP request. **Required in production**: the Railtie raises `Woods::ConfigurationError` when `console_mcp_enabled` is true but no token is set. In non-production a missing token warns at boot and every Console request fails closed with `401 Unauthorized`. A configured token shorter than 32 characters raises `Woods::ConfigurationError` at boot in every environment. Generate with `SecureRandom.hex(32)`. |
|
|
530
|
+
| `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist. Port is stripped before comparison, so `http://localhost` matches any localhost port. Override for tunneled / internal-dashboard access. |
|
|
531
|
+
| `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
|
|
532
|
+
| `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
|
|
533
|
+
| `console_blocked_tables` | Array\<String\> | `Woods::DEFAULT_CONSOLE_BLOCKED_TABLES` | TableGate denylist (case-insensitive). Bare names match every schema; qualified names (`schema.table`) match exactly. |
|
|
534
|
+
| `console_redacted_columns` | Array\<String\> | `Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS` | Column names whose values are replaced with `[REDACTED]` in responses, and which are refused as aggregate, scope, find, and order inputs. |
|
|
535
|
+
| `console_redacted_key_values` | Array\<Hash\> | `[]` | EAV-style redaction patterns. Each entry: `{ key_column:, value_column:, sensitive_keys: [] }`. |
|
|
536
|
+
| `console_credential_defense_enabled` | Boolean | `true` | Layer 5 toggle for the CredentialScanner. Leave on unless you have a specific reason to disable. |
|
|
537
|
+
| `console_credential_rotation_warning` | Boolean | `true` | Emit a structured log warning when any Rails credentials file is modified after process start. |
|
|
538
|
+
| `console_unsafe_eval_enabled` | Boolean | `nil` | Legacy setting. `console_eval` is unavailable; enabling it fails closed at server construction. |
|
|
539
|
+
| `console_unsafe_eval_confirmation` | `Confirmation` | `nil` | Legacy option retained for compatibility; passing it fails closed. |
|
|
540
|
+
| `console_unsafe_eval_audit_log_path` | String/Pathname | `nil` | Legacy option retained for compatibility; passing it fails closed. |
|
|
541
|
+
|
|
542
|
+
## Environment variables
|
|
543
|
+
|
|
544
|
+
These variables are read by the gem and its MCP servers at runtime. They complement (not replace) the configure block, most exist so the MCP servers, rake tasks, and exporters can self-configure or override config without an initializer edit.
|
|
545
|
+
|
|
546
|
+
### Index server (`woods-mcp` / `woods-mcp-http` / `woods-mcp-start`)
|
|
547
|
+
|
|
548
|
+
| Variable | Default | Purpose |
|
|
549
|
+
|----------|---------|---------|
|
|
550
|
+
| `WOODS_DIR` | `Dir.pwd` | Path to the extraction output directory. |
|
|
551
|
+
| `WOODS_REQUIRE_INDEX` | unset | Set to `"1"` to fail closed: the server refuses to boot (raises `MissingArtifact`) unless a real index (`woods.json`) is present. By default an extract-only host boots in pattern/structural mode without it. |
|
|
552
|
+
| `WOODS_ALLOW_AUTODETECT` | unset | **Deprecated no-op.** Auto-detect is now the default; accepted for backward compatibility only. |
|
|
553
|
+
| `WOODS_SEARCH_MAX_SCAN` | `500` | Cap on unit files loaded during a phase-2 (metadata/source_code) `search`. Hitting the cap sets `partial: true` in the response. |
|
|
554
|
+
| `WOODS_SNAPSHOTS` | unset | Set to `"true"` to force-enable temporal snapshot storage, even without a pre-existing SQLite database. |
|
|
555
|
+
| `WOODS_ALLOW_PURGE` | unset | Set to `"1"` to override the 30%-deletion purge guard in `woods:embed`/`woods:embed_incremental`. |
|
|
556
|
+
| `WOODS_PAYLOAD_RETENTION` | `3` | How many past generations' payload directories (`payloads/gen-N/`) to retain, and — when the JSON snapshot store is in use — how many temporal snapshots (`snapshots/`) to keep. A payload pinned by an active reader process is kept temporarily beyond this bound and reconsidered after the pin is released. |
|
|
557
|
+
| `WOODS_MCP_CACHE_TTL_MS` | `10000` | Cache TTL advertised in tool result `_meta`. `0` disables caching. |
|
|
558
|
+
| `WOODS_NO_UPDATE_CHECK` | unset | Set to `"1"` to skip the `woods_status` RubyGems version check. |
|
|
559
|
+
| `XDG_CACHE_HOME` | `~/.cache` | Base directory for the best-effort update-check cache (`$XDG_CACHE_HOME/woods/update_check.json`). An unset or empty value uses `~/.cache`; if the home directory cannot be resolved, Woods falls back to the system temporary directory. |
|
|
560
|
+
| `OPENAI_API_KEY` | n/a | When set and no embedding provider is configured, the server auto-enables OpenAI-backed semantic search with in-memory stores. |
|
|
561
|
+
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Probed (`GET /api/tags`, 500ms timeout) when no embedding provider is configured. A reachable instance auto-enables local semantic search. |
|
|
562
|
+
| `OLLAMA_EMBED_MODEL` | `nomic-embed-text` | Model to use when Ollama is auto-detected. |
|
|
563
|
+
| `WOODS_QDRANT_URL`, `WOODS_QDRANT_COLLECTION`, `WOODS_QDRANT_API_KEY` | n/a | Override/require Qdrant connection settings when a pgvector/Qdrant-backed index is served outside its host application (no `Woods.configuration` available). |
|
|
564
|
+
| `WOODS_PG_URL` | n/a | Required when a pgvector-backed index is served outside its host application. |
|
|
565
|
+
|
|
566
|
+
### Rake tasks
|
|
567
|
+
|
|
568
|
+
| Variable | Default | Purpose |
|
|
569
|
+
|----------|---------|---------|
|
|
570
|
+
| `MAX_DEPTH` | `5` | Maximum dependency traversal depth for `woods:flow[EntryPoint]`. Parsed as an integer. |
|
|
571
|
+
| `FORMAT` | `markdown` | Output format for `woods:flow[EntryPoint]`. Set to `json` for pretty-printed JSON; every other value uses Markdown. |
|
|
572
|
+
|
|
573
|
+
### HTTP transport (`woods-mcp-http`)
|
|
574
|
+
|
|
575
|
+
| Variable | Default | Purpose |
|
|
576
|
+
|----------|---------|---------|
|
|
577
|
+
| `WOODS_MCP_HTTP_TOKEN` | unset | Bearer token required for non-loopback binds; startup refuses without one. |
|
|
578
|
+
| `WOODS_MCP_HTTP_ALLOWED_ORIGINS` | loopback only | Comma-separated origin allow-list. |
|
|
579
|
+
| `WOODS_MCP_HTTP_STATELESS` | `1` (stateless) | Set to `0`/`false`/`no` to restore session-based mode. |
|
|
580
|
+
|
|
581
|
+
### Console server (`woods-console-mcp`)
|
|
582
|
+
|
|
583
|
+
| Variable | Default | Purpose |
|
|
584
|
+
|----------|---------|---------|
|
|
585
|
+
| `WOODS_CONSOLE_CONFIG` | `~/.woods/console.yml` when present | Explicit launcher YAML path. An explicit missing path fails startup. |
|
|
586
|
+
| `WOODS_CONSOLE_MCP_TOKEN` | unset | Bearer token for the embedded Rack middleware; see `console_mcp_token` above. |
|
|
587
|
+
| `WOODS_CONSOLE_UNSAFE_EVAL` | unset | Legacy setting. The exact value `true` requests unavailable eval capability and fails server construction closed. |
|
|
588
|
+
|
|
589
|
+
### Watch daemon (`woods:watch`)
|
|
590
|
+
|
|
591
|
+
| Variable | Default | Purpose |
|
|
592
|
+
|----------|---------|---------|
|
|
593
|
+
| `WOODS_IGNORE_WATCH` | unset | Set to `"1"` to make `woods:incremental`/`woods:clean` proceed even when a daemon is (or claims to be) running. For `woods:incremental` this removes daemon coverage: a git range that fails to resolve then exits 1 instead of standing down (see [Incremental Extraction](./INCREMENTAL_EXTRACTION.md#exit-behavior-in-ci-chains)). |
|
|
594
|
+
| `WOODS_LOCK_WAIT` | `Watch::Daemon::LOCK_STALE_TIMEOUT` (600s) | How long a rake writer waits for `PipelineLock` before exiting non-zero. |
|
|
595
|
+
| `WOODS_WATCH_POLL` | auto-detected | Set to `"1"`/`"0"` to force/disable polling mode (vs. `listen` gem, e.g. in a container without inotify). |
|
|
596
|
+
| `WOODS_WATCH_DEBOUNCE` | `0.4` (seconds) | Delay before processing a batch of file-change events. |
|
|
597
|
+
| `WOODS_WATCH_FULL_THRESHOLD` | `50` | Number of changed paths in one batch that triggers a full extraction instead of incremental. |
|
|
598
|
+
| `WOODS_WATCH_IDLE_TIMEOUT` | unset (no timeout) | Seconds of inactivity before the daemon exits. |
|
|
599
|
+
| `WOODS_WATCH_CATCH_UP` | `1` (enabled) | Set to `"0"` to skip generation-watermark catch-up on daemon start. |
|
|
600
|
+
|
|
601
|
+
### Extraction rake tasks
|
|
602
|
+
|
|
603
|
+
| Variable | Default | Purpose |
|
|
604
|
+
|----------|---------|---------|
|
|
605
|
+
| `WOODS_OUTPUT` | `Woods.configuration.output_dir` | Overrides the output directory for `woods:extract`/`woods:incremental`/`woods:watch` without editing the initializer. |
|
|
606
|
+
| `CHANGED_FILES` | unset | Comma-separated explicit changed-path list for `woods:incremental`; when set, git range resolution is skipped entirely. |
|
|
607
|
+
| `CI_COMMIT_BEFORE_SHA`, `CI_COMMIT_SHA` | unset (GitLab) | Build the diff range `<before>..<after>` for `woods:incremental`. A zero before-SHA (new branch) makes the range unresolvable, which exits 1 unless a running daemon covers the index. |
|
|
608
|
+
| `GITHUB_BASE_REF` | unset (GitHub Actions) | Build the diff range `origin/<ref>...HEAD` for `woods:incremental`; an unfetched ref makes the range unresolvable, same exit behavior. |
|
|
609
|
+
| `RAILS_ENV` | `development` | Rails environment the rake tasks boot in. |
|
|
610
|
+
| `WOODS_PROFILE` | unset | Set to `"1"` to log one `[Woods] [profile] <phase> in N.NNs` line per run phase (payload seed, previous graph load, eager load, extraction or blast radius and re-extraction, type index, graph analysis, flows, manifest and summary, publish). Complements the per-extractor timing lines, which cover extraction only. Off by default and free when off. |
|
|
611
|
+
| `WOODS_GIT_DIR` | unset | Absolute path to the canonical git directory. Wins over the repository Woods would otherwise find, at all three of its git call sites: per-unit `commit_count`/`change_frequency` (enrichment), `manifest.json`'s `git_branch`/`git_sha` (provenance), and the `woods:incremental` diff range. All three build their command line with `Woods::GitCommand.argv`. |
|
|
612
|
+
| `GIT_BRANCH`, `GIT_SHA` | unset | Provenance for a checkout with no `.git` at all (a source tarball, a Docker `COPY` that excludes it). Ignored when a `.git` is present but unresolvable, so a stale build arg cannot mask a worktree. |
|
|
613
|
+
|
|
614
|
+
**`GIT_DIR` alone is not enough for a linked git worktree.** Woods runs git as a
|
|
615
|
+
subprocess, so git's own `GIT_DIR` and `GIT_COMMON_DIR` are honored wherever git
|
|
616
|
+
honors them. But pointing `GIT_DIR` at a worktree's *private* git directory only
|
|
617
|
+
moves the failure: that directory reaches the shared object store through a
|
|
618
|
+
relative `commondir` pointer, which still resolves outside a container mount,
|
|
619
|
+
and `GIT_COMMON_DIR` does not override it. `git rev-parse --git-dir` then
|
|
620
|
+
succeeds while no ref resolves.
|
|
621
|
+
|
|
622
|
+
Woods refuses to enrich in that state rather than writing `commit_count: 0` and
|
|
623
|
+
`change_frequency: "new"` on every unit: the git keys are omitted, provenance is
|
|
624
|
+
`"unknown"`, and one warning names the cause. Point `WOODS_GIT_DIR` at the
|
|
625
|
+
canonical git directory (the one a worktree's `gitdir:` pointer ultimately leads
|
|
626
|
+
to) and mount it:
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
WOODS_GIT_DIR=/canonical-git bundle exec rake woods:extract
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Exporters
|
|
633
|
+
|
|
634
|
+
| Variable | Default | Purpose |
|
|
635
|
+
|----------|---------|---------|
|
|
636
|
+
| `NOTION_API_TOKEN` | `config.notion_api_token` | Overrides the configured Notion token. |
|
|
637
|
+
| `WOODS_NOTION_FORCE` | unset | Set to `"1"` (or pass `force_full: true`) to ignore the Notion sync manifest for one run and re-check every page. |
|
|
638
|
+
| `UNBLOCKED_API_TOKEN`, `UNBLOCKED_COLLECTION_ID`, `UNBLOCKED_REPO_URL` | `config.unblocked_*` | Overrides the configured Unblocked connection settings. |
|
|
639
|
+
| `UNBLOCKED_DAILY_BUDGET` | `1000` | Per-run call cap for `woods:unblocked_sync`. |
|
|
640
|
+
| `UNBLOCKED_FORCE_FULL_SYNC` | unset | Set to `"1"` to re-push every Unblocked document, ignoring the unchanged-hash skip. |
|
|
641
|
+
| `UNBLOCKED_FORCE_PURGE` | unset | Set to `"1"` to bypass the Unblocked 30%-deletion guard. |
|
|
642
|
+
| `WOODS_OBSIDIAN_VAULT` | `<output_dir>/obsidian_vault` | Vault output path for `woods:obsidian`. |
|
|
643
|
+
| `WOODS_OBSIDIAN_INCLUDE_FRAMEWORK`, `WOODS_OBSIDIAN_INCLUDE_SOURCE` | `false` | Include framework units / full source in the exported vault. |
|
|
644
|
+
| `WOODS_OBSIDIAN_FORCE_PURGE` | unset | Bypass the Obsidian 30%-deletion guard on the stale-note sweep. |
|
|
645
|
+
|
|
646
|
+
The `woods-mcp` bootstrapper emits a one-line STDERR banner at startup indicating whether semantic search is enabled and which provider is active. If no key/instance is found, pattern search still works and `codebase_retrieve` surfaces an actionable fix message.
|
|
647
|
+
|
|
648
|
+
## Database compatibility
|
|
649
|
+
|
|
650
|
+
All storage options work with both MySQL and PostgreSQL, except:
|
|
651
|
+
|
|
652
|
+
- **pgvector**: PostgreSQL only (requires the pgvector extension)
|
|
653
|
+
- **SQLite metadata store**: uses a standalone SQLite database file, independent of your app's database
|
|
654
|
+
|
|
655
|
+
See [BACKEND_MATRIX.md](BACKEND_MATRIX.md) for the full compatibility matrix.
|