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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +169 -0
- data/README.md +20 -8
- data/exe/woods-console +51 -6
- data/exe/woods-console-mcp +24 -4
- data/exe/woods-mcp +30 -7
- data/exe/woods-mcp-http +47 -6
- data/lib/generators/woods/install_generator.rb +13 -4
- data/lib/generators/woods/templates/woods.rb.tt +155 -0
- data/lib/tasks/woods.rake +15 -50
- data/lib/woods/builder.rb +174 -9
- data/lib/woods/cache/cache_middleware.rb +360 -31
- data/lib/woods/chunking/semantic_chunker.rb +334 -7
- data/lib/woods/console/adapters/job_adapter.rb +10 -4
- data/lib/woods/console/audit_logger.rb +76 -4
- data/lib/woods/console/bridge.rb +48 -15
- data/lib/woods/console/bridge_protocol.rb +44 -0
- data/lib/woods/console/confirmation.rb +3 -4
- data/lib/woods/console/console_response_renderer.rb +56 -18
- data/lib/woods/console/credential_index.rb +201 -0
- data/lib/woods/console/credential_scanner.rb +302 -0
- data/lib/woods/console/dispatch_pipeline.rb +138 -0
- data/lib/woods/console/embedded_executor.rb +682 -35
- data/lib/woods/console/eval_guard.rb +319 -0
- data/lib/woods/console/model_validator.rb +1 -3
- data/lib/woods/console/rack_middleware.rb +185 -29
- data/lib/woods/console/redactor.rb +161 -0
- data/lib/woods/console/response_context.rb +127 -0
- data/lib/woods/console/safe_context.rb +220 -23
- data/lib/woods/console/scope_predicate_parser.rb +131 -0
- data/lib/woods/console/server.rb +417 -486
- data/lib/woods/console/sql_noise_stripper.rb +87 -0
- data/lib/woods/console/sql_table_scanner.rb +213 -0
- data/lib/woods/console/sql_validator.rb +81 -31
- data/lib/woods/console/table_gate.rb +93 -0
- data/lib/woods/console/tool_specs.rb +552 -0
- data/lib/woods/console/tools/tier1.rb +3 -3
- data/lib/woods/console/tools/tier4.rb +7 -1
- data/lib/woods/dependency_graph.rb +66 -7
- data/lib/woods/embedding/indexer.rb +190 -6
- data/lib/woods/embedding/openai.rb +40 -4
- data/lib/woods/embedding/provider.rb +104 -8
- data/lib/woods/embedding/text_preparer.rb +23 -3
- data/lib/woods/embedding/token_counter.rb +133 -0
- data/lib/woods/evaluation/baseline_runner.rb +20 -2
- data/lib/woods/evaluation/metrics.rb +4 -1
- data/lib/woods/extracted_unit.rb +1 -0
- data/lib/woods/extractor.rb +7 -1
- data/lib/woods/extractors/controller_extractor.rb +6 -0
- data/lib/woods/extractors/mailer_extractor.rb +16 -2
- data/lib/woods/extractors/model_extractor.rb +6 -1
- data/lib/woods/extractors/phlex_extractor.rb +13 -4
- data/lib/woods/extractors/rails_source_extractor.rb +2 -0
- data/lib/woods/extractors/route_helper_resolver.rb +130 -0
- data/lib/woods/extractors/shared_dependency_scanner.rb +130 -2
- data/lib/woods/extractors/view_component_extractor.rb +12 -1
- data/lib/woods/extractors/view_engines/base.rb +141 -0
- data/lib/woods/extractors/view_engines/erb.rb +145 -0
- data/lib/woods/extractors/view_template_extractor.rb +92 -133
- data/lib/woods/flow_assembler.rb +23 -15
- data/lib/woods/flow_precomputer.rb +21 -2
- data/lib/woods/graph_analyzer.rb +3 -4
- data/lib/woods/index_artifact.rb +173 -0
- data/lib/woods/mcp/bearer_auth.rb +45 -0
- data/lib/woods/mcp/bootstrap_state.rb +94 -0
- data/lib/woods/mcp/bootstrapper.rb +337 -16
- data/lib/woods/mcp/config_resolver.rb +288 -0
- data/lib/woods/mcp/errors.rb +134 -0
- data/lib/woods/mcp/index_reader.rb +265 -30
- data/lib/woods/mcp/origin_guard.rb +132 -0
- data/lib/woods/mcp/provider_probe.rb +166 -0
- data/lib/woods/mcp/renderers/claude_renderer.rb +6 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +39 -3
- data/lib/woods/mcp/renderers/plain_renderer.rb +16 -2
- data/lib/woods/mcp/server.rb +737 -137
- data/lib/woods/model_name_cache.rb +78 -2
- data/lib/woods/notion/client.rb +25 -2
- data/lib/woods/notion/mappers/model_mapper.rb +36 -2
- data/lib/woods/railtie.rb +55 -15
- data/lib/woods/resilience/circuit_breaker.rb +9 -2
- data/lib/woods/resilience/retryable_provider.rb +40 -3
- data/lib/woods/resolved_config.rb +299 -0
- data/lib/woods/retrieval/context_assembler.rb +112 -5
- data/lib/woods/retrieval/query_classifier.rb +1 -1
- data/lib/woods/retrieval/ranker.rb +55 -6
- data/lib/woods/retrieval/search_executor.rb +42 -13
- data/lib/woods/retriever.rb +330 -24
- data/lib/woods/session_tracer/middleware.rb +35 -1
- data/lib/woods/storage/graph_store.rb +39 -0
- data/lib/woods/storage/inapplicable_backend.rb +14 -0
- data/lib/woods/storage/metadata_store.rb +129 -1
- data/lib/woods/storage/pgvector.rb +70 -8
- data/lib/woods/storage/qdrant.rb +196 -5
- data/lib/woods/storage/snapshotter/metadata.rb +172 -0
- data/lib/woods/storage/snapshotter/vector.rb +238 -0
- data/lib/woods/storage/snapshotter.rb +24 -0
- data/lib/woods/storage/vector_store.rb +184 -35
- data/lib/woods/tasks.rb +85 -0
- data/lib/woods/temporal/snapshot_store.rb +49 -1
- data/lib/woods/token_utils.rb +44 -5
- data/lib/woods/unblocked/client.rb +1 -1
- data/lib/woods/unblocked/document_builder.rb +35 -10
- data/lib/woods/unblocked/exporter.rb +1 -1
- data/lib/woods/util/host_guard.rb +61 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods.rb +126 -6
- 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
|