woods 1.6.4 → 2.0.0.beta1

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 (282) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1879 -37
  3. data/CONTRIBUTING.md +195 -137
  4. data/README.md +162 -520
  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 +620 -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 +415 -0
  19. data/docs/INTERNALS.md +415 -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 +197 -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 +39 -3
  36. data/exe/woods-console-mcp +21 -35
  37. data/exe/woods-mcp +20 -7
  38. data/exe/woods-mcp-http +78 -24
  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 +40 -1
  52. data/lib/woods/builder.rb +310 -22
  53. data/lib/woods/cache/cache_middleware.rb +18 -13
  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/credential_index.rb +5 -53
  64. data/lib/woods/console/credential_scanner.rb +15 -16
  65. data/lib/woods/console/dispatch_pipeline.rb +46 -34
  66. data/lib/woods/console/embedded_executor.rb +806 -257
  67. data/lib/woods/console/eval_guard.rb +27 -20
  68. data/lib/woods/console/input_contract.rb +78 -0
  69. data/lib/woods/console/model_validator.rb +24 -6
  70. data/lib/woods/console/rack_middleware.rb +62 -63
  71. data/lib/woods/console/redactor.rb +10 -24
  72. data/lib/woods/console/safe_context.rb +45 -45
  73. data/lib/woods/console/scope_predicate_parser.rb +41 -0
  74. data/lib/woods/console/server.rb +136 -267
  75. data/lib/woods/console/sql_noise_stripper.rb +20 -51
  76. data/lib/woods/console/sql_table_scanner.rb +39 -90
  77. data/lib/woods/console/sql_validator.rb +455 -85
  78. data/lib/woods/console/table_gate.rb +2 -2
  79. data/lib/woods/console/tool_specs.rb +462 -88
  80. data/lib/woods/console/tools/tier1.rb +0 -3
  81. data/lib/woods/console/tools/tier4.rb +17 -7
  82. data/lib/woods/coordination/lock_heartbeat.rb +103 -0
  83. data/lib/woods/coordination/pipeline_lock.rb +263 -53
  84. data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
  85. data/lib/woods/db/migrator.rb +3 -9
  86. data/lib/woods/db/schema_version.rb +47 -2
  87. data/lib/woods/dependency_graph.rb +898 -64
  88. data/lib/woods/embedding/fake.rb +138 -0
  89. data/lib/woods/embedding/indexer.rb +832 -40
  90. data/lib/woods/embedding/openai.rb +77 -19
  91. data/lib/woods/embedding/provider.rb +189 -11
  92. data/lib/woods/embedding/text_preparer.rb +1 -1
  93. data/lib/woods/embedding/token_counter.rb +0 -7
  94. data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
  95. data/lib/woods/evaluation/ablation_executor.rb +67 -0
  96. data/lib/woods/evaluation/ablation_provenance.rb +38 -0
  97. data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
  98. data/lib/woods/evaluation/ablation_runner.rb +173 -0
  99. data/lib/woods/evaluation/ablation_summary.rb +65 -0
  100. data/lib/woods/evaluation/ablation_task.rb +66 -0
  101. data/lib/woods/evaluation/ablation_task_set.rb +77 -0
  102. data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
  103. data/lib/woods/evaluation/ablation_worktree.rb +71 -0
  104. data/lib/woods/evaluation/baseline.rb +60 -0
  105. data/lib/woods/evaluation/baseline_runner.rb +11 -3
  106. data/lib/woods/evaluation/evaluator.rb +41 -8
  107. data/lib/woods/evaluation/query_set.rb +79 -13
  108. data/lib/woods/evaluation/report_generator.rb +20 -1
  109. data/lib/woods/export/unit_facts.rb +0 -11
  110. data/lib/woods/extracted_unit.rb +22 -63
  111. data/lib/woods/extractor.rb +2503 -192
  112. data/lib/woods/extractors/action_cable_extractor.rb +9 -4
  113. data/lib/woods/extractors/ast_source_extraction.rb +20 -2
  114. data/lib/woods/extractors/caching_extractor.rb +46 -12
  115. data/lib/woods/extractors/callback_analyzer.rb +39 -9
  116. data/lib/woods/extractors/component_discovery.rb +123 -0
  117. data/lib/woods/extractors/concern_extractor.rb +17 -3
  118. data/lib/woods/extractors/controller_extractor.rb +389 -29
  119. data/lib/woods/extractors/decorator_extractor.rb +7 -14
  120. data/lib/woods/extractors/engine_extractor.rb +53 -8
  121. data/lib/woods/extractors/event_extractor.rb +55 -4
  122. data/lib/woods/extractors/factory_extractor.rb +49 -11
  123. data/lib/woods/extractors/graphql_extractor.rb +162 -66
  124. data/lib/woods/extractors/i18n_extractor.rb +6 -1
  125. data/lib/woods/extractors/job_extractor.rb +51 -21
  126. data/lib/woods/extractors/lib_extractor.rb +23 -17
  127. data/lib/woods/extractors/line_neutralizer.rb +171 -0
  128. data/lib/woods/extractors/mailer_extractor.rb +9 -1
  129. data/lib/woods/extractors/manager_extractor.rb +19 -2
  130. data/lib/woods/extractors/migration_extractor.rb +22 -11
  131. data/lib/woods/extractors/model_extractor.rb +292 -57
  132. data/lib/woods/extractors/package_extractor.rb +154 -0
  133. data/lib/woods/extractors/phlex_extractor.rb +18 -3
  134. data/lib/woods/extractors/policy_extractor.rb +6 -5
  135. data/lib/woods/extractors/poro_extractor.rb +13 -14
  136. data/lib/woods/extractors/pundit_extractor.rb +3 -3
  137. data/lib/woods/extractors/rails_source_extractor.rb +24 -7
  138. data/lib/woods/extractors/rake_task_extractor.rb +158 -30
  139. data/lib/woods/extractors/reference_patterns.rb +38 -0
  140. data/lib/woods/extractors/route_extractor.rb +58 -2
  141. data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
  142. data/lib/woods/extractors/serializer_extractor.rb +3 -4
  143. data/lib/woods/extractors/service_extractor.rb +11 -1
  144. data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
  145. data/lib/woods/extractors/shared_utility_methods.rb +36 -6
  146. data/lib/woods/extractors/source_nesting.rb +560 -0
  147. data/lib/woods/extractors/state_machine_extractor.rb +30 -18
  148. data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
  149. data/lib/woods/extractors/view_component_extractor.rb +28 -3
  150. data/lib/woods/extractors/view_engines/erb.rb +17 -3
  151. data/lib/woods/feedback/gap_detector.rb +9 -3
  152. data/lib/woods/feedback/store.rb +7 -1
  153. data/lib/woods/filename_utils.rb +29 -1
  154. data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
  155. data/lib/woods/flow_assembler.rb +63 -21
  156. data/lib/woods/flow_document.rb +1 -0
  157. data/lib/woods/flow_precomputer.rb +138 -22
  158. data/lib/woods/gem_mapper.rb +285 -0
  159. data/lib/woods/generation.rb +185 -0
  160. data/lib/woods/git_command.rb +38 -0
  161. data/lib/woods/git_provenance.rb +16 -2
  162. data/lib/woods/graph_analyzer.rb +408 -34
  163. data/lib/woods/index_artifact.rb +93 -23
  164. data/lib/woods/mcp/bearer_auth.rb +92 -22
  165. data/lib/woods/mcp/bootstrap_state.rb +77 -0
  166. data/lib/woods/mcp/bootstrapper.rb +582 -77
  167. data/lib/woods/mcp/config_resolver.rb +66 -6
  168. data/lib/woods/mcp/errors.rb +60 -0
  169. data/lib/woods/mcp/index_reader.rb +836 -117
  170. data/lib/woods/mcp/index_reader_pinning.rb +78 -0
  171. data/lib/woods/mcp/origin_guard.rb +108 -23
  172. data/lib/woods/mcp/protocol_policy.rb +98 -0
  173. data/lib/woods/mcp/provider_probe.rb +45 -6
  174. data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
  175. data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
  176. data/lib/woods/mcp/server.rb +907 -154
  177. data/lib/woods/mcp/tasks/extension.rb +196 -0
  178. data/lib/woods/mcp/tasks/request_capture.rb +45 -0
  179. data/lib/woods/mcp/tasks/store.rb +518 -0
  180. data/lib/woods/mcp/tool_contract.rb +171 -0
  181. data/lib/woods/mcp/tool_response_renderer.rb +7 -0
  182. data/lib/woods/mcp/version_aware_tool_dispatch.rb +3 -9
  183. data/lib/woods/model_name_cache.rb +19 -1
  184. data/lib/woods/notion/client.rb +132 -36
  185. data/lib/woods/notion/exporter.rb +456 -61
  186. data/lib/woods/notion/mappers/column_mapper.rb +34 -5
  187. data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
  188. data/lib/woods/notion/mappers/model_mapper.rb +21 -6
  189. data/lib/woods/notion/mappers/shared.rb +45 -3
  190. data/lib/woods/notion/sync_manifest.rb +258 -0
  191. data/lib/woods/obsidian/errors.rb +6 -0
  192. data/lib/woods/obsidian/name_mapper.rb +40 -24
  193. data/lib/woods/obsidian/vault_exporter.rb +103 -36
  194. data/lib/woods/operator/pipeline_guard.rb +118 -21
  195. data/lib/woods/operator/status_reporter.rb +20 -3
  196. data/lib/woods/path_dispatcher.rb +276 -0
  197. data/lib/woods/payload_store.rb +223 -0
  198. data/lib/woods/published_index/edge_shaper.rb +61 -0
  199. data/lib/woods/published_index/generation_catalog.rb +72 -0
  200. data/lib/woods/published_index/typed_unit_reader.rb +48 -0
  201. data/lib/woods/published_index.rb +287 -0
  202. data/lib/woods/railtie.rb +70 -38
  203. data/lib/woods/railtie_support.rb +167 -0
  204. data/lib/woods/release.rb +12 -0
  205. data/lib/woods/reload_policy.rb +206 -0
  206. data/lib/woods/resilience/circuit_breaker.rb +47 -8
  207. data/lib/woods/resilience/index_validator.rb +296 -10
  208. data/lib/woods/resilience/retryable_provider.rb +71 -6
  209. data/lib/woods/resolved_config.rb +55 -11
  210. data/lib/woods/retrieval/context_assembler.rb +132 -40
  211. data/lib/woods/retrieval/query_classifier.rb +25 -6
  212. data/lib/woods/retrieval/ranker.rb +193 -28
  213. data/lib/woods/retrieval/search_executor.rb +206 -39
  214. data/lib/woods/retriever.rb +317 -71
  215. data/lib/woods/retry_after.rb +22 -2
  216. data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
  217. data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
  218. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
  219. data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
  220. data/lib/woods/ruby_analyzer.rb +21 -5
  221. data/lib/woods/session_tracer/file_store.rb +138 -19
  222. data/lib/woods/session_tracer/redis_store.rb +122 -12
  223. data/lib/woods/session_tracer/session_flow_assembler.rb +54 -11
  224. data/lib/woods/session_tracer/session_flow_document.rb +52 -6
  225. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  226. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  227. data/lib/woods/session_tracer/store.rb +14 -1
  228. data/lib/woods/storage/metadata_store.rb +230 -26
  229. data/lib/woods/storage/pgvector.rb +180 -22
  230. data/lib/woods/storage/qdrant.rb +367 -41
  231. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  232. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  233. data/lib/woods/storage/snapshotter.rb +23 -5
  234. data/lib/woods/storage/vector_store.rb +49 -8
  235. data/lib/woods/storage_identity.rb +28 -0
  236. data/lib/woods/tasks.rb +53 -2
  237. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  238. data/lib/woods/temporal/snapshot_store.rb +139 -42
  239. data/lib/woods/unblocked/client.rb +119 -17
  240. data/lib/woods/unblocked/document_builder.rb +34 -2
  241. data/lib/woods/unblocked/exporter.rb +63 -27
  242. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  243. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  244. data/lib/woods/update_check.rb +24 -1
  245. data/lib/woods/util/uuid5.rb +124 -0
  246. data/lib/woods/version.rb +1 -1
  247. data/lib/woods/watch/daemon.rb +1345 -0
  248. data/lib/woods/watch/listen_watcher.rb +81 -0
  249. data/lib/woods/watch/polling_watcher.rb +137 -0
  250. data/lib/woods/watch/status.rb +169 -0
  251. data/lib/woods/watch/tree_scan.rb +163 -0
  252. data/lib/woods/watch/watcher.rb +100 -0
  253. data/lib/woods.rb +53 -9
  254. data/plugin/.claude-plugin/plugin.json +18 -0
  255. data/plugin/hooks/hooks.json +29 -0
  256. data/plugin/hooks/woods-post-edit.sh +226 -0
  257. data/plugin/hooks/woods-session-start.sh +77 -0
  258. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  259. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  260. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  261. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  262. data/plugin/skills/woods-setup/SKILL.md +99 -0
  263. metadata +102 -30
  264. data/lib/woods/console/adapter_family.rb +0 -39
  265. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  266. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  267. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  268. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  269. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  270. data/lib/woods/console/bridge.rb +0 -210
  271. data/lib/woods/console/credential_scanner_registry.rb +0 -36
  272. data/lib/woods/console/encrypted_credential_snapshot.rb +0 -16
  273. data/lib/woods/console/sql_output_policy.rb +0 -535
  274. data/lib/woods/console/sqlite_read_guard.rb +0 -46
  275. data/lib/woods/formatting/claude_adapter.rb +0 -98
  276. data/lib/woods/formatting/generic_adapter.rb +0 -56
  277. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  278. data/lib/woods/mcp/http_transport_options.rb +0 -15
  279. data/lib/woods/mcp/origin_policy.rb +0 -113
  280. data/lib/woods/notion/mapper.rb +0 -40
  281. data/lib/woods/observability/health_check.rb +0 -79
  282. data/lib/woods/observability/instrumentation.rb +0 -34
@@ -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,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'origin_policy'
4
-
5
- module Woods
6
- module MCP
7
- # Keep the SDK DNS-rebinding guard aligned with Woods' explicit allowlist.
8
- # Empty configuration retains the SDK's strict loopback defaults.
9
- module HttpTransportOptions
10
- def self.for(origins)
11
- OriginPolicy.new(allowed_origins: origins).transport_options
12
- end
13
- end
14
- end
15
- end
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'uri'
4
- require_relative '../util/host_guard'
5
-
6
- module Woods
7
- module MCP
8
- # One immutable policy for the outer Rack guard and the legacy MCP SDK.
9
- # Keep SDK rebinding protection active and pass original request headers.
10
- class OriginPolicy
11
- LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
12
- DEFAULT_ORIGINS = %w[
13
- http://localhost http://127.0.0.1 http://[::1]
14
- https://localhost https://127.0.0.1 https://[::1]
15
- ].freeze
16
-
17
- attr_reader :transport_options
18
-
19
- def initialize(allowed_origins: nil)
20
- entries = Array(allowed_origins).compact.reject do |entry|
21
- entry.to_s.valid_encoding? && entry.to_s.strip.empty?
22
- end
23
- @explicit = entries.map { |entry| configured_origin(entry) }.uniq.freeze
24
- @allowed = @explicit.empty? ? DEFAULT_ORIGINS : @explicit
25
- @hosts = @explicit.map do |origin|
26
- host = authority(origin)
27
- (host.end_with?(']') ? host[1...-1] : host).freeze
28
- end.uniq.freeze
29
- @transport_options = if @explicit.empty?
30
- {}.freeze
31
- else
32
- { allowed_origins: transport_origins, allowed_hosts: @hosts }.freeze
33
- end
34
- freeze
35
- end
36
-
37
- def host_allowed?(host)
38
- return true if host.nil?
39
- return false unless host.is_a?(String) && host.valid_encoding?
40
-
41
- parsed = parsed_origin("http://#{host}")
42
- return false unless parsed
43
-
44
- hostname = parsed.hostname.downcase
45
- return false if Util::HostGuard.suspicious_numeric_host?(hostname)
46
- return true if LOOPBACK_HOSTS.include?(hostname)
47
-
48
- @hosts.include?(host.downcase) || @hosts.include?(hostname)
49
- end
50
-
51
- def origin_allowed?(origin, host:)
52
- return true if origin.nil?
53
- return false unless parsed_origin(origin)
54
-
55
- normalized = normalized_origin(origin)
56
- return true if @explicit.include?(normalized)
57
- return false unless @allowed.include?(normalized) || @allowed.include?(normalized.sub(/:\d+\z/, ''))
58
- return false unless host.is_a?(String) && host.valid_encoding?
59
-
60
- default_port = normalized.start_with?('https://') ? ':443' : ':80'
61
- authority(normalized).delete_suffix(default_port) == host.downcase.delete_suffix(default_port)
62
- end
63
-
64
- private
65
-
66
- def configured_origin(entry)
67
- raw = entry.to_s
68
- value = raw.strip.downcase.delete_suffix('/') if raw.valid_encoding?
69
- unless parsed_origin(value)
70
- label = raw.b.byteslice(0, 160).inspect
71
- raise ArgumentError, "Invalid MCP allowed origin #{label}: expected http(s)://host[:port] without a path"
72
- end
73
-
74
- normalized_origin(value).freeze
75
- end
76
-
77
- def normalized_origin(origin)
78
- value = origin.downcase
79
- value.delete_suffix(value.start_with?('https://') ? ':443' : ':80')
80
- end
81
-
82
- # SDK 0.23 and 0.25 compare explicit entries literally. Both default-port
83
- # spellings are needed for cross-origin requests; same-authority matching
84
- # is otherwise performed independently by both layers.
85
- def transport_origins
86
- @explicit.flat_map do |origin|
87
- parsed = parsed_origin(origin)
88
- port = parsed.scheme == 'https' ? 443 : 80
89
- parsed.port == port ? [origin, "#{origin}:#{port}"] : [origin]
90
- end.uniq.map(&:freeze).freeze
91
- end
92
-
93
- def authority(origin)
94
- origin.sub(%r{\Ahttps?://}, '')
95
- end
96
-
97
- def parsed_origin(origin)
98
- return unless origin.is_a?(String) && origin.valid_encoding? && origin.ascii_only?
99
- return if origin.match?(/[[:space:][:cntrl:]]/)
100
-
101
- parsed = URI.parse(origin)
102
- return unless %w[http https].include?(parsed.scheme&.downcase)
103
- return unless parsed.host && !parsed.host.empty?
104
- return unless parsed.path.to_s.empty? && !parsed.userinfo && !parsed.query && !parsed.fragment
105
- return unless parsed.port&.between?(1, 65_535)
106
-
107
- parsed
108
- rescue URI::InvalidURIError, ArgumentError
109
- nil
110
- end
111
- end
112
- end
113
- 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