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.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2035 -0
  3. data/CONTRIBUTING.md +253 -87
  4. data/README.md +161 -513
  5. data/SECURITY.md +92 -0
  6. data/assets/woods-wordmark-white-with-bg.png +0 -0
  7. data/docs/AGENT_GUIDE.md +204 -0
  8. data/docs/AGENT_SETUP.md +205 -0
  9. data/docs/BACKEND_MATRIX.md +470 -0
  10. data/docs/CONFIGURATION_REFERENCE.md +655 -0
  11. data/docs/CONSOLE_MCP_SETUP.md +829 -0
  12. data/docs/DOCKER_SETUP.md +454 -0
  13. data/docs/EMBEDDING_MODELS.md +136 -0
  14. data/docs/EVALUATION.md +91 -0
  15. data/docs/EXTRACTOR_REFERENCE.md +765 -0
  16. data/docs/FAQ.md +544 -0
  17. data/docs/GETTING_STARTED.md +183 -0
  18. data/docs/INCREMENTAL_EXTRACTION.md +455 -0
  19. data/docs/INTERNALS.md +418 -0
  20. data/docs/MCP_HTTP_TRANSPORT.md +144 -0
  21. data/docs/MCP_SERVERS.md +231 -0
  22. data/docs/MCP_TOOL_COOKBOOK.md +987 -0
  23. data/docs/MCP_WORKTREE_SETUP.md +127 -0
  24. data/docs/NOTION_INTEGRATION.md +283 -0
  25. data/docs/OBSIDIAN_INTEGRATION.md +170 -0
  26. data/docs/PUBLISHED_INDEX.md +213 -0
  27. data/docs/README.md +94 -0
  28. data/docs/RETRIEVAL_GUIDE.md +267 -0
  29. data/docs/TOKEN_BENCHMARK.md +68 -0
  30. data/docs/TROUBLESHOOTING.md +841 -0
  31. data/docs/UNBLOCKED_INTEGRATION.md +279 -0
  32. data/docs/UPGRADING_TO_2.md +321 -0
  33. data/docs/WATCH_DAEMON.md +667 -0
  34. data/docs/WHY_WOODS.md +219 -0
  35. data/exe/woods-console +40 -4
  36. data/exe/woods-console-mcp +21 -35
  37. data/exe/woods-mcp +20 -7
  38. data/exe/woods-mcp-http +80 -11
  39. data/exe/woods-mcp-start +57 -52
  40. data/lib/generators/woods/install_generator.rb +6 -5
  41. data/lib/generators/woods/pgvector_generator.rb +6 -3
  42. data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
  43. data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
  44. data/lib/generators/woods/templates/woods.rb.tt +49 -28
  45. data/lib/tasks/woods.rake +622 -168
  46. data/lib/tasks/woods_checks.rake +107 -0
  47. data/lib/tasks/woods_evaluation.rake +164 -80
  48. data/lib/woods/ast/call_site_extractor.rb +6 -15
  49. data/lib/woods/ast/method_extractor.rb +19 -9
  50. data/lib/woods/ast/parser.rb +54 -8
  51. data/lib/woods/atomic_file.rb +171 -2
  52. data/lib/woods/builder.rb +310 -22
  53. data/lib/woods/cache/cache_middleware.rb +7 -2
  54. data/lib/woods/cache/cache_store.rb +9 -1
  55. data/lib/woods/cache/solid_cache_store.rb +6 -4
  56. data/lib/woods/change_set.rb +88 -0
  57. data/lib/woods/checks/generation_resolution.rb +34 -0
  58. data/lib/woods/checks/moved_messages.rb +186 -0
  59. data/lib/woods/chunking/semantic_chunker.rb +160 -18
  60. data/lib/woods/console/audit_logger.rb +12 -3
  61. data/lib/woods/console/bridge_protocol.rb +3 -16
  62. data/lib/woods/console/connection_manager.rb +51 -136
  63. data/lib/woods/console/dispatch_pipeline.rb +42 -12
  64. data/lib/woods/console/embedded_executor.rb +806 -149
  65. data/lib/woods/console/eval_guard.rb +27 -20
  66. data/lib/woods/console/input_contract.rb +78 -0
  67. data/lib/woods/console/model_validator.rb +29 -1
  68. data/lib/woods/console/rack_middleware.rb +65 -42
  69. data/lib/woods/console/redactor.rb +26 -8
  70. data/lib/woods/console/safe_context.rb +58 -10
  71. data/lib/woods/console/scope_predicate_parser.rb +41 -0
  72. data/lib/woods/console/server.rb +119 -247
  73. data/lib/woods/console/sql_noise_stripper.rb +125 -16
  74. data/lib/woods/console/sql_table_scanner.rb +82 -22
  75. data/lib/woods/console/sql_validator.rb +459 -29
  76. data/lib/woods/console/table_gate.rb +2 -2
  77. data/lib/woods/console/tool_specs.rb +463 -90
  78. data/lib/woods/console/tools/tier1.rb +1 -5
  79. data/lib/woods/console/tools/tier4.rb +18 -9
  80. data/lib/woods/coordination/lock_heartbeat.rb +103 -0
  81. data/lib/woods/coordination/pipeline_lock.rb +263 -53
  82. data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
  83. data/lib/woods/db/migrator.rb +3 -9
  84. data/lib/woods/db/schema_version.rb +47 -2
  85. data/lib/woods/dependency_graph.rb +898 -64
  86. data/lib/woods/embedding/fake.rb +138 -0
  87. data/lib/woods/embedding/indexer.rb +832 -40
  88. data/lib/woods/embedding/openai.rb +77 -19
  89. data/lib/woods/embedding/provider.rb +189 -11
  90. data/lib/woods/embedding/text_preparer.rb +1 -1
  91. data/lib/woods/embedding/token_counter.rb +0 -7
  92. data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
  93. data/lib/woods/evaluation/ablation_executor.rb +67 -0
  94. data/lib/woods/evaluation/ablation_provenance.rb +38 -0
  95. data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
  96. data/lib/woods/evaluation/ablation_runner.rb +173 -0
  97. data/lib/woods/evaluation/ablation_summary.rb +65 -0
  98. data/lib/woods/evaluation/ablation_task.rb +66 -0
  99. data/lib/woods/evaluation/ablation_task_set.rb +77 -0
  100. data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
  101. data/lib/woods/evaluation/ablation_worktree.rb +71 -0
  102. data/lib/woods/evaluation/baseline.rb +60 -0
  103. data/lib/woods/evaluation/baseline_runner.rb +11 -3
  104. data/lib/woods/evaluation/evaluator.rb +41 -8
  105. data/lib/woods/evaluation/query_set.rb +79 -13
  106. data/lib/woods/evaluation/report_generator.rb +20 -1
  107. data/lib/woods/export/unit_facts.rb +0 -11
  108. data/lib/woods/extracted_unit.rb +22 -63
  109. data/lib/woods/extractor.rb +2783 -238
  110. data/lib/woods/extractors/action_cable_extractor.rb +9 -4
  111. data/lib/woods/extractors/ast_source_extraction.rb +20 -2
  112. data/lib/woods/extractors/caching_extractor.rb +46 -12
  113. data/lib/woods/extractors/callback_analyzer.rb +39 -9
  114. data/lib/woods/extractors/component_discovery.rb +123 -0
  115. data/lib/woods/extractors/concern_extractor.rb +17 -3
  116. data/lib/woods/extractors/controller_extractor.rb +389 -29
  117. data/lib/woods/extractors/decorator_extractor.rb +7 -14
  118. data/lib/woods/extractors/engine_extractor.rb +53 -8
  119. data/lib/woods/extractors/event_extractor.rb +55 -4
  120. data/lib/woods/extractors/factory_extractor.rb +49 -11
  121. data/lib/woods/extractors/graphql_extractor.rb +162 -66
  122. data/lib/woods/extractors/i18n_extractor.rb +6 -1
  123. data/lib/woods/extractors/job_extractor.rb +51 -21
  124. data/lib/woods/extractors/lib_extractor.rb +23 -17
  125. data/lib/woods/extractors/line_neutralizer.rb +171 -0
  126. data/lib/woods/extractors/mailer_extractor.rb +9 -1
  127. data/lib/woods/extractors/manager_extractor.rb +19 -2
  128. data/lib/woods/extractors/migration_extractor.rb +22 -11
  129. data/lib/woods/extractors/model_extractor.rb +292 -57
  130. data/lib/woods/extractors/package_extractor.rb +154 -0
  131. data/lib/woods/extractors/phlex_extractor.rb +18 -3
  132. data/lib/woods/extractors/policy_extractor.rb +6 -5
  133. data/lib/woods/extractors/poro_extractor.rb +13 -14
  134. data/lib/woods/extractors/pundit_extractor.rb +3 -3
  135. data/lib/woods/extractors/rails_source_extractor.rb +24 -7
  136. data/lib/woods/extractors/rake_task_extractor.rb +158 -30
  137. data/lib/woods/extractors/reference_patterns.rb +38 -0
  138. data/lib/woods/extractors/route_extractor.rb +58 -2
  139. data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
  140. data/lib/woods/extractors/serializer_extractor.rb +3 -4
  141. data/lib/woods/extractors/service_extractor.rb +11 -1
  142. data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
  143. data/lib/woods/extractors/shared_utility_methods.rb +36 -6
  144. data/lib/woods/extractors/source_nesting.rb +560 -0
  145. data/lib/woods/extractors/state_machine_extractor.rb +30 -18
  146. data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
  147. data/lib/woods/extractors/view_component_extractor.rb +28 -3
  148. data/lib/woods/extractors/view_engines/erb.rb +17 -3
  149. data/lib/woods/feedback/gap_detector.rb +9 -3
  150. data/lib/woods/feedback/store.rb +7 -1
  151. data/lib/woods/filename_utils.rb +29 -1
  152. data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
  153. data/lib/woods/flow_assembler.rb +147 -26
  154. data/lib/woods/flow_document.rb +1 -0
  155. data/lib/woods/flow_precomputer.rb +175 -22
  156. data/lib/woods/gem_mapper.rb +285 -0
  157. data/lib/woods/generation.rb +185 -0
  158. data/lib/woods/git_command.rb +38 -0
  159. data/lib/woods/git_provenance.rb +16 -2
  160. data/lib/woods/graph_analyzer.rb +564 -87
  161. data/lib/woods/index_artifact.rb +93 -23
  162. data/lib/woods/mcp/bearer_auth.rb +102 -13
  163. data/lib/woods/mcp/bootstrap_state.rb +77 -0
  164. data/lib/woods/mcp/bootstrapper.rb +582 -77
  165. data/lib/woods/mcp/config_resolver.rb +66 -6
  166. data/lib/woods/mcp/errors.rb +60 -0
  167. data/lib/woods/mcp/index_reader.rb +836 -117
  168. data/lib/woods/mcp/index_reader_pinning.rb +78 -0
  169. data/lib/woods/mcp/origin_guard.rb +66 -7
  170. data/lib/woods/mcp/protocol_policy.rb +98 -0
  171. data/lib/woods/mcp/provider_probe.rb +45 -6
  172. data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
  173. data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
  174. data/lib/woods/mcp/server.rb +898 -152
  175. data/lib/woods/mcp/tasks/extension.rb +196 -0
  176. data/lib/woods/mcp/tasks/request_capture.rb +45 -0
  177. data/lib/woods/mcp/tasks/store.rb +518 -0
  178. data/lib/woods/mcp/tool_contract.rb +171 -0
  179. data/lib/woods/mcp/tool_response_renderer.rb +7 -0
  180. data/lib/woods/model_name_cache.rb +19 -1
  181. data/lib/woods/notion/client.rb +132 -36
  182. data/lib/woods/notion/exporter.rb +456 -61
  183. data/lib/woods/notion/mappers/column_mapper.rb +34 -5
  184. data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
  185. data/lib/woods/notion/mappers/model_mapper.rb +21 -6
  186. data/lib/woods/notion/mappers/shared.rb +45 -3
  187. data/lib/woods/notion/sync_manifest.rb +258 -0
  188. data/lib/woods/obsidian/errors.rb +6 -0
  189. data/lib/woods/obsidian/name_mapper.rb +40 -24
  190. data/lib/woods/obsidian/vault_exporter.rb +103 -36
  191. data/lib/woods/operator/pipeline_guard.rb +118 -21
  192. data/lib/woods/operator/status_reporter.rb +20 -3
  193. data/lib/woods/path_dispatcher.rb +276 -0
  194. data/lib/woods/payload_store.rb +236 -0
  195. data/lib/woods/published_index/edge_shaper.rb +61 -0
  196. data/lib/woods/published_index/generation_catalog.rb +72 -0
  197. data/lib/woods/published_index/typed_unit_reader.rb +48 -0
  198. data/lib/woods/published_index.rb +287 -0
  199. data/lib/woods/railtie.rb +69 -30
  200. data/lib/woods/railtie_support.rb +167 -0
  201. data/lib/woods/release.rb +12 -0
  202. data/lib/woods/reload_policy.rb +206 -0
  203. data/lib/woods/resilience/circuit_breaker.rb +47 -8
  204. data/lib/woods/resilience/index_validator.rb +296 -10
  205. data/lib/woods/resilience/retryable_provider.rb +71 -6
  206. data/lib/woods/resolved_config.rb +55 -11
  207. data/lib/woods/retrieval/context_assembler.rb +132 -40
  208. data/lib/woods/retrieval/query_classifier.rb +26 -8
  209. data/lib/woods/retrieval/ranker.rb +193 -28
  210. data/lib/woods/retrieval/search_executor.rb +206 -39
  211. data/lib/woods/retriever.rb +317 -71
  212. data/lib/woods/retry_after.rb +22 -2
  213. data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
  214. data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
  215. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
  216. data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
  217. data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
  218. data/lib/woods/ruby_analyzer.rb +21 -5
  219. data/lib/woods/session_tracer/file_store.rb +138 -19
  220. data/lib/woods/session_tracer/middleware.rb +1 -2
  221. data/lib/woods/session_tracer/redis_store.rb +122 -12
  222. data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
  223. data/lib/woods/session_tracer/session_flow_document.rb +56 -14
  224. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  225. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  226. data/lib/woods/session_tracer/store.rb +14 -1
  227. data/lib/woods/storage/metadata_store.rb +230 -26
  228. data/lib/woods/storage/pgvector.rb +180 -22
  229. data/lib/woods/storage/qdrant.rb +367 -41
  230. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  231. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  232. data/lib/woods/storage/snapshotter.rb +23 -5
  233. data/lib/woods/storage/vector_store.rb +49 -8
  234. data/lib/woods/storage_identity.rb +28 -0
  235. data/lib/woods/tasks.rb +53 -2
  236. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  237. data/lib/woods/temporal/snapshot_store.rb +139 -42
  238. data/lib/woods/unblocked/client.rb +119 -17
  239. data/lib/woods/unblocked/document_builder.rb +34 -2
  240. data/lib/woods/unblocked/exporter.rb +63 -27
  241. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  242. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  243. data/lib/woods/update_check.rb +24 -1
  244. data/lib/woods/util/uuid5.rb +124 -0
  245. data/lib/woods/version.rb +1 -1
  246. data/lib/woods/watch/daemon.rb +1345 -0
  247. data/lib/woods/watch/listen_watcher.rb +81 -0
  248. data/lib/woods/watch/polling_watcher.rb +137 -0
  249. data/lib/woods/watch/status.rb +169 -0
  250. data/lib/woods/watch/tree_scan.rb +163 -0
  251. data/lib/woods/watch/watcher.rb +100 -0
  252. data/lib/woods.rb +138 -9
  253. data/plugin/.claude-plugin/plugin.json +18 -0
  254. data/plugin/hooks/hooks.json +29 -0
  255. data/plugin/hooks/woods-post-edit.sh +226 -0
  256. data/plugin/hooks/woods-session-start.sh +77 -0
  257. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  258. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  259. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  260. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  261. data/plugin/skills/woods-setup/SKILL.md +99 -0
  262. metadata +134 -23
  263. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  264. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  265. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  266. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  267. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  268. data/lib/woods/console/bridge.rb +0 -210
  269. data/lib/woods/formatting/claude_adapter.rb +0 -98
  270. data/lib/woods/formatting/generic_adapter.rb +0 -56
  271. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  272. data/lib/woods/notion/mapper.rb +0 -40
  273. data/lib/woods/observability/health_check.rb +0 -79
  274. 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&lt;Symbol&gt; | `[: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&lt;String&gt; | `[]` | 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&lt;String&gt; | `["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.