woods 1.2.0 → 1.3.0

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +169 -0
  3. data/README.md +20 -8
  4. data/exe/woods-console +51 -6
  5. data/exe/woods-console-mcp +24 -4
  6. data/exe/woods-mcp +30 -7
  7. data/exe/woods-mcp-http +47 -6
  8. data/lib/generators/woods/install_generator.rb +13 -4
  9. data/lib/generators/woods/templates/woods.rb.tt +155 -0
  10. data/lib/tasks/woods.rake +15 -50
  11. data/lib/woods/builder.rb +174 -9
  12. data/lib/woods/cache/cache_middleware.rb +360 -31
  13. data/lib/woods/chunking/semantic_chunker.rb +334 -7
  14. data/lib/woods/console/adapters/job_adapter.rb +10 -4
  15. data/lib/woods/console/audit_logger.rb +76 -4
  16. data/lib/woods/console/bridge.rb +48 -15
  17. data/lib/woods/console/bridge_protocol.rb +44 -0
  18. data/lib/woods/console/confirmation.rb +3 -4
  19. data/lib/woods/console/console_response_renderer.rb +56 -18
  20. data/lib/woods/console/credential_index.rb +201 -0
  21. data/lib/woods/console/credential_scanner.rb +302 -0
  22. data/lib/woods/console/dispatch_pipeline.rb +138 -0
  23. data/lib/woods/console/embedded_executor.rb +682 -35
  24. data/lib/woods/console/eval_guard.rb +319 -0
  25. data/lib/woods/console/model_validator.rb +1 -3
  26. data/lib/woods/console/rack_middleware.rb +185 -29
  27. data/lib/woods/console/redactor.rb +161 -0
  28. data/lib/woods/console/response_context.rb +127 -0
  29. data/lib/woods/console/safe_context.rb +220 -23
  30. data/lib/woods/console/scope_predicate_parser.rb +131 -0
  31. data/lib/woods/console/server.rb +417 -486
  32. data/lib/woods/console/sql_noise_stripper.rb +87 -0
  33. data/lib/woods/console/sql_table_scanner.rb +213 -0
  34. data/lib/woods/console/sql_validator.rb +81 -31
  35. data/lib/woods/console/table_gate.rb +93 -0
  36. data/lib/woods/console/tool_specs.rb +552 -0
  37. data/lib/woods/console/tools/tier1.rb +3 -3
  38. data/lib/woods/console/tools/tier4.rb +7 -1
  39. data/lib/woods/dependency_graph.rb +66 -7
  40. data/lib/woods/embedding/indexer.rb +190 -6
  41. data/lib/woods/embedding/openai.rb +40 -4
  42. data/lib/woods/embedding/provider.rb +104 -8
  43. data/lib/woods/embedding/text_preparer.rb +23 -3
  44. data/lib/woods/embedding/token_counter.rb +133 -0
  45. data/lib/woods/evaluation/baseline_runner.rb +20 -2
  46. data/lib/woods/evaluation/metrics.rb +4 -1
  47. data/lib/woods/extracted_unit.rb +1 -0
  48. data/lib/woods/extractor.rb +7 -1
  49. data/lib/woods/extractors/controller_extractor.rb +6 -0
  50. data/lib/woods/extractors/mailer_extractor.rb +16 -2
  51. data/lib/woods/extractors/model_extractor.rb +6 -1
  52. data/lib/woods/extractors/phlex_extractor.rb +13 -4
  53. data/lib/woods/extractors/rails_source_extractor.rb +2 -0
  54. data/lib/woods/extractors/route_helper_resolver.rb +130 -0
  55. data/lib/woods/extractors/shared_dependency_scanner.rb +130 -2
  56. data/lib/woods/extractors/view_component_extractor.rb +12 -1
  57. data/lib/woods/extractors/view_engines/base.rb +141 -0
  58. data/lib/woods/extractors/view_engines/erb.rb +145 -0
  59. data/lib/woods/extractors/view_template_extractor.rb +92 -133
  60. data/lib/woods/flow_assembler.rb +23 -15
  61. data/lib/woods/flow_precomputer.rb +21 -2
  62. data/lib/woods/graph_analyzer.rb +3 -4
  63. data/lib/woods/index_artifact.rb +173 -0
  64. data/lib/woods/mcp/bearer_auth.rb +45 -0
  65. data/lib/woods/mcp/bootstrap_state.rb +94 -0
  66. data/lib/woods/mcp/bootstrapper.rb +337 -16
  67. data/lib/woods/mcp/config_resolver.rb +288 -0
  68. data/lib/woods/mcp/errors.rb +134 -0
  69. data/lib/woods/mcp/index_reader.rb +265 -30
  70. data/lib/woods/mcp/origin_guard.rb +132 -0
  71. data/lib/woods/mcp/provider_probe.rb +166 -0
  72. data/lib/woods/mcp/renderers/claude_renderer.rb +6 -0
  73. data/lib/woods/mcp/renderers/markdown_renderer.rb +39 -3
  74. data/lib/woods/mcp/renderers/plain_renderer.rb +16 -2
  75. data/lib/woods/mcp/server.rb +737 -137
  76. data/lib/woods/model_name_cache.rb +78 -2
  77. data/lib/woods/notion/client.rb +25 -2
  78. data/lib/woods/notion/mappers/model_mapper.rb +36 -2
  79. data/lib/woods/railtie.rb +55 -15
  80. data/lib/woods/resilience/circuit_breaker.rb +9 -2
  81. data/lib/woods/resilience/retryable_provider.rb +40 -3
  82. data/lib/woods/resolved_config.rb +299 -0
  83. data/lib/woods/retrieval/context_assembler.rb +112 -5
  84. data/lib/woods/retrieval/query_classifier.rb +1 -1
  85. data/lib/woods/retrieval/ranker.rb +55 -6
  86. data/lib/woods/retrieval/search_executor.rb +42 -13
  87. data/lib/woods/retriever.rb +330 -24
  88. data/lib/woods/session_tracer/middleware.rb +35 -1
  89. data/lib/woods/storage/graph_store.rb +39 -0
  90. data/lib/woods/storage/inapplicable_backend.rb +14 -0
  91. data/lib/woods/storage/metadata_store.rb +129 -1
  92. data/lib/woods/storage/pgvector.rb +70 -8
  93. data/lib/woods/storage/qdrant.rb +196 -5
  94. data/lib/woods/storage/snapshotter/metadata.rb +172 -0
  95. data/lib/woods/storage/snapshotter/vector.rb +238 -0
  96. data/lib/woods/storage/snapshotter.rb +24 -0
  97. data/lib/woods/storage/vector_store.rb +184 -35
  98. data/lib/woods/tasks.rb +85 -0
  99. data/lib/woods/temporal/snapshot_store.rb +49 -1
  100. data/lib/woods/token_utils.rb +44 -5
  101. data/lib/woods/unblocked/client.rb +1 -1
  102. data/lib/woods/unblocked/document_builder.rb +35 -10
  103. data/lib/woods/unblocked/exporter.rb +1 -1
  104. data/lib/woods/util/host_guard.rb +61 -0
  105. data/lib/woods/version.rb +1 -1
  106. data/lib/woods.rb +126 -6
  107. metadata +69 -4
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_validator'
4
+
5
+ module Woods
6
+ module Console
7
+ # Parses Ransack-style predicate suffixes in scope hashes and builds
8
+ # safe Arel predicates without string interpolation.
9
+ #
10
+ # Supported suffixes:
11
+ # _eq, _not_eq — equality / inequality
12
+ # _gt, _gteq, _lt, _lteq — numeric/date comparisons
13
+ # _in, _not_in — set membership (value must be Array)
14
+ # _null, _not_null — IS NULL / IS NOT NULL (value must be boolean)
15
+ # _present — IS NOT NULL AND != '' (value must be boolean)
16
+ # _blank — IS NULL OR = '' (value must be boolean)
17
+ # _matches — LIKE pattern
18
+ #
19
+ # Plain hash keys (no recognised suffix) are treated as equality conditions
20
+ # and handled by the standard ActiveRecord where(hash) path.
21
+ #
22
+ # Every column reference is validated through ModelValidator#validate_column!
23
+ # before an Arel node is built — SQL injection via column names is impossible.
24
+ #
25
+ # @example
26
+ # parser = ScopePredicateParser.new(model_name: 'Order', model_validator: validator)
27
+ # relation = parser.parse(Order, { status: 'paid', total_refund_gt: 0 })
28
+ #
29
+ class ScopePredicateParser
30
+ SUPPORTED_SUFFIXES = %w[
31
+ _eq _not_eq
32
+ _gt _gteq _lt _lteq
33
+ _in _not_in
34
+ _null _not_null
35
+ _present _blank
36
+ _matches
37
+ ].freeze
38
+
39
+ # Suffix pattern — longest suffix match wins because we scan the full list.
40
+ SUFFIX_PATTERN = /(_eq|_not_eq|_gteq|_lteq|_gt|_lt|_not_in|_not_null|_in|_null|_present|_blank|_matches)\z/
41
+
42
+ SUFFIX_HINT = "Supported suffixes: #{SUPPORTED_SUFFIXES.join(', ')}.".freeze
43
+
44
+ # @param model_name [String] ActiveRecord model name (e.g. 'Order')
45
+ # @param model_validator [ModelValidator] Validates column names
46
+ def initialize(model_name:, model_validator:)
47
+ @model_name = model_name
48
+ @model_validator = model_validator
49
+ end
50
+
51
+ # Parse a scope hash and return an ActiveRecord::Relation.
52
+ #
53
+ # Keys without a recognised suffix are collected into a plain equality
54
+ # hash and applied via relation.where(hash) — the fast path.
55
+ # Keys with a recognised suffix are validated and built via Arel.
56
+ #
57
+ # @param relation [ActiveRecord::Relation, Class] Starting relation
58
+ # @param scope_hash [Hash] Scope conditions, possibly with predicate suffixes
59
+ # @return [ActiveRecord::Relation]
60
+ # @raise [ValidationError] on unknown column or unsupported suffix
61
+ def parse(relation, scope_hash)
62
+ equality = {}
63
+ arel_nodes = []
64
+
65
+ scope_hash.each do |raw_key, value|
66
+ key = raw_key.to_s
67
+ match = SUFFIX_PATTERN.match(key)
68
+
69
+ if match
70
+ suffix = match[1]
71
+ column = key.delete_suffix(suffix)
72
+ @model_validator.validate_column!(@model_name, column)
73
+ arel_nodes << build_node(relation, column, suffix, value)
74
+ else
75
+ equality[raw_key] = value
76
+ end
77
+ end
78
+
79
+ relation = relation.where(equality) if equality.any?
80
+ arel_nodes.each { |node| relation = relation.where(node) }
81
+ relation
82
+ end
83
+
84
+ private
85
+
86
+ # Build an Arel predicate node for a validated column + suffix.
87
+ #
88
+ # @param relation [ActiveRecord::Relation, Class] Used to get the arel_table
89
+ # @param column [String] Validated column name
90
+ # @param suffix [String] One of SUPPORTED_SUFFIXES
91
+ # @param value [Object] The predicate value
92
+ # @return [Arel::Nodes::Node]
93
+ def build_node(relation, column, suffix, value) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
94
+ attr = arel_table(relation)[column]
95
+
96
+ case suffix
97
+ when '_eq' then attr.eq(value)
98
+ when '_not_eq' then attr.not_eq(value)
99
+ when '_gt' then attr.gt(value)
100
+ when '_gteq' then attr.gteq(value)
101
+ when '_lt' then attr.lt(value)
102
+ when '_lteq' then attr.lteq(value)
103
+ when '_in' then attr.in(Array(value))
104
+ when '_not_in' then attr.not_in(Array(value))
105
+ when '_null'
106
+ value ? attr.eq(nil) : attr.not_eq(nil)
107
+ when '_not_null'
108
+ value ? attr.not_eq(nil) : attr.eq(nil)
109
+ when '_present'
110
+ # present = NOT NULL AND NOT blank string
111
+ value ? attr.not_eq(nil).and(attr.not_eq('')) : attr.eq(nil).or(attr.eq(''))
112
+ when '_blank'
113
+ # blank = NULL OR empty string
114
+ value ? attr.eq(nil).or(attr.eq('')) : attr.not_eq(nil).and(attr.not_eq(''))
115
+ when '_matches'
116
+ attr.matches(value)
117
+ else
118
+ raise ValidationError, "Unsupported predicate suffix '#{suffix}'. #{SUFFIX_HINT}"
119
+ end
120
+ end
121
+
122
+ # Extract the Arel table from a relation or model class.
123
+ #
124
+ # @param relation [ActiveRecord::Relation, Class]
125
+ # @return [Arel::Table]
126
+ def arel_table(relation)
127
+ relation.respond_to?(:arel_table) ? relation.arel_table : relation.klass.arel_table
128
+ end
129
+ end
130
+ end
131
+ end