woods 2.0.0.beta2 → 2.0.0.beta4
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 +339 -1
- data/CONTRIBUTING.md +188 -12
- data/README.md +93 -174
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +109 -8
- data/docs/AGENT_SETUP.md +98 -7
- data/docs/BACKEND_MATRIX.md +25 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +267 -16
- data/docs/CONSOLE_MCP_SETUP.md +80 -7
- data/docs/DOCKER_SETUP.md +22 -3
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +45 -6
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +147 -7
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +276 -5
- data/docs/MCP_TOOL_COOKBOOK.md +37 -22
- data/docs/MCP_WORKTREE_SETUP.md +43 -83
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +72 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +273 -12
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +129 -18
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +48 -22
- data/docs/WATCH_DAEMON.md +277 -67
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/exe/woods-mcp-start +14 -9
- data/lib/generators/woods/pgvector_generator.rb +8 -2
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +135 -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 +72 -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 +18 -17
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/dispatch_pipeline.rb +7 -0
- data/lib/woods/console/embedded_executor.rb +32 -10
- 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/console/sql_noise_stripper.rb +9 -7
- data/lib/woods/console/sql_table_scanner.rb +47 -7
- data/lib/woods/console/sql_validator.rb +49 -9
- data/lib/woods/console/sqlite_read_guard.rb +46 -0
- data/lib/woods/coordination/pipeline_lock.rb +3 -2
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +114 -60
- 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 +277 -149
- 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/declared_parent.rb +55 -0
- 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 +10 -13
- 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 +13 -9
- data/lib/woods/extractors/mailer_extractor.rb +26 -15
- 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 +26 -34
- 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 +13 -9
- 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 +48 -19
- 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 +22 -13
- data/lib/woods/mcp/bootstrapper.rb +79 -4
- data/lib/woods/mcp/config_resolver.rb +2 -1
- data/lib/woods/mcp/index_reader.rb +334 -162
- 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 +22 -9
- data/lib/woods/mcp/renderers/plain_renderer.rb +18 -8
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +178 -63
- data/lib/woods/mcp/tool_contract.rb +3 -1
- data/lib/woods/mcp/tool_response_renderer.rb +41 -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/mcp/traversal_response.rb +22 -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 +13 -6
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/published_index/typed_unit_reader.rb +40 -3
- data/lib/woods/published_index.rb +2 -2
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +382 -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 +84 -0
- data/lib/woods/retrieval/lexical_index.rb +120 -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/file_store.rb +6 -1
- 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 +31 -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 +35 -10
- 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 +58 -9
- 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 +154 -32
- 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 +19 -0
- data/plugin/skills/woods-diagnose/SKILL.md +319 -1
- data/plugin/skills/woods-investigate/SKILL.md +145 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +90 -2
- data/plugin/skills/woods-setup/SKILL.md +110 -6
- metadata +87 -5
|
@@ -28,12 +28,15 @@ module Woods
|
|
|
28
28
|
ASSOCIATION_ORDER = %w[belongs_to has_one has_many has_and_belongs_to_many].freeze
|
|
29
29
|
|
|
30
30
|
# @param name_mapper [NameMapper] resolves ids to wikilinks/paths
|
|
31
|
-
# @param nodes [Hash]
|
|
31
|
+
# @param nodes [Hash] note-key => graph node for edge-endpoint types
|
|
32
32
|
# @param pagerank [Hash{String=>Float}] persisted pagerank scores
|
|
33
33
|
# @param analysis [Hash, nil] parsed graph_analysis.json (hubs/cycles/orphans/bridges) or nil
|
|
34
34
|
# @param include_source [Boolean] embed scrubbed source code
|
|
35
35
|
# @param scanner [#scan, nil] credential scanner (required when include_source)
|
|
36
|
-
def initialize(name_mapper:, nodes:, pagerank: {}, analysis: nil, include_source: false, scanner: nil
|
|
36
|
+
def initialize(name_mapper:, nodes:, pagerank: {}, analysis: nil, include_source: false, scanner: nil,
|
|
37
|
+
identifiers: {}, ambiguous_identifiers: Set.new)
|
|
38
|
+
@identifiers = identifiers
|
|
39
|
+
@ambiguous_identifiers = ambiguous_identifiers
|
|
37
40
|
@mapper = name_mapper
|
|
38
41
|
@nodes = nodes || {}
|
|
39
42
|
@pagerank = pagerank || {}
|
|
@@ -45,10 +48,10 @@ module Woods
|
|
|
45
48
|
@cycle_members = cycle_member_set(analysis)
|
|
46
49
|
end
|
|
47
50
|
|
|
48
|
-
# @param id [String] bare
|
|
51
|
+
# @param id [String, Array<String>] note key: bare id for standalone use, or [identifier, type]
|
|
49
52
|
# @param unit [Hash] parsed unit JSON (used only for body facts)
|
|
50
53
|
# @param depends_on [Array<Hash>] [{ target:, via: }] filtered to the emitted set
|
|
51
|
-
# @param used_by [Array<String
|
|
54
|
+
# @param used_by [Array<String, Array<String>>] unique dependent note keys filtered to the emitted set
|
|
52
55
|
# @return [String] the rendered note. A failed source scrub omits the source
|
|
53
56
|
# section (the note is still written); build never returns nil.
|
|
54
57
|
def build(id:, unit:, depends_on:, used_by:)
|
|
@@ -74,7 +77,7 @@ module Woods
|
|
|
74
77
|
def frontmatter(id, unit, depends_on, used_by)
|
|
75
78
|
fm = {
|
|
76
79
|
'woods_managed' => true,
|
|
77
|
-
'id' => id,
|
|
80
|
+
'id' => @identifiers.fetch(id, id),
|
|
78
81
|
'type' => unit['type'],
|
|
79
82
|
'file' => unit['file_path'],
|
|
80
83
|
'source_hash' => unit['source_hash'],
|
|
@@ -82,17 +85,23 @@ module Woods
|
|
|
82
85
|
'dependency_count' => depends_on.size,
|
|
83
86
|
'dependent_count' => used_by.size,
|
|
84
87
|
'tags' => tags_for(id, unit),
|
|
85
|
-
'aliases' => [id]
|
|
88
|
+
'aliases' => [@identifiers.fetch(id, id)]
|
|
86
89
|
}.compact
|
|
87
90
|
"#{fm.to_yaml}---"
|
|
88
91
|
end
|
|
89
92
|
|
|
93
|
+
def analysis_identifier(id)
|
|
94
|
+
identifier = @identifiers.fetch(id, id)
|
|
95
|
+
identifier unless @ambiguous_identifiers.include?(identifier)
|
|
96
|
+
end
|
|
97
|
+
|
|
90
98
|
def pagerank_for(id)
|
|
91
|
-
score = @pagerank[id]
|
|
99
|
+
score = @pagerank[analysis_identifier(id)]
|
|
92
100
|
score&.round(6)
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
def tags_for(id, unit)
|
|
104
|
+
id = analysis_identifier(id)
|
|
96
105
|
tags = ['woods/unit', "woods/#{unit['type']}"]
|
|
97
106
|
tags << 'woods/hub' if @hubs.include?(id)
|
|
98
107
|
tags << 'woods/orphan' if @orphans.include?(id)
|
|
@@ -124,11 +133,11 @@ module Woods
|
|
|
124
133
|
end
|
|
125
134
|
|
|
126
135
|
def callout(id, used_by)
|
|
127
|
-
if @hubs.include?(id)
|
|
136
|
+
if @hubs.include?(analysis_identifier(id))
|
|
128
137
|
pr = pagerank_for(id)
|
|
129
138
|
suffix = pr ? " (PageRank #{format('%.4f', pr)})" : ''
|
|
130
139
|
"> [!warning] Hub — high blast radius\n> #{used_by.size} units depend on this#{suffix}."
|
|
131
|
-
elsif @cycle_members.include?(id)
|
|
140
|
+
elsif @cycle_members.include?(analysis_identifier(id))
|
|
132
141
|
"> [!warning] Part of a dependency cycle\n> This unit participates in a circular dependency."
|
|
133
142
|
end
|
|
134
143
|
end
|
|
@@ -181,7 +190,7 @@ module Woods
|
|
|
181
190
|
return [] unless items
|
|
182
191
|
|
|
183
192
|
items.filter_map do |assoc|
|
|
184
|
-
link = @mapper.
|
|
193
|
+
link = @mapper.wikilink_for_identifier(assoc[:target])
|
|
185
194
|
next unless link
|
|
186
195
|
|
|
187
196
|
assoc[:dependent] ? "#{link} (dependent: #{assoc[:dependent]})" : link
|
|
@@ -10,6 +10,7 @@ require_relative 'errors'
|
|
|
10
10
|
require_relative 'name_mapper'
|
|
11
11
|
require_relative 'note_builder'
|
|
12
12
|
require_relative 'vault_assets'
|
|
13
|
+
require_relative 'destination_plan'
|
|
13
14
|
|
|
14
15
|
module Woods
|
|
15
16
|
module Obsidian
|
|
@@ -72,7 +73,7 @@ module Woods
|
|
|
72
73
|
def export_all
|
|
73
74
|
with_pinned_index do
|
|
74
75
|
graph = @reader.raw_graph_data || {}
|
|
75
|
-
|
|
76
|
+
prepare_graph(graph)
|
|
76
77
|
@pagerank = graph['pagerank'] || {}
|
|
77
78
|
analysis = safe_graph_analysis
|
|
78
79
|
owned = vault_owned_at_start?
|
|
@@ -82,16 +83,24 @@ module Woods
|
|
|
82
83
|
edge_map = build_edge_map(emitted.to_set, graph)
|
|
83
84
|
builder = build_note_builder(mapper, analysis)
|
|
84
85
|
|
|
86
|
+
@planned_writes = {}
|
|
85
87
|
written = Set.new
|
|
86
88
|
errors = []
|
|
87
89
|
exported = write_notes(emitted, mapper, edge_map, builder, written, errors)
|
|
88
90
|
indexes = write_indexes(emitted, mapper, written)
|
|
89
91
|
write_sidecar(emitted, mapper, graph, analysis, written)
|
|
90
92
|
write_owned_assets(emitted) if owned
|
|
93
|
+
DestinationPlan.new(@vault, @planned_writes, managed_note: method(:managed_marker?)).commit
|
|
91
94
|
|
|
92
|
-
swept = owned && errors.empty? ? sweep(written) : 0
|
|
95
|
+
swept = owned && errors.empty? && skipped.zero? ? sweep(written) : 0
|
|
93
96
|
warn_foreign unless owned
|
|
94
97
|
{ exported: exported, indexes: indexes, swept: swept, skipped: skipped, errors: cap_errors(errors) }
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
log " Export stopped: #{e.class}: #{e.message}"
|
|
100
|
+
{ exported: 0, indexes: 0, swept: 0, skipped: skipped || 0,
|
|
101
|
+
errors: cap_errors(Array(errors) + ["#{e.class}: #{e.message}"]) }
|
|
102
|
+
ensure
|
|
103
|
+
@planned_writes = nil
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
|
|
@@ -119,19 +128,35 @@ module Woods
|
|
|
119
128
|
|
|
120
129
|
# ── Selection / mapping ──────────────────────────────────────────
|
|
121
130
|
|
|
131
|
+
# Keep typed identity local to the exporter. Public identifiers and graph
|
|
132
|
+
# artifacts retain their original shape; variants have their own outgoing
|
|
133
|
+
# relationships, while persisted edge targets remain bare identifiers.
|
|
134
|
+
def prepare_graph(graph)
|
|
135
|
+
@primary = graph['nodes'] || {}
|
|
136
|
+
@nodes = @primary.to_h { |id, node| [[id, node['type']], node] }
|
|
137
|
+
@forward = @nodes.keys.to_h { |key| [key, Array(graph.dig('edges', key.first))] }
|
|
138
|
+
Array(graph['variants']).grep(Hash).each do |record|
|
|
139
|
+
key = [record['identifier'], record['type']]
|
|
140
|
+
next if key.any?(&:nil?) || @nodes.key?(key)
|
|
141
|
+
|
|
142
|
+
@nodes[key] = record
|
|
143
|
+
@forward[key] = Array(record['edges'])
|
|
144
|
+
end
|
|
145
|
+
@identifiers = @nodes.keys.to_h { |key| [key, key.first] }
|
|
146
|
+
@keys_by_identifier = @nodes.keys.group_by(&:first)
|
|
147
|
+
@ambiguous = @keys_by_identifier.select { |_, keys| keys.size > 1 }.keys.to_set
|
|
148
|
+
end
|
|
149
|
+
|
|
122
150
|
# The emitted set = graph nodes of an exportable type that have a loadable
|
|
123
151
|
# unit file. Confirming the file loads (not just that the index lists it)
|
|
124
152
|
# guarantees every wikilink target is backed by a real note — a node listed
|
|
125
153
|
# in the index but with no readable unit is dropped (counted as skipped).
|
|
126
154
|
#
|
|
127
|
-
# @return [Array(Array<String
|
|
155
|
+
# @return [Array(Array<Array<String>>, Integer)] [sorted typed keys, skipped count]
|
|
128
156
|
def partition_emitted
|
|
129
|
-
known = known_ids
|
|
130
157
|
candidates = @nodes.keys.select do |id|
|
|
131
158
|
type = @nodes[id]['type']
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
known.include?(id)
|
|
159
|
+
@include_framework || !FRAMEWORK_TYPES.include?(type)
|
|
135
160
|
end.sort
|
|
136
161
|
|
|
137
162
|
# Load each candidate once and keep it: confirms the unit is readable
|
|
@@ -151,18 +176,20 @@ module Woods
|
|
|
151
176
|
# Load a unit, treating an unreadable/corrupt file as absent rather than
|
|
152
177
|
# letting one bad JSON parse abort the whole export.
|
|
153
178
|
def load_unit(id)
|
|
154
|
-
@
|
|
179
|
+
unit = if @ambiguous.include?(id.first)
|
|
180
|
+
@reader.find_unit(id.first, type: id.last)
|
|
181
|
+
else
|
|
182
|
+
@reader.find_unit(id.first)
|
|
183
|
+
end
|
|
184
|
+
unit if unit && unit['identifier'] == id.first && unit['type'] == id.last
|
|
155
185
|
rescue StandardError => e
|
|
156
186
|
log " skipped #{id}: #{e.class}: #{e.message}"
|
|
157
187
|
nil
|
|
158
188
|
end
|
|
159
189
|
|
|
160
|
-
def known_ids
|
|
161
|
-
@known_ids ||= @reader.list_units.to_set { |entry| entry['identifier'] }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
190
|
def build_mapper(emitted)
|
|
165
|
-
NameMapper.new(emitted.to_h { |id| [id, dir_for(@nodes[id]['type'])] }
|
|
191
|
+
NameMapper.new(emitted.to_h { |id| [id, dir_for(@nodes[id]['type'])] },
|
|
192
|
+
identifiers: @identifiers, ambiguous_identifiers: @ambiguous)
|
|
166
193
|
end
|
|
167
194
|
|
|
168
195
|
def dir_for(type)
|
|
@@ -182,25 +209,37 @@ module Woods
|
|
|
182
209
|
|
|
183
210
|
def build_note_builder(mapper, analysis)
|
|
184
211
|
NoteBuilder.new(name_mapper: mapper, nodes: @nodes, pagerank: @pagerank,
|
|
185
|
-
analysis: analysis, include_source: @include_source, scanner: scanner
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
analysis: analysis, include_source: @include_source, scanner: scanner,
|
|
213
|
+
identifiers: @identifiers, ambiguous_identifiers: @ambiguous)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Reverse links must come from resolved forward edges: the legacy reverse
|
|
217
|
+
# index merges source types, and cannot assign them to a particular note.
|
|
218
|
+
# A bare target naming several graph types remains ambiguous even if only
|
|
219
|
+
# one sibling is exportable. Do not invent a typed relationship.
|
|
220
|
+
def build_edge_map(emitted, _graph)
|
|
221
|
+
@ambiguous_edges = Set.new
|
|
222
|
+
map = emitted.to_h { |id| [id, { depends_on: [], used_by: [] }] }
|
|
223
|
+
emitted.each do |id|
|
|
224
|
+
deps = @forward[id].filter_map { |edge| forward_edge(edge, id, emitted) }
|
|
225
|
+
.uniq { |edge| [edge[:target], edge[:via]] }
|
|
226
|
+
map[id][:depends_on] = deps
|
|
227
|
+
deps.each { |edge| map[edge[:target]][:used_by] << id }
|
|
198
228
|
end
|
|
229
|
+
map.each_value { |edges| edges[:used_by].uniq! }
|
|
230
|
+
log " omitted #{@ambiguous_edges.size} ambiguous dependency relationships" unless @ambiguous_edges.empty?
|
|
231
|
+
map
|
|
199
232
|
end
|
|
200
233
|
|
|
201
234
|
def forward_edge(edge, source_id, emitted)
|
|
202
235
|
edge = { 'target' => edge } unless edge.is_a?(Hash)
|
|
203
|
-
|
|
236
|
+
identifier = edge['target']
|
|
237
|
+
if @ambiguous.include?(identifier)
|
|
238
|
+
@ambiguous_edges << [source_id, identifier, edge['via']]
|
|
239
|
+
return nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
target = @keys_by_identifier[identifier]&.first
|
|
204
243
|
return nil if target == source_id || !emitted.include?(target)
|
|
205
244
|
|
|
206
245
|
{ target: target, via: edge['via'] }
|
|
@@ -263,11 +302,22 @@ module Woods
|
|
|
263
302
|
# ── Pass 3b: machine sidecar ─────────────────────────────────────
|
|
264
303
|
|
|
265
304
|
def write_sidecar(emitted, mapper, graph, analysis, written)
|
|
266
|
-
notes =
|
|
267
|
-
|
|
268
|
-
|
|
305
|
+
notes = {}
|
|
306
|
+
variants = []
|
|
307
|
+
emitted.each do |key|
|
|
308
|
+
id, type = key
|
|
309
|
+
info = { 'path' => mapper.path_for(key), 'type' => type }
|
|
310
|
+
info['pagerank'] = @pagerank[id].round(6) if @pagerank[id] && !@ambiguous.include?(id)
|
|
311
|
+
if @primary.dig(id, 'type') == type
|
|
312
|
+
notes[id] = info
|
|
313
|
+
else
|
|
314
|
+
variants << { 'identifier' => id }.merge(info)
|
|
315
|
+
end
|
|
269
316
|
end
|
|
270
|
-
|
|
317
|
+
paths = mapper.paths_to_ids.transform_values(&:first)
|
|
318
|
+
manifest = { 'schema_version' => @ambiguous.empty? ? 1 : 2,
|
|
319
|
+
'notes' => sort_hash(notes), 'paths' => sort_hash(paths) }
|
|
320
|
+
manifest['variants'] = variants if manifest['schema_version'] == 2
|
|
271
321
|
write_json(written, "#{SIDECAR_DIR}/manifest.json", manifest)
|
|
272
322
|
write_json(written, "#{SIDECAR_DIR}/dependency_graph.json", graph)
|
|
273
323
|
write_json(written, "#{SIDECAR_DIR}/graph_analysis.json", analysis) if analysis
|
|
@@ -282,6 +332,8 @@ module Woods
|
|
|
282
332
|
safe_write(@vault.join('.obsidian', 'graph.json'), VaultAssets.graph_json(present_types(emitted)))
|
|
283
333
|
safe_write(@vault.join('Units.base'), VaultAssets.units_base)
|
|
284
334
|
end
|
|
335
|
+
return if @vault.join(SENTINEL).exist?
|
|
336
|
+
|
|
285
337
|
safe_write(@vault.join(SENTINEL),
|
|
286
338
|
"woods-managed vault — regenerated by `rake woods:obsidian`. Safe to delete.\n")
|
|
287
339
|
end
|
|
@@ -410,7 +462,11 @@ module Woods
|
|
|
410
462
|
def safe_write(path, content)
|
|
411
463
|
raise PathTraversalError, "refusing to write outside the vault: #{path}" unless safe_inside_vault?(path)
|
|
412
464
|
|
|
413
|
-
|
|
465
|
+
if @planned_writes
|
|
466
|
+
@planned_writes[path] = content
|
|
467
|
+
else
|
|
468
|
+
AtomicFile.write(path, content)
|
|
469
|
+
end
|
|
414
470
|
end
|
|
415
471
|
|
|
416
472
|
# ── Ownership ────────────────────────────────────────────────────
|
|
@@ -85,14 +85,18 @@ module Woods
|
|
|
85
85
|
|
|
86
86
|
# Reset one supported operation or all supported operations atomically.
|
|
87
87
|
#
|
|
88
|
+
# An explicit :all reset also repairs corrupt state; scoped resets leave it
|
|
89
|
+
# untouched so unknown cooldowns are not silently discarded.
|
|
90
|
+
#
|
|
88
91
|
# @param operation [Symbol, String] `:extraction`, `:embedding`, or `:all`
|
|
89
|
-
# @return [Boolean] true when
|
|
92
|
+
# @return [Boolean] true when a cooldown was removed or corrupt state repaired
|
|
90
93
|
# @raise [ArgumentError] when the operation is unsupported
|
|
91
94
|
def reset!(operation)
|
|
92
95
|
operations = reset_operations(operation)
|
|
93
|
-
|
|
96
|
+
repair_corrupt = operation.to_s == 'all'
|
|
97
|
+
return false unless requested_state_present?(operations, repair_corrupt: repair_corrupt)
|
|
94
98
|
|
|
95
|
-
with_existing_locked_state do |state|
|
|
99
|
+
with_existing_locked_state(repair_corrupt: repair_corrupt) do |state|
|
|
96
100
|
changed = false
|
|
97
101
|
operations.each do |key|
|
|
98
102
|
next unless state.key?(key)
|
|
@@ -135,28 +139,29 @@ module Woods
|
|
|
135
139
|
end
|
|
136
140
|
end
|
|
137
141
|
|
|
138
|
-
def parse_state(content)
|
|
139
|
-
state =
|
|
140
|
-
state.is_a?(Hash) ? state :
|
|
142
|
+
def parse_state(content, fallback: {})
|
|
143
|
+
state = JSON.parse(content)
|
|
144
|
+
state.is_a?(Hash) ? state : fallback
|
|
141
145
|
rescue JSON::ParserError
|
|
142
|
-
|
|
146
|
+
fallback
|
|
143
147
|
end
|
|
144
148
|
|
|
145
|
-
def requested_state_present?(operations)
|
|
149
|
+
def requested_state_present?(operations, repair_corrupt:)
|
|
146
150
|
File.open(@state_path, File::RDONLY) do |file|
|
|
147
151
|
file.flock(File::LOCK_SH)
|
|
148
|
-
state = parse_state(file.read)
|
|
149
|
-
operations.any? { |key| state.key?(key) }
|
|
152
|
+
state = parse_state(file.read, fallback: nil)
|
|
153
|
+
state ? operations.any? { |key| state.key?(key) } : repair_corrupt
|
|
150
154
|
end
|
|
151
155
|
rescue Errno::ENOENT
|
|
152
156
|
false
|
|
153
157
|
end
|
|
154
158
|
|
|
155
|
-
def with_existing_locked_state
|
|
159
|
+
def with_existing_locked_state(repair_corrupt:)
|
|
156
160
|
File.open(@state_path, File::RDWR) do |file|
|
|
157
161
|
file.flock(File::LOCK_EX)
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
parsed = parse_state(file.read, fallback: nil)
|
|
163
|
+
state = parsed || {}
|
|
164
|
+
changed = yield(state) || (repair_corrupt && parsed.nil?)
|
|
160
165
|
if changed
|
|
161
166
|
file.rewind
|
|
162
167
|
file.write(JSON.generate(state))
|
|
@@ -16,10 +16,9 @@ module Woods
|
|
|
16
16
|
#
|
|
17
17
|
# * {.file_rules} — file-based extractors, whose per-file method can be
|
|
18
18
|
# pointed straight at the new path.
|
|
19
|
-
# * {.whole_app_rules} — extractors
|
|
20
|
-
# middleware,
|
|
21
|
-
#
|
|
22
|
-
# re-run of that extractor, which is cheap in an already-booted process.
|
|
19
|
+
# * {.whole_app_rules} — extractors needing the complete runtime or source
|
|
20
|
+
# set (routes, middleware, merged Rake tasks, scheduled jobs, etc.). Their
|
|
21
|
+
# trigger paths map to a wholesale re-run of that extractor.
|
|
23
22
|
#
|
|
24
23
|
# Class-based extractors (models, controllers, mailers, components,
|
|
25
24
|
# channels) are deliberately *absent* here. They are reconciled against
|
|
@@ -94,6 +93,11 @@ module Woods
|
|
|
94
93
|
end
|
|
95
94
|
|
|
96
95
|
class << self
|
|
96
|
+
# Runtime-discovered classes have no per-file extractor method.
|
|
97
|
+
def runtime_rules
|
|
98
|
+
@runtime_rules ||= [Rule.new(dirs: %w[app], extensions: %w[.rb])].freeze
|
|
99
|
+
end
|
|
100
|
+
|
|
97
101
|
# Rules for extractors with a per-file entry point.
|
|
98
102
|
#
|
|
99
103
|
# @return [Array<Rule>]
|
|
@@ -147,6 +151,7 @@ module Woods
|
|
|
147
151
|
# ConcernExtractor globs app/**/concerns, not just the two canonical
|
|
148
152
|
# directories — match any .rb under app/ inside a concerns/ segment.
|
|
149
153
|
file_rule(:concerns, :extract_concern_file, %w[app], require_segment: '/concerns/'),
|
|
154
|
+
file_rule(:concerns, :extract_model_mixin_file, %w[app lib], exclude: %w[/concerns/]),
|
|
150
155
|
# GraphQL types sit outside FILE_BASED (they share one extractor
|
|
151
156
|
# method across four unit types via GRAPHQL_TYPES), which is exactly
|
|
152
157
|
# why the FILE_BASED-driven coverage guard never noticed they had no
|
|
@@ -155,7 +160,6 @@ module Woods
|
|
|
155
160
|
file_rule(:graphql, :extract_graphql_file,
|
|
156
161
|
[ex::GraphQLExtractor::GRAPHQL_DIRECTORY], extensions: %w[.rb]),
|
|
157
162
|
file_rule(:i18n, :extract_i18n_file, ex::I18nExtractor::I18N_DIRECTORIES, extensions: %w[.yml]),
|
|
158
|
-
file_rule(:rake_tasks, :extract_rake_file, ex::RakeTaskExtractor::RAKE_DIRECTORIES, extensions: %w[.rake]),
|
|
159
163
|
file_rule(:view_templates, :extract_view_template_file,
|
|
160
164
|
ex::ViewTemplateExtractor::VIEW_DIRECTORIES, extensions: view_template_extensions),
|
|
161
165
|
file_rule(:migrations, :extract_migration_file, %w[db/migrate], recursive: false),
|
|
@@ -182,6 +186,9 @@ module Woods
|
|
|
182
186
|
|
|
183
187
|
def build_whole_app_rules
|
|
184
188
|
[
|
|
189
|
+
# A task may combine definitions from several files; any change or
|
|
190
|
+
# deletion must reconcile the complete task set, not its primary file.
|
|
191
|
+
whole_app_rule(:rake_tasks, Woods::Extractors::RakeTaskExtractor::RAKE_DIRECTORIES, extensions: %w[.rake]),
|
|
185
192
|
whole_app_rule(:routes, %w[config/routes], exact_paths: %w[config/routes.rb]),
|
|
186
193
|
whole_app_rule(:engines, %w[config/routes], exact_paths: %w[config/routes.rb Gemfile.lock]),
|
|
187
194
|
whole_app_rule(:middleware, %w[config/initializers config/environments],
|
|
@@ -258,7 +265,7 @@ module Woods
|
|
|
258
265
|
# @param relative_path [String] Rails.root-relative path
|
|
259
266
|
# @return [Boolean]
|
|
260
267
|
def relevant?(relative_path)
|
|
261
|
-
return true if
|
|
268
|
+
return true if self.class.runtime_rules.any? { |rule| rule.matches?(relative_path) }
|
|
262
269
|
|
|
263
270
|
file_rules_for(relative_path).any? || whole_app_keys_for(relative_path).any?
|
|
264
271
|
end
|
data/lib/woods/payload_store.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
require 'fileutils'
|
|
5
|
-
require 'find'
|
|
6
5
|
|
|
7
6
|
module Woods
|
|
8
7
|
# Writer-side owner of the per-generation payload directories.
|
|
@@ -95,19 +94,19 @@ module Woods
|
|
|
95
94
|
# @param target [Pathname, String] a directory returned by {#create}
|
|
96
95
|
# @return [void]
|
|
97
96
|
def clone(source, target)
|
|
98
|
-
from =
|
|
99
|
-
return unless
|
|
100
|
-
|
|
101
|
-
to =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
from = File.expand_path(source.to_s)
|
|
98
|
+
return unless File.directory?(from)
|
|
99
|
+
|
|
100
|
+
to = target.to_s
|
|
101
|
+
FileUtils.mkdir_p(to)
|
|
102
|
+
pending = [[from, to]]
|
|
103
|
+
until pending.empty?
|
|
104
|
+
source_dir, target_dir = pending.pop
|
|
105
|
+
Dir.each_child(source_dir) do |name|
|
|
106
|
+
entry = File.join(source_dir, name)
|
|
107
|
+
destination = File.join(target_dir, name)
|
|
108
|
+
replicate(entry, destination, pending)
|
|
109
|
+
end
|
|
111
110
|
end
|
|
112
111
|
end
|
|
113
112
|
|
|
@@ -172,7 +171,7 @@ module Woods
|
|
|
172
171
|
# @param destination [Pathname, String] where it should land
|
|
173
172
|
# @return [void]
|
|
174
173
|
def link_or_copy(source, destination)
|
|
175
|
-
|
|
174
|
+
File.link(source.to_s, destination.to_s)
|
|
176
175
|
rescue Errno::EXDEV, Errno::EPERM, Errno::EMLINK, NotImplementedError
|
|
177
176
|
FileUtils.cp(source.to_s, destination.to_s)
|
|
178
177
|
end
|
|
@@ -215,19 +214,21 @@ module Woods
|
|
|
215
214
|
end
|
|
216
215
|
end
|
|
217
216
|
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
# bulk of the incremental seed.
|
|
217
|
+
# Classify once: Find.find followed by File.directory? queried metadata
|
|
218
|
+
# twice per file, costly on FUSE/virtiofs mounts. Parents are created before
|
|
219
|
+
# their children, so regular files need no extra parent-directory checks.
|
|
220
|
+
# Preserve the old walk's symlink behavior: a directory symlink becomes an
|
|
221
|
+
# empty directory, never a traversal into another tree or generation.
|
|
224
222
|
#
|
|
225
|
-
# @param entry [
|
|
226
|
-
# @param destination [
|
|
223
|
+
# @param entry [String] source entry
|
|
224
|
+
# @param destination [String] where it belongs in the target
|
|
225
|
+
# @param pending [Array] directories still to visit
|
|
227
226
|
# @return [void]
|
|
228
|
-
def replicate(entry, destination)
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
def replicate(entry, destination, pending)
|
|
228
|
+
stat = File.lstat(entry)
|
|
229
|
+
if stat.directory? || (stat.symlink? && File.directory?(entry))
|
|
230
|
+
FileUtils.mkdir_p(destination)
|
|
231
|
+
pending << [entry, destination] if stat.directory?
|
|
231
232
|
else
|
|
232
233
|
link_or_copy(entry, destination)
|
|
233
234
|
end
|
|
@@ -21,10 +21,47 @@ module Woods
|
|
|
21
21
|
# @return [Hash, nil] string-keyed unit, or nil when this type has no
|
|
22
22
|
# such identifier
|
|
23
23
|
def self.call(payload_dir, reader, identifier, type)
|
|
24
|
-
dir =
|
|
24
|
+
dir = directory_for(type)
|
|
25
25
|
return nil unless dir
|
|
26
|
-
return nil unless reader.list_units(type: type).any? { |entry| entry['identifier'] == identifier }
|
|
27
26
|
|
|
27
|
+
family = Woods::MCP::IndexReader::DIR_TO_TYPE.fetch(dir)
|
|
28
|
+
return nil unless reader.list_units(type: family).any? { |entry| entry['identifier'] == identifier }
|
|
29
|
+
|
|
30
|
+
unit = read_unit(payload_dir, dir, identifier)
|
|
31
|
+
unit if unit && (unit['type'] == type || type == family)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Resolve actual published types as well as historical directory-family aliases.
|
|
35
|
+
#
|
|
36
|
+
# @param type [String]
|
|
37
|
+
# @return [String, nil]
|
|
38
|
+
def self.directory_for(type)
|
|
39
|
+
Woods::MCP::IndexReader::TYPE_TO_DIR[type] ||
|
|
40
|
+
Woods::MCP::IndexReader::UNIT_TYPES_BY_DIR.find { |_, types| types.include?(type) }&.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Preserve subtype identity in an index entry from a shared type directory.
|
|
44
|
+
#
|
|
45
|
+
# @param payload_dir [Pathname]
|
|
46
|
+
# @param entry [Hash] published index entry
|
|
47
|
+
# @param dir [String] type directory
|
|
48
|
+
# @param requested_type [String, nil] actual type or family alias
|
|
49
|
+
# @return [Hash, nil] entry with actual type, or nil when filtered out
|
|
50
|
+
def self.entry(payload_dir, entry, dir, requested_type)
|
|
51
|
+
family = Woods::MCP::IndexReader::DIR_TO_TYPE.fetch(dir)
|
|
52
|
+
actual_type = if Woods::MCP::IndexReader::UNIT_TYPES_BY_DIR.fetch(dir).size > 1
|
|
53
|
+
read_unit(payload_dir, dir, entry['identifier'])&.fetch('type')
|
|
54
|
+
else
|
|
55
|
+
family
|
|
56
|
+
end
|
|
57
|
+
return unless actual_type
|
|
58
|
+
return if requested_type && requested_type != family && requested_type != actual_type
|
|
59
|
+
|
|
60
|
+
entry.merge('type' => actual_type)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Read a unit whose directory-index membership has already been established.
|
|
64
|
+
def self.read_unit(payload_dir, dir, identifier)
|
|
28
65
|
path = payload_dir.join(dir, filename_for(identifier))
|
|
29
66
|
return nil unless path.file?
|
|
30
67
|
|
|
@@ -42,7 +79,7 @@ module Woods
|
|
|
42
79
|
"#{base}_#{digest}.json"
|
|
43
80
|
end
|
|
44
81
|
|
|
45
|
-
private_class_method :filename_for
|
|
82
|
+
private_class_method :filename_for, :read_unit
|
|
46
83
|
end
|
|
47
84
|
end
|
|
48
85
|
end
|
|
@@ -167,14 +167,14 @@ module Woods
|
|
|
167
167
|
# @return [Array<Hash>]
|
|
168
168
|
def units(type: nil)
|
|
169
169
|
dirs = if type
|
|
170
|
-
dir =
|
|
170
|
+
dir = TypedUnitReader.directory_for(type.to_s)
|
|
171
171
|
dir ? [dir] : []
|
|
172
172
|
else
|
|
173
173
|
Woods::MCP::IndexReader::TYPE_DIRS
|
|
174
174
|
end
|
|
175
175
|
dirs.flat_map do |dir|
|
|
176
176
|
@reader.list_units(type: Woods::MCP::IndexReader::DIR_TO_TYPE[dir])
|
|
177
|
-
.
|
|
177
|
+
.filter_map { |entry| TypedUnitReader.entry(@payload_dir, entry, dir, type&.to_s) }
|
|
178
178
|
end
|
|
179
179
|
end
|
|
180
180
|
|
data/lib/woods/railtie.rb
CHANGED
|
@@ -14,8 +14,8 @@ module Woods
|
|
|
14
14
|
# flags and tokens set in `config/initializers/woods.rb` (the
|
|
15
15
|
# generator-created location) do not exist yet when middleware arguments
|
|
16
16
|
# are captured. Both guards are scoped to `console_mcp_path` — requests
|
|
17
|
-
# outside it pass through untouched — and while `console_mcp_enabled`
|
|
18
|
-
# false the whole stack passes through, so an unconfigured host app is
|
|
17
|
+
# outside it pass through untouched — and while `console_mcp_enabled` or
|
|
18
|
+
# `console_mcp_http_enabled` is false the whole stack passes through, so an unconfigured host app is
|
|
19
19
|
# completely unaffected. Boot-time validation of the settled configuration
|
|
20
20
|
# happens in `after_initialize` via {Woods::RailtieSupport}.
|
|
21
21
|
#
|
|
@@ -84,7 +84,7 @@ module Woods
|
|
|
84
84
|
# BearerAuth next — requires `Authorization: Bearer <token>` at the
|
|
85
85
|
# console path, failing closed (401) while no usable token is
|
|
86
86
|
# configured. Both are scoped to the console path and inert while
|
|
87
|
-
# console_mcp_enabled is false.
|
|
87
|
+
# either console_mcp_enabled or console_mcp_http_enabled is false.
|
|
88
88
|
app.middleware.use(
|
|
89
89
|
Woods::MCP::OriginGuard,
|
|
90
90
|
allowed_origins: Woods::RailtieSupport.console_allowed_origins_proc,
|