Module: Rigor::Analysis::Incremental
- Defined in:
- lib/rigor/analysis/incremental.rb
Overview
ADR-46 slice 2 — the pure set-algebra core of the incremental step, kept side-effect-free and Runner-independent so the soundness property it encodes is unit-testable without the analysis machinery.
Given the files that changed since the baseline run and the baseline’s ‘dependents` index (Runner#file_dependents), the **affected closure** the body tier must re-analyse is the changed set plus every file that read a declaration or method body from a changed file. Every other file is served from the per-file diagnostic cache.
The soundness invariant (the Runner-driven ‘–verify-incremental` gate and the spec assert it): for an edit whose declaration-structure fingerprint is unchanged (a method-body edit — no symbol created, destroyed, moved, or re-parented), the set of files whose diagnostics actually change is a SUBSET of Incremental.affected. A file outside the closure whose diagnostics changed would be served stale — a manufactured false positive/negative, the failure mode this design exists to prevent. Structural edits (fingerprint changed) are out of this tier’s scope — they widen via the negative-dependency / full fallback path (slice 3).
Class Method Summary collapse
-
.affected(changed, dependents) ⇒ Object
The closure the body tier re-analyses.
-
.affected_with_symbols(changed_files, changed_pairs, symbol_dependents, ancestry_dependents) ⇒ Object
ADR-46 slice 4 — the symbol-granularity affected closure.
-
.appeared_classes(changed_files, decls_before, decls_after) ⇒ Object
ADR-46 slice 3 — the qualified class/module names declared in a changed file’s after-state that were absent from its before-state: a class that appeared in this edit.
-
.appeared_symbols(changed_files, fingerprints_before, fingerprints_after) ⇒ Object
ADR-46 slice 3 — the symbol keys (‘“ClassName#method”`) that are present in a changed file’s after-fingerprints but were absent from its before-fingerprints: a symbol that appeared in this edit.
-
.changed_files(before_by_file, after_by_file) ⇒ Object
The files whose per-file diagnostics differ between two runs.
-
.changed_symbol_pairs(changed_files, fingerprints_before, fingerprints_after) ⇒ Object
ADR-46 slice 4 — given a set of changed file paths and two per-file symbol fingerprint maps (before and after), returns the frozen Set of ‘[path, symbol]` pairs whose fingerprints differ (added, removed, or body-changed).
-
.invert(sources_by_consumer) ⇒ Object
Inverts a per-consumer source map (‘consumer → enumerable of source files it read from`) into the `dependents` index (`source → Set of consumers that read from it`).
-
.invert_symbols(symbol_sources_by_consumer) ⇒ Object
ADR-46 slice 4 — inverts a per-consumer symbol-sources map (‘consumer → { source_path → Set<“ClassName#method”> }`) into the symbol-level dependents index: `[source_path, symbol] → Set<consumer>`.
-
.negative_closure(keys, negative_dependents) ⇒ Object
ADR-46 slice 3 — the consumers to re-check because a name they looked up and missed (a negative dependency) now resolves.
Class Method Details
.affected(changed, dependents) ⇒ Object
The closure the body tier re-analyses. ‘changed` is any Enumerable of paths; `dependents` maps a source path to the Set of files that read from it (missing key → no dependents). Returns a frozen Set.
46 47 48 49 50 |
# File 'lib/rigor/analysis/incremental.rb', line 46 def affected(changed, dependents) closure = changed.to_set changed.each { |file| closure.merge(dependents[file] || []) } closure.freeze end |
.affected_with_symbols(changed_files, changed_pairs, symbol_dependents, ancestry_dependents) ⇒ Object
ADR-46 slice 4 — the symbol-granularity affected closure.
A consumer is included when: (a) it is itself a changed file, (b) it has an ancestry dep on a changed file (always re-checked — file-level), or © it has a symbol dep on a ‘[file, symbol]` pair that changed.
Consumers that only have symbol deps on a changed file, and none of their tracked symbols changed, are NOT included — the slice 4 precision win.
94 95 96 97 98 99 |
# File 'lib/rigor/analysis/incremental.rb', line 94 def affected_with_symbols(changed_files, changed_pairs, symbol_dependents, ancestry_dependents) closure = changed_files.to_set changed_files.each { |file| closure.merge(ancestry_dependents[file] || []) } changed_pairs.each { |pair| closure.merge(symbol_dependents[pair] || []) } closure.freeze end |
.appeared_classes(changed_files, decls_before, decls_after) ⇒ Object
ADR-46 slice 3 — the qualified class/module names declared in a changed file’s after-state that were absent from its before-state: a class that appeared in this edit. (For an added file the before-set is empty, so every class it declares appears.) A class that merely moved files still appears here, but its negative-dependents are empty, so the over-report costs nothing. Returns a frozen Set of qualified class-name Strings. ‘decls_before` / `decls_after` map a path to its Set of declared class names.
126 127 128 129 130 131 132 133 134 |
# File 'lib/rigor/analysis/incremental.rb', line 126 def appeared_classes(changed_files, decls_before, decls_after) appeared = Set.new changed_files.each do |path| before = decls_before[path] || Set.new after = decls_after[path] || Set.new appeared.merge(after - before) end appeared.freeze end |
.appeared_symbols(changed_files, fingerprints_before, fingerprints_after) ⇒ Object
ADR-46 slice 3 — the symbol keys (‘“ClassName#method”`) that are present in a changed file’s after-fingerprints but were absent from its before-fingerprints: a symbol that appeared in this edit. A symbol that merely moved between files still appears here for the destination file, but its negative-dependents set is empty (nobody missed a name that already resolved elsewhere), so the over-report costs nothing. Returns a frozen Set of symbol-key Strings.
108 109 110 111 112 113 114 115 116 |
# File 'lib/rigor/analysis/incremental.rb', line 108 def appeared_symbols(changed_files, fingerprints_before, fingerprints_after) appeared = Set.new changed_files.each do |path| before = fingerprints_before[path] || {} after = fingerprints_after[path] || {} (after.keys - before.keys).each { |sym| appeared << sym } end appeared.freeze end |
.changed_files(before_by_file, after_by_file) ⇒ Object
The files whose per-file diagnostics differ between two runs. Each argument maps a path to its diagnostic list; diagnostics are compared structurally via Diagnostic#to_h so identity / ordering of the objects themselves does not matter. A file present in one run and absent (zero diagnostics) in the other counts as changed.
153 154 155 156 157 158 159 |
# File 'lib/rigor/analysis/incremental.rb', line 153 def changed_files(before_by_file, after_by_file) (before_by_file.keys | after_by_file.keys).each_with_object(Set.new) do |path, changed| before = (before_by_file[path] || []).map(&:to_h) after = (after_by_file[path] || []).map(&:to_h) changed << path unless before == after end.freeze end |
.changed_symbol_pairs(changed_files, fingerprints_before, fingerprints_after) ⇒ Object
ADR-46 slice 4 — given a set of changed file paths and two per-file symbol fingerprint maps (before and after), returns the frozen Set of ‘[path, symbol]` pairs whose fingerprints differ (added, removed, or body-changed).
73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/rigor/analysis/incremental.rb', line 73 def changed_symbol_pairs(changed_files, fingerprints_before, fingerprints_after) pairs = Set.new changed_files.each do |path| before = fingerprints_before[path] || {} after = fingerprints_after[path] || {} (before.keys | after.keys).each do |sym| pairs << [path, sym] if before[sym] != after[sym] end end pairs.freeze end |
.invert(sources_by_consumer) ⇒ Object
Inverts a per-consumer source map (‘consumer → enumerable of source files it read from`) into the `dependents` index (`source → Set of consumers that read from it`). The reverse edge the incremental step walks. Returns a frozen hash of frozen Sets; a missing key reads as nil (the default proc is dropped before freezing).
33 34 35 36 37 38 39 40 41 |
# File 'lib/rigor/analysis/incremental.rb', line 33 def invert(sources_by_consumer) index = Hash.new { |hash, key| hash[key] = Set.new } sources_by_consumer.each do |consumer, sources| sources.each { |source| index[source] << consumer } end index.default_proc = nil index.each_value(&:freeze) index.freeze end |
.invert_symbols(symbol_sources_by_consumer) ⇒ Object
ADR-46 slice 4 — inverts a per-consumer symbol-sources map (‘consumer → { source_path → Set<“ClassName#method”> }`) into the symbol-level dependents index: `[source_path, symbol] → Set<consumer>`. Used by affected_with_symbols to limit fan-out to callers of symbols that actually changed rather than all callers of the file.
57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/rigor/analysis/incremental.rb', line 57 def invert_symbols(symbol_sources_by_consumer) index = Hash.new { |h, k| h[k] = Set.new } symbol_sources_by_consumer.each do |consumer, sources_by_file| sources_by_file.each do |source, symbols| symbols.each { |sym| index[[source, sym]] << consumer } end end index.default_proc = nil index.each_value(&:freeze) index.freeze end |
.negative_closure(keys, negative_dependents) ⇒ Object
ADR-46 slice 3 — the consumers to re-check because a name they looked up and missed (a negative dependency) now resolves. ‘keys` is the set of negative-dependency keys (`“toplevel:foo”` / `“method:C#m”`) the appeared symbols would satisfy; `negative_dependents` maps each key to the Set of consumers that recorded the miss. Returns a frozen Set of consumer paths.
142 143 144 145 146 |
# File 'lib/rigor/analysis/incremental.rb', line 142 def negative_closure(keys, negative_dependents) closure = Set.new keys.each { |key| closure.merge(negative_dependents[key] || []) } closure.freeze end |