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
data/lib/tasks/woods.rake
CHANGED
|
@@ -20,359 +20,9 @@
|
|
|
20
20
|
# case in CI and Docker — so a non-ASCII byte (e.g. a git branch name in the
|
|
21
21
|
# manifest) raises Encoding::InvalidByteSequenceError. AtomicFile.write is
|
|
22
22
|
# binmode, so the read side must go through AtomicFile.read to match.
|
|
23
|
-
require 'woods/
|
|
24
|
-
require 'woods/generation'
|
|
25
|
-
require 'woods/git_command'
|
|
23
|
+
require 'woods/rake_helpers'
|
|
26
24
|
|
|
27
25
|
namespace :woods do
|
|
28
|
-
# ── Multi-instance helpers (#164 phase 4) ────────────────────────────────
|
|
29
|
-
#
|
|
30
|
-
# Worktrees are disjoint by construction (each has its own Rails.root and
|
|
31
|
-
# its own output dir), so these only ever mediate writers against the *same*
|
|
32
|
-
# index: a manual rake run, a hook-triggered sync, and the watch daemon.
|
|
33
|
-
|
|
34
|
-
# Run a block holding the extraction lock, waiting for another writer to
|
|
35
|
-
# finish.
|
|
36
|
-
#
|
|
37
|
-
# This used to proceed *without* the lock after 30s, on the reasoning that a
|
|
38
|
-
# daemon cycle is milliseconds so a longer wait meant something unusual. That
|
|
39
|
-
# reasoning was wrong in the case that matters: a cycle includes a
|
|
40
|
-
# storm-triggered `extract_all`, which on a large host app runs for minutes.
|
|
41
|
-
# Proceeding then means two writers load `dependency_graph.json`, mutate
|
|
42
|
-
# divergent copies, and the last one silently discards the other's work — then
|
|
43
|
-
# bumps the generation, marking the clobbered graph fresh. Per-file atomic
|
|
44
|
-
# writes do not help, because the file *set* is not atomic.
|
|
45
|
-
#
|
|
46
|
-
# So the wait is now generous and the failure explicit. `WOODS_LOCK_WAIT`
|
|
47
|
-
# overrides it; exceeding it exits non-zero rather than corrupting the index,
|
|
48
|
-
# which is the outcome a CI job or a developer can actually act on.
|
|
49
|
-
# Fail the task when a run reported per-item errors (INF-4 / EXP-4).
|
|
50
|
-
#
|
|
51
|
-
# A printed-but-green run is invisible in the CI/cron pipelines these tasks
|
|
52
|
-
# target: a revoked API key, a full vector store, or a 401 on every page
|
|
53
|
-
# prints `Errors: N` and the job stays green while the index or the export
|
|
54
|
-
# target rots. `woods:unblocked_sync` and `woods:obsidian` already exit 1 on
|
|
55
|
-
# the same shape, and #270 fixed it for the extraction family; the embedding
|
|
56
|
-
# and Notion tasks carry identical CI exposure.
|
|
57
|
-
#
|
|
58
|
-
# Partial progress is deliberately *not* a carve-out here. The unblocked
|
|
59
|
-
# exporter has one (budget exhaustion with progress is the expected
|
|
60
|
-
# cold-start shape and converges on the next run); an embedding or Notion
|
|
61
|
-
# error is a genuine failure per unit and does not self-heal, so any
|
|
62
|
-
# non-zero count fails.
|
|
63
|
-
#
|
|
64
|
-
# @param errors [Integer, Array, nil] the run's error count or error list
|
|
65
|
-
# @return [void]
|
|
66
|
-
def woods_exit_on_reported_errors(errors)
|
|
67
|
-
# The embed indexer reports a count; the exporters report a list.
|
|
68
|
-
# (`Integer#size` exists and answers 8, so this must branch on the type.)
|
|
69
|
-
count = errors.is_a?(Numeric) ? errors.to_i : Array(errors).size
|
|
70
|
-
return if count.zero?
|
|
71
|
-
|
|
72
|
-
puts
|
|
73
|
-
puts 'Run completed with errors — failing so CI surfaces it.'
|
|
74
|
-
exit 1
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def woods_with_extraction_lock(output_dir, wait: nil, &block)
|
|
78
|
-
# Requires first. The default wait reads a constant from the daemon, so
|
|
79
|
-
# resolving it above these lines NameError'd every write task — the same
|
|
80
|
-
# load-order bug as the missing require in `woods:watch`, reintroduced one
|
|
81
|
-
# method over by the fix for it.
|
|
82
|
-
require 'woods/coordination/pipeline_lock'
|
|
83
|
-
require 'woods/coordination/lock_heartbeat'
|
|
84
|
-
require 'woods/watch/daemon'
|
|
85
|
-
|
|
86
|
-
wait ||= Float(ENV.fetch('WOODS_LOCK_WAIT', Woods::Watch::Daemon::LOCK_STALE_TIMEOUT))
|
|
87
|
-
|
|
88
|
-
lock = Woods::Coordination::PipelineLock.new(
|
|
89
|
-
lock_dir: output_dir.to_s,
|
|
90
|
-
name: Woods::Watch::Daemon::LOCK_NAME,
|
|
91
|
-
stale_timeout: Woods::Watch::Daemon::LOCK_STALE_TIMEOUT
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
woods_abort_on_lock_timeout(wait) unless woods_acquire_within(lock, wait)
|
|
95
|
-
|
|
96
|
-
begin
|
|
97
|
-
Woods::Coordination::LockHeartbeat.run(lock, &block)
|
|
98
|
-
ensure
|
|
99
|
-
lock.release
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Poll for the lock until `wait` seconds have elapsed.
|
|
104
|
-
#
|
|
105
|
-
# Monotonic, so a clock adjustment mid-wait cannot cut the window short or
|
|
106
|
-
# extend it indefinitely.
|
|
107
|
-
#
|
|
108
|
-
# @return [Boolean] whether the lock was acquired
|
|
109
|
-
def woods_acquire_within(lock, wait)
|
|
110
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
|
|
111
|
-
acquired = lock.acquire
|
|
112
|
-
until acquired || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
113
|
-
sleep 0.25
|
|
114
|
-
acquired = lock.acquire
|
|
115
|
-
end
|
|
116
|
-
acquired
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def woods_abort_on_lock_timeout(wait)
|
|
120
|
-
warn "ERROR: another writer has held the extraction lock for #{wait.round}s."
|
|
121
|
-
warn 'Refusing to write: two concurrent writers rewrite the dependency graph from divergent'
|
|
122
|
-
warn 'copies, and the loser\'s work is discarded under a generation that says "fresh".'
|
|
123
|
-
warn 'Set WOODS_LOCK_WAIT to wait longer, or stop the other writer.'
|
|
124
|
-
exit 1
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Is a watch daemon already maintaining this index?
|
|
128
|
-
#
|
|
129
|
-
# A session-start or worktree hook that fires `woods:incremental` on a tree
|
|
130
|
-
# a daemon is already watching is pure duplicated work — and it contends for
|
|
131
|
-
# the lock the daemon needs. Set WOODS_IGNORE_WATCH=1 to run anyway.
|
|
132
|
-
#
|
|
133
|
-
# Liveness alone is not coverage, and the difference matters. A `:running`
|
|
134
|
-
# daemon reconciles everything modified since the index's last successful
|
|
135
|
-
# publish when it starts (Daemon#catch_up), so changes that predate it are
|
|
136
|
-
# covered whether or not it witnessed them — that is what makes standing down
|
|
137
|
-
# safe. A `:degraded` daemon is alive but *cannot* currently update, so
|
|
138
|
-
# standing down for it would report success over work nothing is doing.
|
|
139
|
-
#
|
|
140
|
-
# @return [Symbol] `:none`, `:running`, or `:degraded`
|
|
141
|
-
def woods_daemon_coverage(output_dir)
|
|
142
|
-
return :none if ENV['WOODS_IGNORE_WATCH'] == '1'
|
|
143
|
-
|
|
144
|
-
require 'woods/watch/status'
|
|
145
|
-
status = Woods::Watch::Status.new(output_dir: output_dir)
|
|
146
|
-
return :none unless status.alive?
|
|
147
|
-
|
|
148
|
-
status.read['state'] == 'degraded' ? :degraded : :running
|
|
149
|
-
rescue StandardError
|
|
150
|
-
:none
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Delete the index without yanking it out from under another writer (#170).
|
|
154
|
-
#
|
|
155
|
-
# The old `woods:clean` body was a bare rm_rf. Run mid-daemon-cycle it
|
|
156
|
-
# deleted the daemon's own extraction lock, so the "writers serialize on
|
|
157
|
-
# PipelineLock" invariant evaporated at the exact moment a writer was
|
|
158
|
-
# mid-graph-rewrite. Now: refuse while a daemon is alive (same stand-down
|
|
159
|
-
# check as `woods:incremental`; WOODS_IGNORE_WATCH=1 overrides), then take
|
|
160
|
-
# the extraction lock like every other writer, delete everything EXCEPT the
|
|
161
|
-
# lock file itself, and let release remove the lock last.
|
|
162
|
-
#
|
|
163
|
-
# The directory the published generation's payload lives in.
|
|
164
|
-
#
|
|
165
|
-
# Read-side tasks resolve through this rather than the output root: an index
|
|
166
|
-
# publishing per-generation payloads keeps only `generation.json`, `dumps/`,
|
|
167
|
-
# `payloads/` and the lock files at the root. A flat index resolves to the
|
|
168
|
-
# root unchanged.
|
|
169
|
-
#
|
|
170
|
-
# @param output_dir [Pathname, String] index directory
|
|
171
|
-
# @return [Pathname]
|
|
172
|
-
def woods_payload_dir(output_dir)
|
|
173
|
-
Woods::Generation.new(output_dir: output_dir).payload_dir
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# @param output_dir [Pathname, String] index directory
|
|
177
|
-
# @param wait [Numeric, nil] seconds to wait for the lock (default: the
|
|
178
|
-
# shared writer wait; injectable so a spec need not sit out the window)
|
|
179
|
-
# @return [Symbol] `:cleaned`, or `:refused` when a live daemon is
|
|
180
|
-
# maintaining this index
|
|
181
|
-
def woods_clean_index(output_dir, wait: nil)
|
|
182
|
-
require 'woods/watch/daemon'
|
|
183
|
-
|
|
184
|
-
output_dir = Pathname.new(output_dir)
|
|
185
|
-
|
|
186
|
-
unless woods_daemon_coverage(output_dir) == :none
|
|
187
|
-
warn 'ERROR: a watch daemon is maintaining this index — refusing to delete it out from under it.'
|
|
188
|
-
warn 'Stop the daemon first, or set WOODS_IGNORE_WATCH=1 to clean anyway.'
|
|
189
|
-
return :refused
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
lock_name = Woods::Watch::Daemon::LOCK_NAME
|
|
193
|
-
woods_with_extraction_lock(output_dir, wait: wait) do
|
|
194
|
-
woods_sweep_index_dir(output_dir, lock_name)
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# The lock was released (and its file removed) above; drop the guard so
|
|
198
|
-
# the directory can empty, then remove the now-empty directory. Another
|
|
199
|
-
# writer may legitimately have recreated content between release and here
|
|
200
|
-
# — a non-empty directory is left alone rather than forced.
|
|
201
|
-
woods_remove_if_empty(output_dir, lock_name)
|
|
202
|
-
:cleaned
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Delete every index artifact except the lock file and its transaction
|
|
206
|
-
# guard. The guard lives inside the lock directory and a contender may hold
|
|
207
|
-
# its flock right now — deleting it mid-sweep would split the flock across
|
|
208
|
-
# two inodes and defeat the mutual exclusion it provides.
|
|
209
|
-
def woods_sweep_index_dir(output_dir, lock_name)
|
|
210
|
-
preserved = [
|
|
211
|
-
output_dir.join("#{lock_name}.lock"),
|
|
212
|
-
output_dir.join(Woods::Coordination::PipelineLock.guard_filename(lock_name))
|
|
213
|
-
]
|
|
214
|
-
output_dir.children.each do |entry|
|
|
215
|
-
FileUtils.rm_rf(entry) unless preserved.include?(entry)
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def woods_remove_if_empty(output_dir, lock_name)
|
|
220
|
-
FileUtils.rm_f(output_dir.join(Woods::Coordination::PipelineLock.guard_filename(lock_name)))
|
|
221
|
-
Dir.rmdir(output_dir)
|
|
222
|
-
rescue SystemCallError
|
|
223
|
-
nil
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# The root containing the Rakefile that loaded this task file.
|
|
227
|
-
#
|
|
228
|
-
# `woods:watch_status` intentionally avoids Rails boot, so it cannot ask
|
|
229
|
-
# `Rails.root` for the conventional output path. `Dir.pwd` is not a stable
|
|
230
|
-
# substitute: `rake -f /app/Rakefile` and worktree launchers may invoke the
|
|
231
|
-
# task from somewhere else. Rake retains the selected Rakefile even when it
|
|
232
|
-
# does not chdir, which gives the same application root without loading the
|
|
233
|
-
# environment.
|
|
234
|
-
#
|
|
235
|
-
# @return [String] absolute directory containing the active Rakefile
|
|
236
|
-
def woods_task_root
|
|
237
|
-
rakefile = Rake.application.rakefile
|
|
238
|
-
return Rake.application.original_dir if rakefile.nil? || rakefile.empty?
|
|
239
|
-
|
|
240
|
-
# Rake may keep this relative (`Rakefile`) after searching upward from a
|
|
241
|
-
# nested invocation. At task execution its cwd is the directory it loaded
|
|
242
|
-
# that relative file from. An explicit absolute `-f` is already complete.
|
|
243
|
-
File.dirname(File.expand_path(rakefile))
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Changed paths across a git range, for `woods:incremental`'s CI branches.
|
|
247
|
-
#
|
|
248
|
-
# `git diff --name-only` split on lines corrupted three things at once: a
|
|
249
|
-
# path containing a newline split into two entries; a non-ASCII path came
|
|
250
|
-
# back octal-escaped inside quotes under git's default `core.quotePath`,
|
|
251
|
-
# which the dispatcher then can't match to any rule; and a rename reported
|
|
252
|
-
# only the new path, so the old path's unit was never pruned. `-z` +
|
|
253
|
-
# `--name-status` + `--no-renames` fixes all three: NUL-delimited records,
|
|
254
|
-
# `core.quotePath=false` unescaped, and a rename decomposed by git itself
|
|
255
|
-
# into a separate `A <new>` and `D <old>` record rather than one `R` record
|
|
256
|
-
# naming both.
|
|
257
|
-
#
|
|
258
|
-
# The diff is rooted at the extracted application (`git -C Rails.root`),
|
|
259
|
-
# consistent with {Woods::GitProvenance} (#262): extraction launched from
|
|
260
|
-
# another checkout must not diff that checkout's history.
|
|
261
|
-
#
|
|
262
|
-
# The child status is carried out, not discarded (M1): `Open3.capture2`
|
|
263
|
-
# turned an unresolvable range — a GitLab zero-SHA, an unfetched GitHub base
|
|
264
|
-
# ref, garbage — into an empty change set the caller could not tell apart
|
|
265
|
-
# from "nothing changed", and `woods:incremental` exited 0 over a sync that
|
|
266
|
-
# never ran.
|
|
267
|
-
#
|
|
268
|
-
# @param range [String] a git diff range/revision expression
|
|
269
|
-
# @param root [Pathname, String] repository root the diff runs against
|
|
270
|
-
# @return [Array(Array<String>, String, nil)] the changed paths (both halves
|
|
271
|
-
# of any rename included), or nil paths plus a human-readable failure when
|
|
272
|
-
# the range could not be resolved
|
|
273
|
-
def woods_changed_paths_for_range(range, root: Rails.root)
|
|
274
|
-
require 'open3'
|
|
275
|
-
output, error, status = Open3.capture3(
|
|
276
|
-
*Woods::GitCommand.argv(
|
|
277
|
-
root, '-c', 'core.quotePath=false',
|
|
278
|
-
'diff', '--name-status', '-z', '--no-renames', range
|
|
279
|
-
)
|
|
280
|
-
)
|
|
281
|
-
return [woods_parse_git_diff_name_status(output), nil] if status.success?
|
|
282
|
-
|
|
283
|
-
[nil, "#{error.strip} (git exited #{status.exitstatus})"]
|
|
284
|
-
rescue SystemCallError => e
|
|
285
|
-
# No git binary at all (a slim container image, git removed after
|
|
286
|
-
# checkout). Errno::ENOENT out of `Open3.capture3` used to kill the task
|
|
287
|
-
# with a backtrace: non-zero, so safe, but it bypassed the decision below
|
|
288
|
-
# — the `:running`-daemon stand-down branch was unreachable, and the
|
|
289
|
-
# operator got a stack trace instead of the remediation text. An absent
|
|
290
|
-
# binary is an unresolvable range like any other (INF-12).
|
|
291
|
-
[nil, "git unavailable: #{e.message}"]
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# The changed-path set `woods:incremental` will process, or a stand-down.
|
|
295
|
-
#
|
|
296
|
-
# An explicit `CHANGED_FILES` list bypasses git entirely. Otherwise the
|
|
297
|
-
# range comes from the CI environment (GitLab's before-SHA, GitHub's base
|
|
298
|
-
# ref) or defaults to the last commit.
|
|
299
|
-
#
|
|
300
|
-
# A failed range is a decision, not a skip (M1): the changed-file set is
|
|
301
|
-
# unknown, so silently extracting nothing would exit 0 over work that never
|
|
302
|
-
# happened and leave CI drift unbounded. When a `:running` daemon maintains
|
|
303
|
-
# the index, its start-up catch-up covers whatever changed, so standing down
|
|
304
|
-
# with a printed reason is safe; a degraded daemon covers nothing, so the
|
|
305
|
-
# run fails like any other uncovered case. The decision runs BEFORE the
|
|
306
|
-
# task's empty-range exit — a failed range must never be mistaken for an
|
|
307
|
-
# empty one.
|
|
308
|
-
#
|
|
309
|
-
# @param output_dir [Pathname, String] index directory, for daemon coverage
|
|
310
|
-
# @return [Array<String>] changed paths
|
|
311
|
-
def woods_incremental_changed_paths(output_dir)
|
|
312
|
-
explicit = ENV.fetch('CHANGED_FILES', nil)
|
|
313
|
-
return explicit.split(',').map(&:strip) if explicit
|
|
314
|
-
|
|
315
|
-
range = woods_incremental_range
|
|
316
|
-
changed_files, failure = woods_changed_paths_for_range(range)
|
|
317
|
-
return changed_files unless failure
|
|
318
|
-
|
|
319
|
-
if woods_daemon_coverage(output_dir) == :running
|
|
320
|
-
puts "Could not resolve the git diff range #{range.inspect} (#{failure})."
|
|
321
|
-
puts 'A watch daemon is maintaining this index, so its catch-up covers the changed paths — standing down.'
|
|
322
|
-
puts 'To extract now anyway, repair or provide the range (check the CI env refs),'
|
|
323
|
-
puts 'set CHANGED_FILES explicitly, or run a full woods:extract.'
|
|
324
|
-
exit 0
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
warn "ERROR: could not resolve the git diff range #{range.inspect}: #{failure}"
|
|
328
|
-
warn 'The changed-file set is unknown, so incremental extraction would silently index nothing.'
|
|
329
|
-
warn 'Fix the range (a zero SHA or an unfetched base ref resolve to nothing), or run a full woods:extract.'
|
|
330
|
-
exit 1
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
# The git range `woods:incremental` diffs, from the CI environment or the
|
|
334
|
-
# last-commit default.
|
|
335
|
-
#
|
|
336
|
-
# @return [String]
|
|
337
|
-
def woods_incremental_range
|
|
338
|
-
if ENV['CI_COMMIT_BEFORE_SHA']
|
|
339
|
-
# GitLab CI
|
|
340
|
-
"#{ENV['CI_COMMIT_BEFORE_SHA']}..#{ENV.fetch('CI_COMMIT_SHA', nil)}"
|
|
341
|
-
elsif ENV['GITHUB_BASE_REF']
|
|
342
|
-
# GitHub Actions PR
|
|
343
|
-
"origin/#{ENV['GITHUB_BASE_REF']}...HEAD"
|
|
344
|
-
else
|
|
345
|
-
# Default: changes since last commit
|
|
346
|
-
'HEAD~1'
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# Parse NUL-delimited `git diff --name-status -z --no-renames` output.
|
|
351
|
-
#
|
|
352
|
-
# Each record is `<status>\0<path>\0` — `--no-renames` guarantees a single
|
|
353
|
-
# path per record, since it is what stops git emitting a two-path `R`/`C`
|
|
354
|
-
# record in the first place. Status letters are not inspected beyond "did
|
|
355
|
-
# git report anything at all"; a deleted path still needs to reach the
|
|
356
|
-
# change set so its unit can be pruned.
|
|
357
|
-
#
|
|
358
|
-
# A bare `Open3.capture2` read is tagged with the process's default
|
|
359
|
-
# external encoding — US-ASCII under `LANG=C`, this daemon's usual
|
|
360
|
-
# environment (see `AtomicFile.read`'s gotcha) — so a UTF-8 path is a
|
|
361
|
-
# US-ASCII string containing invalid bytes until re-tagged.
|
|
362
|
-
#
|
|
363
|
-
# @param output [String] raw NUL-delimited git output
|
|
364
|
-
# @return [Array<String>] changed paths
|
|
365
|
-
def woods_parse_git_diff_name_status(output)
|
|
366
|
-
fields = output.dup.force_encoding(Encoding::UTF_8).split("\x00")
|
|
367
|
-
paths = []
|
|
368
|
-
fields.each_slice(2) do |status, path|
|
|
369
|
-
break if path.nil?
|
|
370
|
-
|
|
371
|
-
paths << path unless status.nil? || status.empty?
|
|
372
|
-
end
|
|
373
|
-
paths
|
|
374
|
-
end
|
|
375
|
-
|
|
376
26
|
desc 'Full extraction of codebase for indexing'
|
|
377
27
|
task extract: :environment do
|
|
378
28
|
require 'woods/extractor'
|
|
@@ -384,7 +34,7 @@ namespace :woods do
|
|
|
384
34
|
puts
|
|
385
35
|
|
|
386
36
|
extractor = Woods::Extractor.new(output_dir: output_dir)
|
|
387
|
-
results = woods_with_extraction_lock(output_dir) do
|
|
37
|
+
results = Woods::RakeHelpers.woods_with_extraction_lock(output_dir) do
|
|
388
38
|
extracted = extractor.extract_all
|
|
389
39
|
extractor.raise_on_publication_failure!
|
|
390
40
|
extracted
|
|
@@ -415,7 +65,7 @@ namespace :woods do
|
|
|
415
65
|
# decided here too — stand down under a covering daemon, or fail loudly —
|
|
416
66
|
# BEFORE the empty-range exit below can mistake it for "nothing changed"
|
|
417
67
|
# (M1).
|
|
418
|
-
changed_files = woods_incremental_changed_paths(output_dir)
|
|
68
|
+
changed_files = Woods::RakeHelpers.woods_incremental_changed_paths(output_dir)
|
|
419
69
|
|
|
420
70
|
# Filter to paths that imply extraction work. The rule set lives in
|
|
421
71
|
# PathDispatcher alongside the dispatch itself — a second hand-maintained
|
|
@@ -429,7 +79,7 @@ namespace :woods do
|
|
|
429
79
|
exit 0
|
|
430
80
|
end
|
|
431
81
|
|
|
432
|
-
case woods_daemon_coverage(output_dir)
|
|
82
|
+
case Woods::RakeHelpers.woods_daemon_coverage(output_dir)
|
|
433
83
|
when :running
|
|
434
84
|
puts 'A watch daemon is maintaining this index — skipping.'
|
|
435
85
|
puts 'It reconciles anything changed since the last publish when it starts, so these are covered.'
|
|
@@ -446,7 +96,7 @@ namespace :woods do
|
|
|
446
96
|
puts
|
|
447
97
|
|
|
448
98
|
extractor = Woods::Extractor.new(output_dir: output_dir)
|
|
449
|
-
affected = woods_with_extraction_lock(output_dir) do
|
|
99
|
+
affected = Woods::RakeHelpers.woods_with_extraction_lock(output_dir) do
|
|
450
100
|
extracted = extractor.extract_changed(changed_files)
|
|
451
101
|
extractor.raise_on_publication_failure!
|
|
452
102
|
extracted
|
|
@@ -456,11 +106,30 @@ namespace :woods do
|
|
|
456
106
|
puts "Re-extracted #{affected.size} affected units."
|
|
457
107
|
end
|
|
458
108
|
|
|
109
|
+
desc 'Consume an encoded hook refresh batch (internal plugin transport)'
|
|
110
|
+
task :hook_refresh, [:batch] do |_task, args|
|
|
111
|
+
require 'woods/hooks/refresh'
|
|
112
|
+
Woods::Hooks::Refresh.new(args[:batch]).call
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
desc 'Verify indexed source content without booting Rails (encoded options are optional)'
|
|
116
|
+
task :source_status, [:options] do |_task, args|
|
|
117
|
+
require 'woods/source_inputs/status'
|
|
118
|
+
puts JSON.generate(Woods::SourceInputs::Status.from_transport(args[:options]))
|
|
119
|
+
end
|
|
120
|
+
|
|
459
121
|
desc 'Tend the garden — incremental extraction (alias for incremental)'
|
|
460
122
|
task tend: :incremental
|
|
461
123
|
|
|
462
124
|
desc 'Watch the app and keep the index current (resident daemon)'
|
|
463
|
-
task watch
|
|
125
|
+
task :watch do
|
|
126
|
+
# Observe inputs before Rails initializes. A snapshot taken in Daemon.new
|
|
127
|
+
# would silently bless edits made while initializers were running.
|
|
128
|
+
require 'woods/watch/boot_snapshot'
|
|
129
|
+
environment = Rake::Task[:environment]
|
|
130
|
+
fresh_environment = !environment.already_invoked && !Rails.application.initialized?
|
|
131
|
+
boot_snapshot = Woods::Watch::BootSnapshot.new(root: Rails.root) if fresh_environment
|
|
132
|
+
environment.invoke
|
|
464
133
|
# Both, and the extractor is not optional. The daemon's default
|
|
465
134
|
# extractor_factory names Woods::Extractor lazily, so omitting this require
|
|
466
135
|
# loaded and started cleanly and then NameError'd on the first real cycle —
|
|
@@ -472,6 +141,12 @@ namespace :woods do
|
|
|
472
141
|
|
|
473
142
|
output_dir = ENV.fetch('WOODS_OUTPUT', Woods.configuration.output_dir)
|
|
474
143
|
|
|
144
|
+
poll_interval = begin
|
|
145
|
+
Float(ENV.fetch('WOODS_WATCH_POLL_INTERVAL', Woods::Watch::Watcher::DEFAULT_POLL_INTERVAL))
|
|
146
|
+
rescue ArgumentError, TypeError
|
|
147
|
+
raise ArgumentError, 'WOODS_WATCH_POLL_INTERVAL must be a positive finite number of seconds'
|
|
148
|
+
end
|
|
149
|
+
|
|
475
150
|
daemon = Woods::Watch::Daemon.new(
|
|
476
151
|
output_dir: output_dir,
|
|
477
152
|
root: Rails.root,
|
|
@@ -479,12 +154,14 @@ namespace :woods do
|
|
|
479
154
|
full_extraction_threshold: Integer(
|
|
480
155
|
ENV.fetch('WOODS_WATCH_FULL_THRESHOLD', Woods::Watch::Daemon::DEFAULT_FULL_EXTRACTION_THRESHOLD)
|
|
481
156
|
),
|
|
157
|
+
poll_interval: poll_interval,
|
|
482
158
|
# The documented fix for a container watching a bind mount, where native
|
|
483
159
|
# FS events do not propagate and the daemon would sit silent. Nothing
|
|
484
160
|
# exposed it before, which made the advice unfollowable.
|
|
485
161
|
force_polling: ENV['WOODS_WATCH_POLL'] == '1', # container autodetect also applies; see Watcher.containerized?
|
|
486
162
|
idle_timeout: ENV.fetch('WOODS_WATCH_IDLE_TIMEOUT', nil) && Float(ENV.fetch('WOODS_WATCH_IDLE_TIMEOUT')),
|
|
487
163
|
catch_up: ENV['WOODS_WATCH_CATCH_UP'] != '0',
|
|
164
|
+
boot_snapshot: boot_snapshot,
|
|
488
165
|
logger: Rails.logger
|
|
489
166
|
)
|
|
490
167
|
|
|
@@ -530,7 +207,7 @@ namespace :woods do
|
|
|
530
207
|
require 'woods/watch/status'
|
|
531
208
|
require 'json'
|
|
532
209
|
|
|
533
|
-
output_dir = ENV.fetch('WOODS_OUTPUT') { File.join(woods_task_root, 'tmp/woods') }
|
|
210
|
+
output_dir = ENV.fetch('WOODS_OUTPUT') { File.join(Woods::RakeHelpers.woods_task_root, 'tmp/woods') }
|
|
534
211
|
status = Woods::Watch::Status.new(output_dir: output_dir)
|
|
535
212
|
|
|
536
213
|
puts JSON.pretty_generate(status.read)
|
|
@@ -567,7 +244,7 @@ namespace :woods do
|
|
|
567
244
|
# clobbered state is fresh. Atomic writes do not help: each write is
|
|
568
245
|
# individually intact, the *set* is not. So it serializes like the others.
|
|
569
246
|
begin
|
|
570
|
-
result = woods_with_extraction_lock(output_dir) do
|
|
247
|
+
result = Woods::RakeHelpers.woods_with_extraction_lock(output_dir) do
|
|
571
248
|
refreshed = extractor.refresh(*keys)
|
|
572
249
|
extractor.raise_on_publication_failure!
|
|
573
250
|
refreshed
|
|
@@ -603,7 +280,7 @@ namespace :woods do
|
|
|
603
280
|
# regardless of `include_framework_sources` — an explicit invocation is
|
|
604
281
|
# the escape hatch the knob deliberately leaves open.
|
|
605
282
|
extractor = Woods::Extractor.new(output_dir: output_dir)
|
|
606
|
-
result = woods_with_extraction_lock(output_dir) do
|
|
283
|
+
result = Woods::RakeHelpers.woods_with_extraction_lock(output_dir) do
|
|
607
284
|
refreshed = extractor.refresh(:rails_source)
|
|
608
285
|
extractor.raise_on_publication_failure!
|
|
609
286
|
refreshed
|
|
@@ -624,7 +301,7 @@ namespace :woods do
|
|
|
624
301
|
exit 1
|
|
625
302
|
end
|
|
626
303
|
|
|
627
|
-
manifest_path = woods_payload_dir(output_dir).join('manifest.json')
|
|
304
|
+
manifest_path = Woods::RakeHelpers.woods_payload_dir(output_dir).join('manifest.json')
|
|
628
305
|
unless manifest_path.exist?
|
|
629
306
|
puts 'ERROR: Manifest not found. Run extraction first.'
|
|
630
307
|
exit 1
|
|
@@ -675,7 +352,7 @@ namespace :woods do
|
|
|
675
352
|
exit 1
|
|
676
353
|
end
|
|
677
354
|
|
|
678
|
-
payload_dir = woods_payload_dir(output_dir)
|
|
355
|
+
payload_dir = Woods::RakeHelpers.woods_payload_dir(output_dir)
|
|
679
356
|
manifest_path = payload_dir.join('manifest.json')
|
|
680
357
|
manifest = manifest_path.exist? ? JSON.parse(Woods::AtomicFile.read(manifest_path)) : {}
|
|
681
358
|
|
|
@@ -740,7 +417,7 @@ namespace :woods do
|
|
|
740
417
|
|
|
741
418
|
if output_dir.exist?
|
|
742
419
|
puts "Removing #{output_dir}..."
|
|
743
|
-
exit 1 unless woods_clean_index(output_dir) == :cleaned
|
|
420
|
+
exit 1 unless Woods::RakeHelpers.woods_clean_index(output_dir) == :cleaned
|
|
744
421
|
puts 'Done.'
|
|
745
422
|
else
|
|
746
423
|
puts 'Index directory does not exist.'
|
|
@@ -750,38 +427,11 @@ namespace :woods do
|
|
|
750
427
|
desc 'Clear the brush — remove index (alias for clean)'
|
|
751
428
|
task clear: :clean
|
|
752
429
|
|
|
753
|
-
# Resolve the configured retrieval stack and run one ad-hoc query (#178).
|
|
754
|
-
#
|
|
755
|
-
# Every backend — embedding provider, vector store, metadata store, graph
|
|
756
|
-
# store — is resolved through Woods::Builder from Woods.configuration,
|
|
757
|
-
# the same wiring `woods:embed` writes through
|
|
758
|
-
# (Woods::Tasks.build_embed_indexer). The old task body hardcoded
|
|
759
|
-
# Ollama + InMemory + SQLite + Memory, so on any other configured stack it
|
|
760
|
-
# queried backends the embed run never wrote to and silently returned
|
|
761
|
-
# nothing.
|
|
762
|
-
#
|
|
763
|
-
# In-memory stores start empty in a fresh process; hosts on the :local /
|
|
764
|
-
# :shared_filesystem presets should query through woods-mcp, which
|
|
765
|
-
# hydrates them from the dumps on disk.
|
|
766
|
-
#
|
|
767
|
-
# @param query [String] natural-language retrieval query
|
|
768
|
-
# @return [String] human-formatted retrieval output
|
|
769
|
-
def woods_run_retrieval(query)
|
|
770
|
-
require 'woods'
|
|
771
|
-
require 'woods/formatting/human_adapter'
|
|
772
|
-
|
|
773
|
-
config = Woods.configuration
|
|
774
|
-
retriever = Woods::Builder.new(config).build_retriever
|
|
775
|
-
result = retriever.retrieve(query, budget: config.max_context_tokens)
|
|
776
|
-
|
|
777
|
-
Woods::Formatting::HumanAdapter.new.format(result)
|
|
778
|
-
end
|
|
779
|
-
|
|
780
430
|
# Internal debugging tool — hidden from `rails -T`
|
|
781
431
|
task :retrieve, [:query] => :environment do |_t, args|
|
|
782
432
|
query = args[:query] || raise('Usage: rake woods:retrieve[query]')
|
|
783
433
|
|
|
784
|
-
puts woods_run_retrieval(query)
|
|
434
|
+
puts Woods::RakeHelpers.woods_run_retrieval(query)
|
|
785
435
|
end
|
|
786
436
|
|
|
787
437
|
desc 'Embed all extracted units'
|
|
@@ -800,9 +450,9 @@ namespace :woods do
|
|
|
800
450
|
|
|
801
451
|
indexer = Woods::Tasks.build_embed_indexer
|
|
802
452
|
puts 'Embedding all extracted units...'
|
|
803
|
-
stats = woods_with_extraction_lock(output_dir) { indexer.index_all }
|
|
453
|
+
stats = Woods::RakeHelpers.woods_with_extraction_lock(output_dir) { indexer.index_all }
|
|
804
454
|
Woods::Tasks.print_embed_stats(stats, mode: :full)
|
|
805
|
-
woods_exit_on_reported_errors(stats[:errors])
|
|
455
|
+
Woods::RakeHelpers.woods_exit_on_reported_errors(stats[:errors])
|
|
806
456
|
end
|
|
807
457
|
|
|
808
458
|
desc 'Nest the data — embed all units (alias for embed)'
|
|
@@ -818,9 +468,9 @@ namespace :woods do
|
|
|
818
468
|
|
|
819
469
|
indexer = Woods::Tasks.build_embed_indexer
|
|
820
470
|
puts 'Embedding changed units (incremental)...'
|
|
821
|
-
stats = woods_with_extraction_lock(output_dir) { indexer.index_incremental }
|
|
471
|
+
stats = Woods::RakeHelpers.woods_with_extraction_lock(output_dir) { indexer.index_incremental }
|
|
822
472
|
Woods::Tasks.print_embed_stats(stats, mode: :incremental)
|
|
823
|
-
woods_exit_on_reported_errors(stats[:errors])
|
|
473
|
+
Woods::RakeHelpers.woods_exit_on_reported_errors(stats[:errors])
|
|
824
474
|
end
|
|
825
475
|
|
|
826
476
|
desc 'Hone the blade — incremental embedding (alias for embed_incremental)'
|
|
@@ -939,7 +589,7 @@ namespace :woods do
|
|
|
939
589
|
end
|
|
940
590
|
|
|
941
591
|
output_dir = ENV.fetch('WOODS_OUTPUT', Woods.configuration.output_dir)
|
|
942
|
-
graph_path = File.join(woods_payload_dir(output_dir).to_s, 'dependency_graph.json')
|
|
592
|
+
graph_path = File.join(Woods::RakeHelpers.woods_payload_dir(output_dir).to_s, 'dependency_graph.json')
|
|
943
593
|
|
|
944
594
|
unless File.exist?(graph_path)
|
|
945
595
|
puts "ERROR: Dependency graph not found at #{graph_path}"
|
|
@@ -951,7 +601,7 @@ namespace :woods do
|
|
|
951
601
|
graph = Woods::DependencyGraph.from_h(graph_data)
|
|
952
602
|
|
|
953
603
|
max_depth = ENV.fetch('MAX_DEPTH', 5).to_i
|
|
954
|
-
assembler = Woods::FlowAssembler.new(graph: graph, extracted_dir: woods_payload_dir(output_dir).to_s)
|
|
604
|
+
assembler = Woods::FlowAssembler.new(graph: graph, extracted_dir: Woods::RakeHelpers.woods_payload_dir(output_dir).to_s)
|
|
955
605
|
flow = assembler.assemble(entry_point, max_depth: max_depth)
|
|
956
606
|
|
|
957
607
|
format = ENV.fetch('FORMAT', 'markdown').downcase
|
|
@@ -1020,7 +670,7 @@ namespace :woods do
|
|
|
1020
670
|
puts " Errors: #{stats[:errors].size}"
|
|
1021
671
|
stats[:errors].first(5).each { |e| puts " - #{e}" }
|
|
1022
672
|
puts " ... and #{stats[:errors].size - 5} more" if stats[:errors].size > 5
|
|
1023
|
-
woods_exit_on_reported_errors(stats[:errors])
|
|
673
|
+
Woods::RakeHelpers.woods_exit_on_reported_errors(stats[:errors])
|
|
1024
674
|
end
|
|
1025
675
|
end
|
|
1026
676
|
|