woods 2.0.0.beta1 → 2.0.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/woods.rb CHANGED
@@ -24,6 +24,8 @@
24
24
  require_relative 'woods/version'
25
25
  # Configuration's `component_paths` default lives with the discovery it feeds.
26
26
  require_relative 'woods/extractors/component_discovery'
27
+ # Configuration defaults the cycle caps to the analyzer's own constants.
28
+ require_relative 'woods/graph_analyzer'
27
29
 
28
30
  module Woods
29
31
  class Error < StandardError; end
@@ -146,7 +148,9 @@ module Woods
146
148
  :cache_store, :cache_options,
147
149
  :dump_retention_count, :component_paths
148
150
  attr_reader :embedding_model, :max_context_tokens, :similarity_threshold, :extractors, :pretty_json,
149
- :context_format, :cache_enabled, :volatile_dependency_ratio
151
+ :context_format, :cache_enabled, :volatile_dependency_ratio,
152
+ :graph_cycle_limit, :graph_cycle_max_length,
153
+ :incremental_blast_radius_depth, :durable_payload_writes
150
154
 
151
155
  def initialize # rubocop:disable Metrics/MethodLength
152
156
  @output_dir = nil # Resolved lazily; Rails.root is nil at require time
@@ -205,6 +209,15 @@ module Woods
205
209
  @cache_options = {} # { redis: client, cache: store, ttl: { embeddings: 86400, ... } }
206
210
  @dump_retention_count = 3
207
211
  @volatile_dependency_ratio = 3.0
212
+ @graph_cycle_limit = GraphAnalyzer::DEFAULT_CYCLE_LIMIT
213
+ @graph_cycle_max_length = GraphAnalyzer::DEFAULT_CYCLE_MAX_LENGTH
214
+ # nil = the unbounded transitive closure. See the setter for why the
215
+ # default is not a small number.
216
+ @incremental_blast_radius_depth = nil
217
+ # false = payload files are written without a per-file fsync and the
218
+ # whole payload is flushed once before `generation.json` is written.
219
+ # See the setter.
220
+ @durable_payload_writes = false
208
221
  # Directories the component extractors walk before reading `descendants`,
209
222
  # relative to Rails.root. See Extractors::ComponentDiscovery.
210
223
  @component_paths = Extractors::ComponentDiscovery::DEFAULT_COMPONENT_PATHS.dup
@@ -274,6 +287,29 @@ module Woods
274
287
  @extractors = value
275
288
  end
276
289
 
290
+ # Force an fsync on every payload file as it is written, on top of the
291
+ # single flush the publish already performs.
292
+ #
293
+ # Off by default, and off is not a weaker guarantee. Readers resolve only
294
+ # through `generation.json`; a payload file has no reader until that
295
+ # pointer names it, and {Woods::Extractor#sync_payload} makes the whole
296
+ # payload durable before the pointer is written. Turning this on buys
297
+ # exactly one thing: an individual payload file being durable before the
298
+ # pointer exists.
299
+ #
300
+ # It costs a lot for it. Two forced flushes per file, 8.9ms each on btrfs:
301
+ # 8000 units is 71s of writing against 1s.
302
+ #
303
+ # Turning it on cannot disable the publish flush. That flush *is* the
304
+ # durability contract, so it has no opt-out.
305
+ #
306
+ # @param value [Boolean]
307
+ # @raise [Woods::ConfigurationError] when value is not a boolean
308
+ def durable_payload_writes=(value)
309
+ validate_boolean!(:durable_payload_writes, value)
310
+ @durable_payload_writes = value
311
+ end
312
+
277
313
  # @param value [Boolean] Must be true or false
278
314
  # @raise [ConfigurationError] if value is not a boolean
279
315
  def pretty_json=(value)
@@ -312,6 +348,46 @@ module Woods
312
348
  @volatile_dependency_ratio = value.to_f
313
349
  end
314
350
 
351
+ # How many distinct cycles {Woods::GraphAnalyzer#cycles} enumerates before
352
+ # it stops. `nil` removes the cap and restores exhaustive enumeration.
353
+ #
354
+ # @param value [Integer, nil] must be a positive Integer, or nil
355
+ # @raise [ConfigurationError] otherwise
356
+ def graph_cycle_limit=(value)
357
+ @graph_cycle_limit = validate_optional_positive_integer!(:graph_cycle_limit, value)
358
+ end
359
+
360
+ # The longest cycle {Woods::GraphAnalyzer#cycles} records, in distinct
361
+ # nodes. `nil` removes the cap.
362
+ #
363
+ # @param value [Integer, nil] must be a positive Integer, or nil
364
+ # @raise [ConfigurationError] otherwise
365
+ def graph_cycle_max_length=(value)
366
+ @graph_cycle_max_length = validate_optional_positive_integer!(:graph_cycle_max_length, value)
367
+ end
368
+
369
+ # How many reverse hops an incremental run walks from a changed file
370
+ # before it stops re-extracting dependents. `nil` (the default) keeps the
371
+ # unbounded transitive closure.
372
+ #
373
+ # A unit's extracted content is mostly a function of its own source and
374
+ # its own reflection, so on most graphs a small cap re-extracts the same
375
+ # bytes far faster. It is not safe everywhere, which is why the default
376
+ # is unbounded: an STI grandchild reads its grandparent's reflection
377
+ # (`reflect_on_all_associations`, `_validators` and the callback chain
378
+ # are all inherited), so `SportsCar < Car < Vehicle` has content that
379
+ # changes when `Vehicle` changes while sitting two hops away in the
380
+ # graph. A nested `has_many :through` resolves through the same kind of
381
+ # chain. Set this only on a tree you know has neither.
382
+ #
383
+ # @param value [Integer, nil] must be a positive Integer, or nil
384
+ # @raise [ConfigurationError] otherwise
385
+ def incremental_blast_radius_depth=(value)
386
+ @incremental_blast_radius_depth = validate_optional_positive_integer!(
387
+ :incremental_blast_radius_depth, value
388
+ )
389
+ end
390
+
315
391
  # Accepted for forward compatibility. Nothing reads {gem_configs}; gem
316
392
  # source indexing is not implemented.
317
393
  #
@@ -325,6 +401,15 @@ module Woods
325
401
 
326
402
  private
327
403
 
404
+ # @return [Integer, nil] +value+ when it is nil or a positive Integer
405
+ # @raise [ConfigurationError] otherwise
406
+ def validate_optional_positive_integer!(name, value)
407
+ return value if value.nil?
408
+ return value if value.is_a?(Integer) && value.positive?
409
+
410
+ raise ConfigurationError, "#{name} must be a positive Integer or nil, got #{value.inspect}"
411
+ end
412
+
328
413
  def validate_boolean!(name, value)
329
414
  return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
330
415
 
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: 2.0.0.beta1
4
+ version: 2.0.0.beta2
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-09-09 00:00:00.000000000 Z
11
+ date: 2026-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mcp
@@ -435,10 +435,10 @@ licenses:
435
435
  - MIT
436
436
  metadata:
437
437
  homepage_uri: https://github.com/lost-in-the/woods
438
- source_code_uri: https://github.com/lost-in-the/woods/tree/v2.0.0.beta1
439
- changelog_uri: https://github.com/lost-in-the/woods/blob/v2.0.0.beta1/CHANGELOG.md
438
+ source_code_uri: https://github.com/lost-in-the/woods/tree/v2.0.0.beta2
439
+ changelog_uri: https://github.com/lost-in-the/woods/blob/v2.0.0.beta2/CHANGELOG.md
440
440
  bug_tracker_uri: https://github.com/lost-in-the/woods/issues
441
- documentation_uri: https://github.com/lost-in-the/woods/tree/v2.0.0.beta1/docs
441
+ documentation_uri: https://github.com/lost-in-the/woods/tree/v2.0.0.beta2/docs
442
442
  rubygems_mfa_required: 'true'
443
443
  post_install_message:
444
444
  rdoc_options: []