woods 2.0.0 → 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 +108 -0
- data/CONTRIBUTING.md +135 -10
- 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 -18
- data/docs/CONSOLE_MCP_SETUP.md +141 -12
- 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 +59 -2
- 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 +184 -9
- data/docs/WATCH_DAEMON.md +97 -14
- data/exe/woods-console-mcp +2 -2
- data/exe/woods-mcp-http +16 -9
- 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 +68 -41
- data/lib/woods/chunking/contributor_chunks.rb +119 -0
- data/lib/woods/chunking/semantic_chunker.rb +44 -21
- data/lib/woods/console/adapter_family.rb +39 -0
- data/lib/woods/console/connection_manager.rb +56 -3
- data/lib/woods/console/credential_index.rb +33 -3
- data/lib/woods/console/embedded_executor.rb +431 -48
- data/lib/woods/console/model_validator.rb +8 -0
- data/lib/woods/console/rack_middleware.rb +68 -11
- data/lib/woods/console/redactor.rb +24 -10
- data/lib/woods/console/safe_context.rb +44 -7
- data/lib/woods/console/sql_noise_stripper.rb +41 -12
- data/lib/woods/console/sql_table_scanner.rb +45 -34
- data/lib/woods/console/sql_validator.rb +37 -2
- 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/bearer_auth.rb +1 -1
- 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/origin_guard.rb +24 -77
- data/lib/woods/mcp/origin_policy.rb +124 -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/railtie_support.rb +8 -0
- 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 +336 -2
- data/plugin/skills/woods-investigate/SKILL.md +11 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +86 -0
- data/plugin/skills/woods-setup/SKILL.md +56 -1
- metadata +34 -5
|
@@ -115,7 +115,7 @@ module Woods
|
|
|
115
115
|
# `/*!NNNNN...*/`), capturing the body. Only scanned by
|
|
116
116
|
# {#lock_clause_views} — {SqlNoiseStripper} deliberately leaves these
|
|
117
117
|
# markers in place because their meaning is version-dependent.
|
|
118
|
-
EXECUTABLE_COMMENT_PATTERN = %r{
|
|
118
|
+
EXECUTABLE_COMMENT_PATTERN = %r{/\*(?:M)?!(?:\d{5,6})?(.*?)\*/}m
|
|
119
119
|
|
|
120
120
|
# Forbidden statement prefixes (case-insensitive).
|
|
121
121
|
#
|
|
@@ -321,6 +321,8 @@ module Woods
|
|
|
321
321
|
|
|
322
322
|
SqliteReadGuard.validate!(sql) if dialect == :sqlite
|
|
323
323
|
normalized = sql.strip
|
|
324
|
+
check_balanced_delimiters!(normalized)
|
|
325
|
+
check_supported_identifier_syntax!(normalized)
|
|
324
326
|
|
|
325
327
|
# Reject multiple statements (semicolons not inside string literals)
|
|
326
328
|
if contains_multiple_statements?(normalized)
|
|
@@ -372,6 +374,35 @@ module Woods
|
|
|
372
374
|
|
|
373
375
|
private
|
|
374
376
|
|
|
377
|
+
# The policy scanners retain ordinary quoted identifiers but do not
|
|
378
|
+
# decode PostgreSQL Unicode escapes. Refuse that grammar before the
|
|
379
|
+
# adapter can resolve an identifier differently from the policy gates.
|
|
380
|
+
def check_supported_identifier_syntax!(sql)
|
|
381
|
+
return if @dialect && @dialect != :postgres
|
|
382
|
+
|
|
383
|
+
stripped = SqlNoiseStripper.strip_noise(sql, dialect: :postgres)
|
|
384
|
+
tokens = stripped.scan(/"(?:[^"]|"")*"|(?<![A-Za-z0-9_$\u0080-\u{10ffff}])[uU]&"/)
|
|
385
|
+
return unless tokens.any? { |token| token.start_with?('U&"', 'u&"') }
|
|
386
|
+
|
|
387
|
+
raise SqlValidationError,
|
|
388
|
+
'Rejected: PostgreSQL escaped identifiers are unsupported; use ordinary quoted identifiers ' \
|
|
389
|
+
'or a structured Console tool.'
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# A fragment must not escape a later row-limit wrapper. Noise and
|
|
393
|
+
# quoted identifiers shield their punctuation from the balance check.
|
|
394
|
+
# This is a structural boundary check, not a complete SQL parser.
|
|
395
|
+
def check_balanced_delimiters!(sql)
|
|
396
|
+
stripped = strip_validation_noise(sql)
|
|
397
|
+
depth = 0
|
|
398
|
+
stripped.scan(/"(?:[^"]|"")*"|`(?:[^`]|``)*`|''|[()]/).each do |token|
|
|
399
|
+
depth += 1 if token == '('
|
|
400
|
+
depth -= 1 if token == ')'
|
|
401
|
+
raise SqlValidationError, 'Rejected: unbalanced SQL parentheses' if depth.negative?
|
|
402
|
+
end
|
|
403
|
+
raise SqlValidationError, 'Rejected: unbalanced SQL parentheses' unless depth.zero?
|
|
404
|
+
end
|
|
405
|
+
|
|
375
406
|
def unknown_grammar?
|
|
376
407
|
dialect.nil? || (dialect == :mysql && @mysql_modes.nil?)
|
|
377
408
|
end
|
|
@@ -388,7 +419,11 @@ module Woods
|
|
|
388
419
|
end
|
|
389
420
|
|
|
390
421
|
def strip_validation_noise(sql)
|
|
391
|
-
SqlNoiseStripper.strip_noise(sql, dialect: validation_dialect, **(@mysql_modes || {}))
|
|
422
|
+
SqlNoiseStripper.strip_noise(sql, dialect: validation_dialect, **(@mysql_modes || {})) do |comment|
|
|
423
|
+
next unless comment.match?(/['"`$\\]/)
|
|
424
|
+
|
|
425
|
+
raise SqlValidationError, 'Rejected: quoted executable comments have ambiguous SQL grammar; use ordinary SQL.'
|
|
426
|
+
end
|
|
392
427
|
end
|
|
393
428
|
|
|
394
429
|
def validation_dialect
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'set'
|
|
4
4
|
require 'json'
|
|
5
|
+
require_relative 'source_contributors'
|
|
5
6
|
|
|
6
7
|
module Woods
|
|
7
8
|
# DependencyGraph tracks relationships between code units for:
|
|
@@ -32,7 +33,7 @@ module Woods
|
|
|
32
33
|
# enforce_dependencies: package units (Task 7)
|
|
33
34
|
# commit_count, change_frequency: git enrichment (Task 5)
|
|
34
35
|
NODE_ATTRIBUTE_KEYS = %i[
|
|
35
|
-
database table foreign_key_tables package enforce_dependencies commit_count change_frequency kind
|
|
36
|
+
database table foreign_key_tables package enforce_dependencies commit_count change_frequency kind source_paths
|
|
36
37
|
].freeze
|
|
37
38
|
|
|
38
39
|
# These extractors describe a whole source file rather than one constant.
|
|
@@ -86,7 +87,9 @@ module Woods
|
|
|
86
87
|
|
|
87
88
|
(@edges[unit.identifier] ||= {})[unit.type] =
|
|
88
89
|
self.class.normalize_edges(unit.dependencies, strict: true)
|
|
89
|
-
(@
|
|
90
|
+
node_paths(@nodes[unit.identifier][unit.type]).each do |path|
|
|
91
|
+
(@file_map[path] ||= Set.new).add(unit.identifier)
|
|
92
|
+
end
|
|
90
93
|
|
|
91
94
|
# Type index for filtering (Set-based for O(1) insert)
|
|
92
95
|
(@type_index[unit.type] ||= Set.new).add(unit.identifier)
|
|
@@ -126,7 +129,7 @@ module Woods
|
|
|
126
129
|
old_node = @nodes[identifier]&.[](t)
|
|
127
130
|
next unless old_node
|
|
128
131
|
|
|
129
|
-
drop_from_file_map(identifier,
|
|
132
|
+
node_paths(old_node).each { |path| drop_from_file_map(identifier, path, withdrawing) }
|
|
130
133
|
drop_from_type_index(identifier, old_node[:type])
|
|
131
134
|
end
|
|
132
135
|
end
|
|
@@ -187,7 +190,7 @@ module Woods
|
|
|
187
190
|
return unless path
|
|
188
191
|
|
|
189
192
|
return if (@nodes[identifier] || {}).any? do |type, node|
|
|
190
|
-
!withdrawing.include?(type) && node
|
|
193
|
+
!withdrawing.include?(type) && node_paths(node).include?(path)
|
|
191
194
|
end
|
|
192
195
|
|
|
193
196
|
return unless (ids = @file_map[path])
|
|
@@ -425,8 +428,9 @@ module Woods
|
|
|
425
428
|
def units_for_path(file_path)
|
|
426
429
|
(@file_map[file_path] || []).flat_map do |identifier|
|
|
427
430
|
nodes = sorted_nodes(@nodes[identifier] || {})
|
|
428
|
-
at_path = nodes.select { |_, node| node
|
|
429
|
-
|
|
431
|
+
at_path = nodes.select { |_, node| node_paths(node).include?(file_path) }
|
|
432
|
+
legacy = at_path.empty? && nodes.none? { |_, node| node.key?(:source_paths) }
|
|
433
|
+
(legacy ? nodes : at_path).map { |type, _| [identifier, type] }
|
|
430
434
|
end
|
|
431
435
|
end
|
|
432
436
|
|
|
@@ -640,6 +644,13 @@ module Woods
|
|
|
640
644
|
@suffix_groups = groups
|
|
641
645
|
end
|
|
642
646
|
|
|
647
|
+
# @param node [Hash] one typed graph node
|
|
648
|
+
# @return [Array<String>] every registered physical source
|
|
649
|
+
def node_paths(node)
|
|
650
|
+
node[:source_paths] || Array(node[:file_path])
|
|
651
|
+
end
|
|
652
|
+
private :node_paths
|
|
653
|
+
|
|
643
654
|
# Node attributes a unit's metadata supplies at registration time.
|
|
644
655
|
#
|
|
645
656
|
# `table` is taken only from model units and `enforce_dependencies` only
|
|
@@ -655,6 +666,11 @@ module Woods
|
|
|
655
666
|
attrs[:table] = metadata[:table_name] if unit.type == :model && !metadata[:table_name].nil?
|
|
656
667
|
tables = Array(metadata[:foreign_keys]).filter_map { |fk| fk[:to_table] || fk['to_table'] if fk.is_a?(Hash) }
|
|
657
668
|
attrs[:foreign_key_tables] = tables if tables.any?
|
|
669
|
+
if SourceContributors.multiple?(unit)
|
|
670
|
+
attrs[:source_paths] = SourceContributors.paths(unit).map do |path|
|
|
671
|
+
self.class.absolutize(path, self.class.graph_root)
|
|
672
|
+
end
|
|
673
|
+
end
|
|
658
674
|
attrs[:package] = metadata[:package] unless metadata[:package].nil?
|
|
659
675
|
if unit.type == :package && !metadata[:enforce_dependencies].nil?
|
|
660
676
|
attrs[:enforce_dependencies] = metadata[:enforce_dependencies]
|
|
@@ -928,11 +944,18 @@ module Woods
|
|
|
928
944
|
File.join(root, path)
|
|
929
945
|
end
|
|
930
946
|
|
|
947
|
+
def self.relocate_node_paths(node, &block)
|
|
948
|
+
result = node.dup
|
|
949
|
+
result[:file_path] = yield(node[:file_path]) if node[:file_path]
|
|
950
|
+
result[:source_paths] = node[:source_paths].map(&block) if node[:source_paths]
|
|
951
|
+
result
|
|
952
|
+
end
|
|
953
|
+
|
|
931
954
|
def self.relativize_nodes(nodes, root)
|
|
932
955
|
return nodes if root.nil?
|
|
933
956
|
|
|
934
957
|
nodes.transform_values do |node|
|
|
935
|
-
node
|
|
958
|
+
relocate_node_paths(node) { |path| relativize(path, root) }
|
|
936
959
|
end
|
|
937
960
|
end
|
|
938
961
|
|
|
@@ -940,7 +963,7 @@ module Woods
|
|
|
940
963
|
return nodes if root.nil?
|
|
941
964
|
|
|
942
965
|
nodes.transform_values do |node|
|
|
943
|
-
node
|
|
966
|
+
relocate_node_paths(node) { |path| absolutize(path, root) }
|
|
944
967
|
end
|
|
945
968
|
end
|
|
946
969
|
|
|
@@ -950,7 +973,7 @@ module Woods
|
|
|
950
973
|
return variants if root.nil?
|
|
951
974
|
|
|
952
975
|
variants.map do |record|
|
|
953
|
-
record
|
|
976
|
+
relocate_node_paths(record) { |path| relativize(path, root) }
|
|
954
977
|
end
|
|
955
978
|
end
|
|
956
979
|
|
|
@@ -989,7 +1012,7 @@ module Woods
|
|
|
989
1012
|
# @return [Object]
|
|
990
1013
|
def self.normalize_node_attribute(key, value)
|
|
991
1014
|
case key
|
|
992
|
-
when :foreign_key_tables then Array(value).map(&:to_s).uniq.sort
|
|
1015
|
+
when :source_paths, :foreign_key_tables then Array(value).map(&:to_s).uniq.sort
|
|
993
1016
|
when :commit_count then value.to_i
|
|
994
1017
|
when :enforce_dependencies then [true, false].include?(value) ? value : value.to_s
|
|
995
1018
|
else value.to_s
|
|
@@ -1124,6 +1147,7 @@ module Woods
|
|
|
1124
1147
|
file_path: absolutize(record[:file_path] || record['file_path'], root),
|
|
1125
1148
|
namespace: record[:namespace] || record['namespace']
|
|
1126
1149
|
}.merge(persisted_node_attributes(record))
|
|
1150
|
+
nodes[identifier][type] = relocate_node_paths(nodes[identifier][type]) { |path| absolutize(path, root) }
|
|
1127
1151
|
(edges[identifier] ||= {})[type] = normalize_edges(record[:edges] || record['edges'])
|
|
1128
1152
|
end
|
|
1129
1153
|
end
|
data/lib/woods/embedding/fake.rb
CHANGED
|
@@ -89,6 +89,18 @@ module Woods
|
|
|
89
89
|
@dims
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
alias configured_dimensions dimensions
|
|
93
|
+
|
|
94
|
+
# @return [Array] deterministic, non-secret embedding configuration
|
|
95
|
+
def cache_identity
|
|
96
|
+
[self.class.name, @model, @dims]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Pure constructor settings for restoring the deterministic provider.
|
|
100
|
+
def configuration_options
|
|
101
|
+
{ model: @model, dims: @dims }
|
|
102
|
+
end
|
|
103
|
+
|
|
92
104
|
# @return [String] the configured model name (default
|
|
93
105
|
# {DEFAULT_MODEL_NAME})
|
|
94
106
|
def model_name
|