woods 2.0.0.beta1 → 2.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +400 -1
- data/CONTRIBUTING.md +224 -9
- data/README.md +7 -3
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +83 -4
- data/docs/AGENT_SETUP.md +82 -1
- data/docs/BACKEND_MATRIX.md +20 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +233 -13
- data/docs/CONSOLE_MCP_SETUP.md +35 -5
- data/docs/DOCKER_SETUP.md +21 -2
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +36 -5
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +158 -2
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +15 -7
- data/docs/MCP_SERVERS.md +221 -5
- data/docs/MCP_TOOL_COOKBOOK.md +33 -18
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +71 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +253 -11
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +117 -5
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +44 -22
- data/docs/WATCH_DAEMON.md +259 -59
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +133 -0
- data/lib/woods/agent_configuration/cli.rb +101 -0
- data/lib/woods/agent_configuration/cli_options.rb +29 -0
- data/lib/woods/agent_configuration/document.rb +105 -0
- data/lib/woods/agent_configuration/error.rb +7 -0
- data/lib/woods/agent_configuration/launcher.rb +75 -0
- data/lib/woods/agent_configuration/layout.rb +59 -0
- data/lib/woods/agent_configuration/managed_section.rb +62 -0
- data/lib/woods/agent_configuration/plan.rb +98 -0
- data/lib/woods/agent_configuration/plan_diff.rb +38 -0
- data/lib/woods/agent_configuration/planned_files.rb +61 -0
- data/lib/woods/agent_configuration/planner.rb +63 -0
- data/lib/woods/agent_configuration/planner_validation.rb +77 -0
- data/lib/woods/agent_configuration/preflight.rb +100 -0
- data/lib/woods/agent_configuration/recovery.rb +49 -0
- data/lib/woods/ast/node.rb +2 -0
- data/lib/woods/ast/parser.rb +38 -5
- data/lib/woods/atomic_file.rb +133 -3
- data/lib/woods/builder.rb +21 -5
- data/lib/woods/cache/cache_middleware.rb +28 -7
- data/lib/woods/cache/cache_store.rb +4 -5
- data/lib/woods/change_set.rb +5 -4
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +14 -14
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/embedded_executor.rb +1 -1
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +22 -13
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +90 -46
- data/lib/woods/embedding/openai.rb +17 -6
- data/lib/woods/evaluation/ablation_executor.rb +6 -1
- data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
- data/lib/woods/export/typed_reader.rb +56 -0
- data/lib/woods/extractor.rb +557 -228
- data/lib/woods/extractors/action_cable_extractor.rb +3 -1
- data/lib/woods/extractors/behavioral_profile.rb +9 -7
- data/lib/woods/extractors/caching_extractor.rb +3 -1
- data/lib/woods/extractors/concern_extractor.rb +64 -6
- data/lib/woods/extractors/configuration_extractor.rb +7 -3
- data/lib/woods/extractors/controller_extractor.rb +13 -4
- data/lib/woods/extractors/database_view_extractor.rb +3 -1
- data/lib/woods/extractors/decorator_extractor.rb +3 -1
- data/lib/woods/extractors/engine_extractor.rb +3 -1
- data/lib/woods/extractors/event_extractor.rb +4 -2
- data/lib/woods/extractors/factory_extractor.rb +3 -1
- data/lib/woods/extractors/graphql_extractor.rb +8 -2
- data/lib/woods/extractors/i18n_extractor.rb +3 -1
- data/lib/woods/extractors/job_extractor.rb +6 -19
- data/lib/woods/extractors/lib_extractor.rb +3 -1
- data/lib/woods/extractors/mailer_extractor.rb +20 -5
- data/lib/woods/extractors/manager_extractor.rb +3 -1
- data/lib/woods/extractors/method_parameters.rb +53 -0
- data/lib/woods/extractors/middleware_argument.rb +65 -0
- data/lib/woods/extractors/middleware_extractor.rb +9 -3
- data/lib/woods/extractors/migration_extractor.rb +3 -1
- data/lib/woods/extractors/model_extractor.rb +39 -33
- data/lib/woods/extractors/package_extractor.rb +24 -4
- data/lib/woods/extractors/phlex_extractor.rb +3 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -1
- data/lib/woods/extractors/poro_extractor.rb +3 -1
- data/lib/woods/extractors/pundit_extractor.rb +3 -1
- data/lib/woods/extractors/rails_source_extractor.rb +4 -2
- data/lib/woods/extractors/rake_task_extractor.rb +4 -2
- data/lib/woods/extractors/route_extractor.rb +3 -1
- data/lib/woods/extractors/route_helper_resolver.rb +10 -33
- data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
- data/lib/woods/extractors/serializer_extractor.rb +4 -2
- data/lib/woods/extractors/service_extractor.rb +3 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
- data/lib/woods/extractors/shared_utility_methods.rb +27 -15
- data/lib/woods/extractors/source_nesting.rb +1 -1
- data/lib/woods/extractors/state_machine_extractor.rb +3 -1
- data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
- data/lib/woods/extractors/validator_extractor.rb +3 -1
- data/lib/woods/extractors/view_component_extractor.rb +3 -1
- data/lib/woods/extractors/view_template_extractor.rb +3 -1
- data/lib/woods/flow_assembler.rb +87 -8
- data/lib/woods/flow_precomputer.rb +44 -7
- data/lib/woods/gem_mapper.rb +2 -0
- data/lib/woods/git_history.rb +116 -0
- data/lib/woods/graph_analyzer.rb +195 -63
- data/lib/woods/hooks/context_cli.rb +54 -0
- data/lib/woods/hooks/context_event.rb +88 -0
- data/lib/woods/hooks/context_hint.rb +73 -0
- data/lib/woods/hooks/context_impact.rb +77 -0
- data/lib/woods/hooks/context_output.rb +47 -0
- data/lib/woods/hooks/context_state.rb +102 -0
- data/lib/woods/hooks/refresh.rb +79 -0
- data/lib/woods/hooks/rule_projection.rb +78 -0
- data/lib/woods/input_rules.rb +19 -0
- data/lib/woods/mcp/bearer_auth.rb +20 -12
- data/lib/woods/mcp/bootstrapper.rb +62 -0
- data/lib/woods/mcp/index_reader.rb +323 -160
- data/lib/woods/mcp/initialization_guidance.rb +27 -0
- data/lib/woods/mcp/origin_guard.rb +17 -9
- data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +8 -1
- data/lib/woods/mcp/renderers/plain_renderer.rb +7 -1
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +158 -37
- data/lib/woods/mcp/tool_contract.rb +2 -0
- data/lib/woods/mcp/tool_response_renderer.rb +25 -0
- data/lib/woods/mcp/traversal_evidence.rb +113 -0
- data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
- data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
- data/lib/woods/notion/exporter.rb +56 -17
- data/lib/woods/obsidian/destination_plan.rb +98 -0
- data/lib/woods/obsidian/name_mapper.rb +19 -3
- data/lib/woods/obsidian/note_builder.rb +19 -10
- data/lib/woods/obsidian/vault_exporter.rb +88 -32
- data/lib/woods/operator/pipeline_guard.rb +18 -13
- data/lib/woods/path_dispatcher.rb +7 -1
- data/lib/woods/payload_store.rb +29 -15
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +392 -0
- data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
- data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
- data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
- data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
- data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
- data/lib/woods/resilience/index_validator.rb +112 -23
- data/lib/woods/retrieval/context_assembler.rb +50 -15
- data/lib/woods/retrieval/lexical_assembler.rb +73 -0
- data/lib/woods/retrieval/lexical_index.rb +119 -0
- data/lib/woods/retrieval/ranker.rb +4 -2
- data/lib/woods/retrieval/scope.rb +108 -0
- data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
- data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
- data/lib/woods/retrieval/search_executor.rb +86 -27
- data/lib/woods/retrieval/source_evidence.rb +200 -0
- data/lib/woods/retriever.rb +98 -22
- data/lib/woods/ruby_analyzer/trace_enricher.rb +80 -38
- data/lib/woods/session_tracer/middleware.rb +10 -12
- data/lib/woods/session_tracer/redis_store.rb +22 -6
- data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
- data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
- data/lib/woods/session_tracer/unit_resolver.rb +63 -0
- data/lib/woods/source_inputs/consumer_errors.rb +27 -0
- data/lib/woods/source_inputs/handoff.rb +102 -0
- data/lib/woods/source_inputs/launcher.rb +157 -0
- data/lib/woods/source_inputs/manifest.rb +124 -0
- data/lib/woods/source_inputs/private_key.rb +55 -0
- data/lib/woods/source_inputs/scanner.rb +171 -0
- data/lib/woods/source_inputs/scopes.rb +71 -0
- data/lib/woods/source_inputs/session.rb +214 -0
- data/lib/woods/source_inputs/status.rb +84 -0
- data/lib/woods/source_inputs/verifier.rb +107 -0
- data/lib/woods/storage/metadata_store.rb +25 -25
- data/lib/woods/storage/pgvector.rb +29 -8
- data/lib/woods/storage/qdrant.rb +17 -7
- data/lib/woods/storage/vector_store.rb +18 -6
- data/lib/woods/tasks.rb +3 -2
- data/lib/woods/temporal/json_snapshot_store.rb +29 -8
- data/lib/woods/unblocked/exporter.rb +59 -70
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/boot_snapshot.rb +52 -0
- data/lib/woods/watch/daemon.rb +136 -28
- data/lib/woods/watch/listen_watcher.rb +4 -0
- data/lib/woods/watch/polling_watcher.rb +5 -1
- data/lib/woods/watch/status.rb +20 -15
- data/lib/woods/watch/tree_scan.rb +21 -13
- data/lib/woods/watch/watcher.rb +4 -1
- data/lib/woods.rb +135 -11
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.jq +15 -0
- data/plugin/hooks/adapters/normalize.rb +63 -0
- data/plugin/hooks/hooks.json +20 -0
- data/plugin/hooks/woods-context.sh +50 -0
- data/plugin/hooks/woods-input-rules.sh +159 -0
- data/plugin/hooks/woods-opencode.mjs +65 -0
- data/plugin/hooks/woods-post-edit.sh +2 -225
- data/plugin/hooks/woods-refresh.sh +260 -0
- data/plugin/hooks/woods-session-start.sh +47 -55
- data/plugin/skills/woods-agent-enable/SKILL.md +13 -0
- data/plugin/skills/woods-diagnose/SKILL.md +288 -1
- data/plugin/skills/woods-investigate/SKILL.md +106 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +89 -1
- data/plugin/skills/woods-setup/SKILL.md +107 -6
- metadata +84 -5
|
@@ -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
|
|
|
@@ -431,6 +433,11 @@ module Woods
|
|
|
431
433
|
deps.each { |d| lines << "#{indent} - #{d}" }
|
|
432
434
|
end
|
|
433
435
|
|
|
436
|
+
lines.concat(TraversalEvidenceText.lines(fetch_key(data, :explanation)))
|
|
437
|
+
|
|
438
|
+
if fetch_key(data, :partial)
|
|
439
|
+
lines << "Partial traversal (#{fetch_key(data, :partial_reason)}); narrow depth/types/via or increase max_nodes/max_edges."
|
|
440
|
+
end
|
|
434
441
|
lines << '' << truncation_note(data, nodes.size) if fetch_key(data, :nodes_total)
|
|
435
442
|
|
|
436
443
|
lines.join("\n").rstrip
|
|
@@ -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
|
|
@@ -288,6 +289,11 @@ module Woods
|
|
|
288
289
|
deps.each { |d| lines << "#{indent} -> #{d}" }
|
|
289
290
|
end
|
|
290
291
|
|
|
292
|
+
lines.concat(TraversalEvidenceText.lines(fetch_key(data, :explanation)))
|
|
293
|
+
|
|
294
|
+
if fetch_key(data, :partial)
|
|
295
|
+
lines << "Partial traversal (#{fetch_key(data, :partial_reason)}); narrow depth/types/via or increase max_nodes/max_edges."
|
|
296
|
+
end
|
|
291
297
|
if fetch_key(data, :nodes_total)
|
|
292
298
|
offset = fetch_key(data, :nodes_offset, 0)
|
|
293
299
|
position = offset.positive? ? " from offset #{offset}" : ''
|
|
@@ -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
|