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
@@ -1,210 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require_relative 'bridge_protocol'
5
- require_relative 'model_validator'
6
- require_relative 'safe_context'
7
-
8
- module Woods
9
- module Console
10
- # **PROTOCOL SCAFFOLD — does not execute real queries.** Every handler
11
- # below returns static empty data (`{ 'count' => 0 }`, `{ 'records' =>
12
- # [] }`, etc.). Real in-process execution lives in
13
- # {Woods::Console::EmbeddedExecutor}; the eventual real bridge process
14
- # for Option D will replace this scaffold with a class that performs
15
- # actual ActiveRecord queries.
16
- #
17
- # The scaffold pins the JSON-lines wire protocol — request envelope,
18
- # response envelope, supported-tools list, error shape — so other
19
- # components (EmbeddedExecutor, ConnectionManager, Server) can be
20
- # built and tested against a stable contract before the real
21
- # bridge-process implementation lands. Treat this class the way you'd
22
- # treat a Sinatra fake of a third-party API in tests: it satisfies
23
- # the protocol, nothing more.
24
- #
25
- # ## Why the name carries "Stub"
26
- #
27
- # Round-1 audit Track H-4 flagged this class as a "critical SafeContext
28
- # bypass" because `handle_request` doesn't wrap calls in `SafeContext`.
29
- # That finding wasn't exploitable — no live code path executes through
30
- # this class in the shipped gem — but the bare name `Bridge` made the
31
- # scaffold status invisible to auditors. The `Stub` prefix removes the
32
- # ambiguity. When the real bridge-process implementation is delivered,
33
- # it should claim the `Bridge` name; this class will either be deleted
34
- # (if the protocol is fully owned by the real bridge) or renamed to
35
- # `BridgeProtocol` and reduced to a constants module.
36
- #
37
- # ## Protocol
38
- #
39
- # Reads JSON-lines requests from an input IO, validates model/column names,
40
- # dispatches to (stub) tool handlers, and writes JSON-lines responses to
41
- # an output IO.
42
- #
43
- # Protocol:
44
- # Request: {"id":"req_1","tool":"count","params":{"model":"Order","scope":{"status":"pending"}}}
45
- # Response: {"id":"req_1","ok":true,"result":{"count":1847},"timing_ms":12.3}
46
- # Error: {"id":"req_1","ok":false,"error":"Model not found","error_type":"validation"}
47
- #
48
- # @example Wiring against a fake input/output (testing only — handlers return empty data)
49
- # bridge = StubBridge.new(input: $stdin, output: $stdout,
50
- # model_validator: validator, safe_context: ctx)
51
- # bridge.run
52
- #
53
- class StubBridge
54
- # Protocol constants live on {BridgeProtocol} so the real executor
55
- # (EmbeddedExecutor) and a future real bridge-process class can
56
- # reference them without importing the scaffold. These top-level
57
- # aliases keep `StubBridge::SUPPORTED_TOOLS` working for existing
58
- # callers and specs.
59
- SUPPORTED_TOOLS = BridgeProtocol::SUPPORTED_TOOLS
60
- TIER1_TOOLS = BridgeProtocol::TIER1_TOOLS
61
- TOOL_HANDLERS = BridgeProtocol::TOOL_HANDLERS
62
-
63
- # @param input [IO] Input stream (reads JSON-lines)
64
- # @param output [IO] Output stream (writes JSON-lines)
65
- # @param model_validator [ModelValidator] Validates model/column names
66
- # @param safe_context [SafeContext] Wraps execution in safe transaction
67
- def initialize(input:, output:, model_validator:, safe_context:)
68
- @input = input
69
- @output = output
70
- @model_validator = model_validator
71
- @safe_context = safe_context
72
- end
73
-
74
- # Read loop — processes requests until input is closed.
75
- #
76
- # @return [void]
77
- def run
78
- @input.each_line do |line|
79
- line = line.strip
80
- next if line.empty?
81
-
82
- request = parse_request(line)
83
- next unless request
84
-
85
- response = handle_request(request)
86
- write_response(response)
87
- end
88
- end
89
-
90
- # Process a single request hash and return a response hash.
91
- #
92
- # @param request [Hash] Parsed request with "id", "tool", "params"
93
- # @return [Hash] Response with "id", "ok", and "result" or "error"
94
- def handle_request(request)
95
- id = request['id']
96
- tool = request['tool']
97
- params = request['params'] || {}
98
-
99
- return error_response(id, "Unknown tool: #{tool}", 'unknown_tool') unless SUPPORTED_TOOLS.include?(tool)
100
-
101
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
- result = dispatch(tool, params)
103
- elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)
104
-
105
- { 'id' => id, 'ok' => true, 'result' => result, 'timing_ms' => elapsed }
106
- rescue ValidationError => e
107
- error_response(id, e.message, 'validation')
108
- rescue StandardError => e
109
- error_response(id, e.message, 'execution')
110
- end
111
-
112
- private
113
-
114
- # Parse a JSON line into a request hash.
115
- #
116
- # @param line [String] Raw JSON line
117
- # @return [Hash, nil] Parsed request or nil on parse error
118
- def parse_request(line)
119
- JSON.parse(line)
120
- rescue JSON::ParserError => e
121
- write_response(error_response(nil, "Invalid JSON: #{e.message}", 'parse'))
122
- nil
123
- end
124
-
125
- # Dispatch a tool request to the appropriate handler.
126
- #
127
- # @param tool [String] Tool name
128
- # @param params [Hash] Tool parameters
129
- # @return [Hash] Tool result
130
- def dispatch(tool, params)
131
- case tool
132
- when 'status'
133
- handle_status
134
- when 'schema'
135
- handle_schema(params)
136
- else
137
- validate_model_param(params)
138
- handler = TOOL_HANDLERS.fetch(tool) { raise ValidationError, "Unknown tool: #{tool}" }
139
- send(handler, params)
140
- end
141
- end
142
-
143
- # Validate that the model parameter is present and known.
144
- def validate_model_param(params)
145
- model = params['model']
146
- raise ValidationError, 'Missing required parameter: model' unless model
147
-
148
- @model_validator.validate_model!(model)
149
- end
150
-
151
- # Stub handlers below return empty/zero data by design — see the
152
- # class-level docstring. Real in-process execution happens in
153
- # EmbeddedExecutor; the eventual Option-D bridge process will replace
154
- # this class entirely.
155
-
156
- def handle_count(_params)
157
- { 'count' => 0 }
158
- end
159
-
160
- def handle_sample(_params)
161
- { 'records' => [] }
162
- end
163
-
164
- def handle_find(_params)
165
- { 'record' => nil }
166
- end
167
-
168
- def handle_pluck(params)
169
- @model_validator.validate_columns!(params['model'], params['columns']) if params['columns']
170
- { 'columns' => Array(params['columns']), 'values' => [] }
171
- end
172
-
173
- def handle_aggregate(params)
174
- @model_validator.validate_column!(params['model'], params['column']) if params['column']
175
- { 'value' => nil }
176
- end
177
-
178
- def handle_association_count(_params)
179
- { 'count' => 0 }
180
- end
181
-
182
- def handle_schema(params)
183
- model = params['model']
184
- raise ValidationError, 'Missing required parameter: model' unless model
185
-
186
- @model_validator.validate_model!(model)
187
- { 'columns' => @model_validator.columns_for(model), 'indexes' => [] }
188
- end
189
-
190
- def handle_recent(_params)
191
- { 'records' => [] }
192
- end
193
-
194
- def handle_status
195
- { 'status' => 'ok', 'models' => @model_validator.model_names }
196
- end
197
-
198
- # Build an error response hash.
199
- def error_response(id, message, error_type)
200
- { 'id' => id, 'ok' => false, 'error' => message, 'error_type' => error_type }
201
- end
202
-
203
- # Write a JSON-line response to the output stream.
204
- def write_response(response)
205
- @output.puts(JSON.generate(response))
206
- @output.flush
207
- end
208
- end
209
- end
210
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base'
4
-
5
- module Woods
6
- module Formatting
7
- # Formats assembled context as XML for Claude models.
8
- #
9
- # Produces structured XML with:
10
- # - `<codebase-context>` root element
11
- # - `<meta>` tag with token usage and budget
12
- # - `<content>` section with indented context text
13
- # - `<sources>` section with self-closing `<source>` elements
14
- #
15
- # @example
16
- # adapter = ClaudeAdapter.new
17
- # xml = adapter.format(assembled_context)
18
- # # => "<codebase-context>\n <meta tokens=\"42\" budget=\"8000\" />\n..."
19
- #
20
- class ClaudeAdapter < Base
21
- # Format assembled context as XML for Claude.
22
- #
23
- # @param assembled_context [Woods::Retrieval::AssembledContext]
24
- # @return [String] XML-formatted context
25
- def format(assembled_context)
26
- parts = []
27
- parts << '<codebase-context>'
28
- parts << " <meta tokens=\"#{assembled_context.tokens_used}\" budget=\"#{assembled_context.budget}\" />"
29
- parts << format_content(assembled_context.context)
30
- parts << format_sources(assembled_context.sources)
31
- parts << '</codebase-context>'
32
- parts.join("\n")
33
- end
34
-
35
- private
36
-
37
- # Format the content section with indentation.
38
- #
39
- # @param context [String]
40
- # @return [String]
41
- def format_content(context)
42
- lines = []
43
- lines << ' <content>'
44
- lines << indent(escape_xml(context), 4) unless context.empty?
45
- lines << ' </content>'
46
- lines.join("\n")
47
- end
48
-
49
- # Format the sources section.
50
- #
51
- # @param sources [Array<Hash>]
52
- # @return [String]
53
- def format_sources(sources)
54
- lines = []
55
- lines << ' <sources>'
56
- sources.each do |source|
57
- lines << " <source #{source_attributes(source)} />"
58
- end
59
- lines << ' </sources>'
60
- lines.join("\n")
61
- end
62
-
63
- # Build attribute string for a source element.
64
- #
65
- # @param source [Hash]
66
- # @return [String]
67
- def source_attributes(source)
68
- [
69
- "identifier=\"#{escape_xml(source[:identifier].to_s)}\"",
70
- "type=\"#{escape_xml(source[:type].to_s)}\"",
71
- "score=\"#{source[:score]}\"",
72
- "file=\"#{escape_xml(source[:file_path].to_s)}\""
73
- ].join(' ')
74
- end
75
-
76
- # Indent every line of text by the given number of spaces.
77
- #
78
- # @param text [String]
79
- # @param spaces [Integer]
80
- # @return [String]
81
- def indent(text, spaces)
82
- prefix = ' ' * spaces
83
- text.lines.map { |line| "#{prefix}#{line.chomp}" }.join("\n")
84
- end
85
-
86
- # Escape XML special characters.
87
- #
88
- # @param text [String]
89
- # @return [String]
90
- def escape_xml(text)
91
- text.gsub('&', '&amp;')
92
- .gsub('<', '&lt;')
93
- .gsub('>', '&gt;')
94
- .gsub('"', '&quot;')
95
- end
96
- end
97
- end
98
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base'
4
-
5
- module Woods
6
- module Formatting
7
- # Formats assembled context as plain text for generic LLM consumption.
8
- #
9
- # Produces plain text with:
10
- # - `=== CODEBASE CONTEXT ===` header
11
- # - Token usage line
12
- # - Content separated by `---` dividers
13
- # - Sources in bracket notation
14
- #
15
- # @example
16
- # adapter = GenericAdapter.new
17
- # text = adapter.format(assembled_context)
18
- # # => "=== CODEBASE CONTEXT ===\nTokens: 42 / 8000\n..."
19
- #
20
- class GenericAdapter < Base
21
- # Format assembled context as plain text.
22
- #
23
- # @param assembled_context [Woods::Retrieval::AssembledContext]
24
- # @return [String] Plain text formatted context
25
- def format(assembled_context)
26
- parts = []
27
- parts << '=== CODEBASE CONTEXT ==='
28
- parts << "Tokens: #{assembled_context.tokens_used} / #{assembled_context.budget}"
29
- parts << '---'
30
- parts << assembled_context.context
31
- parts.concat(format_sources(assembled_context.sources))
32
- parts.join("\n")
33
- end
34
-
35
- private
36
-
37
- # Format sources in bracket notation.
38
- #
39
- # @param sources [Array<Hash>]
40
- # @return [Array<String>] Lines to append
41
- def format_sources(sources)
42
- return [] if sources.empty?
43
-
44
- lines = []
45
- lines << '---'
46
- sources.each do |source|
47
- identifier = source[:identifier]
48
- type = source[:type]
49
- score = source[:score]
50
- lines << "[Source: #{identifier} (#{type}) \u2014 score: #{score}]"
51
- end
52
- lines
53
- end
54
- end
55
- end
56
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base'
4
-
5
- module Woods
6
- module Formatting
7
- # Formats assembled context as Markdown for GPT models.
8
- #
9
- # Produces Markdown with:
10
- # - `## Codebase Context` heading
11
- # - Token usage in bold
12
- # - Content in a fenced Ruby code block
13
- # - Sources as a bullet list
14
- #
15
- # @example
16
- # adapter = GptAdapter.new
17
- # md = adapter.format(assembled_context)
18
- # # => "## Codebase Context\n\n**Tokens:** 42/8000\n..."
19
- #
20
- class GptAdapter < Base
21
- # Format assembled context as Markdown for GPT.
22
- #
23
- # @param assembled_context [Woods::Retrieval::AssembledContext]
24
- # @return [String] Markdown-formatted context
25
- def format(assembled_context)
26
- parts = []
27
- parts << '## Codebase Context'
28
- parts << ''
29
- parts << "**Tokens:** #{assembled_context.tokens_used}/#{assembled_context.budget}"
30
- parts << ''
31
- parts << '---'
32
- parts << ''
33
- parts << '```ruby'
34
- parts << assembled_context.context
35
- parts << '```'
36
- parts.concat(format_sources(assembled_context.sources))
37
- parts.join("\n")
38
- end
39
-
40
- private
41
-
42
- # Format sources as a Markdown bullet list.
43
- #
44
- # @param sources [Array<Hash>]
45
- # @return [Array<String>] Lines to append
46
- def format_sources(sources)
47
- return [] if sources.empty?
48
-
49
- lines = []
50
- lines << ''
51
- lines << '### Sources'
52
- lines << ''
53
- sources.each do |source|
54
- identifier = source[:identifier]
55
- type = source[:type]
56
- score = source[:score]
57
- file_path = source[:file_path]
58
- lines << "- **#{identifier}** (#{type}) \u2014 score: #{score}, file: #{file_path}"
59
- end
60
- lines
61
- end
62
- end
63
- end
64
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'mappers/shared'
4
- require_relative 'mappers/model_mapper'
5
- require_relative 'mappers/column_mapper'
6
- require_relative 'mappers/migration_mapper'
7
-
8
- module Woods
9
- module Notion
10
- # Dispatcher for Notion mappers. Returns the appropriate mapper for a unit type.
11
- #
12
- # @example
13
- # mapper = Mapper.for("model")
14
- # properties = mapper.map(unit_data)
15
- #
16
- class Mapper
17
- REGISTRY = {
18
- 'model' => Mappers::ModelMapper,
19
- 'column' => Mappers::ColumnMapper,
20
- 'migration' => Mappers::MigrationMapper
21
- }.freeze
22
-
23
- # Get a mapper instance for a unit type.
24
- #
25
- # @param type [String] Unit type name (e.g. "model", "column", "migration")
26
- # @return [Object, nil] Mapper instance or nil if type is not supported
27
- def self.for(type)
28
- klass = REGISTRY[type]
29
- klass&.new
30
- end
31
-
32
- # List all supported unit types.
33
- #
34
- # @return [Array<String>]
35
- def self.supported_types
36
- REGISTRY.keys
37
- end
38
- end
39
- end
40
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Woods
4
- module Observability
5
- # Probes configured components and reports overall system health.
6
- #
7
- # Checks vector store, metadata store, and embedding provider by calling
8
- # lightweight operations on each. Components that are nil are reported
9
- # as :not_configured and do not affect the overall healthy? status.
10
- #
11
- # @example
12
- # check = HealthCheck.new(
13
- # vector_store: vector_store,
14
- # metadata_store: metadata_store,
15
- # embedding_provider: provider
16
- # )
17
- # status = check.run
18
- # status.healthy? # => true
19
- # status.components # => { vector_store: :ok, metadata_store: :ok, embedding_provider: :ok }
20
- #
21
- class HealthCheck
22
- # Value object representing the result of a health check.
23
- HealthStatus = Struct.new(:healthy?, :components, keyword_init: true)
24
-
25
- # @param vector_store [Object, nil] Vector store adapter (must respond to #count)
26
- # @param metadata_store [Object, nil] Metadata store adapter (must respond to #count)
27
- # @param embedding_provider [Object, nil] Embedding provider (must respond to #embed)
28
- def initialize(vector_store: nil, metadata_store: nil, embedding_provider: nil)
29
- @vector_store = vector_store
30
- @metadata_store = metadata_store
31
- @embedding_provider = embedding_provider
32
- end
33
-
34
- # Run health probes on all configured components.
35
- #
36
- # @return [HealthStatus] Result with healthy? flag and per-component status
37
- def run
38
- components = {
39
- vector_store: probe_store(@vector_store),
40
- metadata_store: probe_store(@metadata_store),
41
- embedding_provider: probe_provider(@embedding_provider)
42
- }
43
-
44
- all_healthy = components.values.all? { |status| %i[ok not_configured].include?(status) }
45
-
46
- HealthStatus.new(healthy?: all_healthy, components: components)
47
- end
48
-
49
- private
50
-
51
- # Probe a store component by calling #count.
52
- #
53
- # @param store [Object, nil] Store adapter
54
- # @return [Symbol] :ok, :error, or :not_configured
55
- def probe_store(store)
56
- return :not_configured if store.nil?
57
-
58
- store.count
59
- :ok
60
- rescue StandardError
61
- :error
62
- end
63
-
64
- # Probe an embedding provider by checking its capabilities without making network calls.
65
- #
66
- # @param provider [Object, nil] Embedding provider
67
- # @return [Symbol] :ok, :error, or :not_configured
68
- def probe_provider(provider)
69
- return :not_configured if provider.nil?
70
-
71
- if provider.respond_to?(:embed) && provider.respond_to?(:dimensions)
72
- :ok
73
- else
74
- :error
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Woods
4
- module Observability
5
- # Lightweight instrumentation wrapper that delegates to ActiveSupport::Notifications
6
- # when available, and falls back to a simple yield otherwise.
7
- #
8
- # @example
9
- # Instrumentation.instrument('woods.extraction', unit: 'User') do
10
- # extract_unit(user_model)
11
- # end
12
- #
13
- module Instrumentation
14
- module_function
15
-
16
- # Instrument a block of code with an event name and payload.
17
- #
18
- # Delegates to ActiveSupport::Notifications.instrument when available.
19
- # Otherwise, yields the block directly.
20
- #
21
- # @param event [String] Event name (e.g., 'woods.extraction')
22
- # @param payload [Hash] Additional data to include with the event
23
- # @yield [payload] The block to instrument
24
- # @return [Object] The return value of the block
25
- def instrument(event, payload = {}, &block)
26
- if defined?(ActiveSupport::Notifications)
27
- ActiveSupport::Notifications.instrument(event, payload, &block)
28
- elsif block
29
- yield payload
30
- end
31
- end
32
- end
33
- end
34
- end