woods 2.0.1 → 2.1.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 +94 -7
- data/CONTRIBUTING.md +134 -19
- data/README.md +1 -1
- data/docs/AGENT_GUIDE.md +19 -0
- data/docs/AGENT_SETUP.md +22 -2
- data/docs/BACKEND_MATRIX.md +7 -0
- data/docs/CLIENT_HOOKS.md +6 -0
- data/docs/CONFIGURATION_REFERENCE.md +133 -25
- data/docs/CONSOLE_MCP_SETUP.md +82 -30
- data/docs/EMBEDDING_MODELS.md +16 -19
- data/docs/EXTRACTOR_REFERENCE.md +219 -21
- data/docs/FAQ.md +11 -25
- data/docs/GETTING_STARTED.md +7 -1
- data/docs/INCREMENTAL_EXTRACTION.md +261 -19
- data/docs/INDEX_LAYOUT.md +5 -0
- data/docs/INTERNALS.md +9 -0
- data/docs/MCP_HTTP_TRANSPORT.md +20 -15
- data/docs/MCP_SERVERS.md +87 -8
- data/docs/MCP_TOOL_COOKBOOK.md +13 -55
- data/docs/NOTION_INTEGRATION.md +7 -1
- data/docs/PUBLISHED_INDEX.md +6 -0
- data/docs/README.md +6 -1
- data/docs/RETRIEVAL_GUIDE.md +17 -0
- data/docs/SOURCE_FRESHNESS.md +157 -5
- data/docs/TOKEN_BENCHMARK.md +10 -18
- data/docs/TROUBLESHOOTING.md +70 -14
- data/docs/UNBLOCKED_INTEGRATION.md +60 -8
- data/docs/UPGRADING_TO_2.md +153 -38
- data/docs/WATCH_DAEMON.md +97 -14
- data/exe/woods-console-mcp +2 -2
- data/lib/generators/woods/templates/woods.rb.tt +2 -1
- data/lib/tasks/woods.rake +23 -7
- data/lib/tasks/woods_checks.rake +2 -2
- data/lib/woods/agent_configuration/cli.rb +1 -1
- data/lib/woods/agent_configuration/layout.rb +16 -2
- data/lib/woods/agent_configuration/plan.rb +13 -3
- data/lib/woods/agent_configuration/planner_validation.rb +4 -2
- data/lib/woods/agent_configuration/preflight.rb +5 -3
- data/lib/woods/builder.rb +17 -57
- data/lib/woods/cache/cache_middleware.rb +56 -30
- data/lib/woods/chunking/contributor_chunks.rb +119 -0
- data/lib/woods/chunking/semantic_chunker.rb +44 -21
- data/lib/woods/console/connection_manager.rb +56 -3
- data/lib/woods/console/embedded_executor.rb +30 -5
- data/lib/woods/console/rack_middleware.rb +29 -1
- data/lib/woods/dependency_graph.rb +34 -10
- data/lib/woods/embedding/fake.rb +12 -0
- data/lib/woods/embedding/indexer.rb +195 -98
- data/lib/woods/embedding/input_budget.rb +67 -0
- data/lib/woods/embedding/openai.rb +70 -20
- data/lib/woods/embedding/provider.rb +37 -25
- data/lib/woods/embedding/text_preparer.rb +76 -32
- data/lib/woods/embedding/token_counter.rb +18 -81
- data/lib/woods/embedding/vector_configuration.rb +48 -0
- data/lib/woods/extraction_identities.rb +175 -0
- data/lib/woods/extractor.rb +304 -107
- data/lib/woods/extractors/action_cable_extractor.rb +8 -3
- data/lib/woods/extractors/assigned_value_discovery.rb +74 -0
- data/lib/woods/extractors/class_declarations.rb +121 -0
- data/lib/woods/extractors/configuration_extractor.rb +11 -3
- data/lib/woods/extractors/declaration_ancestry.rb +92 -0
- data/lib/woods/extractors/event_extractor.rb +8 -0
- data/lib/woods/extractors/graphql_extractor.rb +134 -77
- data/lib/woods/extractors/job_extractor.rb +5 -1
- data/lib/woods/extractors/lib_extractor.rb +132 -15
- data/lib/woods/extractors/mailer_extractor.rb +3 -5
- data/lib/woods/extractors/manager_extractor.rb +7 -21
- data/lib/woods/extractors/migration_declaration.rb +87 -0
- data/lib/woods/extractors/migration_extractor.rb +5 -39
- data/lib/woods/extractors/phlex_extractor.rb +6 -2
- data/lib/woods/extractors/policy_extractor.rb +9 -5
- data/lib/woods/extractors/poro_extractor.rb +112 -53
- data/lib/woods/extractors/pundit_extractor.rb +11 -6
- data/lib/woods/extractors/scheduled_job_extractor.rb +45 -4
- data/lib/woods/extractors/serializer_extractor.rb +34 -22
- data/lib/woods/extractors/shared_utility_methods.rb +18 -1
- data/lib/woods/extractors/source_nesting.rb +142 -106
- data/lib/woods/extractors/standalone_module_discovery.rb +123 -0
- data/lib/woods/extractors/state_machine_extractor.rb +46 -40
- data/lib/woods/extractors/view_component_extractor.rb +9 -7
- data/lib/woods/flow_assembler.rb +4 -1
- data/lib/woods/generation.rb +25 -0
- data/lib/woods/hooks/context_hint.rb +7 -2
- data/lib/woods/mcp/bootstrapper.rb +33 -7
- data/lib/woods/mcp/config_resolver.rb +26 -7
- data/lib/woods/mcp/index_reader.rb +125 -24
- data/lib/woods/mcp/index_reader_pinning.rb +16 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +7 -1
- data/lib/woods/mcp/renderers/plain_renderer.rb +3 -1
- data/lib/woods/mcp/search_results.rb +7 -1
- data/lib/woods/mcp/server.rb +24 -4
- data/lib/woods/module_reconciliation.rb +151 -0
- data/lib/woods/path_dispatcher.rb +7 -2
- data/lib/woods/rake_helpers.rb +43 -11
- data/lib/woods/release.rb +1 -1
- data/lib/woods/resilience/index_validator.rb +8 -3
- data/lib/woods/resilience/retryable_provider.rb +18 -1
- data/lib/woods/resolved_config.rb +68 -8
- data/lib/woods/retrieval/context_assembler.rb +3 -3
- data/lib/woods/retrieval/lexical_assembler.rb +3 -2
- data/lib/woods/retrieval/scope.rb +18 -2
- data/lib/woods/retrieval/source_evidence.rb +14 -2
- data/lib/woods/source_contributor_validation.rb +78 -0
- data/lib/woods/source_contributors.rb +116 -0
- data/lib/woods/source_inputs/handoff.rb +37 -0
- data/lib/woods/source_inputs/launcher.rb +53 -13
- data/lib/woods/source_inputs/manifest.rb +84 -3
- data/lib/woods/source_inputs/private_key.rb +44 -12
- data/lib/woods/source_inputs/scanner.rb +98 -27
- data/lib/woods/source_inputs/scopes.rb +1 -1
- data/lib/woods/source_inputs/session.rb +147 -15
- data/lib/woods/source_inputs/stable_reader.rb +127 -0
- data/lib/woods/source_inputs/status.rb +40 -8
- data/lib/woods/source_inputs/verifier.rb +28 -5
- data/lib/woods/source_path_encoding.rb +33 -0
- data/lib/woods/source_references/cache.rb +284 -0
- data/lib/woods/source_references/collector.rb +120 -0
- data/lib/woods/source_references/extraction.rb +185 -0
- data/lib/woods/source_references/inputs.rb +134 -0
- data/lib/woods/source_references/parser_adapter.rb +134 -0
- data/lib/woods/source_references/pass.rb +152 -0
- data/lib/woods/source_references/prism_adapter.rb +116 -0
- data/lib/woods/source_references/registry.rb +178 -0
- data/lib/woods/source_references/runtime_lookup.rb +127 -0
- data/lib/woods/source_references/value_class.rb +82 -0
- data/lib/woods/storage/metadata_store.rb +4 -1
- data/lib/woods/storage/qdrant.rb +2 -2
- data/lib/woods/unblocked/client.rb +12 -7
- data/lib/woods/unblocked/document_builder.rb +4 -1
- data/lib/woods/unblocked/exporter.rb +127 -37
- data/lib/woods/unblocked/sync_manifest.rb +137 -21
- data/lib/woods/unblocked/uri_migration.rb +105 -0
- data/lib/woods/util/host_guard.rb +3 -2
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/catch_up.rb +138 -0
- data/lib/woods/watch/claim_lease.rb +150 -0
- data/lib/woods/watch/cli.rb +26 -2
- data/lib/woods/watch/daemon.rb +80 -59
- data/lib/woods/watch/installation/options.rb +1 -1
- data/lib/woods/watch/installation/receipt.rb +6 -1
- data/lib/woods/watch/managed_child.rb +1 -1
- data/lib/woods/watch/supervisor.rb +1 -1
- data/lib/woods/watch/tree_scan.rb +14 -2
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.rb +3 -2
- data/plugin/hooks/woods-input-rules.sh +4 -0
- data/plugin/hooks/woods-refresh.sh +15 -7
- data/plugin/hooks/woods-session-start.sh +60 -3
- data/plugin/skills/woods-diagnose/SKILL.md +334 -11
- data/plugin/skills/woods-investigate/SKILL.md +11 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +79 -8
- data/plugin/skills/woods-setup/SKILL.md +53 -6
- metadata +32 -5
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Woods
|
|
4
|
+
module Embedding
|
|
5
|
+
module Provider
|
|
6
|
+
# Keeps an explicit API width request separate from the expected width
|
|
7
|
+
# recorded in an index or discovered by a provider probe.
|
|
8
|
+
module VectorConfiguration
|
|
9
|
+
# @return [Integer, nil] explicitly requested API output width
|
|
10
|
+
attr_reader :requested_dimensions
|
|
11
|
+
|
|
12
|
+
# Non-probing width declaration, also used to validate cache entries.
|
|
13
|
+
# @return [Integer, nil]
|
|
14
|
+
def configured_dimensions
|
|
15
|
+
@expected_dimensions || @requested_dimensions
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def configure_dimensions(dimensions:, expected_dimensions:)
|
|
21
|
+
@requested_dimensions = normalize_dimensions(dimensions)
|
|
22
|
+
@expected_dimensions = normalize_dimensions(expected_dimensions)
|
|
23
|
+
return unless @requested_dimensions && @expected_dimensions
|
|
24
|
+
return if @requested_dimensions == @expected_dimensions
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, 'requested dimensions must match the expected vector dimensions'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def normalize_dimensions(value)
|
|
30
|
+
return if value.nil?
|
|
31
|
+
|
|
32
|
+
dimensions = Integer(value)
|
|
33
|
+
raise ArgumentError, "dimensions must be positive, got #{value.inspect}" unless dimensions.positive?
|
|
34
|
+
|
|
35
|
+
dimensions
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_vectors!(vectors, expected_count:, provider:, indexes: nil)
|
|
39
|
+
VectorValidation.validate!(
|
|
40
|
+
vectors, expected_count: expected_count, provider: provider, indexes: indexes,
|
|
41
|
+
expected_dimensions: configured_dimensions || @observed_dimensions
|
|
42
|
+
)
|
|
43
|
+
@observed_dimensions ||= vectors.first&.size # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'woods'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
# A conflicting source owner must abort even before staged artifacts change.
|
|
7
|
+
class IdentityCollisionError < ExtractionError; end
|
|
8
|
+
|
|
9
|
+
# Shared source-ownership checks for full, incremental and refresh writers.
|
|
10
|
+
module ExtractionIdentities # rubocop:disable Metrics/ModuleLength -- keep replacement authority beside collision checks
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def identity_source(path)
|
|
14
|
+
File.expand_path(path.to_s, Rails.root) unless path.nil?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Validate the complete batch before registering or writing any member.
|
|
18
|
+
# Fresh claims survive removals within this run; only a retained baseline
|
|
19
|
+
# owner may relocate after its old source is positively confirmed absent.
|
|
20
|
+
def verify_identity_claims!(units)
|
|
21
|
+
claims = (@identity_claims || {}).dup
|
|
22
|
+
units.each do |unit|
|
|
23
|
+
key = [unit.type, unit.identifier]
|
|
24
|
+
path = identity_source(unit.file_path)
|
|
25
|
+
if claims.key?(key)
|
|
26
|
+
reject_identity_collision!(unit, claims.fetch(key)) unless claims[key] == path
|
|
27
|
+
elsif !Array(@authoritative_replacement_types).include?(unit.type)
|
|
28
|
+
verify_retained_owner!(unit, path)
|
|
29
|
+
end
|
|
30
|
+
claims[key] = path
|
|
31
|
+
end
|
|
32
|
+
@identity_claims = claims
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def verify_retained_owner!(unit, path)
|
|
36
|
+
prior = @dependency_graph.node(unit.identifier, type: unit.type)
|
|
37
|
+
return unless prior
|
|
38
|
+
|
|
39
|
+
prior_path = identity_source(prior[:file_path])
|
|
40
|
+
return if prior_path == path || confirmed_source_move?(prior_path, path)
|
|
41
|
+
return if confirmed_discovered_move?(unit, prior_path, path)
|
|
42
|
+
|
|
43
|
+
reject_identity_collision!(unit, prior_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A surviving file can release its old identity, but only after completed
|
|
47
|
+
# discovery proves it absent. Fresh claims are still checked as one batch.
|
|
48
|
+
def with_changed_path_ownership(batches)
|
|
49
|
+
previous = @released_identity_owners
|
|
50
|
+
@released_identity_owners = released_path_owners(batches)
|
|
51
|
+
candidates = batches.flat_map { |_path, _rules, entries| entries.flat_map(&:last) }.compact
|
|
52
|
+
verify_identity_claims!(candidates)
|
|
53
|
+
yield
|
|
54
|
+
ensure
|
|
55
|
+
@released_identity_owners = previous
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def released_path_owners(batches)
|
|
59
|
+
return Set.new unless @eager_load_complete
|
|
60
|
+
|
|
61
|
+
batches.each_with_object(Set.new) do |(path, rules, entries), released|
|
|
62
|
+
next if entries.any? { |_rule, units| units.nil? }
|
|
63
|
+
|
|
64
|
+
produced = entries.flat_map { |_rule, units| units }.to_set { |unit| [unit.identifier, unit.type] }
|
|
65
|
+
released.merge(released_owners_for_path(path, rules.to_set(&:extractor_key), produced))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def released_owners_for_path(path, covered, produced)
|
|
70
|
+
@dependency_graph.units_for_path(path).filter_map do |identifier, type|
|
|
71
|
+
key = self.class::TYPE_TO_EXTRACTOR_KEY[type]
|
|
72
|
+
next if produced.include?([identifier, type]) || !covered.include?(key)
|
|
73
|
+
next if @failed_consumers&.include?(key)
|
|
74
|
+
|
|
75
|
+
[type, identifier, identity_source(path)]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def confirmed_discovered_move?(unit, prior, current)
|
|
80
|
+
return false unless @eager_load_complete
|
|
81
|
+
return false unless prior && current && File.file?(current)
|
|
82
|
+
return true if @released_identity_owners&.include?([unit.type, unit.identifier, prior])
|
|
83
|
+
|
|
84
|
+
canonical_runtime_owner?(unit, current)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def canonical_runtime_owner?(unit, current)
|
|
88
|
+
consumer = authoritative_runtime_consumer(unit.type)
|
|
89
|
+
return false unless consumer && Object.respond_to?(:const_source_location)
|
|
90
|
+
|
|
91
|
+
return false unless unique_runtime_owner?(unit, consumer)
|
|
92
|
+
return false unless identity_source(Array(Object.const_source_location(unit.identifier)).first) == current
|
|
93
|
+
|
|
94
|
+
complete_hybrid_owner?(unit, consumer, current)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unique_runtime_owner?(unit, consumer)
|
|
98
|
+
owners = consumer.discoverable_classes.select { |klass| klass.name == unit.identifier }.uniq
|
|
99
|
+
return false if source_consumer_failed?(self.class::TYPE_TO_EXTRACTOR_KEY[unit.type], consumer)
|
|
100
|
+
|
|
101
|
+
owners.size == 1 && owners.first.equal?(constant_for_identifier(unit.identifier))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def complete_hybrid_owner?(unit, consumer, current)
|
|
105
|
+
return true unless self.class::CLASS_DISCOVERED_FALLBACK.key?(unit.type)
|
|
106
|
+
|
|
107
|
+
# Runtime descendants alone cannot rule out a source-only duplicate in a
|
|
108
|
+
# conventional directory. Require the full hybrid inventory before a
|
|
109
|
+
# retained owner can move; the usual fresh-claim checks remain in force.
|
|
110
|
+
key = self.class::TYPE_TO_EXTRACTOR_KEY.fetch(unit.type)
|
|
111
|
+
units = checked_extraction(key, consumer) { consumer.extract_all }
|
|
112
|
+
return false if source_consumer_failed?(key, consumer)
|
|
113
|
+
|
|
114
|
+
claims = Array(units).select do |candidate|
|
|
115
|
+
candidate.type == unit.type && candidate.identifier == unit.identifier
|
|
116
|
+
end
|
|
117
|
+
claims.any? && claims.all? { |candidate| identity_source(candidate.file_path) == current }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# GraphQL's runtime inventory cannot establish sole ownership. Job and
|
|
121
|
+
# serializer inventories identify current runtime classes; fresh file-based
|
|
122
|
+
# claims still pass the complete batch collision check before any write.
|
|
123
|
+
def authoritative_runtime_consumer(type)
|
|
124
|
+
key = self.class::TYPE_TO_EXTRACTOR_KEY[type]
|
|
125
|
+
return unless authoritative_runtime_type?(type, key)
|
|
126
|
+
|
|
127
|
+
consumer = extractor_for(key)
|
|
128
|
+
return unless consumer.respond_to?(:discoverable_classes)
|
|
129
|
+
return if @failed_consumers&.include?(key) || source_consumer_failed?(key, consumer)
|
|
130
|
+
|
|
131
|
+
consumer
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def authoritative_runtime_type?(type, key)
|
|
135
|
+
return true if self.class::CLASS_DISCOVERED_FALLBACK.key?(type)
|
|
136
|
+
|
|
137
|
+
spec = self.class::CLASS_BASED_DISCOVERY[key]
|
|
138
|
+
spec && spec[:reconcile_removals] != false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def confirmed_source_move?(prior, current)
|
|
142
|
+
return false unless prior && current && File.file?(current)
|
|
143
|
+
|
|
144
|
+
File.lstat(prior)
|
|
145
|
+
false
|
|
146
|
+
rescue Errno::ENOENT
|
|
147
|
+
true
|
|
148
|
+
rescue SystemCallError
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def reject_identity_collision!(unit, prior_path)
|
|
153
|
+
raise IdentityCollisionError, same_type_collision_message(unit.type, unit, prior_path)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A complete replacement may relocate an old owner (e.g. a gem upgrade).
|
|
157
|
+
# Partial runtime discovery keeps old nodes and cannot grant that authority.
|
|
158
|
+
def with_replacement_ownership(key)
|
|
159
|
+
previous = @authoritative_replacement_types
|
|
160
|
+
@authoritative_replacement_types = if replacement_discovery_complete?(key)
|
|
161
|
+
self.class::EXTRACTOR_KEY_TO_TYPES.fetch(key, [])
|
|
162
|
+
else
|
|
163
|
+
[]
|
|
164
|
+
end
|
|
165
|
+
yield
|
|
166
|
+
ensure
|
|
167
|
+
@authoritative_replacement_types = previous
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def replacement_discovery_complete?(key)
|
|
171
|
+
runtime = self.class::CLASS_BASED_DISCOVERY.key?(key) || self.class::HYBRID_DISCOVERY_EXTRACTORS.include?(key)
|
|
172
|
+
!runtime || @eager_load_complete
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|