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/docs/RETRIEVAL_GUIDE.md
CHANGED
|
@@ -26,6 +26,16 @@ query
|
|
|
26
26
|
| Assembly | `Woods::Retrieval::ContextAssembler` | Fills a token budget with ranked units, sectioned into structural / primary / supporting / framework blocks |
|
|
27
27
|
| Orchestration | `Woods::Retriever` | Coordinates all four stages; returns a `RetrievalResult` with `context`, `sources`, `strategy`, `tokens_used`, and `trace` |
|
|
28
28
|
|
|
29
|
+
### PageRank importance
|
|
30
|
+
|
|
31
|
+
When a graph store supplies PageRank, ranking converts its scores to ordinal
|
|
32
|
+
percentiles: the highest-ranked unit receives 1.0 and the lowest receives 1/n,
|
|
33
|
+
where n is the number of entries in the PageRank map.
|
|
34
|
+
Equal PageRank scores are ordered lexically by identifier before assigning
|
|
35
|
+
percentiles. This preserves distinct ordinal weights while making ties independent
|
|
36
|
+
of graph insertion order and Ruby version. It does not assign equal importance
|
|
37
|
+
weights to tied units. Missing graph entries retain the metadata-based fallback.
|
|
38
|
+
|
|
29
39
|
### Search strategies
|
|
30
40
|
|
|
31
41
|
`SearchExecutor` selects one of five strategies based on query classification:
|
|
@@ -38,13 +48,22 @@ query
|
|
|
38
48
|
| `:hybrid` | `comprehensive` or `exploratory` scope | Runs vector + keyword + graph expansion, deduplicates |
|
|
39
49
|
| `:direct` | `locate`/`reference` + `pinpoint` scope | Looks up identifiers directly in metadata store; falls back to keyword |
|
|
40
50
|
|
|
51
|
+
Graph queries preserve identifier spelling and namespaces: `trace Billing::Invoice`,
|
|
52
|
+
`trace ReviewAssignment`, and `trace review_assignment` resolve the subject before
|
|
53
|
+
traversing its dependencies and dependents. An exact identifier takes precedence;
|
|
54
|
+
an unqualified name without an exact match can seed up to three namespace matches
|
|
55
|
+
in storage-identifier order. Use the qualified name to disambiguate. A missing
|
|
56
|
+
qualified identifier does not fall back to a different namespace. For unqualified
|
|
57
|
+
prose, metadata fallback searches subject terms without instructions such as
|
|
58
|
+
`trace`, `follow`, or `who calls`.
|
|
59
|
+
|
|
41
60
|
Keyword results are scored by how many distinct fields matched (identifier, source, metadata), not by the store's result order. Each matched field adds 0.25, capped at 1.0, so a result matching on identifier and source scores higher than one matching source alone.
|
|
42
61
|
|
|
43
62
|
---
|
|
44
63
|
|
|
45
64
|
## Configuring Retrieval
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
Semantic retrieval requires an embedding provider and a vector store. Configure these in `config/initializers/woods.rb` before embedding. For provider-free ranked retrieval over extraction output, use [explicit lexical mode](#embedding-free-lexical-retrieval) in the MCP process environment instead.
|
|
48
67
|
|
|
49
68
|
### Presets (recommended)
|
|
50
69
|
|
|
@@ -124,13 +143,95 @@ bundle exec rake woods:extract
|
|
|
124
143
|
bundle exec rake woods:embed
|
|
125
144
|
```
|
|
126
145
|
|
|
146
|
+
### Input integrity and source-empty units
|
|
147
|
+
|
|
148
|
+
Embedding validates a native published generation's manifest, type listings and
|
|
149
|
+
unit payloads before changing vector storage, metadata or checkpoints. A missing,
|
|
150
|
+
corrupt or mismatched listed unit raises `Embedding input incomplete`; repair the
|
|
151
|
+
extraction (or run a full `woods:extract`) before retrying embedding.
|
|
152
|
+
`WOODS_ALLOW_PURGE=1` permits intentional mass deletion; it does not bypass this
|
|
153
|
+
integrity check. Corpus reads hold one generation pin while collecting the input.
|
|
154
|
+
|
|
155
|
+
Legacy flat indexes retain arbitrary unit filenames and do not require a manifest
|
|
156
|
+
or type listing. A `payloads/` directory requires a valid publication pointer;
|
|
157
|
+
missing or null pointers refuse embedding even when stale flat files remain.
|
|
158
|
+
Malformed JSON now refuses embedding instead of silently dropping
|
|
159
|
+
that file. Without an authoritative listing, a missing legacy file still denotes
|
|
160
|
+
a deletion; regenerate extraction into the native publication layout for stronger
|
|
161
|
+
completeness checks.
|
|
162
|
+
|
|
163
|
+
When a current unit has no source text to embed, Woods retains its metadata and
|
|
164
|
+
removes its superseded vectors, including old chunks. It records the current
|
|
165
|
+
source hash without calling the provider; later incremental runs verify that the
|
|
166
|
+
unit still prepares no text before accepting a matching no-vector checkpoint.
|
|
167
|
+
Empty-vector reconciliation waits until all batches succeed, so a later provider
|
|
168
|
+
failure does not retire those old vectors.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Embedding-free lexical retrieval
|
|
173
|
+
|
|
174
|
+
Opt into ranked retrieval over an extract-only published index:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
WOODS_RETRIEVAL_MODE=lexical bundle exec woods-mcp-start ./tmp/woods
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For a Ruby-built retriever, set `config.retrieval_mode = :lexical` and supply a
|
|
181
|
+
populated metadata store to `Builder#build_retriever`. The packaged MCP server
|
|
182
|
+
loads published unit JSON itself; it does not boot Rails or read current source
|
|
183
|
+
files. Neither path constructs an embedding provider or vector adapter. If a
|
|
184
|
+
no-provider error suggests only OpenAI, Ollama or `search`, explicit lexical
|
|
185
|
+
mode is still available from `2.0.0.beta3`: set the variable in the MCP client
|
|
186
|
+
configuration and restart that server. Confirm `woods_status.retriever.mode`
|
|
187
|
+
is `lexical`; setting it only in a Rails initializer does not configure a
|
|
188
|
+
separate MCP process. The
|
|
189
|
+
existing `:semantic` mode remains the default; provider failures never switch
|
|
190
|
+
modes automatically. A Rails initializer is not loaded by the standalone MCP
|
|
191
|
+
process, so set the environment variable in that process's client configuration.
|
|
192
|
+
|
|
193
|
+
Lexical results use field-aware BM25 scoring over identifiers, source paths,
|
|
194
|
+
published source and selected runtime metadata (including callbacks, associations
|
|
195
|
+
and validations). Exact full identifiers rank first, with ambiguous typed owners
|
|
196
|
+
retained. Other ties are deterministic. Responses name the lexical mode and
|
|
197
|
+
matching fields/terms; runtime-field hits include the selected published runtime
|
|
198
|
+
values. Lexical Ruby results leave the semantic-only `type_rank_context` table
|
|
199
|
+
`nil`; they do not report a global vector rank or vector fallback. The top 20
|
|
200
|
+
eligible positive matches form the candidate shortlist for the output budget.
|
|
201
|
+
The lexical header reports `sources included`, `candidates considered`, and
|
|
202
|
+
`candidate limit: 20`. Included sources count the actual returned entries;
|
|
203
|
+
considered candidates count the shortlist after filtering and the limit, not
|
|
204
|
+
all matches or all documents examined. This is ranked discovery, not an
|
|
205
|
+
exhaustive match listing. Explicit
|
|
206
|
+
`types` filters override default exclusions, as in semantic retrieval, and apply
|
|
207
|
+
before that limit. A query with no lexical evidence returns no matches; unrelated
|
|
208
|
+
graph hubs are never added. Query-seeded graph ranking is evaluation-only.
|
|
209
|
+
|
|
210
|
+
The budget covers headers, matching explanations and truncation notices using a
|
|
211
|
+
labelled character-based estimate, not an exact provider tokenizer. Full source
|
|
212
|
+
remains available through `lookup`. Count text is charged before source selection;
|
|
213
|
+
final counts do not trigger a second selection pass. Very small budgets can omit
|
|
214
|
+
all sources or clip the header itself. Zero candidates means no lexical matches;
|
|
215
|
+
positive candidates with zero included sources means no source entry fit the
|
|
216
|
+
available budget. The same counts apply to full, compact, outline and scoped
|
|
217
|
+
retrieval, agreeing with returned source attribution. The
|
|
218
|
+
reader pins one published generation for building and querying its immutable
|
|
219
|
+
lexical snapshot, rebuilding after publication. Corrupt units fail explicitly;
|
|
220
|
+
they cannot quietly become a successful partial index. Older flat indexes rebuild
|
|
221
|
+
on each query because they lack an immutable generation identity.
|
|
222
|
+
|
|
223
|
+
See [evaluation](EVALUATION.md) for measured query coverage and limits. Lexical
|
|
224
|
+
matching cannot infer synonyms absent from the published text; a miss is not proof
|
|
225
|
+
that application behavior is absent. Static Woods self-maps can opt into the same
|
|
226
|
+
mode but remain static source maps, not resolved Rails runtime evidence.
|
|
227
|
+
|
|
127
228
|
---
|
|
128
229
|
|
|
129
230
|
## Running Retrieval
|
|
130
231
|
|
|
131
232
|
### MCP tool: `codebase_retrieve`
|
|
132
233
|
|
|
133
|
-
The primary interface for agents. Available
|
|
234
|
+
The primary interface for agents. Available with explicit lexical mode over extraction output, or with an embedding provider and completed `rake woods:embed` in the default semantic mode.
|
|
134
235
|
|
|
135
236
|
```
|
|
136
237
|
codebase_retrieve(query: "how does billing work?")
|
|
@@ -165,6 +266,12 @@ result.sources # => [{ identifier: "User", type: "model", score: 0.91, ...
|
|
|
165
266
|
result.trace # => RetrievalTrace with elapsed_ms, candidate_count, etc.
|
|
166
267
|
```
|
|
167
268
|
|
|
269
|
+
`result.tokens_used` and `result.trace.tokens_used` count the final returned
|
|
270
|
+
`context`, including the optional formatter output and type-rank table. Counting
|
|
271
|
+
uses the same injected token counter or chars-per-token estimate as assembly;
|
|
272
|
+
it does not guarantee an exact count for the downstream model. `budget` limits
|
|
273
|
+
context assembly, so postprocessing can make the final count exceed it.
|
|
274
|
+
|
|
168
275
|
Override the token budget per call:
|
|
169
276
|
|
|
170
277
|
```ruby
|
|
@@ -201,18 +308,29 @@ In all cases, errors in individual components produce empty candidate sets for t
|
|
|
201
308
|
|
|
202
309
|
### `similarity_threshold`
|
|
203
310
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Lower values return more candidates, which can improve recall for broad queries at the cost of precision. Raise it if results seem loosely related.
|
|
311
|
+
Deprecated and inert. Woods retains numeric validation (`0.0`–`1.0`) and
|
|
312
|
+
readback for compatibility, but setting this option emits a warning. It has not
|
|
313
|
+
filtered retrieval results; changing it does not change scores or candidates.
|
|
314
|
+
Use explicit type, package or source-path scopes to select eligible units, and
|
|
315
|
+
inspect returned ranking evidence to assess relevance. No new score cutoff is
|
|
316
|
+
introduced by this deprecation.
|
|
212
317
|
|
|
213
318
|
### `max_context_tokens`
|
|
214
319
|
|
|
215
|
-
Sets the default token budget for context assembly. Default: `8000`.
|
|
320
|
+
Sets the default token budget for context assembly. Default: `8000`. Builder
|
|
321
|
+
captures this value when constructing semantic or lexical retrievers, including
|
|
322
|
+
cached retrievers. The `budget` parameter on `codebase_retrieve` and
|
|
323
|
+
`Retriever#retrieve` overrides it per call; omitted and explicitly equal budgets
|
|
324
|
+
share a cache entry. Changing configuration affects newly constructed retrievers.
|
|
325
|
+
Direct `Retriever.new` callers can pass `default_budget:`; otherwise they retain
|
|
326
|
+
8000. Custom MCP collaborators without `default_budget` also retain the legacy
|
|
327
|
+
8000 fallback.
|
|
328
|
+
|
|
329
|
+
The setting applies to the serving process's configuration. It is not embedded
|
|
330
|
+
in `woods.json`; a standalone MCP process that does not load the host initializer
|
|
331
|
+
uses its own default unless a tool call supplies `budget`. Token accounting uses
|
|
332
|
+
the configured estimator, and very small budgets can still include formatting
|
|
333
|
+
overhead.
|
|
216
334
|
|
|
217
335
|
```ruby
|
|
218
336
|
config.max_context_tokens = 12_000 # More context per retrieval
|
|
@@ -250,6 +368,10 @@ bundle exec rake woods:embed
|
|
|
250
368
|
| `text-embedding-3-small` (default) | 1536 |
|
|
251
369
|
| `text-embedding-3-large` | 3072 |
|
|
252
370
|
|
|
371
|
+
Woods' pgvector HNSW adapter supports at most 2,000 dimensions. For the large
|
|
372
|
+
model, request a supported output width explicitly or choose another backend;
|
|
373
|
+
see [pgvector configuration](CONFIGURATION_REFERENCE.md#pgvector-postgresql).
|
|
374
|
+
|
|
253
375
|
**Ollama default model:** `nomic-embed-text`. Dimensions are detected dynamically on first embed.
|
|
254
376
|
|
|
255
377
|
---
|
|
@@ -264,4 +386,143 @@ bundle exec rake woods:embed
|
|
|
264
386
|
| Empty results for a known class name | Keyword strategy not finding the identifier | Try a conceptual query with `codebase_retrieve`; or use `search` for exact name lookup |
|
|
265
387
|
| Very slow retrieval | Large vector index without HNSW index, or Qdrant cold start | For pgvector: create an HNSW index (see `BACKEND_MATRIX.md`). For Qdrant: check collection status |
|
|
266
388
|
| `codebase_retrieve` tool listed but disabled | Embedding provider not configured or API key missing | Set `embedding_provider`, run `woods:embed`, and check `woods_status` |
|
|
267
|
-
| Results clustered around one type | Diversity penalty insufficient for codebase shape |
|
|
389
|
+
| Results clustered around one type | Diversity penalty insufficient for codebase shape | Use explicit type filters or a more specific query; inspect ranking evidence |
|
|
390
|
+
|
|
391
|
+
## Explicit package and source-path scopes
|
|
392
|
+
|
|
393
|
+
Both `codebase_retrieve` and `search` accept `packages` and `source_paths` arrays.
|
|
394
|
+
The Ruby API uses the same keyword arguments:
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
retriever.retrieve('How are payments collected?', budget: 1200,
|
|
398
|
+
packages: ['packs/billing'], source_paths: ['packs/billing/app'])
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
These filters select eligible published units **before candidate limits**. Each
|
|
402
|
+
list uses OR; package, path, and type restrictions combine with AND. Empty or
|
|
403
|
+
omitted scope lists add no restriction. Existing unscoped calls keep their behavior.
|
|
404
|
+
|
|
405
|
+
- **Packages:** exact, case-sensitive published nearest owners. A parent package
|
|
406
|
+
excludes its nested packages unless both names are requested. `.` selects only
|
|
407
|
+
root-owned units. Missing ownership does not match a package restriction;
|
|
408
|
+
it can still match a source path. Unknown package names return an argument error;
|
|
409
|
+
a declared package with no eligible units is a valid empty scope.
|
|
410
|
+
- **Paths:** application-relative directory prefixes, compared at segment
|
|
411
|
+
boundaries. `packs/billing` includes descendants, including nested packages,
|
|
412
|
+
and excludes `packs/billing_admin`. `.` covers all published application-relative
|
|
413
|
+
paths. Repeated separators and `.` are normalized; internal `..` segments are
|
|
414
|
+
resolved, while attempts to escape the root, absolute paths, Windows paths,
|
|
415
|
+
backslashes, and NUL are rejected. External gem paths and missing paths do not
|
|
416
|
+
match. No host `realpath` or current source-file reads determine ownership.
|
|
417
|
+
- **Types and graph:** existing inclusion/exclusion rules remain authoritative.
|
|
418
|
+
Scoped discovery uses actual published unit types, including `gem_source` units
|
|
419
|
+
in the shared `rails_source` directory. Graph seeds and expansion stay within
|
|
420
|
+
eligibility. Existing bare graph edges can still be ambiguous between typed
|
|
421
|
+
units sharing an identifier; scope does not establish a missing edge target type.
|
|
422
|
+
|
|
423
|
+
`RetrievalResult#applied_scope` reports normalized lists, `eligible_units`,
|
|
424
|
+
`candidate_count`, `returned_units`, and `outcome` (`empty_scope`, `no_match`, or
|
|
425
|
+
`matched`). A matched candidate need not fit a tiny output budget. Scoped MCP
|
|
426
|
+
retrieval exposes this object in `structuredContent.data.applied_scope` and `_meta`,
|
|
427
|
+
with typed source provenance in `structuredContent.data.sources`; the budget continues
|
|
428
|
+
to apply to the context text. Scoped results omit `type_rank_context`, whose global
|
|
429
|
+
rank fields would otherwise describe the wrong population.
|
|
430
|
+
|
|
431
|
+
Search adds `applied_scope` beside its existing completeness evidence. Zero
|
|
432
|
+
eligible units means an empty scope; positive eligibility with a complete zero-match
|
|
433
|
+
search means no match. A partial zero-match search remains inconclusive.
|
|
434
|
+
|
|
435
|
+
### Storage and cost
|
|
436
|
+
|
|
437
|
+
Scope preparation reads the complete metadata snapshot from the selected store
|
|
438
|
+
bundle. Packaged discovery and lexical retrieval keep that read and the query on
|
|
439
|
+
one published generation. Discovery's deep-field scan budget applies to matching
|
|
440
|
+
after scope preparation; it does **not** cap the initial metadata read.
|
|
441
|
+
|
|
442
|
+
Vector search enumerates eligible raw vector IDs, including typed and chunk IDs,
|
|
443
|
+
and searches every batch of at most 100 IDs before merging the best candidates.
|
|
444
|
+
It embeds the query once per vector execution. In-memory, pgvector, and Qdrant
|
|
445
|
+
adapters support this without adding package fields or re-embedding old vectors.
|
|
446
|
+
Scoped pgvector queries materialize eligible rows before exact distance ranking;
|
|
447
|
+
scoped Qdrant requests use exact search. Custom adapters must explicitly support
|
|
448
|
+
native ID filters and ID enumeration; unsupported adapters return a degraded
|
|
449
|
+
error instead of falling back to global results. Broad scopes can cost more than
|
|
450
|
+
ordinary approximate search. Automatic package balancing is not enabled.
|
|
451
|
+
|
|
452
|
+
See [scope evaluation](EVALUATION.md#explicit-scope-comparison) for the matched
|
|
453
|
+
budget replay and its cross-boundary recall tradeoff.
|
|
454
|
+
|
|
455
|
+
## Compact published evidence and API outlines
|
|
456
|
+
|
|
457
|
+
`codebase_retrieve` and `Retriever#retrieve` accept an explicit `evidence` mode:
|
|
458
|
+
|
|
459
|
+
- `full` (default) preserves existing source formatting and budget truncation.
|
|
460
|
+
- `compact` chooses complete query-relevant methods and published concern display
|
|
461
|
+
blocks within each ranked unit, then relevant resolved runtime metadata.
|
|
462
|
+
- `outline` returns declared method names, kinds, lexical owners and published
|
|
463
|
+
line ranges. It is an API orientation aid, not reconstructed Ruby signatures.
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
retriever.retrieve('How are payments refunded?', budget: 1200, evidence: 'compact')
|
|
467
|
+
# MCP:
|
|
468
|
+
codebase_retrieve(query: 'How are payments refunded?', budget: 1200, evidence: 'compact')
|
|
469
|
+
lookup(identifier: 'Billing::Invoice', type: 'model', evidence: 'outline', budget: 600)
|
|
470
|
+
lookup(identifier: 'Billing::Invoice', type: 'model', evidence: 'compact', query: 'refund', budget: 800)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Ranking, candidate limits and explicit package/path scopes are unchanged. Selection
|
|
474
|
+
uses the original query: method-name term matches weigh more than body matches;
|
|
475
|
+
source order breaks ties. With no matching terms, or no lookup query, source order
|
|
476
|
+
provides deterministic orientation. Nested blocks and definitions stay inside their
|
|
477
|
+
complete containing method. A method that cannot fit is omitted whole; compact mode
|
|
478
|
+
never substitutes a broken prefix. If a heredoc body lies outside its method's
|
|
479
|
+
syntactic range, a `whole_source_fallback` span retains the complete published
|
|
480
|
+
source or omits it whole when it cannot fit. Metadata fields are also included whole. More
|
|
481
|
+
unit names fitting in an outline does not establish that their implementation was
|
|
482
|
+
shown or that retrieval quality improved.
|
|
483
|
+
|
|
484
|
+
The existing retrieval token counter also charges compact headers, notices and
|
|
485
|
+
source spans. Compact lookup defaults to 2000 estimated tokens, using four
|
|
486
|
+
characters per token. These are text-context budgets; MCP JSON envelopes and
|
|
487
|
+
structured provenance add transport/output tokens. Very small budgets may return
|
|
488
|
+
no evidence text. Check omission counts and increase the budget or request full
|
|
489
|
+
source. `query` and `budget` apply only to compact/outline lookup; combining those
|
|
490
|
+
modes with `include_source: false` or nonempty `sections` is an argument error.
|
|
491
|
+
|
|
492
|
+
### Provenance and full-source verification
|
|
493
|
+
|
|
494
|
+
MCP retrieval exposes evidence under `structuredContent.data.sources[].evidence`;
|
|
495
|
+
compact lookup uses `structuredContent.data.evidence`. Ruby retrieval carries the
|
|
496
|
+
same data in `result.sources`. Every record includes the typed unit owner, original
|
|
497
|
+
published display path, source SHA256, generation status, selected span hashes,
|
|
498
|
+
zero-based byte offsets (exclusive end), one-based line ranges and omitted spans.
|
|
499
|
+
Span owners are **lexical declarations**, not inferred runtime dispatch owners.
|
|
500
|
+
|
|
501
|
+
Coordinates are explicitly `published_unit`: they refer to the exact published
|
|
502
|
+
`source_code` bytes. Physical source coordinates are unavailable. Model/controller
|
|
503
|
+
units may contain synthesized headers or commented inlined concerns; the excerpt
|
|
504
|
+
preserves those comments verbatim and labels concern display blocks. Inherited
|
|
505
|
+
behavior may be described by runtime metadata without a corresponding source span.
|
|
506
|
+
The server never opens host application source files to fill these gaps.
|
|
507
|
+
|
|
508
|
+
Published lexical retrieval and lookup report the pinned generation when present.
|
|
509
|
+
Legacy flat indexes and standalone semantic metadata stores do not establish a
|
|
510
|
+
publication generation and report it unavailable. A current reader generation must
|
|
511
|
+
not be substituted for an older semantic artifact's unknown generation.
|
|
512
|
+
|
|
513
|
+
Each record supplies a `full_evidence` lookup call with the actual `type`,
|
|
514
|
+
`identifier`, `evidence: 'full'` and `source_sha256`. Pass that hash back to verify
|
|
515
|
+
the same source bytes; changed source produces a typed `stale_index` refusal.
|
|
516
|
+
Ordinary full lookup remains available without the hash when deliberately
|
|
517
|
+
inspecting the newest publication. Optional `type` disambiguates names shared by
|
|
518
|
+
multiple unit types and preserves actual types within mixed storage directories.
|
|
519
|
+
Full source is never disabled by compact mode. See [evaluation](EVALUATION.md#compact-evidence-comparison-403)
|
|
520
|
+
for measured tradeoffs and the limits of returned-unit metrics.
|
|
521
|
+
|
|
522
|
+
### Source-owner selection remains an offline experiment
|
|
523
|
+
|
|
524
|
+
The [owner-overlap evaluation](EVALUATION.md#source-owner-overlap-experiment-412)
|
|
525
|
+
compares complete-span overlap against current score order. It adds no retrieval
|
|
526
|
+
configuration or MCP option. Display paths alone cannot prove original ownership,
|
|
527
|
+
especially for inlined concern display; representative runtime task evidence is
|
|
528
|
+
still needed before changing which units receive context budget.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Ruby runtime trace enrichment
|
|
2
|
+
|
|
3
|
+
`Woods::RubyAnalyzer::TraceEnricher` records Ruby method events during a block
|
|
4
|
+
and can attach that evidence to Ruby method units. This is a Ruby API; Rails
|
|
5
|
+
session tracing and the MCP `trace_flow` tool are separate features.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'woods/ruby_analyzer'
|
|
9
|
+
|
|
10
|
+
traces = Woods::RubyAnalyzer::TraceEnricher.record { application.run }
|
|
11
|
+
units = Woods::RubyAnalyzer.analyze(paths: ['lib'], trace_data: traces)
|
|
12
|
+
# Or enrich units already analyzed:
|
|
13
|
+
Woods::RubyAnalyzer::TraceEnricher.merge(units: units, trace_data: traces)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Caller evidence
|
|
17
|
+
|
|
18
|
+
Each event contains `class_name`, `method_name`, `event` (`call` or `return`),
|
|
19
|
+
`method_kind`, `path`, `line`, `caller_class`, `caller_method`,
|
|
20
|
+
`caller_method_kind`, and `return_class`.
|
|
21
|
+
For an observed `Caller#invoke` calling `Callee#run`, the callee's call and
|
|
22
|
+
return events record `caller_class: 'Caller'` and `caller_method: 'invoke'`.
|
|
23
|
+
The caller method field contains the method name, not a qualified backtrace label.
|
|
24
|
+
|
|
25
|
+
The caller is the **nearest Ruby method observed by this recording**. The
|
|
26
|
+
recorder does not capture native method frames or block frames, and does not
|
|
27
|
+
infer methods already on the stack when recording begins. An unknown caller
|
|
28
|
+
has all three caller fields set to `nil`; merge omits those unknown caller entries.
|
|
29
|
+
This is scoped execution evidence, not a complete application call graph.
|
|
30
|
+
|
|
31
|
+
Stacks belong to one recording and are separate for each fiber. Recording
|
|
32
|
+
captures the current thread, including fibers running on it, and excludes
|
|
33
|
+
other threads. To record work in another thread, invoke `record` in that thread.
|
|
34
|
+
Recursion and Ruby return events during exception or nonlocal unwinds balance
|
|
35
|
+
the stack. An unmatched return, such as a fiber method entered before recording,
|
|
36
|
+
has no inferred caller. Escaping exceptions still propagate and disable the
|
|
37
|
+
recorder; a later recording starts with fresh stacks.
|
|
38
|
+
|
|
39
|
+
`merge` adds `metadata[:trace]` with `call_count`, `callers`, and `return_types`.
|
|
40
|
+
A return event during exception unwinding does not establish normal completion;
|
|
41
|
+
`return_types` summarizes the return-event values Ruby exposes. Recording does
|
|
42
|
+
not change existing method identifiers.
|
|
43
|
+
|
|
44
|
+
## Method identity
|
|
45
|
+
|
|
46
|
+
`method_kind` is `instance` or `singleton`. Merge matches the defining owner,
|
|
47
|
+
method name, and kind: `Example#run` and `Example.run` keep separate call counts
|
|
48
|
+
and return types. This identity survives JSON serialization; symbol keys and
|
|
49
|
+
symbol kind values are accepted too. Caller entries retain `caller_method_kind`
|
|
50
|
+
when present, so same-name instance and singleton callers remain distinct.
|
|
51
|
+
|
|
52
|
+
Singleton methods use their defining named class or module. If `Child.run`
|
|
53
|
+
inherits `Parent.run`, its events belong to `Parent.run`. Methods supplied by
|
|
54
|
+
an extended module retain that module's instance-method identity. Anonymous
|
|
55
|
+
owners and singleton methods on individual objects have no named unit;
|
|
56
|
+
their `class_name` is `nil`, and merge skips them.
|
|
57
|
+
|
|
58
|
+
## Older recordings
|
|
59
|
+
|
|
60
|
+
Older Woods recordings derived `caller_class` from the callee receiver and
|
|
61
|
+
`caller_method` from an interpreter-dependent backtrace offset. Their caller
|
|
62
|
+
fields can disagree or describe the callee itself. Record again to obtain
|
|
63
|
+
corrected caller evidence; merging old JSON cannot reconstruct missing callers.
|
|
64
|
+
|
|
65
|
+
Recordings without `method_kind` match **instance methods only**. The older
|
|
66
|
+
recorder used a normal owner name for instance methods and an opaque string
|
|
67
|
+
such as `#<Class:Example>` for singleton methods. Those opaque singleton-owner
|
|
68
|
+
records are skipped; record again to obtain a usable singleton identity.
|
|
69
|
+
Handwritten fixtures for `Example.run` must specify `method_kind: 'singleton'`.
|
|
70
|
+
Merge never guesses the kind from which units happen to be supplied. Unknown
|
|
71
|
+
explicit kinds are skipped. Old caller entries without a kind remain untyped.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Source freshness
|
|
2
|
+
|
|
3
|
+
`woods_status.index.source_freshness` compares the served generation's captured
|
|
4
|
+
application inputs with source bytes visible to the reader. It is separate from
|
|
5
|
+
index age, HEAD equality, daemon liveness and external database/runtime state.
|
|
6
|
+
|
|
7
|
+
This capability is unreleased after `2.0.0.beta2`. Check the installed gem's
|
|
8
|
+
`woods-extract --help`, `rake -T woods:source_status`, and `woods_status` schema
|
|
9
|
+
before using it; upgrading the plugin alone does not upgrade Woods.
|
|
10
|
+
|
|
11
|
+
## Read the result
|
|
12
|
+
|
|
13
|
+
| State | Meaning | Next step |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `current` | All covered inputs match their consumer baselines, with a verified fresh boot boundary and complete checks. A dirty checkout can be current. | Use the indexed facts within the coverage below. |
|
|
16
|
+
| `drifted` | At least one captured input differs, was removed, or a relevant input was added. | Inspect the changed paths; choose a full run or a justified targeted refresh. |
|
|
17
|
+
| `unknown` | Evidence is incomplete: for example an old index, missing source/key, scan limit, opaque symlink directory, or unproved boot/consumer boundary. | Inspect `reasons`; use a deep check or fresh full capture as appropriate. |
|
|
18
|
+
|
|
19
|
+
Drift can coexist with incomplete coverage. `reasons` reports both; absence of a
|
|
20
|
+
listed change never proves an omitted input is unchanged. `complete` describes
|
|
21
|
+
capture/traversal completion, while boot and consumer qualifications remain in
|
|
22
|
+
`reasons`. `counts` contains total observed added/changed/removed paths; each
|
|
23
|
+
`changes` list contains at most 30 paths and `truncated` marks longer lists.
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{"source_check":"deep"}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Pass this to `woods_status` for an explicit five-second content scan. The default
|
|
30
|
+
`quick` scan has a 250ms budget. Both read and HMAC source bytes; there is no
|
|
31
|
+
stat-only `current` shortcut, so same-size/same-mtime edits are detected. Limits
|
|
32
|
+
also cap traversal at 50,000 visited files and 128 MiB read. Budgets are checked
|
|
33
|
+
between filesystem operations; an operating-system read that itself stalls can
|
|
34
|
+
outlast the scan deadline. A limit produces unknown coverage, never current.
|
|
35
|
+
|
|
36
|
+
The result is tied to the **served** generation, including a reader holding an
|
|
37
|
+
older generation during a concurrent publication. It is recomputed on each call;
|
|
38
|
+
a source edit does not require an index generation change to become visible.
|
|
39
|
+
|
|
40
|
+
## Establish a fresh baseline
|
|
41
|
+
|
|
42
|
+
Run the launcher through the application's installed bundle:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bundle exec woods-extract full
|
|
46
|
+
bundle exec woods-extract incremental app/services/checkout.rb app/views/orders/show.html.erb
|
|
47
|
+
bundle exec woods-extract refresh routes controllers
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`--root PATH` selects the application root. `--output PATH` selects the index
|
|
51
|
+
(default `WOODS_OUTPUT`, otherwise `tmp/woods`); relative output paths resolve
|
|
52
|
+
under the application root. Repeat `--source-root PATH` to include additional
|
|
53
|
+
application-relative directories used by custom loaders. Use the same declaration
|
|
54
|
+
on subsequent launcher runs. Paths are separate arguments, preserving spaces,
|
|
55
|
+
commas and newlines. Refresh accepts known extractor names. Invalid arguments
|
|
56
|
+
fail before extraction; the launcher propagates the child's failure or daemon
|
|
57
|
+
stand-down exit 75. Split oversized incremental batches or choose full.
|
|
58
|
+
|
|
59
|
+
The launcher captures source before a **fresh child** evaluates its Gemfile,
|
|
60
|
+
Rakefile, Rails boot and eager loading. A private one-use handoff binds the capture
|
|
61
|
+
to root, output, action, rules, nonce and parent process. It waits for the child
|
|
62
|
+
and removes the handoff when the child exits. Source is checked again before
|
|
63
|
+
publication; edits during boot/extraction retain the earlier identity and are
|
|
64
|
+
reported, rather than being silently adopted as a current baseline.
|
|
65
|
+
|
|
66
|
+
Existing Rake tasks, direct `Extractor` calls and the watch daemon remain usable.
|
|
67
|
+
Their post-boot capture is marked `unverified_boot_boundary`; current bytes alone
|
|
68
|
+
cannot prove what a previously booted Rails process consumed. A fresh launcher
|
|
69
|
+
full run replaces that uncertainty. Hooks continue their existing refresh flow;
|
|
70
|
+
enabling hooks does not implicitly restart or replace a daemon.
|
|
71
|
+
|
|
72
|
+
## Scope and partial extraction
|
|
73
|
+
|
|
74
|
+
Coverage follows the shared file/whole-app dispatch rules and reload policy:
|
|
75
|
+
application and lib Ruby, known views/locales/tests/packages/schedules/schema and
|
|
76
|
+
boot configuration. Source-only boot coverage also includes `Rakefile`,
|
|
77
|
+
`config.ru`, root gemspecs and otherwise-unclassified Ruby helpers under `config/`.
|
|
78
|
+
Normal generated/hidden directories are pruned before traversal, using the watch
|
|
79
|
+
scanner's exclusions. Explicit source roots override generic exclusions; the
|
|
80
|
+
index output is always excluded. Contained file symlinks are checked for stable
|
|
81
|
+
resolution; directory symlinks and escaping/unreadable inputs leave uncertainty.
|
|
82
|
+
|
|
83
|
+
Every consuming scope keeps its own identities. An events scan can reread a
|
|
84
|
+
service file while its service unit remains untouched; refreshing events does
|
|
85
|
+
not certify the retained service unit. Successful file/whole-extractor work
|
|
86
|
+
updates only its scopes, including negative results and confirmed deletion.
|
|
87
|
+
Unchanged scopes keep their earlier baseline. Boot inputs advance only on a full
|
|
88
|
+
run. Partial runtime changes retain an explicit `runtime_consumption` uncertainty
|
|
89
|
+
when Woods cannot prove every retained reflected fact was re-serialized. Named
|
|
90
|
+
framework refreshes do not certify unrelated application inputs. A handled
|
|
91
|
+
extractor error retains an explicit `extractor:<name>` uncertainty even if the
|
|
92
|
+
extractor returns an empty result. Successful consumers keep their own evidence;
|
|
93
|
+
a later full run without that failure can replace the uncertainty.
|
|
94
|
+
|
|
95
|
+
Custom loader source outside captured roots, missing eager-load coverage and
|
|
96
|
+
uncaptured application-owned unit paths remain unknown. This is application
|
|
97
|
+
source evidence: it does not certify external database schemas/data, remote
|
|
98
|
+
configuration, installed gem bytes, provider state or live runtime services.
|
|
99
|
+
|
|
100
|
+
## Containers and hooks
|
|
101
|
+
|
|
102
|
+
Run the launcher inside the application container when that is where the bundle
|
|
103
|
+
and source exist, for example `docker compose exec -T app bundle exec woods-extract full`.
|
|
104
|
+
An MCP reader must see the source and original private key; otherwise it reports
|
|
105
|
+
unknown. `woods:source_status` has no Rails environment prerequisite and uses the
|
|
106
|
+
same verifier without Rails initialization or provider work. Its optional
|
|
107
|
+
Base64-encoded JSON transport supports `output`, `root` (an explicit reader-side
|
|
108
|
+
source mapping), and `mode` (`quick` or `deep`).
|
|
109
|
+
|
|
110
|
+
The opt-in SessionStart hook uses `WOODS_HOOK_RAKE` and `WOODS_OUTPUT`, including a
|
|
111
|
+
Docker command prefix without requiring a host application bundle. It prints
|
|
112
|
+
an actionable drift or unknown warning and stays quiet for current evidence.
|
|
113
|
+
Its ten-second process deadline includes command startup; the scan uses quick
|
|
114
|
+
mode. Missing older tasks and failed/timed-out commands report unknown. Cancelling
|
|
115
|
+
Docker exec does not itself prove the process inside the container stopped.
|
|
116
|
+
A quiet session hook does not acknowledge deferred PostToolUse queue entries.
|
|
117
|
+
|
|
118
|
+
## Artifact and cost
|
|
119
|
+
|
|
120
|
+
Each atomic payload contains versioned `source_inputs.json`. It records a compact
|
|
121
|
+
identity table shared by per-consumer scope/path maps, root, rules fingerprint,
|
|
122
|
+
generation, coverage and scan metrics. All file identities use HMAC-SHA256 with
|
|
123
|
+
`<output>/.source-inputs.key`, a 32-byte, owner-only private file outside payloads.
|
|
124
|
+
Low-entropy secret-bearing input files receive the same protection as ordinary
|
|
125
|
+
source; no source bytes or raw secret hashes are added to this artifact. Do not
|
|
126
|
+
publish the key with an index. Missing, insecure or mismatched keys produce
|
|
127
|
+
unknown; Woods does not silently repair permissions or rotate keys. Independent
|
|
128
|
+
outputs have different identities and must be compared using their own keys and
|
|
129
|
+
consumer semantics, not raw manifest equality.
|
|
130
|
+
|
|
131
|
+
Failed/no-op extraction does not advance the artifact's published generation.
|
|
132
|
+
Flat fallback and older indexes lack verified atomic source evidence.
|
|
133
|
+
`WOODS_PROFILE=1` reports `source capture` and `source verification` separately.
|
|
134
|
+
Capture/recheck each allow up to ten seconds with the same file/byte caps.
|
|
135
|
+
|
|
136
|
+
September 2026 fixture measurements: a pinned Writebook source tree (456 visited
|
|
137
|
+
files, 223 hashed, 233KB) completed quick scans in median 36ms native / 45ms on a
|
|
138
|
+
Linux container bind mount. Discourse (26,133 visited, 6,209 hashed, 56.4MB) needed
|
|
139
|
+
about 1.6–2 seconds; quick scans returned unknown, while five-second scans completed
|
|
140
|
+
traversal and still reported an opaque directory symlink. Native Ruby 4.0 and
|
|
141
|
+
container Ruby 3.4 differed, so these are a budget envelope, not a filesystem
|
|
142
|
+
speed comparison or a macOS virtiofs benchmark. Measure your own application;
|
|
143
|
+
a large or slow source tree may need a full extraction rather than a longer read.
|