Class: Rigor::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/configuration.rb,
lib/rigor/configuration/dependencies.rb,
lib/rigor/configuration/severity_profile.rb,
sig/rigor.rbs

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Modules: SeverityProfile Classes: Dependencies

Constant Summary collapse

DISCOVERY_ORDER =

File-discovery order for Configuration.load(nil).

The first file present is loaded; the others are NOT implicitly merged. To extend a base config explicitly the winning file MUST list the base via includes:.

.rigor.yml is a developer-local override (typically gitignored); .rigor.dist.yml is the project default (committed to the repo). When both are present the developer's local override wins outright — there is no implicit auto-merge.

%w[.rigor.yml .rigor.dist.yml].freeze
DEFAULT_PATH =

Back-compat alias. Keep here so external callers that read Configuration::DEFAULT_PATH for help text / fixture paths still work; the discovery list is the canonical source.

Returns:

  • (String)
DISCOVERY_ORDER.first
BUILTIN_EXCLUDES =

Built-in exclusion patterns appended to exclude: so vendored dependencies, Bundler artefacts, and JavaScript node_modules are never analysed by accident when a directory glob expands. Users cannot disable these defaults; the trade-off is that analysing any of these paths is essentially never what the user wants (they're build outputs / external dependencies, not source).

We deliberately keep this list narrow. tmp/ and similar directories vary across project layouts (Rails has tmp/, libraries usually don't); user-supplied exclude: entries in .rigor.yml cover the project-specific cases.

%w[
  **/vendor/bundle/**
  **/.bundle/**
  **/node_modules/**
].freeze
DEFAULTS =

Returns:

  • (Hash[String, untyped])
{
  "target_ruby" => "4.0",
  "paths" => ["lib"],
  "exclude" => [],
  "plugins" => [],
  "disable" => [],
  "libraries" => [],
  "signature_paths" => nil,
  # ADR-17 — project-side monkey-patch pre-evaluation. Empty by default; users opt in by listing explicit
  # files that the analyzer walks before per-file inference so patched-method declarations are visible
  # across the project (e.g. `lib/core_ext/string_extensions.rb`). Slice 1 plumbing only — listed files
  # are validated at config-load time (`pre-eval.file-not-found` on a missing path), but the dispatcher
  # tier consuming the registry lands in slice 2.
  "pre_eval" => [],
  # ADR-22 — baseline file path. nil (default) means no baseline is loaded; the `false` literal is
  # treated as the explicit-disable form for `.rigor.yml`-side override of an upstream `.rigor.dist.yml`
  # `baseline:` declaration. The presence of `.rigor-baseline.yml` on disk alone does NOT activate
  # filtering — the path must be named here (WD2 (b) of ADR-22).
  "baseline" => nil,
  "fold_platform_specific_paths" => false,
  # ADR-67 WD6a — opt-in call-site parameter type inference on the `check` walk. When `true`, `rigor
  # check` runs the {Inference::ParameterInferenceCollector} as a one-round pre-pass into the discovery
  # seed, so an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved
  # call-site argument types (precision-additive only — the in-body negative rules decline on the inferred
  # lower bound, WD6b). `false` (the default, resolved off by every severity profile) keeps the table empty
  # and the run byte-identical to today. Composes with `--incremental` (the WD6c mutual exclusion is
  # lifted): the incremental session recomputes the table each run and diffs it against its snapshot,
  # re-checking any callee whose seeds moved.
  "parameter_inference" => false,
  "cache" => {
    "path" => ".rigor/cache",
    # LRU eviction cap in bytes (ADR-54 WD3). The least-recently-used entries are removed at the end of a
    # run when the total exceeds this limit. The 256 MB default exists to reap orphans — entries whose
    # content key nothing references any more (an rbs gem bump, a signature change) and that no run
    # would otherwise ever delete; a full active per-project set is ~2 MB, so the cap never touches live
    # entries. Set explicitly to `null` to disable eviction (pre-WD3 behaviour: the cache grows until
    # `--clear-cache`).
    "max_bytes" => 268_435_456,
    # ADR-87 WD1 — file-freshness validation strategy. `"stat"` validates a recorded file
    # dependency by stat-ing it first and re-hashing only when the `(size, mtime_ns, ctime_ns, inode)`
    # tuple moved (or the racy window fires), so an unchanged monorepo hashes ~0 bytes on a warm run.
    # `"digest"` restores the pre-ADR-87 behaviour of SHA-256'ing every recorded file on every run — the
    # per-run escape hatch is the `RIGOR_STRICT_VALIDATION=1` env var, which wins over this setting.
    # `"auto"` (default, #190) resolves per environment: `"digest"` when {CiDetector} recognises a CI
    # provider — a fresh checkout regenerates every stat tuple, so the stat tier can never short-circuit
    # there and stat-signature glob slots would recompute on every run — and `"stat"` everywhere else.
    # A self-hosted runner with a persistent workspace opts back into the stat floor with an explicit
    # `"stat"` (or `RIGOR_CI_DETECT=0`).
    "validation" => "auto"
  },
  "plugins_io" => {
    "network" => "disabled",
    "allowed_paths" => [],
    "allowed_url_hosts" => []
  },
  "severity_profile" => "balanced",
  "severity_overrides" => {},
  # ADR-50 § WD2 — bleeding-edge overlay opt-in. Selects which of the *next major's* queued changes
  # ({Rigor::BleedingEdge}) this project adopts early. Orthogonal to `severity_profile:`. Accepts `false`
  # (default — adopt none), `true` (adopt the whole overlay), a list of feature ids (adopt only those),
  # or `{ all: true, except: [ids] }` (adopt all but the named). A queued change is either a severity
  # promotion (composed by {SeverityProfile.resolve}) or a behaviour switch (read at its call site
  # through {#bleeding_edge_active?}); `bleeding_edge:` selects both kinds the same way.
  "bleeding_edge" => false,
  "dependencies" => {
    "source_inference" => [],
    "budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
  },
  "parallel" => {
    # ADR-15 Phase 4c — when greater than zero, `rigor check` dispatches per-file analysis across N
    # Ractor workers built around {Rigor::Analysis::WorkerSession}. `0` (default) keeps the sequential
    # coordinator path bit-for-bit unchanged. The CLI's `--workers=N` flag and the `RIGOR_RACTOR_WORKERS`
    # env var both override this setting; precedence is CLI > env > config > 0.
    "workers" => 0
  },
  "bundler" => {
    # Open item O4 — target-project Bundler awareness. When `bundle_path:` is set (or auto-detected),
    # Rigor walks `<bundle_path>/ruby/*/gems/*/sig/` and adds each gem-shipped sig directory to
    # `signature_paths:`. With O7's failure-memo in place, conflicts (a vendored sig already declares the
    # same constant) degrade gracefully to "no RBS env" with a single-line warning naming the offending
    # file, rather than hanging.
    #
    # `bundle_path:` (String, optional): explicit path to the bundler install root (e.g.,
    # "vendor/bundle" or an absolute path). Resolved relative to the project root (`paths:`'s base) when
    # relative.
    #
    # `auto_detect:` (Boolean, default true): when no explicit `bundle_path:` is set, try
    # `.bundle/config`'s `BUNDLE_PATH:` first; fall back to `vendor/bundle/` under the project root if it
    # exists. When neither is found, no extra sigs are added — the analyzer sees only rigor's vendored
    # RBS and the user's `signature_paths:`.
    #
    # O4 Layer 3 keys:
    #
    # `lockfile:` (String, optional): explicit path to a `Gemfile.lock`. Resolved relative to the
    # project root when relative. When set (or auto-detected via the `auto_detect:` flag below) Rigor
    # parses the lockfile and uses it to FILTER the bundle-discovered `sig/` directories: only gems whose
    # `(name, version, platform)` matches a lockfile entry are admitted to `signature_paths:`. Stale or
    # out-of-band gems sitting in the bundle install tree are silently dropped.
    #
    # `auto_detect:` (Boolean, also gates the lockfile search): when true and `lockfile:` is nil, look
    # for `<project_root>/Gemfile.lock`.
    "bundle_path" => nil,
    "auto_detect" => true,
    "lockfile" => nil
  },
  "rbs_collection" => {
    # Open item O4 Layer 3 slice 2 — `rbs collection install` awareness. When the target project has been
    # set up with `rbs collection install`, the resulting `rbs_collection.lock.yaml` carries the resolved
    # (gem, version, source) triples and `.gem_rbs_collection/` holds the downloaded `.rbs` files. Rigor
    # parses the lockfile and auto-feeds each gem's `<collection_root>/<name>/<version>/` directory into
    # `RbsLoader`'s `signature_paths:`. Sources of type `stdlib` are skipped because rigor's bundled
    # `DEFAULT_LIBRARIES` already covers that surface.
    #
    # `lockfile:` (String, optional): explicit path to `rbs_collection.lock.yaml`. Resolved relative to
    # the project root when relative.
    #
    # `auto_detect:` (Boolean, default true): when no explicit `lockfile:` is set, look for
    # `<project_root>/rbs_collection.lock.yaml`.
    "lockfile" => nil,
    "auto_detect" => true
  }
}.freeze
RESERVED_NAMESPACES =

Top-level keys this implementation DECLARES but never reads — see docs/internal-spec/config.md § "Reserved namespaces" and ADR-99.

A sibling implementation (rigor-rs, which vendors our schema rather than keeping its own) groups keys for concepts we do not have under its own namespace, so one .rigor.yml feeds both. Such a key answers to the SCHEMA TIER ONLY: it is type-checked where it is written, and here it is never read, never validated, never coerced, and never an error however invalid its value.

That is already the emergent behaviour — #initialize fetches each key it owns and never enumerates the rest — so this constant does not gate the runtime. It exists to make the reservation FINDABLE by the next person adding config validation (#166 is exactly that), and to give the schema-parity spec something to key on: a reserved namespace is by definition absent from DEFAULTS, so the DEFAULTS-driven schema gate can never see it.

%w[rigor_rs].freeze
KNOWN_KEYS =

Every top-level key a conforming .rigor.yml may carry: the keys this implementation owns, plus includes:, plus the namespaces reserved for another implementation. Anything else is recorded in #unknown_keys and warned about by Rigor::ConfigAudit, and is also the did-you-mean dictionary for a near-miss.

includes: is a load-time directive — load_with_includes deletes it while merging, so it never reaches #initialize and could not be seen as unknown regardless. It is listed because a user legitimately writes it, so a typo like include: must be able to suggest it.

(DEFAULTS.keys + %w[includes] + RESERVED_NAMESPACES).freeze
AUTOWIRED_RBS_INLINE_GEM =

ADR-93 WD2 — the one bundled plugin default-wired without a plugins: entry. rigor-rbs-inline is the gem; rbs-inline is its manifest id (the loader raises on a duplicate id, so both forms count as "already listed").

"rigor-rbs-inline"
AUTOWIRED_RBS_INLINE_ID =
"rbs-inline"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data = DEFAULTS) ⇒ Configuration

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

Parameters:

  • data (Hash[String, untyped]) (defaults to: DEFAULTS)


397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/rigor/configuration.rb', line 397

def initialize(data = DEFAULTS)
  # Record before the per-key fetches below discard the evidence. Top level only, deliberately —
  # see {ConfigAudit.unknown_key_warnings} for why a nested check cannot key on DEFAULTS.
  @unknown_keys = (data.keys.map(&:to_s) - KNOWN_KEYS).sort.freeze

  cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
  plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))

  @target_ruby = coerce_target_ruby(data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")))
  @paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s).freeze
  user_excludes = Array(data.fetch("exclude", DEFAULTS.fetch("exclude"))).map(&:to_s)
  @exclude_patterns = (BUILTIN_EXCLUDES + user_excludes).uniq.freeze
  @plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map do |entry|
    coerce_plugin_entry(entry)
  end.freeze
  @disabled_rules = Array(data.fetch("disable", DEFAULTS.fetch("disable"))).map(&:to_s).freeze
  @libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
  sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
  @signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
  @pre_eval = expand_pre_eval_entries(
    Array(data.fetch("pre_eval", DEFAULTS.fetch("pre_eval"))).map(&:to_s)
  )
  @baseline_path = coerce_baseline_path(data.fetch("baseline", DEFAULTS.fetch("baseline")))
  @fold_platform_specific_paths = data.fetch(
    "fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
  )
  # ADR-67 WD6a — resolve to a strict Boolean so a truthy non-`true` value (e.g. a stray String) does not
  # silently enable the gate; only the literal `true` activates the check-walk collector pre-pass.
  @parameter_inference = data.fetch("parameter_inference", DEFAULTS.fetch("parameter_inference")) == true
  @cache_path = cache.fetch("path").to_s
  raw_max = cache.fetch("max_bytes")
  @cache_max_bytes = raw_max.nil? ? nil : Integer(raw_max)
  @cache_validation = coerce_cache_validation(cache.fetch("validation", "auto"))
  @plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
  @plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
  @plugins_io_allowed_url_hosts = Array(plugins_io.fetch("allowed_url_hosts")).map(&:to_s).freeze
  @severity_profile = coerce_severity_profile(
    data.fetch("severity_profile", DEFAULTS.fetch("severity_profile"))
  )
  @severity_overrides = coerce_severity_overrides(
    data.fetch("severity_overrides", DEFAULTS.fetch("severity_overrides"))
  )
  @bleeding_edge = coerce_bleeding_edge(
    data.fetch("bleeding_edge", DEFAULTS.fetch("bleeding_edge"))
  )
  @bleeding_edge_severity_overrides = BleedingEdge.severity_overrides_for(@bleeding_edge)
  @bleeding_edge_active_ids = BleedingEdge.active_ids_for(@bleeding_edge)
  @dependencies = Dependencies.from_h(
    data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
  )
  parallel = DEFAULTS.fetch("parallel").merge(data.fetch("parallel", {}))
  @parallel_workers = coerce_parallel_workers(parallel.fetch("workers"))
  bundler = DEFAULTS.fetch("bundler").merge(data.fetch("bundler", {}))
  bp = bundler.fetch("bundle_path")
  @bundler_bundle_path = bp.nil? ? nil : bp.to_s.dup.freeze
  @bundler_auto_detect = bundler.fetch("auto_detect") == true
  lf = bundler.fetch("lockfile")
  @bundler_lockfile = lf.nil? ? nil : lf.to_s.dup.freeze
  rbs_collection = DEFAULTS.fetch("rbs_collection").merge(data.fetch("rbs_collection", {}))
  rclf = rbs_collection.fetch("lockfile")
  @rbs_collection_lockfile = rclf.nil? ? nil : rclf.to_s.dup.freeze
  @rbs_collection_auto_detect = rbs_collection.fetch("auto_detect") == true
  # Ractor migration Phase 2a: deep-freeze the Configuration so it is `Ractor.shareable?`. Every ivar
  # above is now either a frozen value (Symbol / nil / Boolean) or an explicitly frozen collection /
  # value object; freezing `self` makes the whole carrier safe to send across Ractor boundaries (and
  # catches accidental post-init mutation in any caller). See `docs/design/20260514-ractor-migration.md`.
  freeze
end

Instance Attribute Details

#baseline_pathString? (readonly)

Returns the value of attribute baseline_path.

Returns:

  • (String, nil)


204
205
206
# File 'lib/rigor/configuration.rb', line 204

def baseline_path
  @baseline_path
end

#bleeding_edgeObject (readonly)

Returns the value of attribute bleeding_edge.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def bleeding_edge
  @bleeding_edge
end

#bleeding_edge_severity_overridesObject (readonly)

Returns the value of attribute bleeding_edge_severity_overrides.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def bleeding_edge_severity_overrides
  @bleeding_edge_severity_overrides
end

#bundler_auto_detectObject (readonly)

Returns the value of attribute bundler_auto_detect.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def bundler_auto_detect
  @bundler_auto_detect
end

#bundler_bundle_pathObject (readonly)

Returns the value of attribute bundler_bundle_path.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def bundler_bundle_path
  @bundler_bundle_path
end

#bundler_lockfileObject (readonly)

Returns the value of attribute bundler_lockfile.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def bundler_lockfile
  @bundler_lockfile
end

#cache_max_bytesObject (readonly)

Returns the value of attribute cache_max_bytes.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def cache_max_bytes
  @cache_max_bytes
end

#cache_pathString (readonly)

Returns the value of attribute cache_path.

Returns:

  • (String)


204
205
206
# File 'lib/rigor/configuration.rb', line 204

def cache_path
  @cache_path
end

#cache_validationObject (readonly)

Returns the value of attribute cache_validation.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def cache_validation
  @cache_validation
end

#dependenciesObject (readonly)

Returns the value of attribute dependencies.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def dependencies
  @dependencies
end

#disabled_rulesObject (readonly)

Returns the value of attribute disabled_rules.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def disabled_rules
  @disabled_rules
end

#exclude_patternsObject (readonly)

Returns the value of attribute exclude_patterns.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def exclude_patterns
  @exclude_patterns
end

#fold_platform_specific_pathsObject (readonly)

Returns the value of attribute fold_platform_specific_paths.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def fold_platform_specific_paths
  @fold_platform_specific_paths
end

#librariesObject (readonly)

Returns the value of attribute libraries.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def libraries
  @libraries
end

#parallel_workersObject (readonly)

Returns the value of attribute parallel_workers.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def parallel_workers
  @parallel_workers
end

#parameter_inferenceObject (readonly)

Returns the value of attribute parameter_inference.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def parameter_inference
  @parameter_inference
end

#pathsArray[String] (readonly)

Returns the value of attribute paths.

Returns:

  • (Array[String])


204
205
206
# File 'lib/rigor/configuration.rb', line 204

def paths
  @paths
end

#pluginsArray[String] (readonly)

Returns the value of attribute plugins.

Returns:

  • (Array[String])


204
205
206
# File 'lib/rigor/configuration.rb', line 204

def plugins
  @plugins
end

#plugins_io_allowed_pathsObject (readonly)

Returns the value of attribute plugins_io_allowed_paths.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def plugins_io_allowed_paths
  @plugins_io_allowed_paths
end

#plugins_io_allowed_url_hostsObject (readonly)

Returns the value of attribute plugins_io_allowed_url_hosts.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def plugins_io_allowed_url_hosts
  @plugins_io_allowed_url_hosts
end

#plugins_io_networkObject (readonly)

Returns the value of attribute plugins_io_network.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def plugins_io_network
  @plugins_io_network
end

#pre_evalObject (readonly)

Returns the value of attribute pre_eval.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def pre_eval
  @pre_eval
end

#rbs_collection_auto_detectObject (readonly)

Returns the value of attribute rbs_collection_auto_detect.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def rbs_collection_auto_detect
  @rbs_collection_auto_detect
end

#rbs_collection_lockfileObject (readonly)

Returns the value of attribute rbs_collection_lockfile.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def rbs_collection_lockfile
  @rbs_collection_lockfile
end

#severity_overridesObject (readonly)

Returns the value of attribute severity_overrides.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def severity_overrides
  @severity_overrides
end

#severity_profileObject (readonly)

Returns the value of attribute severity_profile.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def severity_profile
  @severity_profile
end

#signature_pathsObject (readonly)

Returns the value of attribute signature_paths.



204
205
206
# File 'lib/rigor/configuration.rb', line 204

def signature_paths
  @signature_paths
end

#target_rubyString (readonly)

Returns the value of attribute target_ruby.

Returns:

  • (String)


204
205
206
# File 'lib/rigor/configuration.rb', line 204

def target_ruby
  @target_ruby
end

#unknown_keysObject (readonly)

Top-level keys the loaded config carried that this implementation does not own — neither a DEFAULTS key, nor includes:, nor a reserved namespace. Recorded rather than acted on: an unknown key stays as inert at run time as it has always been, and Rigor::ConfigAudit turns the record into a warning. Empty for every conforming config.

This exists because #initialize fetches each key it owns and never enumerates the rest, so by the time anything holds a Configuration the unknown keys are gone. The audit reads a Configuration, so without this the class of mistake it exists to catch — a value that silently resolves to nothing — was structurally invisible to it for whole keys.



202
203
204
# File 'lib/rigor/configuration.rb', line 202

def unknown_keys
  @unknown_keys
end

Class Method Details

.autowire_default_plugins(data) ⇒ Object

ADR-93 WD2 — default-wire the bundled rigor-rbs-inline plugin, in WD1's annotation-gated, magic-comment-free mode, when the upstream rbs-inline library is resolvable and the user has not already listed the plugin. Runs from load only — the real-project route — never from a bare Configuration.new, so the suite's unit constructions do not auto-wire. The gate is presence-based (ADR-72's shape): an annotation-free project pays only a comment scan and contributes nothing, so wiring it on cannot regress the run. This partially reverses the ADR-27/ADR-31 auto-load deferral for this one bundled plugin (the executed code is already vendored, not arbitrary third-party plugin code). Opt out with a plugins: entry disabling it (enabled: false), or per-file # rbs_inline: disabled.



252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/rigor/configuration.rb', line 252

def self.autowire_default_plugins(data)
  entries = Array(data["plugins"])
  return data if entries.any? { |entry| rbs_inline_plugin_entry?(entry) }
  return data unless rbs_inline_library_resolvable?

  merged = data.dup
  merged["plugins"] = entries + [{
    "gem" => AUTOWIRED_RBS_INLINE_GEM,
    "id" => AUTOWIRED_RBS_INLINE_ID,
    "config" => { "require_magic_comment" => false }
  }]
  merged
end

.discoverString?

Returns the path to the config file Rigor would load under auto-discovery, or nil when neither candidate exists. Public so the CLI / spec drift checks can introspect the resolved file.

Returns:

  • (String, nil)


294
295
296
# File 'lib/rigor/configuration.rb', line 294

def self.discover
  DISCOVERY_ORDER.find { |candidate| File.exist?(candidate) }
end

.load(path = nil) ⇒ Configuration

Loads a configuration file.

path == nil triggers auto-discovery against DISCOVERY_ORDER. The first present file in that list is loaded; if none exist the built-in DEFAULTS are used.

When a path is supplied (whether by auto-discovery or by the caller) the YAML body is processed for includes: recursively, and every relative path inside path-bearing keys (paths:, signature_paths:, plugins_io.allowed_paths:, includes:) is resolved against THAT file's directory. The resolution is per-file: an included file's relative paths resolve against the included file's directory, not the top-level file. Path resolution mirrors PHPStan.

Parameters:

  • path (String, nil) (defaults to: nil)

Returns:



227
228
229
230
231
232
233
234
235
236
# File 'lib/rigor/configuration.rb', line 227

def self.load(path = nil)
  resolved = path || discover
  data =
    if resolved.nil? || !File.exist?(resolved)
      DEFAULTS
    else
      DEFAULTS.merge(load_with_includes(resolved))
    end
  new(autowire_default_plugins(data))
end

.load_with_includes(path, visited: Set.new) ⇒ Hash[String, untyped]

Reads path (which MUST exist) plus every file listed in its includes: chain, merging them under the order: included files first (in declaration order), then the current file's own keys override. Relative paths inside each file are resolved against that file's directory. Public so the CLI can run the include-aware load before applying --treat-all-as-inline-rbs's plugin injection (see Rigor::CLI::CheckCommand#load_check_configuration).

Parameters:

  • path (String)
  • visited: (Set[String]) (defaults to: Set.new)

Returns:

  • (Hash[String, untyped])

Raises:

  • (ArgumentError)


303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/rigor/configuration.rb', line 303

def self.load_with_includes(path, visited: Set.new)
  absolute = File.expand_path(path)
  raise ArgumentError, "circular include: #{absolute}" if visited.include?(absolute)

  raw = YAML.safe_load_file(absolute, aliases: false) || {}
  raise ArgumentError, "config file must be a YAML mapping: #{absolute}" unless raw.is_a?(Hash)

  base_dir = File.dirname(absolute)
  includes = Array(raw.delete("includes") || [])
  data = resolve_paths_in(raw, base_dir)
  next_visited = visited + [absolute]
  merge_includes(data, includes, base_dir, next_visited)
end

.rbs_inline_library_resolvable?Boolean

A probe with no load side effect: it does not require the library, only asks whether RubyGems can resolve the name. Gem::Specification.find_by_name raises Gem::MissingSpecError (a Gem::LoadError) when the gem is absent — the standalone gem install rigortype case, WD3's residual. ADR-90's project-bundle fallback resolves the same name at the plugin's own require time.

Returns:

  • (Boolean)


285
286
287
288
289
290
# File 'lib/rigor/configuration.rb', line 285

def self.rbs_inline_library_resolvable?
  Gem::Specification.find_by_name("rbs-inline")
  true
rescue Gem::LoadError
  false
end

.rbs_inline_plugin_entry?(entry) ⇒ Boolean

True when a plugins: entry (String or Hash, string- or symbol-keyed) already references the rigor-rbs-inline plugin by gem name or by manifest id. Single-homed here so the CLI's --treat-all-as-inline-rbs injection and the WD2 auto-wire gate agree on what "already listed" means.

Returns:

  • (Boolean)


269
270
271
272
273
274
275
276
277
278
279
# File 'lib/rigor/configuration.rb', line 269

def self.rbs_inline_plugin_entry?(entry)
  case entry
  when String
    entry == AUTOWIRED_RBS_INLINE_GEM
  when Hash
    string_keyed = entry.to_h { |k, v| [k.to_s, v] }
    string_keyed["gem"] == AUTOWIRED_RBS_INLINE_GEM || string_keyed["id"] == AUTOWIRED_RBS_INLINE_ID
  else
    false
  end
end

.resolve_path_key!(out, key, base_dir) ⇒ Object



345
346
347
348
349
# File 'lib/rigor/configuration.rb', line 345

def self.resolve_path_key!(out, key, base_dir)
  return unless out.key?(key) && !out[key].nil?

  out[key] = Array(out[key]).map { |p| File.expand_path(p.to_s, base_dir) }
end

.resolve_plugins_io_paths!(out, base_dir) ⇒ Object



351
352
353
354
355
356
357
358
# File 'lib/rigor/configuration.rb', line 351

def self.resolve_plugins_io_paths!(out, base_dir)
  plugins_io = out["plugins_io"]
  return unless plugins_io.is_a?(Hash) && plugins_io["allowed_paths"]

  duped = plugins_io.dup
  duped["allowed_paths"] = Array(plugins_io["allowed_paths"]).map { |p| File.expand_path(p.to_s, base_dir) }
  out["plugins_io"] = duped
end

Instance Method Details

#bleeding_edge_active?(id) ⇒ Boolean

ADR-50 § WD2 — whether the bleeding-edge feature id is in force for this run.

This is how a behaviour feature reaches its call site: a severity feature composes itself into bleeding_edge_severity_overrides and needs no engine code, but a queued change to a measurement, an algorithm, or a default has to be asked about where it happens. Answering from Configuration keeps the selection logic in one place and puts nothing but the id — already ADR-50 WD1 contract vocabulary — into engine code.

Two deliberate asymmetries with the bleeding_edge: config key:

  • An id in BleedingEdge::GRADUATED answers true unconditionally. Graduation (§ WD7) turns a feature on for everyone, so a gate that outlives it keeps the graduated behaviour rather than silently reverting, and the call-site cleanup can land whenever it is convenient.
  • An id that is in neither BleedingEdge::FEATURES nor BleedingEdge::GRADUATED raises. An unknown id in a config file stays inert on purpose (it may come from a newer gem — see #coerce_bleeding_edge), but an id here is written by a Rigor contributor against the registry in the same checkout: a typo is a bug, and reading it as false would ship the feature permanently off with no signal.

Parameters:

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)

    if id names no known feature.



544
545
546
547
548
549
550
551
552
553
# File 'lib/rigor/configuration.rb', line 544

def bleeding_edge_active?(id)
  return true if BleedingEdge.graduated?(id)
  unless BleedingEdge.known_id?(id)
    raise ArgumentError,
          "unknown bleeding-edge feature id #{id.inspect}; known ids: " \
          "#{(BleedingEdge.feature_ids + BleedingEdge::GRADUATED).inspect}"
  end

  @bleeding_edge_active_ids.include?(id)
end

#cache_validation_strict?(env = ENV) ⇒ Boolean

Resolves the cache.validation tri-state to the boolean the run installs via Rigor::Cache::FileDigest.with_run's strict:. Explicit "digest" / "stat" win outright; the "auto" default is strict exactly when Rigor::CiDetector recognises a CI provider (#190) — deliberately NOT stored at construction so the resolution honours the environment of the run, not of Configuration.load, and the frozen carrier stays env-independent. RIGOR_STRICT_VALIDATION=1 is enforced separately inside Rigor::Cache::FileDigest.strict_validation? and wins over all three values.

Returns:

  • (Boolean)


473
474
475
476
477
478
479
# File 'lib/rigor/configuration.rb', line 473

def cache_validation_strict?(env = ENV)
  case cache_validation
  when "digest" then true
  when "stat" then false
  else !CiDetector.detect(env).nil?
  end
end

#to_hHash[String, untyped]

rubocop:disable Metrics/MethodLength, Metrics/AbcSize

Returns:

  • (Hash[String, untyped])


481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/rigor/configuration.rb', line 481

def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
  {
    "target_ruby" => target_ruby,
    "paths" => paths,
    "exclude" => exclude_patterns - BUILTIN_EXCLUDES,
    "plugins" => plugins,
    "disable" => disabled_rules,
    "libraries" => libraries,
    "signature_paths" => signature_paths,
    "pre_eval" => pre_eval,
    "fold_platform_specific_paths" => fold_platform_specific_paths,
    "parameter_inference" => parameter_inference,
    "cache" => {
      "path" => cache_path,
      "max_bytes" => cache_max_bytes,
      "validation" => cache_validation
    },
    "plugins_io" => {
      "network" => plugins_io_network.to_s,
      "allowed_paths" => plugins_io_allowed_paths,
      "allowed_url_hosts" => plugins_io_allowed_url_hosts
    },
    "severity_profile" => severity_profile.to_s,
    "severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
    "bleeding_edge" => bleeding_edge_to_h,
    "dependencies" => dependencies.to_h,
    "parallel" => {
      "workers" => parallel_workers
    },
    "bundler" => {
      "bundle_path" => bundler_bundle_path,
      "auto_detect" => bundler_auto_detect,
      "lockfile" => bundler_lockfile
    },
    "rbs_collection" => {
      "lockfile" => rbs_collection_lockfile,
      "auto_detect" => rbs_collection_auto_detect
    }
  }
end

#with_bleeding_edge(value) ⇒ Object

ADR-50 § WD2 — returns a sibling Configuration whose bleeding-edge selection (and the derived bleeding_edge_severity_overrides the two Rigor::Configuration::SeverityProfile.resolve sites consult) is replaced by value, leaving every other field shared with the receiver. value takes the same forms as the bleeding_edge: config key — false / true / a feature-id Array / { "all" => true, "except" => [...] } — and is normalised through the same #coerce_bleeding_edge path, so an unknown id stays inert.

The CLI's --bleeding-edge[=ids] / --no-bleeding-edge flag uses this to override the configured selection for a single run (the same CLI-over-config precedence --workers and --no-cache follow). It is a frozen dup with the bleeding-edge ivars re-set: dup returns an unfrozen shallow copy (every other ivar is the receiver's deeply-frozen value, safe to share read-only), the replacements are themselves deeply frozen, and the re-freeze keeps the result Ractor.shareable? for the worker path. Every ivar #initialize derives from @bleeding_edge has to be re-derived here — the selector, the severity map, and the active-id set the #bleeding_edge_active? predicate reads.



569
570
571
572
573
574
575
576
577
# File 'lib/rigor/configuration.rb', line 569

def with_bleeding_edge(value)
  selector = coerce_bleeding_edge(value)
  copy = dup
  copy.instance_variable_set(:@bleeding_edge, selector)
  copy.instance_variable_set(:@bleeding_edge_severity_overrides,
                             BleedingEdge.severity_overrides_for(selector))
  copy.instance_variable_set(:@bleeding_edge_active_ids, BleedingEdge.active_ids_for(selector))
  copy.freeze
end