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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Woods
|
|
4
|
+
module MCP
|
|
5
|
+
# Short, client-neutral instructions for initialize and modern discovery.
|
|
6
|
+
# Only tool registration shapes the text: readiness and retrieval can
|
|
7
|
+
# change after startup, so clients must consult live status before use.
|
|
8
|
+
module InitializationGuidance
|
|
9
|
+
# Contract budget for all supported wiring combinations, tested at build.
|
|
10
|
+
MAX_BYTES = 2048
|
|
11
|
+
|
|
12
|
+
WORKFLOW = <<~TEXT
|
|
13
|
+
Start with woods_status: check index readiness, generation freshness and relevant type counts before relying on results.
|
|
14
|
+
For exact names, discover identifiers with search (prefer literal exact_prefix/exact_suffix), then inspect with lookup. For conceptual questions, use codebase_retrieve only when woods_status reports retrieval enabled. Otherwise use search and lookup.
|
|
15
|
+
Follow dependencies or dependents at depth 1 or 2; narrow types and via before paging. Respect partial results and limits: a missing match is not proof of absence, and a partial traversal does not establish every dependent or leaf.
|
|
16
|
+
Verify important conclusions against current source and tests. Recorded relationships and inferred downstream impact do not prove runtime execution or test coverage.
|
|
17
|
+
Registration does not authorize extraction, configuration changes, or live Console access. Use only the tools registered here and operate within the user's authorized scope.
|
|
18
|
+
TEXT
|
|
19
|
+
|
|
20
|
+
# @param tool_names [Array<String>] actual names after conditional registration
|
|
21
|
+
# @return [String] stable instructions, without index data or provider calls
|
|
22
|
+
def self.for(tool_names)
|
|
23
|
+
"#{WORKFLOW}Registered tools: #{tool_names.sort.join(', ')}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -62,15 +62,12 @@ module Woods
|
|
|
62
62
|
# @param enabled [#call, nil] Optional request-time predicate. When it
|
|
63
63
|
# returns falsy the request passes through unguarded. Nil (the
|
|
64
64
|
# default) means always guard.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
else
|
|
72
|
-
build_allow_list(allowed_origins)
|
|
73
|
-
end
|
|
65
|
+
# Rails 6.0 forwards middleware options as a positional hash on Ruby 3.
|
|
66
|
+
# Delegate to explicit keywords to preserve required/unknown option checks.
|
|
67
|
+
def initialize(app, options = {}, **keywords)
|
|
68
|
+
raise TypeError, 'middleware options must be a Hash' unless options.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
initialize_options(app, **options, **keywords)
|
|
74
71
|
end
|
|
75
72
|
|
|
76
73
|
# Rack entry point. Out-of-scope requests (non-matching `path:` prefix
|
|
@@ -97,6 +94,17 @@ module Woods
|
|
|
97
94
|
|
|
98
95
|
private
|
|
99
96
|
|
|
97
|
+
def initialize_options(app, allowed_origins: nil, path: nil, enabled: nil)
|
|
98
|
+
@app = app
|
|
99
|
+
@path = path
|
|
100
|
+
@enabled = enabled
|
|
101
|
+
if allowed_origins.respond_to?(:call)
|
|
102
|
+
@allowed_source = allowed_origins
|
|
103
|
+
else
|
|
104
|
+
build_allow_list(allowed_origins)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
100
108
|
# @param env [Hash] Rack environment
|
|
101
109
|
# @return [Boolean] whether this request falls under the guard
|
|
102
110
|
def guard?(env)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'index_reader'
|
|
4
|
+
require_relative '../retriever'
|
|
5
|
+
require_relative '../storage/metadata_store'
|
|
6
|
+
require_relative '../storage_identity'
|
|
7
|
+
|
|
8
|
+
module Woods
|
|
9
|
+
module MCP
|
|
10
|
+
# Loads only authoritative published extraction units. The reader pin spans
|
|
11
|
+
# snapshot construction AND the query, so a publication cannot mix units.
|
|
12
|
+
# No provider, vector adapter, host source read, or context cache is involved.
|
|
13
|
+
class PublishedLexicalRetriever
|
|
14
|
+
attr_reader :reader, :snapshot, :default_budget
|
|
15
|
+
|
|
16
|
+
def initialize(index_dir:, reader: nil, default_budget: 8000)
|
|
17
|
+
@default_budget = default_budget
|
|
18
|
+
@index_dir = Pathname.new(index_dir)
|
|
19
|
+
@reader = reader || IndexReader.new(index_dir)
|
|
20
|
+
@snapshot_mutex = Mutex.new
|
|
21
|
+
@snapshot = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def mode = :lexical
|
|
25
|
+
def vector_store = nil
|
|
26
|
+
def graph_store = nil
|
|
27
|
+
def metadata_store = @snapshot&.last&.metadata_store
|
|
28
|
+
|
|
29
|
+
# Server uses one reader for status, tool pinning and lexical retrieval.
|
|
30
|
+
# Called only during server construction, before request threads start.
|
|
31
|
+
def bind_reader(reader)
|
|
32
|
+
@reader = reader
|
|
33
|
+
@snapshot = nil
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def warmup!
|
|
38
|
+
validate_publication!
|
|
39
|
+
reader.with_pinned_generation { snapshot_for_reader }
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def retrieve(query, budget: @default_budget, **options)
|
|
44
|
+
validate_publication!
|
|
45
|
+
reader.with_pinned_generation do
|
|
46
|
+
retriever = snapshot_for_reader
|
|
47
|
+
options[:evidence_generation] = reader.loaded_generation if options[:evidence] && options[:evidence] != 'full'
|
|
48
|
+
result = retriever.retrieve(query, budget: budget, **options)
|
|
49
|
+
result.sources.each { |source| source[:generation] = reader.loaded_generation }
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
rescue Retriever::StoreError
|
|
53
|
+
raise
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise if e.is_a?(Woods::InvalidQueryError) || e.is_a?(ArgumentError)
|
|
56
|
+
|
|
57
|
+
raise Retriever::StoreError, "lexical index read failed: #{e.class}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Explicit invalidation is used by reload after its candidate has been
|
|
61
|
+
# validated. Ordinary generation changes rebuild lazily under the pin.
|
|
62
|
+
def invalidate_snapshot!
|
|
63
|
+
@snapshot_mutex.synchronize { @snapshot = nil }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def install_snapshot!(snapshot)
|
|
67
|
+
@snapshot_mutex.synchronize { @snapshot = snapshot }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def validate_publication!
|
|
73
|
+
path = @index_dir.join(Generation::FILENAME)
|
|
74
|
+
unless path.exist?
|
|
75
|
+
raise IOError, 'published generation marker disappeared' if reader.loaded_generation
|
|
76
|
+
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
data = JSON.parse(AtomicFile.read(path))
|
|
80
|
+
unless data.is_a?(Hash) && data['number'].is_a?(Integer) && data['number'].positive?
|
|
81
|
+
raise IOError, 'invalid published generation marker'
|
|
82
|
+
end
|
|
83
|
+
return if data['payload'].nil?
|
|
84
|
+
|
|
85
|
+
payload = data['payload']
|
|
86
|
+
unless payload.is_a?(String) && !payload.empty? && !Pathname.new(payload).absolute?
|
|
87
|
+
raise IOError, 'invalid generation payload pointer'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
directory = @index_dir.join(payload)
|
|
91
|
+
return if directory.directory? && directory.realpath.to_s.start_with?("#{@index_dir.realpath}/")
|
|
92
|
+
|
|
93
|
+
raise IOError, 'missing or escaping generation payload'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def snapshot_for_reader
|
|
97
|
+
@snapshot_mutex.synchronize do
|
|
98
|
+
key = reader.generation_identity
|
|
99
|
+
# Flat legacy indexes have no immutable generation identity. Rebuild
|
|
100
|
+
# each time instead of serving changed files from a permanent cache.
|
|
101
|
+
return @snapshot.last if key.first && @snapshot&.first == key
|
|
102
|
+
|
|
103
|
+
metadata = Storage::MetadataStore::InMemory.new
|
|
104
|
+
reader.each_unit do |unit|
|
|
105
|
+
metadata.store(StorageIdentity.key(unit.fetch('identifier'), unit.fetch('type')), unit)
|
|
106
|
+
end
|
|
107
|
+
candidate = Retriever.new(vector_store: nil, metadata_store: metadata, graph_store: nil,
|
|
108
|
+
embedding_provider: nil, mode: :lexical)
|
|
109
|
+
@snapshot = [key.freeze, candidate].freeze
|
|
110
|
+
candidate
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -60,7 +60,7 @@ module Woods
|
|
|
60
60
|
lines = []
|
|
61
61
|
lines << "## Search: \"#{query}\""
|
|
62
62
|
lines << ''
|
|
63
|
-
lines << "#{count} result#{'s' unless count == 1}
|
|
63
|
+
lines << "#{count} result#{'s' unless count == 1} returned."
|
|
64
64
|
lines << ''
|
|
65
65
|
|
|
66
66
|
results.each do |r|
|
|
@@ -80,6 +80,8 @@ module Woods
|
|
|
80
80
|
lines << "**note:** #{note}" if note
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
lines.concat(search_completeness_lines(data))
|
|
84
|
+
|
|
83
85
|
lines.join("\n").rstrip
|
|
84
86
|
end
|
|
85
87
|
|
|
@@ -150,11 +152,11 @@ module Woods
|
|
|
150
152
|
- **units_indexed** (manifest.json, `structure` tool) — total
|
|
151
153
|
ExtractedUnits written by the extractor. Canonical count.
|
|
152
154
|
- **graph_nodes** (`pagerank`, `dependencies`, `dependents`) —
|
|
153
|
-
units present in the dependency graph
|
|
154
|
-
|
|
155
|
+
units present in the dependency graph, including isolated units
|
|
156
|
+
with no incoming or outgoing edges.
|
|
155
157
|
- **searchable_entries** (`codebase_retrieve`) — retriever-store
|
|
156
|
-
entries
|
|
157
|
-
|
|
158
|
+
entries. Semantic mode may include per-chunk rows; lexical mode
|
|
159
|
+
ranks published units. Coverage depends on the configured store.
|
|
158
160
|
GLOSSARY
|
|
159
161
|
end
|
|
160
162
|
|
|
@@ -175,7 +177,7 @@ module Woods
|
|
|
175
177
|
|
|
176
178
|
GRAPH_ANALYSIS_SECTIONS.each do |section|
|
|
177
179
|
items = fetch_key(data, section)
|
|
178
|
-
next unless items.is_a?(Array) && items.any?
|
|
180
|
+
next unless items.is_a?(Array) && (items.any? || fetch_key(data, "#{section}_total"))
|
|
179
181
|
|
|
180
182
|
lines << "### #{section.tr('_', ' ').capitalize}"
|
|
181
183
|
lines << ''
|
|
@@ -192,11 +194,11 @@ module Woods
|
|
|
192
194
|
end
|
|
193
195
|
|
|
194
196
|
total_key = "#{section}_total"
|
|
195
|
-
if data
|
|
197
|
+
if fetch_key(data, total_key)
|
|
196
198
|
lines << ''
|
|
197
199
|
offset = fetch_key(data, "#{section}_offset", 0)
|
|
198
200
|
position = offset.positive? ? " from offset #{offset}" : ''
|
|
199
|
-
lines << "_Showing #{items.size} of #{data
|
|
201
|
+
lines << "_Showing #{items.size} of #{fetch_key(data, total_key)}#{position} (truncated)_"
|
|
200
202
|
end
|
|
201
203
|
lines << ''
|
|
202
204
|
end
|
|
@@ -415,6 +417,8 @@ module Woods
|
|
|
415
417
|
return lines.join("\n").rstrip
|
|
416
418
|
end
|
|
417
419
|
|
|
420
|
+
lines.concat(traversal_coverage_lines(data))
|
|
421
|
+
|
|
418
422
|
nodes.each do |id, info|
|
|
419
423
|
depth = fetch_key(info, :depth) || 0
|
|
420
424
|
deps = fetch_key(info, :deps, [])
|
|
@@ -431,7 +435,16 @@ module Woods
|
|
|
431
435
|
deps.each { |d| lines << "#{indent} - #{d}" }
|
|
432
436
|
end
|
|
433
437
|
|
|
434
|
-
lines
|
|
438
|
+
lines.concat(TraversalEvidenceText.lines(fetch_key(data, :explanation)))
|
|
439
|
+
|
|
440
|
+
if fetch_key(data, :partial)
|
|
441
|
+
lines << "Partial traversal (#{fetch_key(data, :partial_reason)}); narrow depth/types/via or increase max_nodes/max_edges."
|
|
442
|
+
end
|
|
443
|
+
if (note = traversal_lower_bound_note(data, nodes.size))
|
|
444
|
+
lines << '' << "_#{note}_"
|
|
445
|
+
elsif fetch_key(data, :nodes_total)
|
|
446
|
+
lines << '' << truncation_note(data, nodes.size)
|
|
447
|
+
end
|
|
435
448
|
|
|
436
449
|
lines.join("\n").rstrip
|
|
437
450
|
end
|
|
@@ -63,7 +63,7 @@ module Woods
|
|
|
63
63
|
results = fetch_key(data, :results, [])
|
|
64
64
|
|
|
65
65
|
lines = []
|
|
66
|
-
lines << "Search: \"#{query}\" (#{count}
|
|
66
|
+
lines << "Search: \"#{query}\" (#{count} result#{'s' unless count == 1} returned)"
|
|
67
67
|
lines << DIVIDER
|
|
68
68
|
|
|
69
69
|
results.each do |r|
|
|
@@ -79,6 +79,7 @@ module Woods
|
|
|
79
79
|
partial = fetch_key(data, :partial, false)
|
|
80
80
|
lines << 'partial: true' if partial
|
|
81
81
|
lines << "note: #{note}" if note
|
|
82
|
+
lines.concat(search_completeness_lines(data))
|
|
82
83
|
|
|
83
84
|
lines.join("\n").rstrip
|
|
84
85
|
end
|
|
@@ -125,9 +126,9 @@ module Woods
|
|
|
125
126
|
lines << 'Denominators:'
|
|
126
127
|
lines << ' units_indexed (manifest, structure): total ExtractedUnits written.'
|
|
127
128
|
lines << ' graph_nodes (pagerank, dependencies, dependents): units in the graph'
|
|
128
|
-
lines << ' (
|
|
129
|
-
lines << ' searchable_entries (codebase_retrieve): retriever-store entries
|
|
130
|
-
lines << ' per-chunk rows.
|
|
129
|
+
lines << ' (includes isolated units with no incoming/outgoing edges).'
|
|
130
|
+
lines << ' searchable_entries (codebase_retrieve): retriever-store entries; semantic mode may include'
|
|
131
|
+
lines << ' per-chunk rows; lexical mode ranks published units. Store coverage varies.'
|
|
131
132
|
|
|
132
133
|
lines.join("\n").rstrip
|
|
133
134
|
end
|
|
@@ -145,7 +146,7 @@ module Woods
|
|
|
145
146
|
|
|
146
147
|
GRAPH_ANALYSIS_SECTIONS.each do |section|
|
|
147
148
|
items = fetch_key(data, section)
|
|
148
|
-
next unless items.is_a?(Array) && items.any?
|
|
149
|
+
next unless items.is_a?(Array) && (items.any? || fetch_key(data, "#{section}_total"))
|
|
149
150
|
|
|
150
151
|
lines << "#{section.tr('_', ' ').upcase}:"
|
|
151
152
|
items.each do |item|
|
|
@@ -160,9 +161,9 @@ module Woods
|
|
|
160
161
|
|
|
161
162
|
total_key = "#{section}_total"
|
|
162
163
|
offset = fetch_key(data, "#{section}_offset", 0)
|
|
163
|
-
if data
|
|
164
|
+
if fetch_key(data, total_key)
|
|
164
165
|
position = offset.positive? ? " from offset #{offset}" : ''
|
|
165
|
-
lines << " (showing #{items.size} of #{data
|
|
166
|
+
lines << " (showing #{items.size} of #{fetch_key(data, total_key)}#{position}; truncated)"
|
|
166
167
|
end
|
|
167
168
|
lines << ''
|
|
168
169
|
end
|
|
@@ -278,6 +279,8 @@ module Woods
|
|
|
278
279
|
return lines.join("\n").rstrip
|
|
279
280
|
end
|
|
280
281
|
|
|
282
|
+
lines.concat(traversal_coverage_lines(data))
|
|
283
|
+
|
|
281
284
|
nodes.each do |id, info|
|
|
282
285
|
depth = fetch_key(info, :depth) || 0
|
|
283
286
|
deps = fetch_key(info, :deps, [])
|
|
@@ -288,7 +291,14 @@ module Woods
|
|
|
288
291
|
deps.each { |d| lines << "#{indent} -> #{d}" }
|
|
289
292
|
end
|
|
290
293
|
|
|
291
|
-
|
|
294
|
+
lines.concat(TraversalEvidenceText.lines(fetch_key(data, :explanation)))
|
|
295
|
+
|
|
296
|
+
if fetch_key(data, :partial)
|
|
297
|
+
lines << "Partial traversal (#{fetch_key(data, :partial_reason)}); narrow depth/types/via or increase max_nodes/max_edges."
|
|
298
|
+
end
|
|
299
|
+
if (note = traversal_lower_bound_note(data, nodes.size))
|
|
300
|
+
lines << note
|
|
301
|
+
elsif fetch_key(data, :nodes_total)
|
|
292
302
|
offset = fetch_key(data, :nodes_offset, 0)
|
|
293
303
|
position = offset.positive? ? " from offset #{offset}" : ''
|
|
294
304
|
lines << " (showing #{nodes.size} of #{fetch_key(data, :nodes_total)}#{position}; truncated)"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module MCP
|
|
7
|
+
# Accumulates a bounded search prefix and evidence about its completeness.
|
|
8
|
+
# A full page is not evidence of truncation: only an additional distinct
|
|
9
|
+
# typed match establishes that more results exist. Budget/timeout cutoffs
|
|
10
|
+
# leave both the total and the existence of another match unknown.
|
|
11
|
+
class SearchResults
|
|
12
|
+
NARROWING_HINT = 'Narrow types, literal exact_prefix/exact_suffix filters, or the requested deep fields.'
|
|
13
|
+
|
|
14
|
+
def self.unavailable
|
|
15
|
+
{ status: 'unknown', reason: 'unreadable_or_corrupt_source', has_more: nil,
|
|
16
|
+
total_matches: nil, matched_lower_bound: nil }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(limit:)
|
|
20
|
+
@limit = limit
|
|
21
|
+
@matches = []
|
|
22
|
+
@seen = Set.new
|
|
23
|
+
@reason = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Admit at most one lookahead match beyond the returned page.
|
|
27
|
+
# @return [self]
|
|
28
|
+
def add(identifier:, type:, match_field:)
|
|
29
|
+
return self if result_limit_reached?
|
|
30
|
+
return self unless @seen.add?([type, identifier])
|
|
31
|
+
|
|
32
|
+
@matches << { identifier: identifier, type: type, match_field: match_field }
|
|
33
|
+
@reason = 'result_limit' if @matches.size > @limit
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def result_limit_reached?
|
|
38
|
+
@reason == 'result_limit'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Stop because the remaining search domain could not be examined.
|
|
42
|
+
def stop(reason)
|
|
43
|
+
@reason = reason
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The caller must establish exhaustion rather than infer it from page size.
|
|
47
|
+
def finish
|
|
48
|
+
@reason ||= 'exhausted'
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param note [String, nil] existing advisory/timeout guidance
|
|
53
|
+
# @return [Hash] returned rows plus additive completeness metadata
|
|
54
|
+
def response(note: nil)
|
|
55
|
+
raise ArgumentError, 'Search has not finished' unless @reason
|
|
56
|
+
|
|
57
|
+
complete = @reason == 'exhausted'
|
|
58
|
+
result = {
|
|
59
|
+
results: @matches.first(@limit),
|
|
60
|
+
completeness: {
|
|
61
|
+
status: complete ? 'complete' : 'partial', reason: @reason,
|
|
62
|
+
has_more: complete ? false : (true if result_limit_reached?),
|
|
63
|
+
total_matches: complete ? @matches.size : nil,
|
|
64
|
+
matched_lower_bound: @matches.size
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
result[:partial] = true unless complete
|
|
68
|
+
result[:hint] = NARROWING_HINT unless complete
|
|
69
|
+
result[:note] = note unless note.nil? || note.empty?
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|