woods 1.4.1 → 1.5.0
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 +48 -0
- data/CONTRIBUTING.md +33 -0
- data/README.md +4 -2
- data/lib/woods/extractor.rb +14 -5
- data/lib/woods/git_provenance.rb +121 -0
- data/lib/woods/index_artifact.rb +3 -3
- data/lib/woods/mcp/bootstrapper.rb +6 -5
- data/lib/woods/mcp/config_resolver.rb +42 -21
- data/lib/woods/mcp/errors.rb +5 -3
- data/lib/woods/temporal/json_snapshot_store.rb +4 -1
- data/lib/woods/temporal/snapshot_store.rb +4 -1
- data/lib/woods/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6be4722cd45cc31537b12abfe0dfc48e211aeffc4cc5ff9d0a378b3c2130eb1d
|
|
4
|
+
data.tar.gz: ba720873764980d05cb85a8598218e7061fbad8df5a9f29fb091cface39dd500
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0273f5abb61b0cdf7ba2ee95bdc8632a7e31f717d2eb039ddea18f21085bd83dd5c158497c693ccdc59e5d480be1162629fcd3324b7a9457e607d91183c4a5dc
|
|
7
|
+
data.tar.gz: fa24e32b452eb7c9f3a64267f7c1cd45149e9211611ef87432ec6ce82ca9f2955813bfe778f76cf1003daf2e0cdbf76e3b6590067f1c5fa7cf8a93ff482a0aed
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.0] - 2026-06-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Rails 6.0 support: the `railties` floor is lowered from `>= 6.1` to `>= 6.0`**
|
|
15
|
+
(#135). Woods runs cleanly on the Rails 6.0 series — extraction and index-MCP
|
|
16
|
+
serving need no 6.1-only API. The only 6.1-introduced calls touched
|
|
17
|
+
(`connection_db_config`, `has_many_inversing`) are `respond_to?`-guarded and
|
|
18
|
+
degrade on 6.0; a regression spec locks that in.
|
|
19
|
+
- **CI now runs a Rails version matrix plus a booted-app extraction test** (#136).
|
|
20
|
+
`Appraisals` + `gemfiles/rails_*.gemfile` exercise Rails 6.0, 6.1, 7.0, 7.1,
|
|
21
|
+
7.2, and 8.0; the matrix excludes invalid Ruby×Rails pairs (e.g. Rails 6.0 only
|
|
22
|
+
on Ruby 3.0) and adds a **Ruby 4.0** lane. A new booted-app test
|
|
23
|
+
(`spec/integration/booted_extraction_spec.rb`, against the minimal `spec/dummy`
|
|
24
|
+
app) boots Rails in-process and runs a real end-to-end extraction, asserting a
|
|
25
|
+
non-zero unit count and the expected models/associations — gating the
|
|
26
|
+
version-sensitive introspection path the unit suite (which stubs Rails) can't.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **The Index Server now boots in pattern-only mode by default when no embedding
|
|
31
|
+
index is present** (#138). Since 1.3.0, `woods-mcp` raised `MissingArtifact` at
|
|
32
|
+
boot unless `woods.json` existed or `WOODS_ALLOW_AUTODETECT=1` was set — which
|
|
33
|
+
surprised the most common setup: `rake woods:extract` with no embedding
|
|
34
|
+
provider, where pattern/regex/structural search works perfectly. The server now
|
|
35
|
+
auto-detects by default: it serves all always-on tools (`lookup`, `search`,
|
|
36
|
+
`dependencies`, `structure`, `graph_analysis`, `pagerank`, …) with no env var,
|
|
37
|
+
and `codebase_retrieve` (semantic search) activates automatically once an
|
|
38
|
+
embedding provider is configured. Set `WOODS_REQUIRE_INDEX=1` to restore
|
|
39
|
+
fail-closed behavior (raise `MissingArtifact` when `woods.json` is absent).
|
|
40
|
+
`WOODS_ALLOW_AUTODETECT` is retained as a no-op for backward compatibility.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **`manifest.json` no longer reports a stale `git_branch`/`git_sha` in a git
|
|
45
|
+
worktree** (#137). In a linked worktree, `.git` is a file pointing at the real
|
|
46
|
+
git directory (often an absolute host path). When that directory couldn't be
|
|
47
|
+
resolved — e.g. inside a container where the host path isn't mounted — git
|
|
48
|
+
enrichment failed silently and the branch/SHA fell back to a baked
|
|
49
|
+
`GIT_BRANCH`/`GIT_SHA` build arg, reporting an unrelated branch. A new
|
|
50
|
+
`Woods::GitProvenance` resolves provenance with worktree-aware plumbing
|
|
51
|
+
(`git -C <root> rev-parse`), and emits `"unknown"` when a `.git` is present but
|
|
52
|
+
the ref can't be resolved (the worktree case) rather than a misleading value.
|
|
53
|
+
The `GIT_BRANCH`/`GIT_SHA` env vars are honored only when there is **no** `.git`
|
|
54
|
+
at the root at all (a non-repo checkout — e.g. a Docker `COPY` that excludes
|
|
55
|
+
`.git` — with build args supplying the SHA) or git is unavailable. Temporal
|
|
56
|
+
snapshots skip an `"unknown"` SHA so it can't key or collide a snapshot.
|
|
57
|
+
|
|
10
58
|
## [1.4.1] - 2026-06-10
|
|
11
59
|
|
|
12
60
|
### Fixed
|
data/CONTRIBUTING.md
CHANGED
|
@@ -49,6 +49,39 @@ Woods has two test suites:
|
|
|
49
49
|
|
|
50
50
|
All new features need tests. Bug fixes should include a regression test.
|
|
51
51
|
|
|
52
|
+
### Rails version matrix
|
|
53
|
+
|
|
54
|
+
The gem supports `railties >= 6.0`. Coverage is split across two CI jobs:
|
|
55
|
+
|
|
56
|
+
- The base unit `test` job runs `rake spec` across **Ruby 3.0–4.0** on the
|
|
57
|
+
default (newest) Rails. The unit specs stub Rails, so they run once on the base
|
|
58
|
+
Gemfile rather than per Rails version.
|
|
59
|
+
- The `rails-matrix` job runs the **booted-app extraction test**
|
|
60
|
+
(`spec/integration/booted_extraction_spec.rb` against `spec/dummy`) under each
|
|
61
|
+
supported Rails — 6.0, 6.1, 7.0, 7.1, 7.2, 8.0 — using per-version gemfiles
|
|
62
|
+
under `gemfiles/`. This is the version-sensitive gate: it boots a real Rails
|
|
63
|
+
app in-process and runs an extraction. (The booted spec is tagged `:booted_app`
|
|
64
|
+
and excluded from the default `rake spec`; `WOODS_RUN_BOOTED_APP=1` opts it in,
|
|
65
|
+
and it must run in its own process — it can't share one with the unit suite.)
|
|
66
|
+
|
|
67
|
+
The Rails pins live in `Appraisals`; the gemfiles are hand-maintained
|
|
68
|
+
(`eval_gemfile`-ing the base `Gemfile` and pinning Rails) because Appraisal can't
|
|
69
|
+
generate from the conditional base Gemfile. To run a single Rails row locally:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.2.gemfile bundle install
|
|
73
|
+
WOODS_RUN_BOOTED_APP=1 BUNDLE_GEMFILE=gemfiles/rails_7.2.gemfile \
|
|
74
|
+
bundle exec rspec spec/integration/booted_extraction_spec.rb
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
When adding a Rails line: add it to both `Appraisals` and `gemfiles/`, and add a
|
|
78
|
+
valid Ruby×Rails pair to the `rails-matrix` job in `.github/workflows/ci.yml`.
|
|
79
|
+
**For a row below Rails 7.1**, the gemfile must set `ENV['WOODS_SQLITE3_REQ'] =
|
|
80
|
+
'~> 1.4'` *before* `eval_gemfile` (those Rails versions pin `sqlite3 ~> 1.4` in
|
|
81
|
+
their adapter at load time) and pin `concurrent-ruby '< 1.3.5'` (1.3.5 dropped
|
|
82
|
+
the implicit `require "logger"` those releases rely on under Ruby 3.x) — copy an
|
|
83
|
+
existing `gemfiles/rails_6.0.gemfile` as the template.
|
|
84
|
+
|
|
52
85
|
## Code Style
|
|
53
86
|
|
|
54
87
|
- `frozen_string_literal: true` on every file
|
data/README.md
CHANGED
|
@@ -573,8 +573,10 @@ See [Architecture](docs/ARCHITECTURE.md) for the deep dive — extraction phases
|
|
|
573
573
|
|
|
574
574
|
## Requirements
|
|
575
575
|
|
|
576
|
-
- Ruby >= 3.0
|
|
577
|
-
- Rails >= 6.
|
|
576
|
+
- Ruby >= 3.0 (through Ruby 4.0)
|
|
577
|
+
- Rails >= 6.0
|
|
578
|
+
|
|
579
|
+
CI exercises Rails 6.0, 6.1, 7.0, 7.1, 7.2, and 8.0 across Ruby 3.0–4.0 — see the supported-version table in [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md).
|
|
578
580
|
|
|
579
581
|
Works with MySQL, PostgreSQL, and SQLite. No additional infrastructure required for basic extraction — embedding and vector search are optional add-ons.
|
|
580
582
|
|
data/lib/woods/extractor.rb
CHANGED
|
@@ -10,6 +10,7 @@ require 'set'
|
|
|
10
10
|
require_relative 'filename_utils'
|
|
11
11
|
require_relative 'extracted_unit'
|
|
12
12
|
require_relative 'dependency_graph'
|
|
13
|
+
require_relative 'git_provenance'
|
|
13
14
|
require_relative 'extractors/model_extractor'
|
|
14
15
|
require_relative 'extractors/controller_extractor'
|
|
15
16
|
require_relative 'extractors/phlex_extractor'
|
|
@@ -745,6 +746,12 @@ module Woods
|
|
|
745
746
|
end
|
|
746
747
|
|
|
747
748
|
def write_manifest
|
|
749
|
+
# Worktree-aware git provenance. In a linked worktree +.git+ is a file
|
|
750
|
+
# pointing at the real git dir; when that dir is unreachable (e.g. an
|
|
751
|
+
# unmounted host path inside a container) this resolves to "unknown"
|
|
752
|
+
# rather than a stale GIT_BRANCH/GIT_SHA build arg. See GitProvenance (#137).
|
|
753
|
+
provenance = GitProvenance.new(root: Rails.root).to_h
|
|
754
|
+
|
|
748
755
|
manifest = {
|
|
749
756
|
extracted_at: Time.current.iso8601,
|
|
750
757
|
rails_version: Rails.version,
|
|
@@ -757,10 +764,9 @@ module Woods
|
|
|
757
764
|
total_units: @results.values.sum(&:size),
|
|
758
765
|
total_chunks: @results.sum { |_, units| units.sum { |u| u.chunks.size } },
|
|
759
766
|
|
|
760
|
-
# Git
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
git_branch: run_git('rev-parse', '--abbrev-ref', 'HEAD').presence || ENV['GIT_BRANCH'].presence,
|
|
767
|
+
# Git provenance (branch/sha), or "unknown" when unresolvable
|
|
768
|
+
git_sha: provenance[:git_sha],
|
|
769
|
+
git_branch: provenance[:git_branch],
|
|
764
770
|
|
|
765
771
|
# For change detection
|
|
766
772
|
gemfile_lock_sha: gemfile_lock_sha,
|
|
@@ -787,7 +793,10 @@ module Woods
|
|
|
787
793
|
return unless manifest_path.exist?
|
|
788
794
|
|
|
789
795
|
manifest = JSON.parse(File.read(manifest_path))
|
|
790
|
-
|
|
796
|
+
# Snapshots are keyed on the commit SHA — an unresolvable provenance
|
|
797
|
+
# ("unknown", see GitProvenance/#137) must not key or collide a snapshot.
|
|
798
|
+
git_sha = manifest['git_sha']
|
|
799
|
+
return if git_sha.nil? || git_sha == Woods::GitProvenance::UNKNOWN
|
|
791
800
|
|
|
792
801
|
store = build_snapshot_store
|
|
793
802
|
return unless store
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
# Resolves git provenance (branch + commit SHA) for the extracted codebase,
|
|
7
|
+
# correctly handling linked git worktrees where +.git+ is a *file* containing a
|
|
8
|
+
# +gitdir:+ pointer rather than a directory.
|
|
9
|
+
#
|
|
10
|
+
# The reported failure (#137): in a worktree, +.git+ points at a real git
|
|
11
|
+
# directory that may be an absolute host path. When extraction runs somewhere
|
|
12
|
+
# that path cannot be resolved — e.g. inside a container where the host path is
|
|
13
|
+
# not mounted — plain +git rev-parse+ fails silently and the manifest used to
|
|
14
|
+
# fall back to a baked-in +GIT_BRANCH+/+GIT_SHA+ build arg, reporting a stale,
|
|
15
|
+
# misleading branch.
|
|
16
|
+
#
|
|
17
|
+
# Resolution precedence:
|
|
18
|
+
#
|
|
19
|
+
# 1. +git -C <root> rev-parse+ — independent of the process working directory,
|
|
20
|
+
# and resolves a worktree's +.git+-file pointer when the target git directory
|
|
21
|
+
# is reachable. This covers normal checkouts and reachable worktrees.
|
|
22
|
+
# 2. If that fails, {fallback} decides:
|
|
23
|
+
# - A +.git+ entry exists at the root but git couldn't resolve the ref (an
|
|
24
|
+
# unmounted worktree gitdir, a corrupted repo): emit {UNKNOWN}. A working
|
|
25
|
+
# tree whose ref can't be determined must not be papered over with a
|
|
26
|
+
# possibly-stale build arg — that is the #137 bug.
|
|
27
|
+
# - No +.git+ at the root at all (a source tarball, or a Docker +COPY+ that
|
|
28
|
+
# excludes +.git+), or no +git+ binary: the +GIT_BRANCH+/+GIT_SHA+ env vars
|
|
29
|
+
# are the only provenance signal, so honor them if set, else {UNKNOWN}.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# provenance = Woods::GitProvenance.new(root: Rails.root)
|
|
33
|
+
# provenance.branch # => "main" (or "unknown")
|
|
34
|
+
# provenance.sha # => "a1b2c3..." (or "unknown")
|
|
35
|
+
class GitProvenance
|
|
36
|
+
# Sentinel emitted when provenance cannot be determined, in preference to a
|
|
37
|
+
# stale or misleading fallback value.
|
|
38
|
+
UNKNOWN = 'unknown'
|
|
39
|
+
|
|
40
|
+
# @param root [String, Pathname] repository root (typically +Rails.root+)
|
|
41
|
+
# @param env [Hash] environment source (default +ENV+; overridable in specs)
|
|
42
|
+
def initialize(root:, env: ENV)
|
|
43
|
+
@root = root.to_s
|
|
44
|
+
@env = env
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String] current branch name, or {UNKNOWN}
|
|
48
|
+
def branch
|
|
49
|
+
from_git = rev_parse('--abbrev-ref', 'HEAD')
|
|
50
|
+
# 'HEAD' means detached — not a branch name.
|
|
51
|
+
return from_git if present?(from_git) && from_git != 'HEAD'
|
|
52
|
+
|
|
53
|
+
fallback('GIT_BRANCH')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [String] current commit SHA, or {UNKNOWN}
|
|
57
|
+
def sha
|
|
58
|
+
from_git = rev_parse('HEAD')
|
|
59
|
+
return from_git if present?(from_git)
|
|
60
|
+
|
|
61
|
+
fallback('GIT_SHA')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Hash{Symbol => String}] +{ git_branch:, git_sha: }+ for the manifest
|
|
65
|
+
def to_h
|
|
66
|
+
{ git_branch: branch, git_sha: sha }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def present?(value)
|
|
72
|
+
value && !value.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Run git rooted at +@root+ so the result is independent of the process
|
|
76
|
+
# working directory and resolves a worktree's +.git+-file pointer when the
|
|
77
|
+
# target git directory is reachable.
|
|
78
|
+
#
|
|
79
|
+
# @return [String] stripped output, or empty string on any failure
|
|
80
|
+
def rev_parse(*args)
|
|
81
|
+
out, status = Open3.capture2('git', '-C', @root, 'rev-parse', *args)
|
|
82
|
+
status.success? ? out.strip : ''
|
|
83
|
+
rescue StandardError
|
|
84
|
+
''
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Decide what to emit when git resolution produced nothing. A baked +GIT_*+
|
|
88
|
+
# env var is honored only when this checkout is not a git working tree (no
|
|
89
|
+
# +.git+ at the root) or git is unavailable. When git is present AND a +.git+
|
|
90
|
+
# exists but the ref couldn't be resolved (unmounted worktree gitdir), emit
|
|
91
|
+
# {UNKNOWN} rather than a possibly-stale value (#137).
|
|
92
|
+
def fallback(env_key)
|
|
93
|
+
return UNKNOWN if git_available? && git_working_tree?
|
|
94
|
+
|
|
95
|
+
value = @env[env_key]
|
|
96
|
+
present?(value) ? value : UNKNOWN
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# True when the +git+ command itself can run, independent of any repository.
|
|
100
|
+
# Memoized — probed at most once per instance even though both +branch+ and
|
|
101
|
+
# +sha+ may reach {fallback}.
|
|
102
|
+
def git_available?
|
|
103
|
+
return @git_available if defined?(@git_available)
|
|
104
|
+
|
|
105
|
+
@git_available = begin
|
|
106
|
+
_out, status = Open3.capture2('git', '--version')
|
|
107
|
+
status.success?
|
|
108
|
+
rescue StandardError
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# True when +<root>/.git+ exists — a directory for a normal checkout, or a
|
|
114
|
+
# file for a linked worktree. Distinguishes "git couldn't resolve this
|
|
115
|
+
# working tree" (emit UNKNOWN) from "there is no working tree here" (the
|
|
116
|
+
# env-var build-arg path is legitimate).
|
|
117
|
+
def git_working_tree?
|
|
118
|
+
File.exist?(File.join(@root, '.git'))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/woods/index_artifact.rb
CHANGED
|
@@ -57,9 +57,9 @@ module Woods
|
|
|
57
57
|
# Returns true when the artifact has never been populated — +woods.json+
|
|
58
58
|
# is absent AND the +dumps/latest+ pointer does not exist.
|
|
59
59
|
#
|
|
60
|
-
# Once either file is present the artifact is considered non-fresh
|
|
61
|
-
# Bootstrapper
|
|
62
|
-
#
|
|
60
|
+
# Once either file is present the artifact is considered non-fresh and the
|
|
61
|
+
# Bootstrapper loads from it. A fresh artifact boots in pattern-only mode by
|
|
62
|
+
# default, raising {Woods::MCP::MissingArtifact} only under WOODS_REQUIRE_INDEX=1.
|
|
63
63
|
#
|
|
64
64
|
# @return [Boolean]
|
|
65
65
|
def fresh?
|
|
@@ -84,9 +84,10 @@ module Woods
|
|
|
84
84
|
#
|
|
85
85
|
# Flow:
|
|
86
86
|
# 1. Wrap output_dir in an IndexArtifact (owns path semantics).
|
|
87
|
-
# 2. If woods.json is present, resolve config from it; otherwise
|
|
88
|
-
#
|
|
89
|
-
#
|
|
87
|
+
# 2. If woods.json is present, resolve config from it; otherwise fall
|
|
88
|
+
# back to env-var auto-detect by default (pattern/structural mode when
|
|
89
|
+
# nothing is found). Set WOODS_REQUIRE_INDEX=1 to fail closed instead
|
|
90
|
+
# (raise MissingArtifact). See #138.
|
|
90
91
|
# 3. Build provider + stores from config (no mutation of
|
|
91
92
|
# Woods.configuration — the host's initializer stays intact).
|
|
92
93
|
# 4. Hydrate in-memory stores from dumps (stubs in PR 2; real in PR 3).
|
|
@@ -103,8 +104,8 @@ module Woods
|
|
|
103
104
|
# When nil, uses Woods.configuration.output_dir.
|
|
104
105
|
# @return [Array(Woods::Retriever, Woods::MCP::BootstrapState)]
|
|
105
106
|
# @raise [Woods::MCP::BootstrapError] on config-invalid (missing
|
|
106
|
-
# credentials, dimension mismatch, unsupported artifact, missing
|
|
107
|
-
# artifact
|
|
107
|
+
# credentials, dimension mismatch, unsupported artifact, or a missing
|
|
108
|
+
# artifact under WOODS_REQUIRE_INDEX=1).
|
|
108
109
|
def self.build_retriever(index_dir: nil)
|
|
109
110
|
state = BootstrapState.new
|
|
110
111
|
state.mark(:hydrating)
|
|
@@ -23,9 +23,12 @@ module Woods
|
|
|
23
23
|
# compatible via {ResolvedConfig#assert_compatible!}.
|
|
24
24
|
# 2. +woods.json+ snapshot alone (MCP server running without a host initializer).
|
|
25
25
|
# The stored config is used to populate +config+ in place.
|
|
26
|
-
# 3. Environment-variable auto-detect (
|
|
27
|
-
#
|
|
28
|
-
#
|
|
26
|
+
# 3. Environment-variable auto-detect (the default when +woods.json+ is
|
|
27
|
+
# absent and no host provider is set). Wires semantic search if
|
|
28
|
+
# +OPENAI_API_KEY+ or a reachable Ollama is found; otherwise leaves the
|
|
29
|
+
# provider nil so the server boots in pattern/structural-only mode.
|
|
30
|
+
# 4. +WOODS_REQUIRE_INDEX=1+ overrides (3) to fail closed, raising
|
|
31
|
+
# {MissingArtifact} when +woods.json+ is absent.
|
|
29
32
|
#
|
|
30
33
|
# @example Typical Bootstrapper usage
|
|
31
34
|
# config, source = ConfigResolver.resolve(Woods.configuration, artifact: artifact)
|
|
@@ -43,10 +46,11 @@ module Woods
|
|
|
43
46
|
# When +woods.json+ is absent and the host already has a provider
|
|
44
47
|
# configured, the host config is trusted and used as-is.
|
|
45
48
|
#
|
|
46
|
-
# When +woods.json+ is absent and the host has no provider configured
|
|
47
|
-
# -
|
|
48
|
-
#
|
|
49
|
-
#
|
|
49
|
+
# When +woods.json+ is absent and the host has no provider configured,
|
|
50
|
+
# auto-detect runs by default: pattern/structural tools always work, and
|
|
51
|
+
# semantic search wires up only if +OPENAI_API_KEY+ or a reachable Ollama
|
|
52
|
+
# is found. Set +WOODS_REQUIRE_INDEX=1+ to fail closed and raise
|
|
53
|
+
# {MissingArtifact} instead.
|
|
50
54
|
#
|
|
51
55
|
# @param config [Woods::Configuration] the live host configuration object.
|
|
52
56
|
# May be mutated in the auto-detect path.
|
|
@@ -61,7 +65,7 @@ module Woods
|
|
|
61
65
|
# where +source+ is one of +:snapshot+, +:host_config+, +:autodetect+,
|
|
62
66
|
# or +:none+. The config is the same object passed in, possibly mutated.
|
|
63
67
|
# @raise [Woods::MCP::MissingArtifact] when +woods.json+ is absent, the
|
|
64
|
-
# host has no provider configured, and +
|
|
68
|
+
# host has no provider configured, and +WOODS_REQUIRE_INDEX=1+ is set.
|
|
65
69
|
# @raise [Woods::MCP::UnsupportedArtifact] when +woods.json+ has an
|
|
66
70
|
# unsupported +schema_version+.
|
|
67
71
|
# @raise [Woods::MCP::DimensionMismatch] when stored and live provider
|
|
@@ -201,36 +205,53 @@ module Woods
|
|
|
201
205
|
|
|
202
206
|
# Handle the no-artifact, no-host-provider case.
|
|
203
207
|
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
208
|
+
# Extract-only hosts — those that ran +woods:extract+ but never
|
|
209
|
+
# +woods:embed+ and configured no embedding provider — boot in
|
|
210
|
+
# pattern/structural mode by default. The env-var auto-detect path runs,
|
|
211
|
+
# wiring semantic search only if +OPENAI_API_KEY+ or a reachable Ollama
|
|
212
|
+
# instance is found. With neither, +config+ comes back with a nil provider
|
|
213
|
+
# and {Bootstrapper.build_retriever} returns a nil retriever, leaving the
|
|
214
|
+
# always-on pattern/regex/structural tools fully functional.
|
|
215
|
+
#
|
|
216
|
+
# Set +WOODS_REQUIRE_INDEX=1+ to fail closed instead: demand a real
|
|
217
|
+
# +woods.json+ and raise {MissingArtifact} when it is absent.
|
|
218
|
+
# +WOODS_ALLOW_AUTODETECT+ is retained for backward compatibility but no
|
|
219
|
+
# longer changes behaviour — auto-detect is now the default.
|
|
206
220
|
#
|
|
207
221
|
# @param config [Woods::Configuration]
|
|
208
222
|
# @param artifact [Woods::IndexArtifact, nil]
|
|
209
223
|
# @param env [Hash]
|
|
210
224
|
# @param ollama_probe [#call, nil]
|
|
211
|
-
# @return [Woods::Configuration]
|
|
212
|
-
# @raise [Woods::MCP::MissingArtifact]
|
|
225
|
+
# @return [Array(Woods::Configuration, Symbol)]
|
|
226
|
+
# @raise [Woods::MCP::MissingArtifact] only when +WOODS_REQUIRE_INDEX=1+.
|
|
213
227
|
def self.resolve_without_artifact(config, artifact:, env:, ollama_probe:)
|
|
214
|
-
if env['
|
|
228
|
+
if env['WOODS_REQUIRE_INDEX'] == '1'
|
|
215
229
|
raise MissingArtifact.new(
|
|
216
|
-
'No woods.json found and
|
|
217
|
-
'Run `bundle exec rake woods:extract`
|
|
218
|
-
'
|
|
230
|
+
'No woods.json found and WOODS_REQUIRE_INDEX=1 demands a real index. ' \
|
|
231
|
+
'Run `bundle exec rake woods:extract` (then `woods:embed` for semantic ' \
|
|
232
|
+
'search) in your host app, or unset WOODS_REQUIRE_INDEX to boot in ' \
|
|
233
|
+
'pattern-only mode.',
|
|
219
234
|
details: { output_dir: artifact&.output_dir&.to_s }
|
|
220
235
|
)
|
|
221
236
|
end
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
resolved = autodetect_from_env(config, env: env, ollama_probe: ollama_probe)
|
|
239
|
+
unless resolved.embedding_provider
|
|
240
|
+
warn '[woods-mcp] no woods.json and no embedding provider — serving ' \
|
|
241
|
+
'pattern/structural tools only. Run `rake woods:embed`, or set ' \
|
|
242
|
+
'OPENAI_API_KEY / run Ollama, to enable semantic search.'
|
|
243
|
+
end
|
|
244
|
+
[resolved, :autodetect]
|
|
225
245
|
end
|
|
226
246
|
private_class_method :resolve_without_artifact
|
|
227
247
|
|
|
228
248
|
# Probe environment variables for provider credentials and configure
|
|
229
249
|
# +config+ accordingly.
|
|
230
250
|
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
251
|
+
# Reached by default when no +woods.json+ is present and the host
|
|
252
|
+
# configured no provider (unless +WOODS_REQUIRE_INDEX=1+). Mutates
|
|
253
|
+
# +config+ to set provider and stores when a credential or reachable
|
|
254
|
+
# Ollama instance is found; leaves the provider nil otherwise.
|
|
234
255
|
#
|
|
235
256
|
# @param config [Woods::Configuration]
|
|
236
257
|
# @param env [Hash]
|
data/lib/woods/mcp/errors.rb
CHANGED
|
@@ -72,9 +72,11 @@ module Woods
|
|
|
72
72
|
# )
|
|
73
73
|
class UnsupportedArtifact < BootstrapError; end
|
|
74
74
|
|
|
75
|
-
# Raised when +woods.json+ is absent from +output_dir+ and the
|
|
76
|
-
#
|
|
77
|
-
#
|
|
75
|
+
# Raised when +woods.json+ is absent from +output_dir+ and the operator has
|
|
76
|
+
# opted into strict mode with +WOODS_REQUIRE_INDEX=1+. By default an absent
|
|
77
|
+
# artifact is *not* an error — the server boots in pattern/structural-only
|
|
78
|
+
# mode (see {ConfigResolver.resolve_without_artifact}). Strict mode exists
|
|
79
|
+
# for deployments that want to fail closed when an index is missing.
|
|
78
80
|
#
|
|
79
81
|
# @example
|
|
80
82
|
# raise Woods::MCP::MissingArtifact.new(
|
|
@@ -29,7 +29,10 @@ module Woods
|
|
|
29
29
|
|
|
30
30
|
def capture(manifest, unit_hashes)
|
|
31
31
|
git_sha = mget(manifest, 'git_sha')
|
|
32
|
-
|
|
32
|
+
# Snapshots are keyed by commit SHA — skip a missing or non-SHA value
|
|
33
|
+
# (e.g. the "unknown" provenance sentinel, #137) rather than letting it
|
|
34
|
+
# reach snapshot_path, which raises ArgumentError on a non-hex SHA.
|
|
35
|
+
return nil unless git_sha.is_a?(String) && git_sha.match?(/\A[0-9a-f]+\z/i)
|
|
33
36
|
|
|
34
37
|
previous = find_latest
|
|
35
38
|
snapshot = build_snapshot(manifest, git_sha, unit_hashes)
|
|
@@ -85,7 +85,10 @@ module Woods
|
|
|
85
85
|
# @return [Hash] Snapshot record with diff stats
|
|
86
86
|
def capture(manifest, unit_hashes)
|
|
87
87
|
git_sha = mget(manifest, 'git_sha')
|
|
88
|
-
|
|
88
|
+
# Snapshots are keyed by commit SHA — skip a missing or non-SHA value
|
|
89
|
+
# (e.g. the "unknown" provenance sentinel, #137) so it can't key or
|
|
90
|
+
# collide a snapshot row. Mirrors JsonSnapshotStore's path validation.
|
|
91
|
+
return nil unless git_sha.is_a?(String) && git_sha.match?(/\A[0-9a-f]+\z/i)
|
|
89
92
|
|
|
90
93
|
previous = find_latest
|
|
91
94
|
upsert_snapshot(manifest, git_sha, unit_hashes.size)
|
data/lib/woods/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: woods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leah Armstrong
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mcp
|
|
@@ -64,14 +64,14 @@ dependencies:
|
|
|
64
64
|
requirements:
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '6.
|
|
67
|
+
version: '6.0'
|
|
68
68
|
type: :runtime
|
|
69
69
|
prerelease: false
|
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '6.
|
|
74
|
+
version: '6.0'
|
|
75
75
|
description: |
|
|
76
76
|
Woods extracts structured data from Rails applications for use in
|
|
77
77
|
AI-assisted development tooling. It provides version-specific context by
|
|
@@ -233,6 +233,7 @@ files:
|
|
|
233
233
|
- lib/woods/formatting/generic_adapter.rb
|
|
234
234
|
- lib/woods/formatting/gpt_adapter.rb
|
|
235
235
|
- lib/woods/formatting/human_adapter.rb
|
|
236
|
+
- lib/woods/git_provenance.rb
|
|
236
237
|
- lib/woods/graph_analyzer.rb
|
|
237
238
|
- lib/woods/index_artifact.rb
|
|
238
239
|
- lib/woods/mcp/bearer_auth.rb
|