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,841 @@
1
+ # Troubleshooting Woods
2
+
3
+ This guide covers the most common problems encountered when installing, extracting, and using Woods. Each section follows the pattern: **symptom → cause → fix**.
4
+
5
+ ---
6
+
7
+ ## Quick Reference
8
+
9
+ | Error message | Cause | Fix |
10
+ |---------------|-------|-----|
11
+ | `No manifest.json found` | Wrong index path or no published generation | Use the path visible to the server process; run `woods:validate` |
12
+ | `uninitialized constant Rails` | Not running inside Rails app | Run via `bundle exec rake` in Rails root |
13
+ | `type "vector" does not exist` | pgvector not installed | `CREATE EXTENSION vector` in PostgreSQL |
14
+ | `Connection refused (localhost:11434)` | Ollama not running | `ollama serve` |
15
+ | `Connection refused (localhost:6333)` | Qdrant not running | Start Qdrant container |
16
+ | Qdrant private/loopback URL rejected | SSRF guard is working | Add `allow_private_hosts: true` only for a deliberately trusted endpoint |
17
+ | Missing `console_sql` / `console_query` | Read tools disabled | Enable `console_embedded_read_tools` |
18
+ | `database is locked` | SQLite concurrent access | Run one extraction at a time |
19
+ | `Dimension mismatch` | Embedding model changed | Full re-index: extract + embed |
20
+ | `401 Unauthorized` (Notion) | Invalid API token | Check `NOTION_API_TOKEN` env var |
21
+ | `404 Not Found` (Notion) | Wrong database ID | Verify ID + integration access |
22
+ | `broken pipe` (Docker console) | Missing `-i` flag | Add `-i` to docker exec args |
23
+ | `No such container` | Wrong container name | Check with `docker ps --format '{{.Names}}'` |
24
+ | `JSON parse errors` (MCP) | Rails boot noise on stdout | Remove `puts` calls from initializers |
25
+ | Query timeout | Large table, no scope | Add scope conditions to narrow results |
26
+ | Empty extraction output | `eager_load!` failure | Check for `NameError` in boot output |
27
+ | Git metadata missing | Shallow clone in CI | Use `fetch-depth: 2` or higher |
28
+ | Parallel tool calls all fail | MCP client batches calls | Send calls sequentially, validate params first |
29
+ | HTTP transport refuses to start on `0.0.0.0` | Missing bearer token | Set `WOODS_MCP_HTTP_TOKEN=…` or bind loopback only |
30
+ | HTTP transport returns `403 Origin not allowed` | Origin header not in allow-list | Set `WOODS_MCP_HTTP_ALLOWED_ORIGINS="https://example.com"` (comma-separated; default is loopback-only) |
31
+ | Tool returns `error_code: :not_configured` | Feature flag or credential not set | Check `config_key` in `_meta` and the linked `doc_link` |
32
+ | Tool returns `error_code: :rate_limited` | `PipelineGuard` 5-min cooldown hit | Wait `retry_after_seconds` from `_meta`, then retry |
33
+
34
+ ### First-Pass Diagnostics
35
+
36
+ For a single-call health snapshot, call the Index Server's `woods_status` tool. It reports:
37
+
38
+ - Extraction freshness (last run time, unit count, index version)
39
+ - Overall readiness plus index, watch, retriever, and bootstrap state (`ready`, `index`, `watch`, `retriever`, `bootstrap` sections)
40
+ - Which optional features are configured (embedding provider, Notion, session tracer)
41
+ - Per-feature config-key hints for anything missing
42
+ - `server.update`: the installed gem version, the newest version the process knows about (the latest published release, or the installed version itself when the install is ahead of the registry or the check could not run), and an `update_available` flag (a best-effort RubyGems check, cached 24h; disable with `WOODS_NO_UPDATE_CHECK=1`)
43
+
44
+ Agents cold-connecting to a server should call `woods_status` before any other tool, it eliminates most "why is this empty?" guesswork.
45
+
46
+ If a tool call fails with **"Tool not found: … not available in the installed Woods v…"**, the client is asking for a tool a newer gem provides. Run `bundle update woods` and reconnect the MCP server, then retry.
47
+
48
+ ## Extraction Problems
49
+
50
+ ### Extraction produces empty or incomplete output
51
+
52
+ **Symptom:** Running `rake woods:extract` completes without errors but produces fewer units than expected, or only some model types appear.
53
+
54
+ **Cause:** `eager_load!` failed silently partway through loading your application. Zeitwerk processes directories alphabetically, if a directory early in the alphabet (e.g., `app/graphql/`) fails to load due to a missing gem, Zeitwerk aborts that pass and never reaches `app/models/`. Woods detects this and falls back to per-directory loading, but some units may still be missing.
55
+
56
+ **Fix:**
57
+
58
+ 1. Check for `NameError` or `LoadError` in the extraction output:
59
+
60
+ ```bash
61
+ bundle exec rake woods:extract 2>&1 | grep -i "error\|uninitialized"
62
+ ```
63
+
64
+ 2. Either install the missing gem(s) referenced in the error, or exclude the problem directory from eager loading:
65
+
66
+ ```ruby
67
+ # config/application.rb
68
+ config.eager_load_paths -= [Rails.root.join('app/graphql')]
69
+ ```
70
+
71
+ 3. Re-run extraction after resolving the load issue.
72
+
73
+ ---
74
+
75
+ ### Extraction fails with "Cannot find Rails" or "uninitialized constant"
76
+
77
+ **Symptom:** Running a rake task fails immediately with `NameError: uninitialized constant Rails` or a similar error about ActiveRecord, ApplicationRecord, or other Rails constants.
78
+
79
+ **Cause:** Extraction requires a booted Rails environment. Woods uses runtime introspection (`ActiveRecord::Base.descendants`, `Rails.application.routes`, reflection APIs), these APIs do not exist outside a running Rails app.
80
+
81
+ **Fix:** Always run extraction rake tasks inside your Rails app:
82
+
83
+ ```bash
84
+ # Correct: run from Rails app root
85
+ bundle exec rake woods:extract
86
+
87
+ # Docker: run inside container
88
+ docker compose exec app bundle exec rake woods:extract
89
+ ```
90
+
91
+ Woods cannot extract from source files alone. It is not a static analysis tool.
92
+
93
+ ---
94
+
95
+ ### Extraction is very slow
96
+
97
+ **Symptom:** A full extraction is much slower than this application's established baseline.
98
+
99
+ **Cause:** Two common causes, a very large codebase (500+ models), or framework source extraction enabled on an app with many gems.
100
+
101
+ **Fix:**
102
+
103
+ Disable framework source extraction if you don't need Rails/gem internals:
104
+
105
+ ```ruby
106
+ config.include_framework_sources = false
107
+ ```
108
+
109
+ For subsequent runs, use incremental mode instead of full extraction:
110
+
111
+ ```bash
112
+ bundle exec rake woods:incremental
113
+ ```
114
+
115
+ Incremental extraction only re-extracts files that changed since the last run. It skips unchanged units and is typically 5-10× faster.
116
+
117
+ ---
118
+
119
+ ### Some extractor types are not appearing in output
120
+
121
+ **Symptom:** You expect state machines, events, decorators, or other unit types but they don't appear in the output directory.
122
+
123
+ **Cause:** All 35 extractors always run during extraction, there is no opt-in/opt-out mechanism. If a unit type is missing, it means the extractor found nothing to extract. Common reasons:
124
+
125
+ - The expected directory doesn't exist (e.g., no `app/decorators/` for decorators)
126
+ - The required gem isn't installed (e.g., `aasm` or `state_machines` for state machine extraction)
127
+ - The code doesn't match the extractor's expected patterns
128
+
129
+ **Fix:** Verify the code exists and matches what the extractor looks for:
130
+
131
+ ```bash
132
+ # Check if the directory exists
133
+ ls app/decorators/ app/state_machines/ 2>/dev/null
134
+
135
+ # Check extraction output for that type
136
+ ls tmp/woods/decorators/ tmp/woods/state_machines/ 2>/dev/null
137
+ ```
138
+
139
+ Note: `config.extractors` does not control anything today, it's accepted for forward compatibility only and is not consulted by extraction or retrieval. See [EXTRACTOR_REFERENCE.md](EXTRACTOR_REFERENCE.md) for what each extractor looks for.
140
+
141
+ ---
142
+
143
+ ### Incremental extraction doesn't seem to update routes, middleware, or engines
144
+
145
+ **Symptom:** After changing your routes file or adding a middleware, `rake woods:incremental` doesn't seem to update those units.
146
+
147
+ **Cause:** Nine unit types don't map to individual files, so they can't be diffed per file: `route`, `middleware`, `engine`, `scheduled_job`, `state_machine`, `factory`, `event`, `database_view`, and `rails_source`. Incremental mode still updates them, it re-runs the whole extractor when a specific trigger path changes, instead of skipping the type:
148
+
149
+ | Type | Trigger path |
150
+ |------|--------------|
151
+ | `route` | `config/routes.rb` |
152
+ | `engine` | `config/routes.rb`, `Gemfile.lock` |
153
+ | `middleware` | `config/application.rb`, `Gemfile.lock`, any file under `config/initializers`/`config/environments` |
154
+ | `scheduled_job` | `config/recurring.yml`, `config/sidekiq_cron.yml`, `config/schedule.rb` |
155
+ | `state_machine` | any `.rb` change under the scanned model directories |
156
+ | `factory` | any `.rb` change under `spec/factories`/`test/factories` |
157
+ | `event` | any `.rb` change under `app/` |
158
+ | `database_view` | any `.sql` change under `db/views` |
159
+ | `rails_source` | `Gemfile.lock` (only when `include_framework_sources` is enabled) |
160
+
161
+ If your change doesn't match one of these trigger paths, the type genuinely wasn't updated, that's the actual bug to chase, not a documented limitation.
162
+
163
+ **Fix:** Route and event changes that match the triggers above need no manual
164
+ full extraction; `woods:incremental` and `woods:watch` rerun their whole-app
165
+ extractors. If `woods:validate` still reports drift outside the trigger
166
+ contract, use a full extraction as the recovery step:
167
+
168
+ ```bash
169
+ bundle exec rake woods:extract
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Git metadata is missing or shows zeros
175
+
176
+ **Symptom:** Units have `last_modified_at: null` or `change_frequency: 0` in the JSON output.
177
+
178
+ **Cause:** The git repository is a shallow clone (common in CI with `fetch-depth: 1`). Woods uses `git log` to compute change frequency, a shallow clone has no history to analyze.
179
+
180
+ **Fix:** Fetch at least two commits:
181
+
182
+ ```yaml
183
+ # .github/workflows/index.yml
184
+ - uses: actions/checkout@v4
185
+ with:
186
+ fetch-depth: 2 # minimum for incremental; use 0 for full history
187
+ ```
188
+
189
+ ---
190
+
191
+ ### Every unit reports `commit_count: 0` and `change_frequency: "new"`
192
+
193
+ **Symptom:** Not a few units, all of them, in an application whose files clearly
194
+ have history. `volatile_dependencies` comes back empty at the same time.
195
+
196
+ **Cause:** git ran but could not resolve any ref. The usual shape is a
197
+ containerized linked worktree: `GIT_DIR` points at the worktree's *private*
198
+ git directory, `git rev-parse --git-dir` succeeds, and every ref lookup fails,
199
+ because that private directory reaches the shared object store through a
200
+ relative `commondir` pointer that resolves outside the mount. `git log` then
201
+ exits 0 with no output, and zero commits is indistinguishable from a file that
202
+ was never committed.
203
+
204
+ Woods now requires `git rev-parse HEAD` to succeed before enriching anything.
205
+ When it does not, the git keys are omitted from every unit, provenance records
206
+ `"unknown"`, and one warning names git's own reason. Absent keys mean "not
207
+ known"; they never mean "brand new".
208
+
209
+ **Fix:** Point `WOODS_GIT_DIR` at the *canonical* git directory, the one the
210
+ worktree's `gitdir:` pointer ultimately leads to, and make sure it is mounted:
211
+
212
+ ```bash
213
+ # docker-compose.yml, mounting the parent repository's git directory
214
+ # volumes:
215
+ # - /path/to/repo/.git:/canonical-git:ro
216
+ WOODS_GIT_DIR=/canonical-git bundle exec rake woods:extract
217
+ ```
218
+
219
+ `WOODS_GIT_DIR` wins over whatever the worktree pointer says, and applies to
220
+ every git call Woods makes: per-unit enrichment, `manifest.json` provenance,
221
+ and the diff range `woods:incremental` resolves. All three run through
222
+ `Woods::GitCommand.argv`, so the override cannot reach two of them and miss the
223
+ third.
224
+
225
+ **`GIT_DIR` alone is not enough for a linked worktree.** Woods honors git's own
226
+ `GIT_DIR` and `GIT_COMMON_DIR` because git does, but setting `GIT_DIR` to a
227
+ worktree's private git directory only moves the failure: the `commondir`
228
+ pointer inside it is relative, so it still resolves to a path that is not
229
+ mounted, and `GIT_COMMON_DIR` does not override it. Either mount the canonical
230
+ git directory at the same absolute path the pointer names, or use
231
+ `WOODS_GIT_DIR`.
232
+
233
+ ---
234
+
235
+ ### `woods:incremental` exits 1 with "could not resolve the git diff range"
236
+
237
+ **Symptom:** A CI job fails with `ERROR: could not resolve the git diff range "..."` instead of indexing.
238
+
239
+ **Cause:** The range git was asked to diff does not resolve in the checkout: a GitLab `CI_COMMIT_BEFORE_SHA` of all zeros (new branch), a GitHub base ref that was never fetched, a shallow clone with no `HEAD~1`, or a typo'd revision. This used to read as "no relevant files changed" and the task exited 0 while the sync never ran; it now fails closed, because a green job hiding a skipped sync lets index drift grow unbounded. The one stand-down: a *running* watch daemon maintaining the same index exits 0 with a printed reason, since its start-up catch-up covers the changes. A degraded daemon covers nothing and still exits 1.
240
+
241
+ **Fix:** Repair or provide the range (fetch the base ref, e.g. `fetch-depth: 2` or more, or correct the CI environment variables), set `CHANGED_FILES` explicitly to bypass range resolution, or run a full extraction:
242
+
243
+ ```bash
244
+ bundle exec rake woods:extract
245
+ ```
246
+
247
+ Note `WOODS_IGNORE_WATCH=1` removes daemon coverage rather than bypassing the failure — an unresolved range then exits 1. The full exit-behavior table lives in [Incremental Extraction](./INCREMENTAL_EXTRACTION.md#exit-behavior-in-ci-chains).
248
+
249
+ An image with no `git` binary at all reports `git unavailable: …` and takes the same decision — install git in the image, or set `CHANGED_FILES` so git is never consulted.
250
+
251
+ ---
252
+
253
+ ### `woods:embed`, `woods:embed_incremental`, or `woods:notion_sync` exits 1 after printing `Errors: N`
254
+
255
+ **Symptom:** The task prints its normal summary, reports a non-zero error count, and the job fails.
256
+
257
+ **Cause:** Per-item failures accumulated during the run — a revoked or rate-limited API key that kept failing after the resilience stack exhausted its retries, a full or unreachable vector store, or a Notion 401/400 on every page. These tasks used to print the count and exit 0, which left CI green while the embedding index or the Notion database drifted stale indefinitely. They now fail like their siblings `woods:unblocked_sync` and `woods:obsidian`, and like the extraction family.
258
+
259
+ **Fix:** Read the first five errors the task prints — they name the failing units or pages. Then:
260
+
261
+ - Embedding: check `OPENAI_API_KEY` (or the Ollama endpoint), the provider's rate limits, and free space in the vector store. Re-run `woods:embed_incremental`; the checkpoint means already-embedded units are not paid for twice.
262
+ - Notion: check `NOTION_API_TOKEN` and that every database ID in `notion_database_ids` is shared with the integration.
263
+
264
+ A partial run is not rolled back: whatever succeeded is durable, and re-running after the fix converges.
265
+
266
+ ---
267
+
268
+ ### Extraction exits non-zero after "Could not publish generation"
269
+
270
+ **Symptom:** `woods:extract`, `woods:incremental`, `woods:refresh`, or
271
+ `woods:extract_framework` raises `Woods::ExtractionError` after writing its
272
+ payload, with a message saying that the previous generation remains active.
273
+
274
+ **Cause:** Woods writes a complete candidate payload first and commits it by
275
+ atomically updating `generation.json` last. The marker write failed (commonly
276
+ permissions, a read-only mount, no free space, or an unhealthy filesystem), so
277
+ readers cannot safely discover the candidate payload. One-shot tasks fail
278
+ instead of printing a false success; existing readers continue serving the
279
+ previous complete generation.
280
+
281
+ **Fix:** Correct the filesystem or mount problem named in the exception, then
282
+ rerun the same task. Do not edit `generation.json` by hand or point readers at
283
+ the unreachable payload. A resident `woods:watch` process handles the same
284
+ failure differently: it reports `degraded`, carries the changed paths, and
285
+ retries after a later filesystem event.
286
+
287
+ ---
288
+
289
+ ### `manifest.json` shows the wrong branch (or `git_branch: "unknown"`) in a worktree
290
+
291
+ **Symptom:** `git_branch` / `git_sha` in `manifest.json` name a different branch than the worktree is actually on, or report `"unknown"`. The extracted units themselves are correct, only the provenance metadata is off.
292
+
293
+ **Cause:** In a linked git worktree, `.git` is a *file* containing a `gitdir:` pointer to the real git directory, often an absolute host path. When extraction runs where that path can't be resolved (e.g. inside a container where the host path isn't mounted), git can't read the ref. Woods now reports `"unknown"` in that case rather than emitting a stale, misleading value (previously it fell back to a baked `GIT_BRANCH`/`GIT_SHA` build arg).
294
+
295
+ **Fix:** Make the worktree's git directory reachable from the extraction environment, for example, mount the parent repository (the directory the `gitdir:` pointer references) into the container, or run extraction from a normal (non-worktree) checkout. With the real git directory reachable, `git_branch`/`git_sha` resolve correctly. If the checkout legitimately ships without a `.git` at all (a source tarball, or a Docker `COPY` that excludes it), set `GIT_BRANCH` / `GIT_SHA` explicitly. Woods honors these when there is no `.git` at the root (or no git binary), but suppresses them when a `.git` *is* present but unresolvable (so a stale build arg can't mask a worktree).
296
+
297
+ When the canonical git directory is mounted but not at the path the pointer
298
+ names, set `WOODS_GIT_DIR` to where it actually is. It wins over the pointer for
299
+ provenance and for unit-level git metadata both. Setting git's own `GIT_DIR` to
300
+ the worktree's private git directory does not work: its `commondir` pointer is
301
+ relative and resolves outside the mount.
302
+
303
+ ---
304
+
305
+ ## MCP Server Problems
306
+
307
+ ### "No manifest.json" error when starting the Index Server
308
+
309
+ **Symptom:** `woods-mcp-start` exits with an error like `No manifest.json found at /path/to/...` even though extraction completed.
310
+
311
+ **Cause:** The Index Server is using the container-internal path rather than the host-side path to the volume-mounted output. The server runs on the host and cannot access container filesystem paths.
312
+
313
+ **Fix:** Use the host path in your `.mcp.json`:
314
+
315
+ ```json
316
+ {
317
+ "mcpServers": {
318
+ "codebase": {
319
+ "command": "woods-mcp-start",
320
+ "args": ["./tmp/woods"]
321
+ }
322
+ }
323
+ }
324
+ ```
325
+
326
+ Verify the output is accessible from the host:
327
+
328
+ ```bash
329
+ ls ./tmp/woods/manifest.json
330
+ ```
331
+
332
+ **Since Woods 2.0, a healthy index may not have `manifest.json` at the output root at all.** Extraction publishes each generation into an immutable `payloads/gen-<N>/` directory and points to it from `generation.json`. If the flat path is missing, check the payload path instead before assuming extraction failed:
333
+
334
+ ```bash
335
+ cat ./tmp/woods/generation.json # {"number": 42, "payload": "payloads/gen-42", ...}
336
+ ls ./tmp/woods/payloads/gen-42/manifest.json
337
+ ```
338
+
339
+ `woods-mcp-start` and `IndexReader` already resolve this automatically, this is only for manual inspection. If neither path has a manifest, your Docker volume mount is not configured correctly. See [DOCKER_SETUP.md](DOCKER_SETUP.md).
340
+
341
+ ---
342
+
343
+ ### Index Server exits with `MissingArtifact`
344
+
345
+ **Symptom:** `woods-mcp` exits 2 with `MissingArtifact: No woods.json found ...`.
346
+
347
+ **Cause:** Strict mode is enabled (`WOODS_REQUIRE_INDEX=1`) but no embedding index has been written. By default the server boots without `woods.json`, it serves pattern/regex/structural tools and skips semantic search. You only see this error when you've explicitly opted into fail-closed behavior.
348
+
349
+ **Fix:** Either generate the index so semantic search is available:
350
+
351
+ ```bash
352
+ bundle exec rake woods:extract
353
+ bundle exec rake woods:embed # writes woods.json + vector dumps
354
+ ```
355
+
356
+ …or unset `WOODS_REQUIRE_INDEX` to boot in pattern-only mode. (The older `WOODS_ALLOW_AUTODETECT=1` flag is no longer needed, auto-detect is the default.)
357
+
358
+ ---
359
+
360
+ ### `codebase_retrieve` reports degraded semantic search
361
+
362
+ **Symptom:** `codebase_retrieve` answers with a tool error carrying `error_code: degraded_index` ("Semantic search is degraded…") instead of results, and never with a silently empty context. `woods_status` shows `bootstrap.status: "degraded"`, often with a `hydration_failures` report.
363
+
364
+ **Cause:** The retriever is not healthy, and the server refuses to disguise that as "no matches". Two phases are distinguished in the error metadata:
365
+
366
+ - `phase: "boot"` — a dump failed to hydrate at startup (corrupt or unreadable `vectors.bin` / `metadata.msgpack`), so the affected in-memory store is empty. Before this guidance existed, the server reported a healthy boot and answered every query with empty results as if they were legitimate.
367
+ - `phase: "query"` — the metadata store failed while serving (storage outage, permissions). The typed `Woods::Retriever::StoreError` is mapped to the same degraded payload.
368
+ - `phase: "reload"` — a `reload` attempt failed (M7). Candidate stores are built off-side from one captured generation marker and one captured promoted-dump identity, and a candidate that cannot hydrate aborts the whole transaction: nothing is swapped, the reader keeps the previous generation, and the old retriever keeps answering queries. The reload tool answers with a `degraded_index` error carrying `phase: "reload"`, the `generation:` still being served, and the `stores:` whose refresh failed; `woods_status` exposes the same condition additively as `bootstrap.reload_failure`. This is distinct from the boot `degraded` state — the old stores are healthy, so `codebase_retrieve` keeps working while the condition is visible.
369
+
370
+ The `stores:` field names what is affected (`vector`, `metadata`, `graph`) and `reason:` carries the underlying error.
371
+
372
+ **Fix:**
373
+
374
+ 1. Read `reason:` from the error payload, or call `woods_status` and read `bootstrap.reason` / `bootstrap.hydration_failures`. For a `reload` failure, read `bootstrap.reload_failure` instead — its `generation` field names the generation still being served.
375
+ 2. For a `boot` failure: confirm the index directory is readable, re-run `bundle exec rake woods:embed` if the dump may be corrupt, then restart the MCP server.
376
+ 3. For a `query` failure: check the backing metadata store (the SQLite database in the index directory, or your remote vector backend) for availability and permissions.
377
+ 4. For a `reload` failure: no restart is needed — the server is serving the previous complete generation. Fix the underlying issue the `reason:` names (a corrupt or missing dump usually means re-running `woods:embed`), then invoke `reload` again. A successful reload swaps the new bundle in and clears the condition (`bootstrap.reload_failure` disappears from `woods_status`). Three reasons resolve on their own:
378
+ - `promoted dump changed during reload` — an embed published while the reload was building candidates. Invoke `reload` again once the writer finishes.
379
+ - `index generation moved during reload` — same, for a unit-index publication. Invoke `reload` again.
380
+ - `could not acquire the extraction writer lock` — a writer held the extraction PipelineLock for the whole wait. Invoke `reload` again once the writer finishes. The reload needs write access to the index directory for this lock, same as every writer.
381
+
382
+ ---
383
+
384
+ ### No tools appear in the MCP client
385
+
386
+ **Symptom:** The MCP client connects but shows no tools, or the server exits immediately.
387
+
388
+ **Cause:** The server failed to start, typically due to missing gem dependencies or an incorrect working directory.
389
+
390
+ **Fix:**
391
+
392
+ 1. Check stderr for errors:
393
+
394
+ ```bash
395
+ woods-mcp-start ./tmp/woods 2>&1
396
+ ```
397
+
398
+ 2. Ensure the gem's executables are installed:
399
+
400
+ ```bash
401
+ bundle install
402
+ which woods-mcp-start
403
+ ```
404
+
405
+ 3. For the Console Server, run the rake task directly to see error output:
406
+
407
+ ```bash
408
+ bundle exec rake woods:console
409
+ # Should hang waiting for MCP protocol input: if it exits, check the error
410
+ ```
411
+
412
+ ---
413
+
414
+ ### A console inventory tool is not listed
415
+
416
+ **Symptom:** A tool from the 31-schema inventory is absent from `tools/list`.
417
+
418
+ **Cause:** Supported servers advertise only executable tools: 9 Tier 1 tools by
419
+ default, plus SQL/query when explicitly enabled.
420
+
421
+ **Fix:** Enable `console_embedded_read_tools` for `console_sql` and
422
+ `console_query`. Tier 2, Tier 3, and eval remain inventory only.
423
+
424
+ ---
425
+
426
+ ### MCP client shows JSON parse errors
427
+
428
+ **Symptom:** The MCP client reports protocol errors, malformed JSON, or unexpected tokens.
429
+
430
+ **Cause:** Rails boot emits output to stdout (OpenTelemetry notices, gem warnings, initializer `puts` calls). The `woods:console` rake task redirects stdout to stderr before Rails boots, but custom initializers that print output before this capture can break the MCP protocol.
431
+
432
+ **Fix:**
433
+
434
+ 1. Check for `puts` or `print` calls in your `config/initializers/` files that run at boot.
435
+ 2. Use `Rails.logger` instead of `puts` in initializers.
436
+ 3. Test by running the rake task and isolating streams:
437
+
438
+ ```bash
439
+ bundle exec rake woods:console 2>/dev/null
440
+ # MCP protocol output (stdout) should be valid JSON-RPC
441
+ ```
442
+
443
+ ---
444
+
445
+ ### Parallel tool calls fail together (sibling call failures)
446
+
447
+ **Symptom:** When an MCP client sends multiple tool calls in parallel and one fails, all sibling calls in the same batch also fail.
448
+
449
+ **Cause:** This is MCP client behavior, not a server bug. Some clients (including Claude Desktop and Claude Code) batch parallel tool calls into one request. If any call in the batch raises an error, the transport may reject the entire response frame.
450
+
451
+ **Fix:** There is no server-side fix. Workarounds:
452
+
453
+ 1. **Send one tool call at a time.** If your client supports sequential mode, use it for unreliable calls.
454
+ 2. **Validate parameters before calling.** Use `search` to confirm identifiers exist before passing them to `dependencies` or `lookup`.
455
+ 3. **Avoid mixing high-risk and low-risk calls.** A `lookup` with a typo will take down a parallel `search` that would have succeeded.
456
+
457
+ ---
458
+
459
+ ### MCP client disconnects mid-session
460
+
461
+ **Symptom:** The MCP client reports "server disconnected" or "transport closed" during normal use.
462
+
463
+ **Cause:** Several possible causes, the server process crashed, the stdio transport pipe was broken, or the client's idle timeout expired.
464
+
465
+ **Fix:**
466
+
467
+ 1. Check server stderr for crash output.
468
+ 2. Run the configured command manually from the same `cwd` and inspect stderr. `woods-mcp-start` validates the index before launch but does not restart a crashed server.
469
+ 3. For Docker setups, ensure the container stays running: `docker compose exec -d app tail -f /dev/null` keeps it alive.
470
+
471
+ ---
472
+
473
+ ### Console queries time out on large tables
474
+
475
+ **Symptom:** `console_count` or `console_sample` times out with an error mentioning statement timeout.
476
+
477
+ **Cause:** The default statement timeout is 5000ms (5 seconds). Large tables without a scope condition can exceed this.
478
+
479
+ **Fix:** Add scope conditions to narrow the result set:
480
+
481
+ ```
482
+ console_count(model: "Order", scope: { status: "pending" })
483
+ console_sample(model: "Order", scope: { created_at_gteq: "2025-01-01" })
484
+ ```
485
+
486
+ Scope keys are flat, Ransack-style predicates (`_eq`, `_gt`, `_gteq`, `_lt`, `_lteq`, `_in`, `_not_in`, `_null`, `_not_null`, `_present`, `_blank`, `_matches`) suffixed onto a column name, `scope: { created_at: { gte: "..." } }` (a nested hash) is rejected. A plain key with no suffix (`status: "pending"`) is an equality match.
487
+
488
+ ---
489
+
490
+ ## Embedding Problems
491
+
492
+ ### Configuring vector search on MySQL
493
+
494
+ **Symptom:** You're on MySQL (or Percona / MariaDB / Aurora MySQL) and `config.vector_store = :pgvector` fails at boot, or you can't find a `:mysql` vector adapter in `lib/woods/storage/`.
495
+
496
+ **Cause:** MySQL has no native vector-search extension equivalent to `pgvector`. Woods does not emulate vector search in MySQL, every vector adapter the gem ships delegates to a real vector engine. Note that being on MySQL only constrains the *vector* choice: Woods keeps its own metadata in SQLite or memory (`metadata_store: :sqlite | :in_memory`), never in your application database, so there is no MySQL metadata adapter to configure. (Native `:mysql` / `:postgresql` metadata adapters are future work, `BACKEND_MATRIX.md` documents the shape they would take.)
497
+
498
+ **Fix:** Pair the host app with one of the supported external vector backends. Qdrant is the recommended default for self-hosted / Docker stacks:
499
+
500
+ ```ruby
501
+ # config/initializers/woods.rb: MySQL host app: vectors go to Qdrant
502
+ Woods.configure do |config|
503
+ config.metadata_store = :sqlite # Woods-internal metadata, not your app DB
504
+ config.vector_store = :qdrant
505
+ config.vector_store_options = {
506
+ url: ENV.fetch("QDRANT_URL", "http://localhost:6333"),
507
+ collection: "woods_units",
508
+ allow_private_hosts: true # explicit opt-in for trusted localhost/private URL
509
+ }
510
+ config.embedding_provider = :openai
511
+ config.embedding_options = { api_key: ENV.fetch("OPENAI_API_KEY") }
512
+ end
513
+ ```
514
+
515
+ The Postgres equivalent (in-database vectors via pgvector) is shown for contrast:
516
+
517
+ ```ruby
518
+ # PostgreSQL host: vectors can live in the same database via pgvector
519
+ Woods.configure do |config|
520
+ config.metadata_store = :sqlite
521
+ config.vector_store = :pgvector
522
+ config.vector_store_options = {
523
+ connection: your_pg_connection, # a PG::Connection to a pgvector-enabled DB
524
+ dimensions: 1536
525
+ }
526
+ config.embedding_provider = :openai
527
+ config.embedding_options = { api_key: ENV.fetch("OPENAI_API_KEY") }
528
+ end
529
+ ```
530
+
531
+ For local development against a MySQL app, the `:local` preset (`Woods.configure_with_preset(:local)`, in-memory vectors, SQLite metadata, Ollama embeddings) is a reasonable stand-in. It requires the `sqlite3` gem plus a running Ollama service, but does not exercise the production vector engine. Production MySQL stacks should run Qdrant; the `:production` preset (`vector_store: :qdrant`) is the matching starting point.
532
+
533
+ See [`docs/BACKEND_MATRIX.md`](BACKEND_MATRIX.md#database-compatibility) for the full matrix and the [MySQL + Qdrant section](BACKEND_MATRIX.md#mysql--qdrant-classic-rails) for graph-traversal details (recursive CTEs on 8.0+).
534
+
535
+ ---
536
+
537
+ ### "Dimension mismatch" error when querying embeddings
538
+
539
+ **Symptom:** `codebase_retrieve` raises an error about vector dimensions not matching.
540
+
541
+ **Cause:** The embedding model was changed after embeddings were already stored. The existing vectors have a different dimensionality than the current model produces, and the vector store cannot mix them.
542
+
543
+ **Fix:** Run a full re-index to regenerate all embeddings with the new model:
544
+
545
+ ```bash
546
+ bundle exec rake woods:extract
547
+ bundle exec rake woods:embed
548
+ ```
549
+
550
+ Woods detects the dimension mismatch and raises `Woods::MCP::DimensionMismatch` rather than letting it become a runtime error: `rake woods:embed` refuses before embedding anything (comparing the provider's dimension against the width the `woods_vectors` table or Qdrant collection was created with), and the MCP server refuses at boot (comparing against the dump's WVF1 header). The message names both dimensions and the remedy, drop the vector store and re-index.
551
+
552
+ **A dimension mismatch is never silently tolerated.** If you are getting poor results without seeing this error, the cause is something else.
553
+
554
+ ---
555
+
556
+ ### OpenAI API errors during embedding
557
+
558
+ **Symptom:** Embedding generation fails with `401 Unauthorized` or `429 Too Many Requests`.
559
+
560
+ **Cause:** Missing `OPENAI_API_KEY` environment variable (401), or hitting OpenAI rate limits (429).
561
+
562
+ **Fix:**
563
+
564
+ For 401, set the API key:
565
+
566
+ ```bash
567
+ export OPENAI_API_KEY=sk-...
568
+ bundle exec rake woods:embed
569
+ ```
570
+
571
+ Or configure it in your initializer:
572
+
573
+ ```ruby
574
+ config.embedding_options = { api_key: ENV['OPENAI_API_KEY'] }
575
+ ```
576
+
577
+ For 429, embedding generation is automatically retried with backoff. If rate limits persist, consider batching with smaller codebases or switching to Ollama for local embeddings.
578
+
579
+ ---
580
+
581
+ ### Ollama connection refused
582
+
583
+ **Symptom:** Embedding generation fails with `Connection refused` pointing to `localhost:11434`.
584
+
585
+ **Cause:** The Ollama server is not running, or it is running on a different port.
586
+
587
+ **Fix:**
588
+
589
+ 1. Start Ollama: `ollama serve`
590
+ 2. Verify the model is available: `ollama list`
591
+ 3. If using a non-default port, update config:
592
+
593
+ ```ruby
594
+ config.embedding_options = { host: 'http://localhost:11434' }
595
+ ```
596
+
597
+ ---
598
+
599
+ ### Ollama `400 "the input length exceeds the context length"`
600
+
601
+ **Symptom:** `rake woods:embed` fails with `Ollama API error: 400 {"error":"the input length exceeds the context length"}`. Individual chunks may look smaller than the configured `num_ctx`.
602
+
603
+ **Cause:** Ollama's `/api/embed` endpoint enforces the model's **native** `context_length`, not the `options.num_ctx` override (see [ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)). For `nomic-embed-text` that's 2048 tokens, regardless of what `num_ctx` is set to. Separately, without the `tokenizers` gem, Woods estimates token counts from character length, which under-counts dense Ruby source, so chunks that look safe by char count still trip the 2048-token ceiling.
604
+
605
+ **Fix:** Use Woods 2.0 and install the `tokenizers` gem:
606
+
607
+ ```ruby
608
+ # Gemfile
609
+ gem 'woods', '~> 2.0'
610
+ gem 'tokenizers', '~> 0.5' # exact BERT WordPiece token counting
611
+ ```
612
+
613
+ Woods now:
614
+
615
+ 1. Advertises the native context ceiling per model (2048 for `nomic-embed-text`, 8192 for `bge-m3`/`snowflake-arctic-embed2`, etc.) so the chunker sizes inputs correctly.
616
+ 2. Uses the real BERT tokenizer to verify every chunk, catching the 10–20% gap between char-based estimates and Ollama's internal count.
617
+
618
+ If you want fewer chunks per unit and have the disk space, switch to a larger-context model:
619
+
620
+ ```ruby
621
+ config.embedding_options = {
622
+ model: 'bge-m3', # 8192 native context, 1024 dims
623
+ host: 'http://localhost:11434'
624
+ }
625
+ ```
626
+
627
+ Pull the model first (`ollama pull bge-m3`) and **drop the vector index before re-embedding**: the dimension change (768 → 1024) is incompatible with existing vectors. See [EMBEDDING_MODELS.md](EMBEDDING_MODELS.md) for the full tradeoff matrix.
628
+
629
+ ---
630
+
631
+ ## Storage Problems
632
+
633
+ ### "pgvector extension not found" in PostgreSQL
634
+
635
+ **Symptom:** Running migrations or extraction fails with `PG::UndefinedObject: ERROR: type "vector" does not exist`.
636
+
637
+ **Cause:** The pgvector PostgreSQL extension is not installed in the database.
638
+
639
+ **Fix:**
640
+
641
+ ```sql
642
+ CREATE EXTENSION vector;
643
+ ```
644
+
645
+ Then run the Woods pgvector generator if you haven't already:
646
+
647
+ ```bash
648
+ bundle exec rails generate woods:pgvector
649
+ bundle exec rails db:migrate
650
+ ```
651
+
652
+ ---
653
+
654
+ ### Qdrant connection refused
655
+
656
+ **Symptom:** Embedding or retrieval fails with a connection error to port 6333.
657
+
658
+ **Cause:** The Qdrant server is not running.
659
+
660
+ **Fix:** Start Qdrant via Docker:
661
+
662
+ ```bash
663
+ docker run -p 6333:6333 qdrant/qdrant
664
+ ```
665
+
666
+ Or update your `vector_store_options` to point at the correct host/port:
667
+
668
+ ```ruby
669
+ config.vector_store_options = {
670
+ url: 'http://localhost:6333',
671
+ collection: 'woods',
672
+ allow_private_hosts: true
673
+ }
674
+ ```
675
+
676
+ ---
677
+
678
+ ### SQLite locking errors under concurrent access
679
+
680
+ **Symptom:** Extraction or embedding fails with `SQLite3::BusyException: database is locked`.
681
+
682
+ **Cause:** SQLite does not support concurrent writers. If multiple extraction processes run simultaneously, they contend on the metadata store.
683
+
684
+ **Fix:** Use one embedding publisher at a time. A pgvector backend can accept
685
+ concurrent vector writes, but Woods' SQLite metadata/output artifact still
686
+ needs a coordinated publisher. Configure the hosted preset completely:
687
+
688
+ ```ruby
689
+ Woods.configure_with_preset(:postgresql) do |config|
690
+ config.embedding_options = { api_key: ENV.fetch('OPENAI_API_KEY') }
691
+ config.vector_store_options = { connection: ActiveRecord::Base.connection }
692
+ end
693
+ ```
694
+
695
+ ---
696
+
697
+ ## Docker Problems
698
+
699
+ ### Extraction output not visible on the host
700
+
701
+ **Symptom:** `ls tmp/woods/manifest.json` fails on the host after successful extraction in the container.
702
+
703
+ **Cause:** The extraction output directory (`tmp/woods/`) inside the container is not volume-mounted to the host.
704
+
705
+ **Fix:** Add a volume mount to your `docker-compose.yml`:
706
+
707
+ ```yaml
708
+ services:
709
+ app:
710
+ volumes:
711
+ - .:/app # Full app mount, output lands at ./tmp/woods/
712
+ ```
713
+
714
+ Then re-run extraction. Prefer `docker compose exec app bundle exec rake woods:validate` and `woods:stats`; these checks follow the active v2 generation. Host visibility is only required for an optional host-side Index Server.
715
+
716
+ ---
717
+
718
+ ### Console server exits immediately with "broken pipe"
719
+
720
+ **Symptom:** The MCP client reports a broken pipe or immediate disconnection when using Docker.
721
+
722
+ **Cause:** Plain `docker exec` lacks `-i`, or Docker Compose allocated its default pseudo-TTY. Either breaks stdio MCP communication.
723
+
724
+ **Fix:** Use `-T` with Compose (`stdin` remains attached), or `-i` with plain `docker exec`:
725
+
726
+ ```json
727
+ {
728
+ "mcpServers": {
729
+ "codebase-console": {
730
+ "command": "docker",
731
+ "args": ["compose", "exec", "-T", "app",
732
+ "bundle", "exec", "rake", "woods:console"],
733
+ "cwd": "/absolute/host/path/to/app"
734
+ }
735
+ }
736
+ }
737
+ ```
738
+
739
+ ---
740
+
741
+ ### "No such container" error
742
+
743
+ **Symptom:** `docker exec` fails with `Error response from daemon: No such container: my_app_web_1`.
744
+
745
+ **Cause:** The container name in your `.mcp.json` or `console.yml` doesn't match the actual running container name. Docker Compose generates names following the pattern `<project>-<service>-<index>`.
746
+
747
+ **Fix:** Find the exact name:
748
+
749
+ ```bash
750
+ docker ps --format '{{.Names}}'
751
+ ```
752
+
753
+ Update the container name in your configuration to match exactly.
754
+
755
+ ---
756
+
757
+ ### Path confusion: Index Server uses container path
758
+
759
+ **Symptom:** Index Server starts but fails to load units, or `woods-mcp-start` reports a missing manifest.
760
+
761
+ **Cause:** The `.mcp.json` is pointing at the container-internal path (e.g., `/app/tmp/woods`) instead of the host path.
762
+
763
+ **Fix:** Use the host path in `.mcp.json`. With a standard `.:/app` volume mount, the output is at `./tmp/woods` on the host:
764
+
765
+ ```text
766
+ "args": ["./tmp/woods"] ✓ host path
767
+ "args": ["/app/tmp/woods"] ✗ container path. Index Server cannot read this
768
+ ```
769
+
770
+ ---
771
+
772
+ ## Notion Integration Problems
773
+
774
+ ### 401 Unauthorized from Notion API
775
+
776
+ **Symptom:** `rake woods:notion_sync` fails with a 401 error.
777
+
778
+ **Cause:** The Notion API token is missing or invalid.
779
+
780
+ **Fix:** Set the token via environment variable (takes priority over config):
781
+
782
+ ```bash
783
+ export NOTION_API_TOKEN=secret_...
784
+ bundle exec rake woods:notion_sync
785
+ ```
786
+
787
+ Or configure it in your initializer:
788
+
789
+ ```ruby
790
+ config.notion_api_token = ENV['NOTION_API_TOKEN']
791
+ ```
792
+
793
+ ---
794
+
795
+ ### 404 Not Found from Notion API
796
+
797
+ **Symptom:** Notion sync fails with a 404 error.
798
+
799
+ **Cause:** The database ID in `notion_database_ids` doesn't match any database the integration has access to.
800
+
801
+ **Fix:**
802
+
803
+ 1. Verify the database ID from the Notion URL (the 32-character hex string).
804
+ 2. Confirm the integration has been added to the database in Notion's share settings.
805
+
806
+ ```ruby
807
+ config.notion_database_ids = {
808
+ data_models: 'your-32-char-database-id',
809
+ columns: 'your-other-32-char-database-id'
810
+ }
811
+ ```
812
+
813
+ ---
814
+
815
+ ### 400 Bad Request from Notion API
816
+
817
+ **Symptom:** Notion sync fails with a 400 error mentioning property names or types.
818
+
819
+ **Cause:** The Notion database schema doesn't match the expected property structure. This happens when the database was created manually with different property names or types.
820
+
821
+ **Fix:** Use the Woods-generated database template. Re-create the database or update its properties to match the expected schema. Check the error message for which property name caused the mismatch.
822
+
823
+ ---
824
+
825
+ ### Notion sync is slow but eventually succeeds
826
+
827
+ **Symptom:** Notion sync takes much longer than expected on large codebases.
828
+
829
+ **Cause:** The Notion API enforces a 3 requests/second rate limit. `RateLimiter` handles this automatically, but a codebase with hundreds of models will take proportionally longer.
830
+
831
+ **Behavior:** This is expected and handled automatically. No action needed, the sync will complete.
832
+
833
+ ---
834
+
835
+ ## Units with the same name but different types
836
+
837
+ After upgrading, run embedding again if semantic results omit a factory or database
838
+ view sharing the same name. Current writers distinguish typed storage identities;
839
+ public names remain unchanged. Snapshot migration 007 runs automatically and keeps
840
+ existing rows, but cannot recover variants lost by older writers. See the
841
+ [upgrade guide](UPGRADING_TO_2.md) for storage, flow rebuild, and rollback details.