woods 2.0.0.beta2 → 2.0.0.beta3
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 +262 -1
- data/CONTRIBUTING.md +173 -9
- data/README.md +7 -3
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +83 -4
- data/docs/AGENT_SETUP.md +82 -1
- data/docs/BACKEND_MATRIX.md +20 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +199 -14
- data/docs/CONSOLE_MCP_SETUP.md +35 -5
- data/docs/DOCKER_SETUP.md +21 -2
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +36 -5
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +117 -1
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +221 -5
- data/docs/MCP_TOOL_COOKBOOK.md +33 -18
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +55 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +253 -11
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +117 -5
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +44 -22
- data/docs/WATCH_DAEMON.md +259 -59
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +133 -0
- data/lib/woods/agent_configuration/cli.rb +101 -0
- data/lib/woods/agent_configuration/cli_options.rb +29 -0
- data/lib/woods/agent_configuration/document.rb +105 -0
- data/lib/woods/agent_configuration/error.rb +7 -0
- data/lib/woods/agent_configuration/launcher.rb +75 -0
- data/lib/woods/agent_configuration/layout.rb +59 -0
- data/lib/woods/agent_configuration/managed_section.rb +62 -0
- data/lib/woods/agent_configuration/plan.rb +98 -0
- data/lib/woods/agent_configuration/plan_diff.rb +38 -0
- data/lib/woods/agent_configuration/planned_files.rb +61 -0
- data/lib/woods/agent_configuration/planner.rb +63 -0
- data/lib/woods/agent_configuration/planner_validation.rb +77 -0
- data/lib/woods/agent_configuration/preflight.rb +100 -0
- data/lib/woods/agent_configuration/recovery.rb +49 -0
- data/lib/woods/ast/node.rb +2 -0
- data/lib/woods/ast/parser.rb +38 -5
- data/lib/woods/builder.rb +21 -5
- data/lib/woods/cache/cache_middleware.rb +28 -7
- data/lib/woods/cache/cache_store.rb +4 -5
- data/lib/woods/change_set.rb +5 -4
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +14 -14
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/embedded_executor.rb +1 -1
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +22 -13
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +90 -46
- data/lib/woods/embedding/openai.rb +17 -6
- data/lib/woods/evaluation/ablation_executor.rb +6 -1
- data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
- data/lib/woods/export/typed_reader.rb +56 -0
- data/lib/woods/extractor.rb +232 -137
- data/lib/woods/extractors/action_cable_extractor.rb +3 -1
- data/lib/woods/extractors/behavioral_profile.rb +9 -7
- data/lib/woods/extractors/caching_extractor.rb +3 -1
- data/lib/woods/extractors/concern_extractor.rb +64 -6
- data/lib/woods/extractors/configuration_extractor.rb +7 -3
- data/lib/woods/extractors/controller_extractor.rb +13 -4
- data/lib/woods/extractors/database_view_extractor.rb +3 -1
- data/lib/woods/extractors/decorator_extractor.rb +3 -1
- data/lib/woods/extractors/engine_extractor.rb +3 -1
- data/lib/woods/extractors/event_extractor.rb +4 -2
- data/lib/woods/extractors/factory_extractor.rb +3 -1
- data/lib/woods/extractors/graphql_extractor.rb +8 -2
- data/lib/woods/extractors/i18n_extractor.rb +3 -1
- data/lib/woods/extractors/job_extractor.rb +6 -19
- data/lib/woods/extractors/lib_extractor.rb +3 -1
- data/lib/woods/extractors/mailer_extractor.rb +20 -5
- data/lib/woods/extractors/manager_extractor.rb +3 -1
- data/lib/woods/extractors/method_parameters.rb +53 -0
- data/lib/woods/extractors/middleware_argument.rb +65 -0
- data/lib/woods/extractors/middleware_extractor.rb +9 -3
- data/lib/woods/extractors/migration_extractor.rb +3 -1
- data/lib/woods/extractors/model_extractor.rb +39 -33
- data/lib/woods/extractors/package_extractor.rb +24 -4
- data/lib/woods/extractors/phlex_extractor.rb +3 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -1
- data/lib/woods/extractors/poro_extractor.rb +3 -1
- data/lib/woods/extractors/pundit_extractor.rb +3 -1
- data/lib/woods/extractors/rails_source_extractor.rb +4 -2
- data/lib/woods/extractors/rake_task_extractor.rb +4 -2
- data/lib/woods/extractors/route_extractor.rb +3 -1
- data/lib/woods/extractors/route_helper_resolver.rb +10 -33
- data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
- data/lib/woods/extractors/serializer_extractor.rb +4 -2
- data/lib/woods/extractors/service_extractor.rb +3 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
- data/lib/woods/extractors/shared_utility_methods.rb +27 -15
- data/lib/woods/extractors/source_nesting.rb +1 -1
- data/lib/woods/extractors/state_machine_extractor.rb +3 -1
- data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
- data/lib/woods/extractors/validator_extractor.rb +3 -1
- data/lib/woods/extractors/view_component_extractor.rb +3 -1
- data/lib/woods/extractors/view_template_extractor.rb +3 -1
- data/lib/woods/gem_mapper.rb +2 -0
- data/lib/woods/git_history.rb +116 -0
- data/lib/woods/graph_analyzer.rb +35 -6
- data/lib/woods/hooks/context_cli.rb +54 -0
- data/lib/woods/hooks/context_event.rb +88 -0
- data/lib/woods/hooks/context_hint.rb +73 -0
- data/lib/woods/hooks/context_impact.rb +77 -0
- data/lib/woods/hooks/context_output.rb +47 -0
- data/lib/woods/hooks/context_state.rb +102 -0
- data/lib/woods/hooks/refresh.rb +79 -0
- data/lib/woods/hooks/rule_projection.rb +78 -0
- data/lib/woods/input_rules.rb +19 -0
- data/lib/woods/mcp/bearer_auth.rb +20 -12
- data/lib/woods/mcp/bootstrapper.rb +62 -0
- data/lib/woods/mcp/index_reader.rb +323 -160
- data/lib/woods/mcp/initialization_guidance.rb +27 -0
- data/lib/woods/mcp/origin_guard.rb +17 -9
- data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +8 -1
- data/lib/woods/mcp/renderers/plain_renderer.rb +7 -1
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +158 -37
- data/lib/woods/mcp/tool_contract.rb +2 -0
- data/lib/woods/mcp/tool_response_renderer.rb +25 -0
- data/lib/woods/mcp/traversal_evidence.rb +113 -0
- data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
- data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
- data/lib/woods/notion/exporter.rb +56 -17
- data/lib/woods/obsidian/destination_plan.rb +98 -0
- data/lib/woods/obsidian/name_mapper.rb +19 -3
- data/lib/woods/obsidian/note_builder.rb +19 -10
- data/lib/woods/obsidian/vault_exporter.rb +88 -32
- data/lib/woods/operator/pipeline_guard.rb +18 -13
- data/lib/woods/path_dispatcher.rb +7 -1
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +392 -0
- data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
- data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
- data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
- data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
- data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
- data/lib/woods/resilience/index_validator.rb +112 -23
- data/lib/woods/retrieval/context_assembler.rb +50 -15
- data/lib/woods/retrieval/lexical_assembler.rb +73 -0
- data/lib/woods/retrieval/lexical_index.rb +119 -0
- data/lib/woods/retrieval/ranker.rb +4 -2
- data/lib/woods/retrieval/scope.rb +108 -0
- data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
- data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
- data/lib/woods/retrieval/search_executor.rb +86 -27
- data/lib/woods/retrieval/source_evidence.rb +200 -0
- data/lib/woods/retriever.rb +98 -22
- data/lib/woods/ruby_analyzer/trace_enricher.rb +77 -38
- data/lib/woods/session_tracer/middleware.rb +10 -12
- data/lib/woods/session_tracer/redis_store.rb +22 -6
- data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
- data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
- data/lib/woods/session_tracer/unit_resolver.rb +63 -0
- data/lib/woods/source_inputs/consumer_errors.rb +27 -0
- data/lib/woods/source_inputs/handoff.rb +102 -0
- data/lib/woods/source_inputs/launcher.rb +157 -0
- data/lib/woods/source_inputs/manifest.rb +124 -0
- data/lib/woods/source_inputs/private_key.rb +55 -0
- data/lib/woods/source_inputs/scanner.rb +171 -0
- data/lib/woods/source_inputs/scopes.rb +71 -0
- data/lib/woods/source_inputs/session.rb +214 -0
- data/lib/woods/source_inputs/status.rb +84 -0
- data/lib/woods/source_inputs/verifier.rb +107 -0
- data/lib/woods/storage/metadata_store.rb +25 -25
- data/lib/woods/storage/pgvector.rb +29 -8
- data/lib/woods/storage/qdrant.rb +17 -7
- data/lib/woods/storage/vector_store.rb +18 -6
- data/lib/woods/tasks.rb +3 -2
- data/lib/woods/temporal/json_snapshot_store.rb +29 -8
- data/lib/woods/unblocked/exporter.rb +59 -70
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/boot_snapshot.rb +52 -0
- data/lib/woods/watch/daemon.rb +136 -28
- data/lib/woods/watch/listen_watcher.rb +4 -0
- data/lib/woods/watch/polling_watcher.rb +5 -1
- data/lib/woods/watch/status.rb +20 -15
- data/lib/woods/watch/tree_scan.rb +21 -13
- data/lib/woods/watch/watcher.rb +4 -1
- data/lib/woods.rb +50 -11
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.jq +15 -0
- data/plugin/hooks/adapters/normalize.rb +63 -0
- data/plugin/hooks/hooks.json +20 -0
- data/plugin/hooks/woods-context.sh +50 -0
- data/plugin/hooks/woods-input-rules.sh +159 -0
- data/plugin/hooks/woods-opencode.mjs +65 -0
- data/plugin/hooks/woods-post-edit.sh +2 -225
- data/plugin/hooks/woods-refresh.sh +260 -0
- data/plugin/hooks/woods-session-start.sh +47 -55
- data/plugin/skills/woods-agent-enable/SKILL.md +13 -0
- data/plugin/skills/woods-diagnose/SKILL.md +288 -1
- data/plugin/skills/woods-investigate/SKILL.md +106 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +89 -1
- data/plugin/skills/woods-setup/SKILL.md +107 -6
- metadata +84 -5
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
|
|
@@ -199,7 +201,7 @@ module Woods
|
|
|
199
201
|
def log_extraction_error(name, error)
|
|
200
202
|
return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract channel #{name}: #{error.message}")
|
|
203
205
|
end
|
|
204
206
|
end
|
|
205
207
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
module Woods
|
|
4
6
|
module Extractors
|
|
5
7
|
# BehavioralProfile introspects resolved Rails.application.config values
|
|
@@ -52,7 +54,7 @@ module Woods
|
|
|
52
54
|
|
|
53
55
|
build_unit(profile)
|
|
54
56
|
rescue StandardError => e
|
|
55
|
-
|
|
57
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile extraction failed: #{e.message}")
|
|
56
58
|
nil
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -86,7 +88,7 @@ module Woods
|
|
|
86
88
|
|
|
87
89
|
result
|
|
88
90
|
rescue StandardError => e
|
|
89
|
-
|
|
91
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile database section failed: #{e.message}")
|
|
90
92
|
{}
|
|
91
93
|
end
|
|
92
94
|
|
|
@@ -102,7 +104,7 @@ module Woods
|
|
|
102
104
|
Object.const_defined?(constant_name)
|
|
103
105
|
end
|
|
104
106
|
rescue StandardError => e
|
|
105
|
-
|
|
107
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile frameworks section failed: #{e.message}")
|
|
106
108
|
{}
|
|
107
109
|
end
|
|
108
110
|
|
|
@@ -132,7 +134,7 @@ module Woods
|
|
|
132
134
|
|
|
133
135
|
flags
|
|
134
136
|
rescue StandardError => e
|
|
135
|
-
|
|
137
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile behavior_flags section failed: #{e.message}")
|
|
136
138
|
{}
|
|
137
139
|
end
|
|
138
140
|
|
|
@@ -152,7 +154,7 @@ module Woods
|
|
|
152
154
|
|
|
153
155
|
{ adapter: aj.queue_adapter }
|
|
154
156
|
rescue StandardError => e
|
|
155
|
-
|
|
157
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile background section failed: #{e.message}")
|
|
156
158
|
{}
|
|
157
159
|
end
|
|
158
160
|
|
|
@@ -172,7 +174,7 @@ module Woods
|
|
|
172
174
|
|
|
173
175
|
{ store: store }
|
|
174
176
|
rescue StandardError => e
|
|
175
|
-
|
|
177
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile caching section failed: #{e.message}")
|
|
176
178
|
{}
|
|
177
179
|
end
|
|
178
180
|
|
|
@@ -192,7 +194,7 @@ module Woods
|
|
|
192
194
|
|
|
193
195
|
{ delivery_method: am.delivery_method }
|
|
194
196
|
rescue StandardError => e
|
|
195
|
-
|
|
197
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile email section failed: #{e.message}")
|
|
196
198
|
{}
|
|
197
199
|
end
|
|
198
200
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
|
|
@@ -98,7 +100,7 @@ module Woods
|
|
|
98
100
|
|
|
99
101
|
unit
|
|
100
102
|
rescue StandardError => e
|
|
101
|
-
|
|
103
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract caching info from #{file_path}: #{e.message}")
|
|
102
104
|
nil
|
|
103
105
|
end
|
|
104
106
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
require_relative 'source_nesting'
|
|
@@ -50,18 +52,20 @@ module Woods
|
|
|
50
52
|
#
|
|
51
53
|
# @return [Array<ExtractedUnit>] List of concern units
|
|
52
54
|
def extract_all
|
|
53
|
-
find_files_in_directories(@directories).filter_map
|
|
54
|
-
|
|
55
|
-
end
|
|
55
|
+
conventional = find_files_in_directories(@directories).filter_map { |file| extract_concern_file(file) }
|
|
56
|
+
conventional + runtime_model_mixins.keys.flat_map { |file| extract_model_mixin_file(file) }
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# Extract a single concern file
|
|
59
60
|
#
|
|
60
61
|
# @param file_path [String] Path to the concern file
|
|
62
|
+
# @param module_name [String, nil] Runtime identity for a discovered model mixin
|
|
61
63
|
# @return [ExtractedUnit, nil] The extracted unit or nil if not a concern
|
|
62
|
-
def extract_concern_file(file_path)
|
|
64
|
+
def extract_concern_file(file_path, module_name: nil)
|
|
63
65
|
source = File.read(file_path)
|
|
64
|
-
|
|
66
|
+
# Runtime discovery supplies each included module's identity. Ordinary
|
|
67
|
+
# concern files retain their canonical identity, including nested helpers.
|
|
68
|
+
module_name ||= extract_module_name(file_path, source)
|
|
65
69
|
|
|
66
70
|
return nil unless module_name
|
|
67
71
|
return nil unless concern_module?(source)
|
|
@@ -79,12 +83,66 @@ module Woods
|
|
|
79
83
|
|
|
80
84
|
unit
|
|
81
85
|
rescue StandardError => e
|
|
82
|
-
|
|
86
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract concern #{file_path}: #{e.message}")
|
|
83
87
|
nil
|
|
84
88
|
end
|
|
85
89
|
|
|
90
|
+
# Extract a module outside concerns directories only when a live model
|
|
91
|
+
# includes it. Filenames alone cannot establish that a module is a mixin.
|
|
92
|
+
#
|
|
93
|
+
# @param file_path [String] Changed application source path
|
|
94
|
+
# @return [Array<ExtractedUnit>, nil]
|
|
95
|
+
def extract_model_mixin_file(file_path)
|
|
96
|
+
modules = model_mixin_paths[file_path.to_s]
|
|
97
|
+
return unless modules
|
|
98
|
+
|
|
99
|
+
modules.filter_map { |mod| extract_concern_file(file_path, module_name: mod.name) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Runtime mixins that have no conventional concern-directory discovery.
|
|
103
|
+
# @return [Hash<String, Array<Module>>] Source paths and their included modules
|
|
104
|
+
def runtime_model_mixins
|
|
105
|
+
model_mixin_paths.reject { |path, _mod| conventional_concern_path?(path) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param path [String] Absolute source path
|
|
109
|
+
# @return [Boolean] Whether directory-based discovery owns this source
|
|
110
|
+
def conventional_concern_path?(path)
|
|
111
|
+
@directories.any? { |directory| path.to_s.start_with?("#{directory}/") }
|
|
112
|
+
end
|
|
113
|
+
|
|
86
114
|
private
|
|
87
115
|
|
|
116
|
+
# Runtime ownership is authoritative; never import gem mixins even when
|
|
117
|
+
# the application adds methods to them.
|
|
118
|
+
# @return [Hash<String, Array<Module>>] App-owned included modules by source path
|
|
119
|
+
def model_mixin_paths
|
|
120
|
+
return {} unless defined?(ActiveRecord::Base)
|
|
121
|
+
|
|
122
|
+
@model_mixin_paths ||= discover_model_mixin_paths
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Hash<String, Array<Module>>] Reflected module source locations
|
|
126
|
+
def discover_model_mixin_paths
|
|
127
|
+
models = ActiveRecord::Base.descendants.reject { |model| model.respond_to?(:abstract_class?) && model.abstract_class? }
|
|
128
|
+
models.flat_map(&:included_modules).uniq.each_with_object({}) do |mod, paths|
|
|
129
|
+
next unless mod.name
|
|
130
|
+
|
|
131
|
+
definition = begin
|
|
132
|
+
Object.const_source_location(mod.name)&.first
|
|
133
|
+
rescue NameError
|
|
134
|
+
# Removed/reloaded models can remain in descendants until GC.
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
next if definition && !app_source?(definition, "#{Rails.root}/")
|
|
138
|
+
|
|
139
|
+
path = resolve_source_location(mod, app_root: "#{Rails.root}/", fallback: nil)
|
|
140
|
+
next unless app_source?(path, "#{Rails.root}/") && File.file?(path)
|
|
141
|
+
|
|
142
|
+
(paths[path] ||= []) << mod
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
88
146
|
# ──────────────────────────────────────────────────────────────────────
|
|
89
147
|
# Module Discovery
|
|
90
148
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
require_relative 'behavioral_profile'
|
|
@@ -40,12 +42,14 @@ module Woods
|
|
|
40
42
|
extract_configuration_file(file)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
profiler = BehavioralProfile.new
|
|
46
|
+
profile = profiler.extract
|
|
47
|
+
SourceInputs::ConsumerErrors.record(self) if SourceInputs::ConsumerErrors.failed?(profiler)
|
|
44
48
|
units << profile if profile
|
|
45
49
|
|
|
46
50
|
units
|
|
47
51
|
rescue StandardError => e
|
|
48
|
-
|
|
52
|
+
SourceInputs::ConsumerErrors.log(self, "BehavioralProfile integration failed: #{e.message}")
|
|
49
53
|
units || []
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -71,7 +75,7 @@ module Woods
|
|
|
71
75
|
|
|
72
76
|
unit
|
|
73
77
|
rescue StandardError => e
|
|
74
|
-
|
|
78
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract configuration #{file_path}: #{e.message}")
|
|
75
79
|
nil
|
|
76
80
|
end
|
|
77
81
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require 'digest'
|
|
4
6
|
require_relative 'ast_source_extraction'
|
|
5
7
|
require_relative 'shared_utility_methods'
|
|
@@ -120,8 +122,8 @@ module Woods
|
|
|
120
122
|
|
|
121
123
|
unit
|
|
122
124
|
rescue StandardError => e
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
SourceInputs::ConsumerErrors.log(self, "[Woods] Failed to extract controller #{controller.name}: #{e.class}: #{e.message}")
|
|
126
|
+
SourceInputs::ConsumerErrors.log(self, "[Woods] #{e.backtrace&.first(5)&.join("\n ")}")
|
|
125
127
|
nil
|
|
126
128
|
end
|
|
127
129
|
|
|
@@ -253,6 +255,7 @@ module Woods
|
|
|
253
255
|
opts << "only: [#{f[:only].map { |a| ":#{a}" }.join(', ')}]" if f[:only]&.any?
|
|
254
256
|
opts << "except: [#{f[:except].map { |a| ":#{a}" }.join(', ')}]" if f[:except]&.any?
|
|
255
257
|
opts << "if: #{f[:if]}" if f[:if]
|
|
258
|
+
opts << "unless: #{f[:unless]}" if f[:unless]
|
|
256
259
|
|
|
257
260
|
opts_str = opts.any? ? " (#{opts.join('; ')})" : ''
|
|
258
261
|
" #{f[:kind].to_s.ljust(8)} :#{f[:filter]}#{opts_str}"
|
|
@@ -272,7 +275,7 @@ module Woods
|
|
|
272
275
|
controller._process_action_callbacks.map do |callback|
|
|
273
276
|
only, except, if_conds, unless_conds = extract_callback_conditions(callback)
|
|
274
277
|
|
|
275
|
-
result = { kind: callback.kind, filter: callback
|
|
278
|
+
result = { kind: callback.kind, filter: callback_filter(callback) }
|
|
276
279
|
result[:only] = only if only.any?
|
|
277
280
|
result[:except] = except if except.any?
|
|
278
281
|
result[:if] = if_conds.join(', ') if if_conds.any?
|
|
@@ -281,6 +284,12 @@ module Woods
|
|
|
281
284
|
end
|
|
282
285
|
end
|
|
283
286
|
|
|
287
|
+
# Override only controller Proc conditions; the shared model/mailer
|
|
288
|
+
# condition format is a separate extraction contract.
|
|
289
|
+
def condition_label(condition)
|
|
290
|
+
condition.is_a?(Proc) ? stable_filter(condition) : super
|
|
291
|
+
end
|
|
292
|
+
|
|
284
293
|
# ──────────────────────────────────────────────────────────────────────
|
|
285
294
|
# Concern Detection & Inlining
|
|
286
295
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -734,7 +743,7 @@ module Woods
|
|
|
734
743
|
applicable = controller._process_action_callbacks.select do |cb|
|
|
735
744
|
callback_applies_to_action?(cb, action_name)
|
|
736
745
|
end
|
|
737
|
-
applicable.map { |cb| { kind: cb.kind, filter: cb
|
|
746
|
+
applicable.map { |cb| { kind: cb.kind, filter: callback_filter(cb) } }
|
|
738
747
|
end
|
|
739
748
|
|
|
740
749
|
# Determine if a callback applies to a given action name.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
|
|
@@ -85,7 +87,7 @@ module Woods
|
|
|
85
87
|
|
|
86
88
|
unit
|
|
87
89
|
rescue StandardError => e
|
|
88
|
-
|
|
90
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract database view #{file_path}: #{e.message}")
|
|
89
91
|
nil
|
|
90
92
|
end
|
|
91
93
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
|
|
@@ -78,7 +80,7 @@ module Woods
|
|
|
78
80
|
|
|
79
81
|
unit
|
|
80
82
|
rescue StandardError => e
|
|
81
|
-
|
|
83
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract decorator #{file_path}: #{e.message}")
|
|
82
84
|
nil
|
|
83
85
|
end
|
|
84
86
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
|
|
5
7
|
module Woods
|
|
@@ -184,7 +186,7 @@ module Woods
|
|
|
184
186
|
|
|
185
187
|
unit
|
|
186
188
|
rescue StandardError => e
|
|
187
|
-
|
|
189
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract engine #{engine.name}: #{e.message}")
|
|
188
190
|
nil
|
|
189
191
|
end
|
|
190
192
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
|
|
@@ -59,14 +61,14 @@ module Woods
|
|
|
59
61
|
def scan_file(file_path, event_map)
|
|
60
62
|
source = cached_source(file_path)
|
|
61
63
|
unless source
|
|
62
|
-
|
|
64
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to scan #{file_path} for events: file unreadable")
|
|
63
65
|
return
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
scan_active_support_notifications(source, file_path, event_map)
|
|
67
69
|
scan_wisper_patterns(source, file_path, event_map)
|
|
68
70
|
rescue StandardError => e
|
|
69
|
-
|
|
71
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to scan #{file_path} for events: #{e.message}")
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
private
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'line_neutralizer'
|
|
4
6
|
require_relative 'shared_utility_methods'
|
|
5
7
|
require_relative 'shared_dependency_scanner'
|
|
@@ -53,7 +55,7 @@ module Woods
|
|
|
53
55
|
|
|
54
56
|
factories.map { |factory_data| build_unit(factory_data, file_path, source) }
|
|
55
57
|
rescue StandardError => e
|
|
56
|
-
|
|
58
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract factories from #{file_path}: #{e.message}")
|
|
57
59
|
[]
|
|
58
60
|
end
|
|
59
61
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
require_relative 'source_nesting'
|
|
@@ -151,7 +153,9 @@ module Woods
|
|
|
151
153
|
|
|
152
154
|
unit
|
|
153
155
|
rescue StandardError => e
|
|
154
|
-
|
|
156
|
+
if defined?(Rails)
|
|
157
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract GraphQL file #{file_path}: #{e.message}")
|
|
158
|
+
end
|
|
155
159
|
nil
|
|
156
160
|
end
|
|
157
161
|
|
|
@@ -191,7 +195,9 @@ module Woods
|
|
|
191
195
|
|
|
192
196
|
unit
|
|
193
197
|
rescue StandardError => e
|
|
194
|
-
|
|
198
|
+
if defined?(Rails)
|
|
199
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract GraphQL type #{type_class.name}: #{e.message}")
|
|
200
|
+
end
|
|
195
201
|
nil
|
|
196
202
|
end
|
|
197
203
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require 'yaml'
|
|
4
6
|
|
|
5
7
|
require_relative 'shared_utility_methods'
|
|
@@ -70,7 +72,7 @@ module Woods
|
|
|
70
72
|
|
|
71
73
|
unit
|
|
72
74
|
rescue StandardError => e
|
|
73
|
-
|
|
75
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract i18n #{file_path}: #{e.message}")
|
|
74
76
|
nil
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'reference_patterns'
|
|
4
6
|
require_relative 'shared_utility_methods'
|
|
5
7
|
require_relative 'shared_dependency_scanner'
|
|
@@ -107,7 +109,7 @@ module Woods
|
|
|
107
109
|
|
|
108
110
|
unit
|
|
109
111
|
rescue StandardError => e
|
|
110
|
-
|
|
112
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract job #{file_path}: #{e.message}")
|
|
111
113
|
nil
|
|
112
114
|
end
|
|
113
115
|
|
|
@@ -134,7 +136,7 @@ module Woods
|
|
|
134
136
|
|
|
135
137
|
unit
|
|
136
138
|
rescue StandardError => e
|
|
137
|
-
|
|
139
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract job #{job_class.name}: #{e.message}")
|
|
138
140
|
nil
|
|
139
141
|
end
|
|
140
142
|
|
|
@@ -318,24 +320,9 @@ module Woods
|
|
|
318
320
|
end
|
|
319
321
|
|
|
320
322
|
def extract_perform_params(source)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
params_str = ::Regexp.last_match(1)
|
|
324
|
-
params = []
|
|
325
|
-
|
|
326
|
-
params_str.scan(/(\*?\*?\w+)(?:\s*=\s*([^,]+))?/) do |name, default|
|
|
327
|
-
params << {
|
|
328
|
-
name: name.gsub(/^\*+/, ''),
|
|
329
|
-
splat: if name.start_with?('**')
|
|
330
|
-
:double
|
|
331
|
-
else
|
|
332
|
-
(name.start_with?('*') ? :single : nil)
|
|
333
|
-
end,
|
|
334
|
-
has_default: !default.nil?
|
|
335
|
-
}
|
|
323
|
+
MethodParameters.extract(source, :perform).map do |parameter|
|
|
324
|
+
parameter.slice(:name, :splat, :has_default)
|
|
336
325
|
end
|
|
337
|
-
|
|
338
|
-
params
|
|
339
326
|
end
|
|
340
327
|
|
|
341
328
|
def extract_discard_on(source)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
require_relative 'source_nesting'
|
|
@@ -85,7 +87,7 @@ module Woods
|
|
|
85
87
|
|
|
86
88
|
unit
|
|
87
89
|
rescue StandardError => e
|
|
88
|
-
|
|
90
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract lib file #{file_path}: #{e.message}")
|
|
89
91
|
nil
|
|
90
92
|
end
|
|
91
93
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require 'digest'
|
|
4
6
|
require_relative 'ast_source_extraction'
|
|
5
7
|
require_relative 'shared_utility_methods'
|
|
@@ -27,7 +29,9 @@ module Woods
|
|
|
27
29
|
include RouteHelperResolver
|
|
28
30
|
|
|
29
31
|
def initialize
|
|
30
|
-
|
|
32
|
+
if defined?(ActionMailer::Base)
|
|
33
|
+
@mailer_base = defined?(ApplicationMailer) ? ApplicationMailer : ActionMailer::Base
|
|
34
|
+
end
|
|
31
35
|
build_route_helper_map
|
|
32
36
|
end
|
|
33
37
|
|
|
@@ -45,7 +49,9 @@ module Woods
|
|
|
45
49
|
#
|
|
46
50
|
# @return [Array<Class>]
|
|
47
51
|
def discoverable_classes
|
|
48
|
-
@mailer_base
|
|
52
|
+
return [] unless @mailer_base
|
|
53
|
+
|
|
54
|
+
@mailer_base.descendants.select { |mailer| app_defined_mailer?(mailer) }
|
|
49
55
|
end
|
|
50
56
|
|
|
51
57
|
# Extract a single mailer
|
|
@@ -53,8 +59,7 @@ module Woods
|
|
|
53
59
|
# @param mailer [Class] The mailer class
|
|
54
60
|
# @return [ExtractedUnit] The extracted unit
|
|
55
61
|
def extract_mailer(mailer)
|
|
56
|
-
return nil
|
|
57
|
-
return nil if mailer == ActionMailer::Base
|
|
62
|
+
return nil unless app_defined_mailer?(mailer)
|
|
58
63
|
|
|
59
64
|
file_path = source_file_for(mailer)
|
|
60
65
|
|
|
@@ -76,12 +81,22 @@ module Woods
|
|
|
76
81
|
|
|
77
82
|
unit
|
|
78
83
|
rescue StandardError => e
|
|
79
|
-
|
|
84
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract mailer #{mailer.name}: #{e.message}")
|
|
80
85
|
nil
|
|
81
86
|
end
|
|
82
87
|
|
|
83
88
|
private
|
|
84
89
|
|
|
90
|
+
# Discovery and direct incremental extraction must agree on ownership;
|
|
91
|
+
# otherwise a gem class with a fabricated path becomes a phantom unit.
|
|
92
|
+
def app_defined_mailer?(mailer)
|
|
93
|
+
return false unless @mailer_base && mailer.name
|
|
94
|
+
return false if mailer == ActionMailer::Base
|
|
95
|
+
|
|
96
|
+
path = source_file_for(mailer)
|
|
97
|
+
path && File.file?(path) && app_source?(path, Rails.root.to_s)
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
# Locate the source file for a mailer class.
|
|
86
101
|
#
|
|
87
102
|
# Convention path first, then introspection via {#resolve_source_location}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../source_inputs/consumer_errors'
|
|
4
|
+
|
|
3
5
|
require_relative 'shared_utility_methods'
|
|
4
6
|
require_relative 'shared_dependency_scanner'
|
|
5
7
|
|
|
@@ -69,7 +71,7 @@ module Woods
|
|
|
69
71
|
|
|
70
72
|
unit
|
|
71
73
|
rescue StandardError => e
|
|
72
|
-
|
|
74
|
+
SourceInputs::ConsumerErrors.log(self, "Failed to extract manager #{file_path}: #{e.message}")
|
|
73
75
|
nil
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module Extractors
|
|
7
|
+
# Syntactic method signatures only: never evaluate a default expression or
|
|
8
|
+
# mistake names inside one for additional parameters. Consumers retain their
|
|
9
|
+
# existing metadata shapes by selecting the fields they expose.
|
|
10
|
+
module MethodParameters
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def extract(source, method_name)
|
|
14
|
+
parsed = Prism.parse(source)
|
|
15
|
+
return [] unless parsed.success?
|
|
16
|
+
|
|
17
|
+
method = find_method(parsed.value, method_name.to_sym)
|
|
18
|
+
return [] unless method&.parameters
|
|
19
|
+
|
|
20
|
+
method.parameters.compact_child_nodes.flat_map { |node| describe(node) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_method(node, name)
|
|
24
|
+
return nil if node.is_a?(Prism::SingletonClassNode)
|
|
25
|
+
return node if node.is_a?(Prism::DefNode) && node.receiver.nil? && node.name == name
|
|
26
|
+
|
|
27
|
+
node.compact_child_nodes.each do |child|
|
|
28
|
+
found = find_method(child, name)
|
|
29
|
+
return found if found
|
|
30
|
+
end
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def describe(node)
|
|
35
|
+
# Destructured positional arguments contain declarations, while a
|
|
36
|
+
# default's subtree contains values and must never be walked.
|
|
37
|
+
return node.compact_child_nodes.flat_map { |child| describe(child) } if node.is_a?(Prism::MultiTargetNode)
|
|
38
|
+
return [] unless node.respond_to?(:name) && node.name
|
|
39
|
+
|
|
40
|
+
optional = node.is_a?(Prism::OptionalParameterNode) || node.is_a?(Prism::OptionalKeywordParameterNode)
|
|
41
|
+
keyword = node.is_a?(Prism::RequiredKeywordParameterNode) ||
|
|
42
|
+
node.is_a?(Prism::OptionalKeywordParameterNode) || node.is_a?(Prism::KeywordRestParameterNode)
|
|
43
|
+
splat = if node.is_a?(Prism::KeywordRestParameterNode)
|
|
44
|
+
:double
|
|
45
|
+
elsif node.is_a?(Prism::RestParameterNode)
|
|
46
|
+
:single
|
|
47
|
+
end
|
|
48
|
+
[{ name: node.name.to_s, has_default: optional, keyword: keyword, splat: splat }]
|
|
49
|
+
end
|
|
50
|
+
private_class_method :find_method, :describe
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|