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.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -7
  3. data/CONTRIBUTING.md +134 -19
  4. data/README.md +1 -1
  5. data/docs/AGENT_GUIDE.md +19 -0
  6. data/docs/AGENT_SETUP.md +22 -2
  7. data/docs/BACKEND_MATRIX.md +7 -0
  8. data/docs/CLIENT_HOOKS.md +6 -0
  9. data/docs/CONFIGURATION_REFERENCE.md +133 -25
  10. data/docs/CONSOLE_MCP_SETUP.md +82 -30
  11. data/docs/EMBEDDING_MODELS.md +16 -19
  12. data/docs/EXTRACTOR_REFERENCE.md +219 -21
  13. data/docs/FAQ.md +11 -25
  14. data/docs/GETTING_STARTED.md +7 -1
  15. data/docs/INCREMENTAL_EXTRACTION.md +261 -19
  16. data/docs/INDEX_LAYOUT.md +5 -0
  17. data/docs/INTERNALS.md +9 -0
  18. data/docs/MCP_HTTP_TRANSPORT.md +20 -15
  19. data/docs/MCP_SERVERS.md +87 -8
  20. data/docs/MCP_TOOL_COOKBOOK.md +13 -55
  21. data/docs/NOTION_INTEGRATION.md +7 -1
  22. data/docs/PUBLISHED_INDEX.md +6 -0
  23. data/docs/README.md +6 -1
  24. data/docs/RETRIEVAL_GUIDE.md +17 -0
  25. data/docs/SOURCE_FRESHNESS.md +157 -5
  26. data/docs/TOKEN_BENCHMARK.md +10 -18
  27. data/docs/TROUBLESHOOTING.md +70 -14
  28. data/docs/UNBLOCKED_INTEGRATION.md +60 -8
  29. data/docs/UPGRADING_TO_2.md +153 -38
  30. data/docs/WATCH_DAEMON.md +97 -14
  31. data/exe/woods-console-mcp +2 -2
  32. data/lib/generators/woods/templates/woods.rb.tt +2 -1
  33. data/lib/tasks/woods.rake +23 -7
  34. data/lib/tasks/woods_checks.rake +2 -2
  35. data/lib/woods/agent_configuration/cli.rb +1 -1
  36. data/lib/woods/agent_configuration/layout.rb +16 -2
  37. data/lib/woods/agent_configuration/plan.rb +13 -3
  38. data/lib/woods/agent_configuration/planner_validation.rb +4 -2
  39. data/lib/woods/agent_configuration/preflight.rb +5 -3
  40. data/lib/woods/builder.rb +17 -57
  41. data/lib/woods/cache/cache_middleware.rb +56 -30
  42. data/lib/woods/chunking/contributor_chunks.rb +119 -0
  43. data/lib/woods/chunking/semantic_chunker.rb +44 -21
  44. data/lib/woods/console/connection_manager.rb +56 -3
  45. data/lib/woods/console/embedded_executor.rb +30 -5
  46. data/lib/woods/console/rack_middleware.rb +29 -1
  47. data/lib/woods/dependency_graph.rb +34 -10
  48. data/lib/woods/embedding/fake.rb +12 -0
  49. data/lib/woods/embedding/indexer.rb +195 -98
  50. data/lib/woods/embedding/input_budget.rb +67 -0
  51. data/lib/woods/embedding/openai.rb +70 -20
  52. data/lib/woods/embedding/provider.rb +37 -25
  53. data/lib/woods/embedding/text_preparer.rb +76 -32
  54. data/lib/woods/embedding/token_counter.rb +18 -81
  55. data/lib/woods/embedding/vector_configuration.rb +48 -0
  56. data/lib/woods/extraction_identities.rb +175 -0
  57. data/lib/woods/extractor.rb +304 -107
  58. data/lib/woods/extractors/action_cable_extractor.rb +8 -3
  59. data/lib/woods/extractors/assigned_value_discovery.rb +74 -0
  60. data/lib/woods/extractors/class_declarations.rb +121 -0
  61. data/lib/woods/extractors/configuration_extractor.rb +11 -3
  62. data/lib/woods/extractors/declaration_ancestry.rb +92 -0
  63. data/lib/woods/extractors/event_extractor.rb +8 -0
  64. data/lib/woods/extractors/graphql_extractor.rb +134 -77
  65. data/lib/woods/extractors/job_extractor.rb +5 -1
  66. data/lib/woods/extractors/lib_extractor.rb +132 -15
  67. data/lib/woods/extractors/mailer_extractor.rb +3 -5
  68. data/lib/woods/extractors/manager_extractor.rb +7 -21
  69. data/lib/woods/extractors/migration_declaration.rb +87 -0
  70. data/lib/woods/extractors/migration_extractor.rb +5 -39
  71. data/lib/woods/extractors/phlex_extractor.rb +6 -2
  72. data/lib/woods/extractors/policy_extractor.rb +9 -5
  73. data/lib/woods/extractors/poro_extractor.rb +112 -53
  74. data/lib/woods/extractors/pundit_extractor.rb +11 -6
  75. data/lib/woods/extractors/scheduled_job_extractor.rb +45 -4
  76. data/lib/woods/extractors/serializer_extractor.rb +34 -22
  77. data/lib/woods/extractors/shared_utility_methods.rb +18 -1
  78. data/lib/woods/extractors/source_nesting.rb +142 -106
  79. data/lib/woods/extractors/standalone_module_discovery.rb +123 -0
  80. data/lib/woods/extractors/state_machine_extractor.rb +46 -40
  81. data/lib/woods/extractors/view_component_extractor.rb +9 -7
  82. data/lib/woods/flow_assembler.rb +4 -1
  83. data/lib/woods/generation.rb +25 -0
  84. data/lib/woods/hooks/context_hint.rb +7 -2
  85. data/lib/woods/mcp/bootstrapper.rb +33 -7
  86. data/lib/woods/mcp/config_resolver.rb +26 -7
  87. data/lib/woods/mcp/index_reader.rb +125 -24
  88. data/lib/woods/mcp/index_reader_pinning.rb +16 -0
  89. data/lib/woods/mcp/renderers/markdown_renderer.rb +7 -1
  90. data/lib/woods/mcp/renderers/plain_renderer.rb +3 -1
  91. data/lib/woods/mcp/search_results.rb +7 -1
  92. data/lib/woods/mcp/server.rb +24 -4
  93. data/lib/woods/module_reconciliation.rb +151 -0
  94. data/lib/woods/path_dispatcher.rb +7 -2
  95. data/lib/woods/rake_helpers.rb +43 -11
  96. data/lib/woods/release.rb +1 -1
  97. data/lib/woods/resilience/index_validator.rb +8 -3
  98. data/lib/woods/resilience/retryable_provider.rb +18 -1
  99. data/lib/woods/resolved_config.rb +68 -8
  100. data/lib/woods/retrieval/context_assembler.rb +3 -3
  101. data/lib/woods/retrieval/lexical_assembler.rb +3 -2
  102. data/lib/woods/retrieval/scope.rb +18 -2
  103. data/lib/woods/retrieval/source_evidence.rb +14 -2
  104. data/lib/woods/source_contributor_validation.rb +78 -0
  105. data/lib/woods/source_contributors.rb +116 -0
  106. data/lib/woods/source_inputs/handoff.rb +37 -0
  107. data/lib/woods/source_inputs/launcher.rb +53 -13
  108. data/lib/woods/source_inputs/manifest.rb +84 -3
  109. data/lib/woods/source_inputs/private_key.rb +44 -12
  110. data/lib/woods/source_inputs/scanner.rb +98 -27
  111. data/lib/woods/source_inputs/scopes.rb +1 -1
  112. data/lib/woods/source_inputs/session.rb +147 -15
  113. data/lib/woods/source_inputs/stable_reader.rb +127 -0
  114. data/lib/woods/source_inputs/status.rb +40 -8
  115. data/lib/woods/source_inputs/verifier.rb +28 -5
  116. data/lib/woods/source_path_encoding.rb +33 -0
  117. data/lib/woods/source_references/cache.rb +284 -0
  118. data/lib/woods/source_references/collector.rb +120 -0
  119. data/lib/woods/source_references/extraction.rb +185 -0
  120. data/lib/woods/source_references/inputs.rb +134 -0
  121. data/lib/woods/source_references/parser_adapter.rb +134 -0
  122. data/lib/woods/source_references/pass.rb +152 -0
  123. data/lib/woods/source_references/prism_adapter.rb +116 -0
  124. data/lib/woods/source_references/registry.rb +178 -0
  125. data/lib/woods/source_references/runtime_lookup.rb +127 -0
  126. data/lib/woods/source_references/value_class.rb +82 -0
  127. data/lib/woods/storage/metadata_store.rb +4 -1
  128. data/lib/woods/storage/qdrant.rb +2 -2
  129. data/lib/woods/unblocked/client.rb +12 -7
  130. data/lib/woods/unblocked/document_builder.rb +4 -1
  131. data/lib/woods/unblocked/exporter.rb +127 -37
  132. data/lib/woods/unblocked/sync_manifest.rb +137 -21
  133. data/lib/woods/unblocked/uri_migration.rb +105 -0
  134. data/lib/woods/util/host_guard.rb +3 -2
  135. data/lib/woods/version.rb +1 -1
  136. data/lib/woods/watch/catch_up.rb +138 -0
  137. data/lib/woods/watch/claim_lease.rb +150 -0
  138. data/lib/woods/watch/cli.rb +26 -2
  139. data/lib/woods/watch/daemon.rb +80 -59
  140. data/lib/woods/watch/installation/options.rb +1 -1
  141. data/lib/woods/watch/installation/receipt.rb +6 -1
  142. data/lib/woods/watch/managed_child.rb +1 -1
  143. data/lib/woods/watch/supervisor.rb +1 -1
  144. data/lib/woods/watch/tree_scan.rb +14 -2
  145. data/plugin/.claude-plugin/plugin.json +1 -1
  146. data/plugin/hooks/adapters/normalize.rb +3 -2
  147. data/plugin/hooks/woods-input-rules.sh +4 -0
  148. data/plugin/hooks/woods-refresh.sh +15 -7
  149. data/plugin/hooks/woods-session-start.sh +60 -3
  150. data/plugin/skills/woods-diagnose/SKILL.md +334 -11
  151. data/plugin/skills/woods-investigate/SKILL.md +11 -0
  152. data/plugin/skills/woods-mcp-config/SKILL.md +79 -8
  153. data/plugin/skills/woods-setup/SKILL.md +53 -6
  154. 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