woods 1.6.1 → 2.0.0.beta2
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 +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
data/lib/woods/console/bridge.rb
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require_relative 'bridge_protocol'
|
|
5
|
-
require_relative 'model_validator'
|
|
6
|
-
require_relative 'safe_context'
|
|
7
|
-
|
|
8
|
-
module Woods
|
|
9
|
-
module Console
|
|
10
|
-
# **PROTOCOL SCAFFOLD — does not execute real queries.** Every handler
|
|
11
|
-
# below returns static empty data (`{ 'count' => 0 }`, `{ 'records' =>
|
|
12
|
-
# [] }`, etc.). Real in-process execution lives in
|
|
13
|
-
# {Woods::Console::EmbeddedExecutor}; the eventual real bridge process
|
|
14
|
-
# for Option D will replace this scaffold with a class that performs
|
|
15
|
-
# actual ActiveRecord queries.
|
|
16
|
-
#
|
|
17
|
-
# The scaffold pins the JSON-lines wire protocol — request envelope,
|
|
18
|
-
# response envelope, supported-tools list, error shape — so other
|
|
19
|
-
# components (EmbeddedExecutor, ConnectionManager, Server) can be
|
|
20
|
-
# built and tested against a stable contract before the real
|
|
21
|
-
# bridge-process implementation lands. Treat this class the way you'd
|
|
22
|
-
# treat a Sinatra fake of a third-party API in tests: it satisfies
|
|
23
|
-
# the protocol, nothing more.
|
|
24
|
-
#
|
|
25
|
-
# ## Why the name carries "Stub"
|
|
26
|
-
#
|
|
27
|
-
# Round-1 audit Track H-4 flagged this class as a "critical SafeContext
|
|
28
|
-
# bypass" because `handle_request` doesn't wrap calls in `SafeContext`.
|
|
29
|
-
# That finding wasn't exploitable — no live code path executes through
|
|
30
|
-
# this class in the shipped gem — but the bare name `Bridge` made the
|
|
31
|
-
# scaffold status invisible to auditors. The `Stub` prefix removes the
|
|
32
|
-
# ambiguity. When the real bridge-process implementation is delivered,
|
|
33
|
-
# it should claim the `Bridge` name; this class will either be deleted
|
|
34
|
-
# (if the protocol is fully owned by the real bridge) or renamed to
|
|
35
|
-
# `BridgeProtocol` and reduced to a constants module.
|
|
36
|
-
#
|
|
37
|
-
# ## Protocol
|
|
38
|
-
#
|
|
39
|
-
# Reads JSON-lines requests from an input IO, validates model/column names,
|
|
40
|
-
# dispatches to (stub) tool handlers, and writes JSON-lines responses to
|
|
41
|
-
# an output IO.
|
|
42
|
-
#
|
|
43
|
-
# Protocol:
|
|
44
|
-
# Request: {"id":"req_1","tool":"count","params":{"model":"Order","scope":{"status":"pending"}}}
|
|
45
|
-
# Response: {"id":"req_1","ok":true,"result":{"count":1847},"timing_ms":12.3}
|
|
46
|
-
# Error: {"id":"req_1","ok":false,"error":"Model not found","error_type":"validation"}
|
|
47
|
-
#
|
|
48
|
-
# @example Wiring against a fake input/output (testing only — handlers return empty data)
|
|
49
|
-
# bridge = StubBridge.new(input: $stdin, output: $stdout,
|
|
50
|
-
# model_validator: validator, safe_context: ctx)
|
|
51
|
-
# bridge.run
|
|
52
|
-
#
|
|
53
|
-
class StubBridge
|
|
54
|
-
# Protocol constants live on {BridgeProtocol} so the real executor
|
|
55
|
-
# (EmbeddedExecutor) and a future real bridge-process class can
|
|
56
|
-
# reference them without importing the scaffold. These top-level
|
|
57
|
-
# aliases keep `StubBridge::SUPPORTED_TOOLS` working for existing
|
|
58
|
-
# callers and specs.
|
|
59
|
-
SUPPORTED_TOOLS = BridgeProtocol::SUPPORTED_TOOLS
|
|
60
|
-
TIER1_TOOLS = BridgeProtocol::TIER1_TOOLS
|
|
61
|
-
TOOL_HANDLERS = BridgeProtocol::TOOL_HANDLERS
|
|
62
|
-
|
|
63
|
-
# @param input [IO] Input stream (reads JSON-lines)
|
|
64
|
-
# @param output [IO] Output stream (writes JSON-lines)
|
|
65
|
-
# @param model_validator [ModelValidator] Validates model/column names
|
|
66
|
-
# @param safe_context [SafeContext] Wraps execution in safe transaction
|
|
67
|
-
def initialize(input:, output:, model_validator:, safe_context:)
|
|
68
|
-
@input = input
|
|
69
|
-
@output = output
|
|
70
|
-
@model_validator = model_validator
|
|
71
|
-
@safe_context = safe_context
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Read loop — processes requests until input is closed.
|
|
75
|
-
#
|
|
76
|
-
# @return [void]
|
|
77
|
-
def run
|
|
78
|
-
@input.each_line do |line|
|
|
79
|
-
line = line.strip
|
|
80
|
-
next if line.empty?
|
|
81
|
-
|
|
82
|
-
request = parse_request(line)
|
|
83
|
-
next unless request
|
|
84
|
-
|
|
85
|
-
response = handle_request(request)
|
|
86
|
-
write_response(response)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Process a single request hash and return a response hash.
|
|
91
|
-
#
|
|
92
|
-
# @param request [Hash] Parsed request with "id", "tool", "params"
|
|
93
|
-
# @return [Hash] Response with "id", "ok", and "result" or "error"
|
|
94
|
-
def handle_request(request)
|
|
95
|
-
id = request['id']
|
|
96
|
-
tool = request['tool']
|
|
97
|
-
params = request['params'] || {}
|
|
98
|
-
|
|
99
|
-
return error_response(id, "Unknown tool: #{tool}", 'unknown_tool') unless SUPPORTED_TOOLS.include?(tool)
|
|
100
|
-
|
|
101
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
102
|
-
result = dispatch(tool, params)
|
|
103
|
-
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)
|
|
104
|
-
|
|
105
|
-
{ 'id' => id, 'ok' => true, 'result' => result, 'timing_ms' => elapsed }
|
|
106
|
-
rescue ValidationError => e
|
|
107
|
-
error_response(id, e.message, 'validation')
|
|
108
|
-
rescue StandardError => e
|
|
109
|
-
error_response(id, e.message, 'execution')
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
private
|
|
113
|
-
|
|
114
|
-
# Parse a JSON line into a request hash.
|
|
115
|
-
#
|
|
116
|
-
# @param line [String] Raw JSON line
|
|
117
|
-
# @return [Hash, nil] Parsed request or nil on parse error
|
|
118
|
-
def parse_request(line)
|
|
119
|
-
JSON.parse(line)
|
|
120
|
-
rescue JSON::ParserError => e
|
|
121
|
-
write_response(error_response(nil, "Invalid JSON: #{e.message}", 'parse'))
|
|
122
|
-
nil
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Dispatch a tool request to the appropriate handler.
|
|
126
|
-
#
|
|
127
|
-
# @param tool [String] Tool name
|
|
128
|
-
# @param params [Hash] Tool parameters
|
|
129
|
-
# @return [Hash] Tool result
|
|
130
|
-
def dispatch(tool, params)
|
|
131
|
-
case tool
|
|
132
|
-
when 'status'
|
|
133
|
-
handle_status
|
|
134
|
-
when 'schema'
|
|
135
|
-
handle_schema(params)
|
|
136
|
-
else
|
|
137
|
-
validate_model_param(params)
|
|
138
|
-
handler = TOOL_HANDLERS.fetch(tool) { raise ValidationError, "Unknown tool: #{tool}" }
|
|
139
|
-
send(handler, params)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Validate that the model parameter is present and known.
|
|
144
|
-
def validate_model_param(params)
|
|
145
|
-
model = params['model']
|
|
146
|
-
raise ValidationError, 'Missing required parameter: model' unless model
|
|
147
|
-
|
|
148
|
-
@model_validator.validate_model!(model)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Stub handlers below return empty/zero data by design — see the
|
|
152
|
-
# class-level docstring. Real in-process execution happens in
|
|
153
|
-
# EmbeddedExecutor; the eventual Option-D bridge process will replace
|
|
154
|
-
# this class entirely.
|
|
155
|
-
|
|
156
|
-
def handle_count(_params)
|
|
157
|
-
{ 'count' => 0 }
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def handle_sample(_params)
|
|
161
|
-
{ 'records' => [] }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def handle_find(_params)
|
|
165
|
-
{ 'record' => nil }
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def handle_pluck(params)
|
|
169
|
-
@model_validator.validate_columns!(params['model'], params['columns']) if params['columns']
|
|
170
|
-
{ 'columns' => Array(params['columns']), 'values' => [] }
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def handle_aggregate(params)
|
|
174
|
-
@model_validator.validate_column!(params['model'], params['column']) if params['column']
|
|
175
|
-
{ 'value' => nil }
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def handle_association_count(_params)
|
|
179
|
-
{ 'count' => 0 }
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def handle_schema(params)
|
|
183
|
-
model = params['model']
|
|
184
|
-
raise ValidationError, 'Missing required parameter: model' unless model
|
|
185
|
-
|
|
186
|
-
@model_validator.validate_model!(model)
|
|
187
|
-
{ 'columns' => @model_validator.columns_for(model), 'indexes' => [] }
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def handle_recent(_params)
|
|
191
|
-
{ 'records' => [] }
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def handle_status
|
|
195
|
-
{ 'status' => 'ok', 'models' => @model_validator.model_names }
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Build an error response hash.
|
|
199
|
-
def error_response(id, message, error_type)
|
|
200
|
-
{ 'id' => id, 'ok' => false, 'error' => message, 'error_type' => error_type }
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Write a JSON-line response to the output stream.
|
|
204
|
-
def write_response(response)
|
|
205
|
-
@output.puts(JSON.generate(response))
|
|
206
|
-
@output.flush
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base'
|
|
4
|
-
|
|
5
|
-
module Woods
|
|
6
|
-
module Formatting
|
|
7
|
-
# Formats assembled context as XML for Claude models.
|
|
8
|
-
#
|
|
9
|
-
# Produces structured XML with:
|
|
10
|
-
# - `<codebase-context>` root element
|
|
11
|
-
# - `<meta>` tag with token usage and budget
|
|
12
|
-
# - `<content>` section with indented context text
|
|
13
|
-
# - `<sources>` section with self-closing `<source>` elements
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# adapter = ClaudeAdapter.new
|
|
17
|
-
# xml = adapter.format(assembled_context)
|
|
18
|
-
# # => "<codebase-context>\n <meta tokens=\"42\" budget=\"8000\" />\n..."
|
|
19
|
-
#
|
|
20
|
-
class ClaudeAdapter < Base
|
|
21
|
-
# Format assembled context as XML for Claude.
|
|
22
|
-
#
|
|
23
|
-
# @param assembled_context [Woods::Retrieval::AssembledContext]
|
|
24
|
-
# @return [String] XML-formatted context
|
|
25
|
-
def format(assembled_context)
|
|
26
|
-
parts = []
|
|
27
|
-
parts << '<codebase-context>'
|
|
28
|
-
parts << " <meta tokens=\"#{assembled_context.tokens_used}\" budget=\"#{assembled_context.budget}\" />"
|
|
29
|
-
parts << format_content(assembled_context.context)
|
|
30
|
-
parts << format_sources(assembled_context.sources)
|
|
31
|
-
parts << '</codebase-context>'
|
|
32
|
-
parts.join("\n")
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
# Format the content section with indentation.
|
|
38
|
-
#
|
|
39
|
-
# @param context [String]
|
|
40
|
-
# @return [String]
|
|
41
|
-
def format_content(context)
|
|
42
|
-
lines = []
|
|
43
|
-
lines << ' <content>'
|
|
44
|
-
lines << indent(escape_xml(context), 4) unless context.empty?
|
|
45
|
-
lines << ' </content>'
|
|
46
|
-
lines.join("\n")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Format the sources section.
|
|
50
|
-
#
|
|
51
|
-
# @param sources [Array<Hash>]
|
|
52
|
-
# @return [String]
|
|
53
|
-
def format_sources(sources)
|
|
54
|
-
lines = []
|
|
55
|
-
lines << ' <sources>'
|
|
56
|
-
sources.each do |source|
|
|
57
|
-
lines << " <source #{source_attributes(source)} />"
|
|
58
|
-
end
|
|
59
|
-
lines << ' </sources>'
|
|
60
|
-
lines.join("\n")
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Build attribute string for a source element.
|
|
64
|
-
#
|
|
65
|
-
# @param source [Hash]
|
|
66
|
-
# @return [String]
|
|
67
|
-
def source_attributes(source)
|
|
68
|
-
[
|
|
69
|
-
"identifier=\"#{escape_xml(source[:identifier].to_s)}\"",
|
|
70
|
-
"type=\"#{escape_xml(source[:type].to_s)}\"",
|
|
71
|
-
"score=\"#{source[:score]}\"",
|
|
72
|
-
"file=\"#{escape_xml(source[:file_path].to_s)}\""
|
|
73
|
-
].join(' ')
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Indent every line of text by the given number of spaces.
|
|
77
|
-
#
|
|
78
|
-
# @param text [String]
|
|
79
|
-
# @param spaces [Integer]
|
|
80
|
-
# @return [String]
|
|
81
|
-
def indent(text, spaces)
|
|
82
|
-
prefix = ' ' * spaces
|
|
83
|
-
text.lines.map { |line| "#{prefix}#{line.chomp}" }.join("\n")
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Escape XML special characters.
|
|
87
|
-
#
|
|
88
|
-
# @param text [String]
|
|
89
|
-
# @return [String]
|
|
90
|
-
def escape_xml(text)
|
|
91
|
-
text.gsub('&', '&')
|
|
92
|
-
.gsub('<', '<')
|
|
93
|
-
.gsub('>', '>')
|
|
94
|
-
.gsub('"', '"')
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base'
|
|
4
|
-
|
|
5
|
-
module Woods
|
|
6
|
-
module Formatting
|
|
7
|
-
# Formats assembled context as plain text for generic LLM consumption.
|
|
8
|
-
#
|
|
9
|
-
# Produces plain text with:
|
|
10
|
-
# - `=== CODEBASE CONTEXT ===` header
|
|
11
|
-
# - Token usage line
|
|
12
|
-
# - Content separated by `---` dividers
|
|
13
|
-
# - Sources in bracket notation
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# adapter = GenericAdapter.new
|
|
17
|
-
# text = adapter.format(assembled_context)
|
|
18
|
-
# # => "=== CODEBASE CONTEXT ===\nTokens: 42 / 8000\n..."
|
|
19
|
-
#
|
|
20
|
-
class GenericAdapter < Base
|
|
21
|
-
# Format assembled context as plain text.
|
|
22
|
-
#
|
|
23
|
-
# @param assembled_context [Woods::Retrieval::AssembledContext]
|
|
24
|
-
# @return [String] Plain text formatted context
|
|
25
|
-
def format(assembled_context)
|
|
26
|
-
parts = []
|
|
27
|
-
parts << '=== CODEBASE CONTEXT ==='
|
|
28
|
-
parts << "Tokens: #{assembled_context.tokens_used} / #{assembled_context.budget}"
|
|
29
|
-
parts << '---'
|
|
30
|
-
parts << assembled_context.context
|
|
31
|
-
parts.concat(format_sources(assembled_context.sources))
|
|
32
|
-
parts.join("\n")
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
# Format sources in bracket notation.
|
|
38
|
-
#
|
|
39
|
-
# @param sources [Array<Hash>]
|
|
40
|
-
# @return [Array<String>] Lines to append
|
|
41
|
-
def format_sources(sources)
|
|
42
|
-
return [] if sources.empty?
|
|
43
|
-
|
|
44
|
-
lines = []
|
|
45
|
-
lines << '---'
|
|
46
|
-
sources.each do |source|
|
|
47
|
-
identifier = source[:identifier]
|
|
48
|
-
type = source[:type]
|
|
49
|
-
score = source[:score]
|
|
50
|
-
lines << "[Source: #{identifier} (#{type}) \u2014 score: #{score}]"
|
|
51
|
-
end
|
|
52
|
-
lines
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base'
|
|
4
|
-
|
|
5
|
-
module Woods
|
|
6
|
-
module Formatting
|
|
7
|
-
# Formats assembled context as Markdown for GPT models.
|
|
8
|
-
#
|
|
9
|
-
# Produces Markdown with:
|
|
10
|
-
# - `## Codebase Context` heading
|
|
11
|
-
# - Token usage in bold
|
|
12
|
-
# - Content in a fenced Ruby code block
|
|
13
|
-
# - Sources as a bullet list
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# adapter = GptAdapter.new
|
|
17
|
-
# md = adapter.format(assembled_context)
|
|
18
|
-
# # => "## Codebase Context\n\n**Tokens:** 42/8000\n..."
|
|
19
|
-
#
|
|
20
|
-
class GptAdapter < Base
|
|
21
|
-
# Format assembled context as Markdown for GPT.
|
|
22
|
-
#
|
|
23
|
-
# @param assembled_context [Woods::Retrieval::AssembledContext]
|
|
24
|
-
# @return [String] Markdown-formatted context
|
|
25
|
-
def format(assembled_context)
|
|
26
|
-
parts = []
|
|
27
|
-
parts << '## Codebase Context'
|
|
28
|
-
parts << ''
|
|
29
|
-
parts << "**Tokens:** #{assembled_context.tokens_used}/#{assembled_context.budget}"
|
|
30
|
-
parts << ''
|
|
31
|
-
parts << '---'
|
|
32
|
-
parts << ''
|
|
33
|
-
parts << '```ruby'
|
|
34
|
-
parts << assembled_context.context
|
|
35
|
-
parts << '```'
|
|
36
|
-
parts.concat(format_sources(assembled_context.sources))
|
|
37
|
-
parts.join("\n")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
# Format sources as a Markdown bullet list.
|
|
43
|
-
#
|
|
44
|
-
# @param sources [Array<Hash>]
|
|
45
|
-
# @return [Array<String>] Lines to append
|
|
46
|
-
def format_sources(sources)
|
|
47
|
-
return [] if sources.empty?
|
|
48
|
-
|
|
49
|
-
lines = []
|
|
50
|
-
lines << ''
|
|
51
|
-
lines << '### Sources'
|
|
52
|
-
lines << ''
|
|
53
|
-
sources.each do |source|
|
|
54
|
-
identifier = source[:identifier]
|
|
55
|
-
type = source[:type]
|
|
56
|
-
score = source[:score]
|
|
57
|
-
file_path = source[:file_path]
|
|
58
|
-
lines << "- **#{identifier}** (#{type}) \u2014 score: #{score}, file: #{file_path}"
|
|
59
|
-
end
|
|
60
|
-
lines
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
data/lib/woods/notion/mapper.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'mappers/shared'
|
|
4
|
-
require_relative 'mappers/model_mapper'
|
|
5
|
-
require_relative 'mappers/column_mapper'
|
|
6
|
-
require_relative 'mappers/migration_mapper'
|
|
7
|
-
|
|
8
|
-
module Woods
|
|
9
|
-
module Notion
|
|
10
|
-
# Dispatcher for Notion mappers. Returns the appropriate mapper for a unit type.
|
|
11
|
-
#
|
|
12
|
-
# @example
|
|
13
|
-
# mapper = Mapper.for("model")
|
|
14
|
-
# properties = mapper.map(unit_data)
|
|
15
|
-
#
|
|
16
|
-
class Mapper
|
|
17
|
-
REGISTRY = {
|
|
18
|
-
'model' => Mappers::ModelMapper,
|
|
19
|
-
'column' => Mappers::ColumnMapper,
|
|
20
|
-
'migration' => Mappers::MigrationMapper
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
|
-
# Get a mapper instance for a unit type.
|
|
24
|
-
#
|
|
25
|
-
# @param type [String] Unit type name (e.g. "model", "column", "migration")
|
|
26
|
-
# @return [Object, nil] Mapper instance or nil if type is not supported
|
|
27
|
-
def self.for(type)
|
|
28
|
-
klass = REGISTRY[type]
|
|
29
|
-
klass&.new
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# List all supported unit types.
|
|
33
|
-
#
|
|
34
|
-
# @return [Array<String>]
|
|
35
|
-
def self.supported_types
|
|
36
|
-
REGISTRY.keys
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Woods
|
|
4
|
-
module Observability
|
|
5
|
-
# Probes configured components and reports overall system health.
|
|
6
|
-
#
|
|
7
|
-
# Checks vector store, metadata store, and embedding provider by calling
|
|
8
|
-
# lightweight operations on each. Components that are nil are reported
|
|
9
|
-
# as :not_configured and do not affect the overall healthy? status.
|
|
10
|
-
#
|
|
11
|
-
# @example
|
|
12
|
-
# check = HealthCheck.new(
|
|
13
|
-
# vector_store: vector_store,
|
|
14
|
-
# metadata_store: metadata_store,
|
|
15
|
-
# embedding_provider: provider
|
|
16
|
-
# )
|
|
17
|
-
# status = check.run
|
|
18
|
-
# status.healthy? # => true
|
|
19
|
-
# status.components # => { vector_store: :ok, metadata_store: :ok, embedding_provider: :ok }
|
|
20
|
-
#
|
|
21
|
-
class HealthCheck
|
|
22
|
-
# Value object representing the result of a health check.
|
|
23
|
-
HealthStatus = Struct.new(:healthy?, :components, keyword_init: true)
|
|
24
|
-
|
|
25
|
-
# @param vector_store [Object, nil] Vector store adapter (must respond to #count)
|
|
26
|
-
# @param metadata_store [Object, nil] Metadata store adapter (must respond to #count)
|
|
27
|
-
# @param embedding_provider [Object, nil] Embedding provider (must respond to #embed)
|
|
28
|
-
def initialize(vector_store: nil, metadata_store: nil, embedding_provider: nil)
|
|
29
|
-
@vector_store = vector_store
|
|
30
|
-
@metadata_store = metadata_store
|
|
31
|
-
@embedding_provider = embedding_provider
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Run health probes on all configured components.
|
|
35
|
-
#
|
|
36
|
-
# @return [HealthStatus] Result with healthy? flag and per-component status
|
|
37
|
-
def run
|
|
38
|
-
components = {
|
|
39
|
-
vector_store: probe_store(@vector_store),
|
|
40
|
-
metadata_store: probe_store(@metadata_store),
|
|
41
|
-
embedding_provider: probe_provider(@embedding_provider)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
all_healthy = components.values.all? { |status| %i[ok not_configured].include?(status) }
|
|
45
|
-
|
|
46
|
-
HealthStatus.new(healthy?: all_healthy, components: components)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
# Probe a store component by calling #count.
|
|
52
|
-
#
|
|
53
|
-
# @param store [Object, nil] Store adapter
|
|
54
|
-
# @return [Symbol] :ok, :error, or :not_configured
|
|
55
|
-
def probe_store(store)
|
|
56
|
-
return :not_configured if store.nil?
|
|
57
|
-
|
|
58
|
-
store.count
|
|
59
|
-
:ok
|
|
60
|
-
rescue StandardError
|
|
61
|
-
:error
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Probe an embedding provider by checking its capabilities without making network calls.
|
|
65
|
-
#
|
|
66
|
-
# @param provider [Object, nil] Embedding provider
|
|
67
|
-
# @return [Symbol] :ok, :error, or :not_configured
|
|
68
|
-
def probe_provider(provider)
|
|
69
|
-
return :not_configured if provider.nil?
|
|
70
|
-
|
|
71
|
-
if provider.respond_to?(:embed) && provider.respond_to?(:dimensions)
|
|
72
|
-
:ok
|
|
73
|
-
else
|
|
74
|
-
:error
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Woods
|
|
4
|
-
module Observability
|
|
5
|
-
# Lightweight instrumentation wrapper that delegates to ActiveSupport::Notifications
|
|
6
|
-
# when available, and falls back to a simple yield otherwise.
|
|
7
|
-
#
|
|
8
|
-
# @example
|
|
9
|
-
# Instrumentation.instrument('woods.extraction', unit: 'User') do
|
|
10
|
-
# extract_unit(user_model)
|
|
11
|
-
# end
|
|
12
|
-
#
|
|
13
|
-
module Instrumentation
|
|
14
|
-
module_function
|
|
15
|
-
|
|
16
|
-
# Instrument a block of code with an event name and payload.
|
|
17
|
-
#
|
|
18
|
-
# Delegates to ActiveSupport::Notifications.instrument when available.
|
|
19
|
-
# Otherwise, yields the block directly.
|
|
20
|
-
#
|
|
21
|
-
# @param event [String] Event name (e.g., 'woods.extraction')
|
|
22
|
-
# @param payload [Hash] Additional data to include with the event
|
|
23
|
-
# @yield [payload] The block to instrument
|
|
24
|
-
# @return [Object] The return value of the block
|
|
25
|
-
def instrument(event, payload = {}, &block)
|
|
26
|
-
if defined?(ActiveSupport::Notifications)
|
|
27
|
-
ActiveSupport::Notifications.instrument(event, payload, &block)
|
|
28
|
-
elsif block
|
|
29
|
-
yield payload
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|