woods 1.4.1 → 1.6.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 +208 -0
- data/CONTRIBUTING.md +63 -1
- data/README.md +35 -2
- data/lib/tasks/woods.rake +52 -2
- data/lib/woods/atomic_file.rb +43 -0
- data/lib/woods/cache/cache_middleware.rb +18 -1
- data/lib/woods/console/embedded_executor.rb +20 -2
- data/lib/woods/console/eval_guard.rb +8 -2
- data/lib/woods/console/sql_noise_stripper.rb +113 -0
- data/lib/woods/console/sql_table_scanner.rb +22 -9
- data/lib/woods/console/sql_validator.rb +1 -2
- data/lib/woods/coordination/pipeline_lock.rb +112 -7
- data/lib/woods/dependency_graph.rb +38 -1
- data/lib/woods/embedding/text_preparer.rb +6 -1
- data/lib/woods/export/unit_facts.rb +80 -0
- data/lib/woods/extractor.rb +152 -31
- data/lib/woods/extractors/concern_extractor.rb +3 -5
- data/lib/woods/extractors/configuration_extractor.rb +3 -5
- data/lib/woods/extractors/controller_extractor.rb +1 -1
- data/lib/woods/extractors/database_view_extractor.rb +1 -1
- data/lib/woods/extractors/decorator_extractor.rb +3 -5
- data/lib/woods/extractors/event_extractor.rb +2 -2
- data/lib/woods/extractors/factory_extractor.rb +2 -4
- data/lib/woods/extractors/graphql_extractor.rb +1 -1
- data/lib/woods/extractors/i18n_extractor.rb +6 -4
- data/lib/woods/extractors/job_extractor.rb +4 -6
- data/lib/woods/extractors/lib_extractor.rb +1 -1
- data/lib/woods/extractors/mailer_extractor.rb +1 -1
- data/lib/woods/extractors/manager_extractor.rb +3 -5
- data/lib/woods/extractors/migration_extractor.rb +1 -1
- data/lib/woods/extractors/model_extractor.rb +26 -10
- data/lib/woods/extractors/phlex_extractor.rb +1 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -5
- data/lib/woods/extractors/poro_extractor.rb +1 -1
- data/lib/woods/extractors/pundit_extractor.rb +3 -5
- data/lib/woods/extractors/rake_task_extractor.rb +2 -4
- data/lib/woods/extractors/serializer_extractor.rb +4 -6
- data/lib/woods/extractors/service_extractor.rb +3 -5
- data/lib/woods/extractors/shared_dependency_scanner.rb +88 -16
- data/lib/woods/extractors/shared_utility_methods.rb +14 -0
- data/lib/woods/extractors/state_machine_extractor.rb +2 -4
- data/lib/woods/extractors/validator_extractor.rb +3 -5
- data/lib/woods/extractors/view_component_extractor.rb +1 -1
- data/lib/woods/filename_utils.rb +31 -2
- data/lib/woods/flow_assembler.rb +45 -28
- data/lib/woods/flow_precomputer.rb +2 -1
- data/lib/woods/git_provenance.rb +121 -0
- data/lib/woods/index_artifact.rb +8 -4
- data/lib/woods/mcp/bootstrapper.rb +6 -5
- data/lib/woods/mcp/config_resolver.rb +42 -21
- data/lib/woods/mcp/errors.rb +5 -3
- data/lib/woods/mcp/server.rb +68 -15
- data/lib/woods/mcp/version_aware_tool_dispatch.rb +47 -0
- data/lib/woods/model_name_cache.rb +6 -1
- data/lib/woods/notion/client.rb +4 -1
- data/lib/woods/notion/exporter.rb +7 -1
- data/lib/woods/obsidian/errors.rb +11 -0
- data/lib/woods/obsidian/name_mapper.rb +132 -0
- data/lib/woods/obsidian/note_builder.rb +260 -0
- data/lib/woods/obsidian/vault_assets.rb +104 -0
- data/lib/woods/obsidian/vault_exporter.rb +412 -0
- data/lib/woods/operator/error_escalator.rb +15 -4
- data/lib/woods/resilience/circuit_breaker.rb +98 -24
- data/lib/woods/resolved_config.rb +4 -1
- data/lib/woods/retrieval/ranker.rb +45 -7
- data/lib/woods/retry_after.rb +36 -0
- data/lib/woods/temporal/json_snapshot_store.rb +4 -1
- data/lib/woods/temporal/snapshot_store.rb +4 -1
- data/lib/woods/unblocked/client.rb +4 -1
- data/lib/woods/unblocked/document_builder.rb +21 -27
- data/lib/woods/update_check.rb +190 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods.rb +24 -0
- metadata +15 -4
data/lib/woods/extractor.rb
CHANGED
|
@@ -8,8 +8,10 @@ require 'pathname'
|
|
|
8
8
|
require 'set'
|
|
9
9
|
|
|
10
10
|
require_relative 'filename_utils'
|
|
11
|
+
require_relative 'token_utils'
|
|
11
12
|
require_relative 'extracted_unit'
|
|
12
13
|
require_relative 'dependency_graph'
|
|
14
|
+
require_relative 'git_provenance'
|
|
13
15
|
require_relative 'extractors/model_extractor'
|
|
14
16
|
require_relative 'extractors/controller_extractor'
|
|
15
17
|
require_relative 'extractors/phlex_extractor'
|
|
@@ -248,12 +250,6 @@ module Woods
|
|
|
248
250
|
Rails.logger.info '[Woods] Analyzing dependency graph...'
|
|
249
251
|
@graph_analysis = GraphAnalyzer.new(@dependency_graph).analyze
|
|
250
252
|
|
|
251
|
-
# Phase 3.5: Precompute request flows (opt-in)
|
|
252
|
-
if Woods.configuration.precompute_flows
|
|
253
|
-
Rails.logger.info '[Woods] Precomputing request flows...'
|
|
254
|
-
precompute_flows
|
|
255
|
-
end
|
|
256
|
-
|
|
257
253
|
# Phase 4: Enrich with git data
|
|
258
254
|
Rails.logger.info '[Woods] Enriching with git data...'
|
|
259
255
|
enrich_with_git_data
|
|
@@ -265,6 +261,17 @@ module Woods
|
|
|
265
261
|
# Phase 5: Write output
|
|
266
262
|
Rails.logger.info '[Woods] Writing output...'
|
|
267
263
|
write_results
|
|
264
|
+
|
|
265
|
+
# Phase 5.5: Precompute request flows (opt-in). Must run AFTER
|
|
266
|
+
# write_results — FlowAssembler loads unit JSON from disk, so running
|
|
267
|
+
# earlier assembled every flow from absent (fresh output dir) or
|
|
268
|
+
# stale (previous run's) data. precompute_flows re-writes the
|
|
269
|
+
# controller units it annotates with metadata[:flow_paths].
|
|
270
|
+
if Woods.configuration.precompute_flows
|
|
271
|
+
Rails.logger.info '[Woods] Precomputing request flows...'
|
|
272
|
+
precompute_flows
|
|
273
|
+
end
|
|
274
|
+
|
|
268
275
|
write_dependency_graph
|
|
269
276
|
write_graph_analysis
|
|
270
277
|
write_manifest
|
|
@@ -315,11 +322,17 @@ module Woods
|
|
|
315
322
|
regenerate_type_index(type_key)
|
|
316
323
|
end
|
|
317
324
|
|
|
318
|
-
# Update graph, manifest, and summary
|
|
325
|
+
# Update graph, manifest, and summary. No capture_snapshot here:
|
|
326
|
+
# snapshots must hash the FULL unit set, and incremental runs only
|
|
327
|
+
# re-extract affected units (@results stays empty) — capturing would
|
|
328
|
+
# record a snapshot whose diff reports every unit as deleted.
|
|
329
|
+
# Snapshots are captured on full extraction only.
|
|
319
330
|
write_dependency_graph
|
|
320
|
-
write_manifest
|
|
331
|
+
write_manifest(incremental: true)
|
|
321
332
|
write_structural_summary
|
|
322
|
-
|
|
333
|
+
if Woods.configuration.enable_snapshots
|
|
334
|
+
Rails.logger.info '[Woods] Skipping snapshot capture — snapshots are captured on full extraction only'
|
|
335
|
+
end
|
|
323
336
|
|
|
324
337
|
affected_ids
|
|
325
338
|
end
|
|
@@ -505,11 +518,42 @@ module Woods
|
|
|
505
518
|
all_units = @results.values.flatten(1)
|
|
506
519
|
precomputer = FlowPrecomputer.new(units: all_units, graph: @dependency_graph, output_dir: @output_dir.to_s)
|
|
507
520
|
flow_map = precomputer.precompute
|
|
521
|
+
rewrite_flow_annotated_units
|
|
508
522
|
Rails.logger.info "[Woods] Precomputed #{flow_map.size} request flows"
|
|
509
523
|
rescue StandardError => e
|
|
510
524
|
Rails.logger.error "[Woods] Flow precomputation failed: #{e.message}"
|
|
511
525
|
end
|
|
512
526
|
|
|
527
|
+
# Precompute runs after write_results (FlowAssembler reads unit JSON
|
|
528
|
+
# from disk), so units annotated in memory with metadata[:flow_paths]
|
|
529
|
+
# must be re-written and their type index refreshed to pick up the
|
|
530
|
+
# annotation.
|
|
531
|
+
#
|
|
532
|
+
# The index is rebuilt from the in-memory `units` (the authoritative
|
|
533
|
+
# full-extraction `@results`), NOT from a disk glob: this is a full
|
|
534
|
+
# extraction, the output dir is never wiped, and globbing would
|
|
535
|
+
# resurrect stale unit files for app classes deleted since the last run.
|
|
536
|
+
# (The incremental path, which only holds changed units in memory, still
|
|
537
|
+
# rebuilds from disk via {#regenerate_type_index}.)
|
|
538
|
+
def rewrite_flow_annotated_units
|
|
539
|
+
@results.each do |type, units|
|
|
540
|
+
annotated = units.select { |u| u.metadata[:flow_paths] }
|
|
541
|
+
next if annotated.empty?
|
|
542
|
+
|
|
543
|
+
type_dir = @output_dir.join(type.to_s)
|
|
544
|
+
annotated.each do |unit|
|
|
545
|
+
File.write(
|
|
546
|
+
type_dir.join(collision_safe_filename(unit.identifier)),
|
|
547
|
+
json_serialize(unit.to_h)
|
|
548
|
+
)
|
|
549
|
+
end
|
|
550
|
+
File.write(
|
|
551
|
+
type_dir.join('_index.json'),
|
|
552
|
+
json_serialize(type_index_entries(units))
|
|
553
|
+
)
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
513
557
|
# ──────────────────────────────────────────────────────────────────────
|
|
514
558
|
# Git Enrichment
|
|
515
559
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -701,23 +745,32 @@ module Woods
|
|
|
701
745
|
end
|
|
702
746
|
|
|
703
747
|
# Also write a type index for fast lookups
|
|
704
|
-
index = units.map do |u|
|
|
705
|
-
{
|
|
706
|
-
identifier: u.identifier,
|
|
707
|
-
file_path: u.file_path,
|
|
708
|
-
namespace: u.namespace,
|
|
709
|
-
estimated_tokens: u.estimated_tokens,
|
|
710
|
-
chunk_count: u.chunks.size
|
|
711
|
-
}
|
|
712
|
-
end
|
|
713
|
-
|
|
714
748
|
File.write(
|
|
715
749
|
type_dir.join('_index.json'),
|
|
716
|
-
json_serialize(
|
|
750
|
+
json_serialize(type_index_entries(units))
|
|
717
751
|
)
|
|
718
752
|
end
|
|
719
753
|
end
|
|
720
754
|
|
|
755
|
+
# Build the `_index.json` entry list for a set of in-memory units.
|
|
756
|
+
# Shared by {#write_results} and {#rewrite_flow_annotated_units} so both
|
|
757
|
+
# emit the index from the authoritative in-memory `@results` rather than
|
|
758
|
+
# re-deriving it from disk.
|
|
759
|
+
#
|
|
760
|
+
# @param units [Array<ExtractedUnit>]
|
|
761
|
+
# @return [Array<Hash>]
|
|
762
|
+
def type_index_entries(units)
|
|
763
|
+
units.map do |u|
|
|
764
|
+
{
|
|
765
|
+
identifier: u.identifier,
|
|
766
|
+
file_path: u.file_path,
|
|
767
|
+
namespace: u.namespace,
|
|
768
|
+
estimated_tokens: u.estimated_tokens,
|
|
769
|
+
chunk_count: u.chunks.size
|
|
770
|
+
}
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
721
774
|
def write_dependency_graph
|
|
722
775
|
graph_data = @dependency_graph.to_h
|
|
723
776
|
graph_data[:pagerank] = @dependency_graph.pagerank
|
|
@@ -744,23 +797,39 @@ module Woods
|
|
|
744
797
|
)
|
|
745
798
|
end
|
|
746
799
|
|
|
747
|
-
def write_manifest
|
|
800
|
+
def write_manifest(incremental: false)
|
|
801
|
+
# Worktree-aware git provenance. In a linked worktree +.git+ is a file
|
|
802
|
+
# pointing at the real git dir; when that dir is unreachable (e.g. an
|
|
803
|
+
# unmounted host path inside a container) this resolves to "unknown"
|
|
804
|
+
# rather than a stale GIT_BRANCH/GIT_SHA build arg. See GitProvenance (#137).
|
|
805
|
+
provenance = GitProvenance.new(root: Rails.root).to_h
|
|
806
|
+
|
|
807
|
+
# Incremental runs never populate @results — deriving counts from
|
|
808
|
+
# memory would clobber a good manifest with zeros. Recompute from the
|
|
809
|
+
# persisted per-type _index.json files instead.
|
|
810
|
+
counts, total_chunks =
|
|
811
|
+
if incremental
|
|
812
|
+
persisted_counts
|
|
813
|
+
else
|
|
814
|
+
[@results.transform_values(&:size),
|
|
815
|
+
@results.sum { |_, units| units.sum { |u| u.chunks.size } }]
|
|
816
|
+
end
|
|
817
|
+
|
|
748
818
|
manifest = {
|
|
749
819
|
extracted_at: Time.current.iso8601,
|
|
750
820
|
rails_version: Rails.version,
|
|
751
821
|
ruby_version: RUBY_VERSION,
|
|
752
822
|
|
|
753
823
|
# Counts by type
|
|
754
|
-
counts:
|
|
824
|
+
counts: counts,
|
|
755
825
|
|
|
756
826
|
# Total stats
|
|
757
|
-
total_units:
|
|
758
|
-
total_chunks:
|
|
827
|
+
total_units: counts.values.sum,
|
|
828
|
+
total_chunks: total_chunks,
|
|
759
829
|
|
|
760
|
-
# Git
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
git_branch: run_git('rev-parse', '--abbrev-ref', 'HEAD').presence || ENV['GIT_BRANCH'].presence,
|
|
830
|
+
# Git provenance (branch/sha), or "unknown" when unresolvable
|
|
831
|
+
git_sha: provenance[:git_sha],
|
|
832
|
+
git_branch: provenance[:git_branch],
|
|
764
833
|
|
|
765
834
|
# For change detection
|
|
766
835
|
gemfile_lock_sha: gemfile_lock_sha,
|
|
@@ -773,6 +842,30 @@ module Woods
|
|
|
773
842
|
)
|
|
774
843
|
end
|
|
775
844
|
|
|
845
|
+
# Unit and chunk counts derived from the per-type _index.json files on
|
|
846
|
+
# disk — the source of truth after an incremental run, where only the
|
|
847
|
+
# affected units were re-extracted.
|
|
848
|
+
#
|
|
849
|
+
# @return [Array(Hash{Symbol => Integer}, Integer)] counts by type, total chunk count
|
|
850
|
+
def persisted_counts
|
|
851
|
+
counts = {}
|
|
852
|
+
chunks = 0
|
|
853
|
+
|
|
854
|
+
Dir[@output_dir.join('*/_index.json').to_s].each do |index_path|
|
|
855
|
+
entries = JSON.parse(File.read(index_path))
|
|
856
|
+
counts[File.basename(File.dirname(index_path)).to_sym] = entries.size
|
|
857
|
+
chunks += entries.sum { |e| e['chunk_count'].to_i }
|
|
858
|
+
rescue JSON::ParserError => e
|
|
859
|
+
# An unreadable index silently drops that whole type from the manifest
|
|
860
|
+
# counts — warn rather than undercount without a trace.
|
|
861
|
+
type = File.basename(File.dirname(index_path))
|
|
862
|
+
Rails.logger.warn("[Woods] Skipping unreadable #{type}/_index.json in manifest counts: #{e.message}")
|
|
863
|
+
next
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
[counts, chunks]
|
|
867
|
+
end
|
|
868
|
+
|
|
776
869
|
# Capture a temporal snapshot after extraction completes.
|
|
777
870
|
#
|
|
778
871
|
# Reads the manifest and computes per-unit content hashes, then delegates
|
|
@@ -787,7 +880,10 @@ module Woods
|
|
|
787
880
|
return unless manifest_path.exist?
|
|
788
881
|
|
|
789
882
|
manifest = JSON.parse(File.read(manifest_path))
|
|
790
|
-
|
|
883
|
+
# Snapshots are keyed on the commit SHA — an unresolvable provenance
|
|
884
|
+
# ("unknown", see GitProvenance/#137) must not key or collide a snapshot.
|
|
885
|
+
git_sha = manifest['git_sha']
|
|
886
|
+
return if git_sha.nil? || git_sha == Woods::GitProvenance::UNKNOWN
|
|
791
887
|
|
|
792
888
|
store = build_snapshot_store
|
|
793
889
|
return unless store
|
|
@@ -906,7 +1002,10 @@ module Woods
|
|
|
906
1002
|
identifier: data['identifier'],
|
|
907
1003
|
file_path: data['file_path'],
|
|
908
1004
|
namespace: data['namespace'],
|
|
909
|
-
estimated_tokens
|
|
1005
|
+
# Unit JSON has no estimated_tokens field (ExtractedUnit#to_h
|
|
1006
|
+
# doesn't emit one) — recompute it, or every unit of a type
|
|
1007
|
+
# touched by an incremental run would index as null.
|
|
1008
|
+
estimated_tokens: estimated_tokens_from(data),
|
|
910
1009
|
chunk_count: (data['chunks'] || []).size
|
|
911
1010
|
}
|
|
912
1011
|
end
|
|
@@ -917,6 +1016,20 @@ module Woods
|
|
|
917
1016
|
)
|
|
918
1017
|
end
|
|
919
1018
|
|
|
1019
|
+
# Token estimate for a unit parsed back from JSON, mirroring
|
|
1020
|
+
# ExtractedUnit#estimated_tokens (see docs/TOKEN_BENCHMARK.md).
|
|
1021
|
+
#
|
|
1022
|
+
# @param data [Hash] Parsed unit JSON (string keys)
|
|
1023
|
+
# @return [Integer]
|
|
1024
|
+
def estimated_tokens_from(data)
|
|
1025
|
+
source = data['source_code']
|
|
1026
|
+
metadata = data['metadata'] || {}
|
|
1027
|
+
|
|
1028
|
+
source_tokens = source ? TokenUtils.estimate_tokens(source) : 0
|
|
1029
|
+
metadata_tokens = metadata.any? ? TokenUtils.estimate_tokens(metadata.to_json) : 0
|
|
1030
|
+
source_tokens + metadata_tokens
|
|
1031
|
+
end
|
|
1032
|
+
|
|
920
1033
|
# ──────────────────────────────────────────────────────────────────────
|
|
921
1034
|
# Helpers
|
|
922
1035
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -1014,12 +1127,20 @@ module Woods
|
|
|
1014
1127
|
|
|
1015
1128
|
return unless unit
|
|
1016
1129
|
|
|
1017
|
-
# Update dependency graph
|
|
1130
|
+
# Update dependency graph. Register BEFORE normalizing the path —
|
|
1131
|
+
# the graph's file_map stores absolute paths (affected_by matches
|
|
1132
|
+
# changed files against them), exactly as full extraction registers
|
|
1133
|
+
# in Phase 1 and only normalizes in Phase 4.5.
|
|
1018
1134
|
@dependency_graph.register(unit)
|
|
1019
1135
|
|
|
1020
1136
|
# Track which type was affected
|
|
1021
1137
|
affected_types&.add(extractor_key)
|
|
1022
1138
|
|
|
1139
|
+
# Unit JSON carries Rails.root-relative paths (full extraction's
|
|
1140
|
+
# Phase 4.5); writing the raw absolute source_location here would
|
|
1141
|
+
# leak container-absolute paths into the index after incremental runs.
|
|
1142
|
+
unit.file_path = normalize_file_path(unit.file_path)
|
|
1143
|
+
|
|
1023
1144
|
# Write updated unit
|
|
1024
1145
|
type_dir = @output_dir.join(extractor_key.to_s)
|
|
1025
1146
|
|
|
@@ -48,10 +48,8 @@ module Woods
|
|
|
48
48
|
#
|
|
49
49
|
# @return [Array<ExtractedUnit>] List of concern units
|
|
50
50
|
def extract_all
|
|
51
|
-
@directories.
|
|
52
|
-
|
|
53
|
-
extract_concern_file(file)
|
|
54
|
-
end
|
|
51
|
+
find_files_in_directories(@directories).filter_map do |file|
|
|
52
|
+
extract_concern_file(file)
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
|
|
@@ -285,7 +283,7 @@ module Woods
|
|
|
285
283
|
deps.concat(scan_service_dependencies(source))
|
|
286
284
|
deps.concat(scan_job_dependencies(source))
|
|
287
285
|
|
|
288
|
-
deps
|
|
286
|
+
consolidate_dependencies(deps)
|
|
289
287
|
end
|
|
290
288
|
end
|
|
291
289
|
end
|
|
@@ -36,10 +36,8 @@ module Woods
|
|
|
36
36
|
#
|
|
37
37
|
# @return [Array<ExtractedUnit>] List of configuration units
|
|
38
38
|
def extract_all
|
|
39
|
-
units = @directories.
|
|
40
|
-
|
|
41
|
-
extract_configuration_file(file)
|
|
42
|
-
end
|
|
39
|
+
units = find_files_in_directories(@directories).filter_map do |file|
|
|
40
|
+
extract_configuration_file(file)
|
|
43
41
|
end
|
|
44
42
|
|
|
45
43
|
profile = BehavioralProfile.new.extract
|
|
@@ -212,7 +210,7 @@ module Woods
|
|
|
212
210
|
|
|
213
211
|
deps.concat(scan_service_dependencies(source))
|
|
214
212
|
|
|
215
|
-
deps
|
|
213
|
+
consolidate_dependencies(deps)
|
|
216
214
|
end
|
|
217
215
|
end
|
|
218
216
|
end
|
|
@@ -320,7 +320,7 @@ module Woods
|
|
|
320
320
|
deps.concat(scan_navigation_dependencies(source, via_type: :redirect_to))
|
|
321
321
|
end
|
|
322
322
|
|
|
323
|
-
deps
|
|
323
|
+
consolidate_dependencies(deps)
|
|
324
324
|
end
|
|
325
325
|
|
|
326
326
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -49,10 +49,8 @@ module Woods
|
|
|
49
49
|
#
|
|
50
50
|
# @return [Array<ExtractedUnit>] List of decorator units
|
|
51
51
|
def extract_all
|
|
52
|
-
@directories.
|
|
53
|
-
|
|
54
|
-
extract_decorator_file(file)
|
|
55
|
-
end
|
|
52
|
+
find_files_in_directories(@directories).filter_map do |file|
|
|
53
|
+
extract_decorator_file(file)
|
|
56
54
|
end
|
|
57
55
|
end
|
|
58
56
|
|
|
@@ -246,7 +244,7 @@ module Woods
|
|
|
246
244
|
|
|
247
245
|
deps.concat(scan_common_dependencies(source))
|
|
248
246
|
|
|
249
|
-
deps
|
|
247
|
+
consolidate_dependencies(deps)
|
|
250
248
|
end
|
|
251
249
|
end
|
|
252
250
|
end
|
|
@@ -42,7 +42,7 @@ module Woods
|
|
|
42
42
|
def extract_all
|
|
43
43
|
event_map = {}
|
|
44
44
|
|
|
45
|
-
@directories
|
|
45
|
+
find_files_in_directories(@directories).each do |file_path|
|
|
46
46
|
scan_file(file_path, event_map)
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -204,7 +204,7 @@ module Woods
|
|
|
204
204
|
# @return [Array<Hash>]
|
|
205
205
|
def build_dependencies(combined_source)
|
|
206
206
|
deps = scan_common_dependencies(combined_source)
|
|
207
|
-
deps
|
|
207
|
+
consolidate_dependencies(deps)
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
210
|
end
|
|
@@ -35,9 +35,7 @@ module Woods
|
|
|
35
35
|
#
|
|
36
36
|
# @return [Array<ExtractedUnit>] List of factory units
|
|
37
37
|
def extract_all
|
|
38
|
-
@directories.flat_map
|
|
39
|
-
Dir[dir.join('**/*.rb')].flat_map { |file| extract_factory_file(file) }
|
|
40
|
-
end
|
|
38
|
+
find_files_in_directories(@directories).flat_map { |file| extract_factory_file(file) }
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
# Extract factory definitions from a single factory file.
|
|
@@ -282,7 +280,7 @@ module Woods
|
|
|
282
280
|
deps << { type: :factory, target: assoc, via: :factory_association }
|
|
283
281
|
end
|
|
284
282
|
|
|
285
|
-
deps
|
|
283
|
+
consolidate_dependencies(deps)
|
|
286
284
|
end
|
|
287
285
|
end
|
|
288
286
|
end
|
|
@@ -770,7 +770,7 @@ module Woods
|
|
|
770
770
|
deps << { type: :graphql_resolver, target: resolver, via: :field_resolver }
|
|
771
771
|
end
|
|
772
772
|
|
|
773
|
-
deps
|
|
773
|
+
consolidate_dependencies(deps)
|
|
774
774
|
end
|
|
775
775
|
|
|
776
776
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
|
|
5
|
+
require_relative 'shared_utility_methods'
|
|
6
|
+
|
|
5
7
|
module Woods
|
|
6
8
|
module Extractors
|
|
7
9
|
# I18nExtractor handles internationalization locale file extraction.
|
|
@@ -16,6 +18,8 @@ module Woods
|
|
|
16
18
|
# en = units.find { |u| u.identifier == "en.yml" }
|
|
17
19
|
#
|
|
18
20
|
class I18nExtractor
|
|
21
|
+
include SharedUtilityMethods
|
|
22
|
+
|
|
19
23
|
# Directories to scan for locale files
|
|
20
24
|
I18N_DIRECTORIES = %w[
|
|
21
25
|
config/locales
|
|
@@ -30,10 +34,8 @@ module Woods
|
|
|
30
34
|
#
|
|
31
35
|
# @return [Array<ExtractedUnit>] List of i18n units
|
|
32
36
|
def extract_all
|
|
33
|
-
@directories.
|
|
34
|
-
|
|
35
|
-
extract_i18n_file(file)
|
|
36
|
-
end
|
|
37
|
+
find_files_in_directories(@directories, '**/*.yml').filter_map do |file|
|
|
38
|
+
extract_i18n_file(file)
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -46,11 +46,9 @@ module Woods
|
|
|
46
46
|
units = []
|
|
47
47
|
|
|
48
48
|
# File-based discovery (catches everything)
|
|
49
|
-
@directories.each do |
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
units << unit if unit
|
|
53
|
-
end
|
|
49
|
+
find_files_in_directories(@directories).each do |file|
|
|
50
|
+
unit = extract_job_file(file)
|
|
51
|
+
units << unit if unit
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
# Also try class-based discovery for ActiveJob
|
|
@@ -356,7 +354,7 @@ module Woods
|
|
|
356
354
|
|
|
357
355
|
deps << { type: :infrastructure, target: :redis, via: :code_reference } if source.match?(/Redis\.current|REDIS/)
|
|
358
356
|
|
|
359
|
-
deps
|
|
357
|
+
consolidate_dependencies(deps)
|
|
360
358
|
end
|
|
361
359
|
|
|
362
360
|
# Scan source for job class enqueue calls and return the list of enqueued job names.
|
|
@@ -211,7 +211,7 @@ module Woods
|
|
|
211
211
|
# @return [Array<Hash>] Dependency hashes with :type, :target, :via
|
|
212
212
|
def extract_dependencies(source)
|
|
213
213
|
deps = scan_common_dependencies(source)
|
|
214
|
-
deps
|
|
214
|
+
consolidate_dependencies(deps)
|
|
215
215
|
end
|
|
216
216
|
end
|
|
217
217
|
end
|
|
@@ -243,7 +243,7 @@ module Woods
|
|
|
243
243
|
deps << { type: :route, target: route, via: :url_helper }
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
-
deps
|
|
246
|
+
consolidate_dependencies(deps)
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -40,10 +40,8 @@ module Woods
|
|
|
40
40
|
#
|
|
41
41
|
# @return [Array<ExtractedUnit>] List of manager units
|
|
42
42
|
def extract_all
|
|
43
|
-
@directories.
|
|
44
|
-
|
|
45
|
-
extract_manager_file(file)
|
|
46
|
-
end
|
|
43
|
+
find_files_in_directories(@directories).filter_map do |file|
|
|
44
|
+
extract_manager_file(file)
|
|
47
45
|
end
|
|
48
46
|
end
|
|
49
47
|
|
|
@@ -181,7 +179,7 @@ module Woods
|
|
|
181
179
|
|
|
182
180
|
deps.concat(scan_common_dependencies(source))
|
|
183
181
|
|
|
184
|
-
deps
|
|
182
|
+
consolidate_dependencies(deps)
|
|
185
183
|
end
|
|
186
184
|
end
|
|
187
185
|
end
|
|
@@ -501,7 +501,7 @@ module Woods
|
|
|
501
501
|
options: v.options.except(:if, :unless, :on),
|
|
502
502
|
conditions: format_validation_conditions(v)
|
|
503
503
|
}
|
|
504
|
-
entry[:implicit_belongs_to] = true if implicit_belongs_to_validator?(v)
|
|
504
|
+
entry[:implicit_belongs_to] = true if implicit_belongs_to_validator?(model, v)
|
|
505
505
|
entry
|
|
506
506
|
end
|
|
507
507
|
end
|
|
@@ -654,7 +654,7 @@ module Woods
|
|
|
654
654
|
end
|
|
655
655
|
end
|
|
656
656
|
|
|
657
|
-
deps
|
|
657
|
+
consolidate_dependencies(deps)
|
|
658
658
|
end
|
|
659
659
|
|
|
660
660
|
# Enrich callback metadata with side-effect analysis.
|
|
@@ -922,17 +922,30 @@ module Woods
|
|
|
922
922
|
conditions
|
|
923
923
|
end
|
|
924
924
|
|
|
925
|
-
# Detect Rails-generated implicit belongs_to presence validators
|
|
925
|
+
# Detect Rails-generated implicit belongs_to presence validators.
|
|
926
926
|
#
|
|
927
|
+
# `belongs_to` (with belongs_to_required_by_default, Rails 5+)
|
|
928
|
+
# registers an ActiveRecord PresenceValidator on the association
|
|
929
|
+
# attribute. Reflection offers no declaration provenance — the
|
|
930
|
+
# validator class and its #validate source_location are identical for
|
|
931
|
+
# an explicit `validates :assoc, presence: true` — so this flags any
|
|
932
|
+
# AR presence validator whose attributes are all belongs_to
|
|
933
|
+
# association names. (The previous source_location heuristic matched
|
|
934
|
+
# EVERY AR presence validator, since PresenceValidator#validate is
|
|
935
|
+
# always defined in the gem, never under Rails.root.)
|
|
936
|
+
#
|
|
937
|
+
# @param model [Class] The ActiveRecord model class
|
|
927
938
|
# @param validator [ActiveModel::Validator]
|
|
928
939
|
# @return [Boolean]
|
|
929
|
-
def implicit_belongs_to_validator?(validator)
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
940
|
+
def implicit_belongs_to_validator?(model, validator)
|
|
941
|
+
return false unless defined?(ActiveRecord::Validations::PresenceValidator)
|
|
942
|
+
return false unless validator.is_a?(ActiveRecord::Validations::PresenceValidator)
|
|
943
|
+
|
|
944
|
+
belongs_to_names = model.reflect_on_all_associations(:belongs_to).map { |r| r.name.to_sym }
|
|
945
|
+
return false if belongs_to_names.empty?
|
|
933
946
|
|
|
934
|
-
|
|
935
|
-
|
|
947
|
+
attributes = validator.respond_to?(:attributes) ? Array(validator.attributes) : []
|
|
948
|
+
attributes.any? && attributes.all? { |a| belongs_to_names.include?(a.to_sym) }
|
|
936
949
|
rescue StandardError
|
|
937
950
|
false
|
|
938
951
|
end
|
|
@@ -975,7 +988,10 @@ module Woods
|
|
|
975
988
|
|
|
976
989
|
def callback_count(model)
|
|
977
990
|
%i[validation save create update destroy commit rollback].sum do |type|
|
|
978
|
-
|
|
991
|
+
# CallbackChain includes Enumerable but defines no #size on any
|
|
992
|
+
# supported Rails version — #size raises and the rescue would
|
|
993
|
+
# zero the count. #count is the only correct API here.
|
|
994
|
+
model.send("_#{type}_callbacks").count
|
|
979
995
|
rescue StandardError
|
|
980
996
|
0
|
|
981
997
|
end
|
|
@@ -43,10 +43,8 @@ module Woods
|
|
|
43
43
|
#
|
|
44
44
|
# @return [Array<ExtractedUnit>] List of policy units
|
|
45
45
|
def extract_all
|
|
46
|
-
@directories.
|
|
47
|
-
|
|
48
|
-
extract_policy_file(file)
|
|
49
|
-
end
|
|
46
|
+
find_files_in_directories(@directories).filter_map do |file|
|
|
47
|
+
extract_policy_file(file)
|
|
50
48
|
end
|
|
51
49
|
end
|
|
52
50
|
|
|
@@ -184,7 +182,7 @@ module Woods
|
|
|
184
182
|
deps.concat(scan_service_dependencies(source))
|
|
185
183
|
deps.concat(scan_job_dependencies(source))
|
|
186
184
|
|
|
187
|
-
deps
|
|
185
|
+
consolidate_dependencies(deps)
|
|
188
186
|
end
|
|
189
187
|
end
|
|
190
188
|
end
|
|
@@ -222,7 +222,7 @@ module Woods
|
|
|
222
222
|
# @return [Array<Hash>] Dependency hashes with :type, :target, :via
|
|
223
223
|
def extract_dependencies(source)
|
|
224
224
|
deps = scan_common_dependencies(source)
|
|
225
|
-
deps
|
|
225
|
+
consolidate_dependencies(deps)
|
|
226
226
|
end
|
|
227
227
|
end
|
|
228
228
|
end
|
|
@@ -38,10 +38,8 @@ module Woods
|
|
|
38
38
|
#
|
|
39
39
|
# @return [Array<ExtractedUnit>] List of Pundit policy units
|
|
40
40
|
def extract_all
|
|
41
|
-
@directories.
|
|
42
|
-
|
|
43
|
-
extract_pundit_file(file)
|
|
44
|
-
end
|
|
41
|
+
find_files_in_directories(@directories).filter_map do |file|
|
|
42
|
+
extract_pundit_file(file)
|
|
45
43
|
end
|
|
46
44
|
end
|
|
47
45
|
|
|
@@ -216,7 +214,7 @@ module Woods
|
|
|
216
214
|
deps.concat(scan_model_dependencies(source))
|
|
217
215
|
deps.concat(scan_service_dependencies(source))
|
|
218
216
|
|
|
219
|
-
deps
|
|
217
|
+
consolidate_dependencies(deps)
|
|
220
218
|
end
|
|
221
219
|
end
|
|
222
220
|
end
|