Class: Rigor::Analysis::IncrementalSession
- Inherits:
-
Object
- Object
- Rigor::Analysis::IncrementalSession
- Defined in:
- lib/rigor/analysis/incremental_session.rb
Overview
ADR-46 slice 2 — the in-memory incremental orchestrator that composes the recorded dependency graph (Runner#file_dependents), the affected closure (Rigor::Analysis::Incremental.affected), and the subset-analysis hook (Runner ‘analyze_only:`) into a working incremental re-check.
‘#baseline` runs a full analysis with dependency recording and keeps, per analyzed file, its diagnostics (the cache), its content digest, and the per-file source set (to maintain the dependents index across rounds). `#recheck` digests the files again, computes the changed set ΔF, re-analyzes only `ΔF ∪ dependents`, and serves every other analyzed file from the cache — the body tier.
The invariant the verify harness (and the spec) assert: ‘#recheck`’s merged diagnostics are byte-identical (as a sorted set) to a full ‘–no-cache` re-analysis of the edited tree. This is the `–verify-incremental` acceptance gate, here without disk persistence or CLI wiring (the cache is in-process). It models the body tier only: an edit that adds / removes / moves a file is outside the analyzed set it maintains and falls to a fresh #baseline (the structural tier is a later slice).
Defined Under Namespace
Classes: Recheck
Instance Method Summary collapse
-
#affected_closure(changed, added, removed) ⇒ Object
The frozen set of files a #recheck must re-analyse: the symbol/ancestry-granularity closure of the changed files (slice 4), the added files themselves, the consumers of any symbol / class that appeared in a changed OR added file (slice 3 — a now-defined ‘call.unresolved-toplevel` target or `def.override-*` ancestor), and the consumers of every removed file (which now miss what it provided).
-
#analyzed_files ⇒ Object
The project files analyzed at the last baseline / recheck — the set a verify pass partitions and the merge subtracts the affected closure from.
-
#baseline ⇒ Object
Full baseline analysis with recording.
-
#current_files ⇒ Object
The current project file set (cheap directory expansion, no analysis), used to detect files added / removed since the last run.
-
#initialize(configuration:, paths: nil) ⇒ IncrementalSession
constructor
A new instance of IncrementalSession.
-
#reanalyze_subset(subset) ⇒ Object
Verification engine (the ‘–verify-incremental` gate): with NO source edit, re-analyze `subset` fresh and serve every other analyzed file from the baseline cache.
-
#recheck ⇒ Object
Re-check after on-disk edits, including files added or removed since the last run (the structural tier).
-
#run_incremental(snapshot:, fingerprint:) ⇒ Object
Cross-process incremental run (the ‘–incremental` flag’s engine).
Constructor Details
#initialize(configuration:, paths: nil) ⇒ IncrementalSession
Returns a new instance of IncrementalSession.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/rigor/analysis/incremental_session.rb', line 38 def initialize(configuration:, paths: nil) @configuration = configuration @paths = paths @cache = {} # analyzed path => [Diagnostic] @sources = {} # analyzed path => Set<source path it read from> @digests = {} # analyzed path => content digest at last analysis @analyzed = [] # the project files analyzed last round @dependents = {} # inverted @sources (file-level) # ADR-46 slice 4 — symbol-granularity tracking. @symbol_sources = {} # consumer => { source_path => Set<"ClassName#method"> } @ancestry_sources = {} # consumer => Set<source_path> (class-ancestry deps) @symbol_fingerprints = {} # path => { "ClassName#method" => sha256_hex } @symbol_dependents = {} # [source, symbol] => Set<consumer> @ancestry_dependents = {} # source => Set<consumer> (inverted ancestry_sources) # ADR-46 slice 3 — negative (missing) dependencies: a consumer that # looked up a name and resolved nothing must be re-checked when that # name later appears (e.g. a `call.unresolved-toplevel` whose target # is defined by a later edit). @missing = {} # consumer => Set<"kind:name"> it looked up and missed @negative_dependents = {} # "kind:name" => Set<consumer> (inverted @missing) @class_decls = {} # path => Set<qualified class name declared in the file> end |
Instance Method Details
#affected_closure(changed, added, removed) ⇒ Object
The frozen set of files a #recheck must re-analyse: the symbol/ancestry-granularity closure of the changed files (slice 4), the added files themselves, the consumers of any symbol / class that appeared in a changed OR added file (slice 3 — a now-defined ‘call.unresolved-toplevel` target or `def.override-*` ancestor), and the consumers of every removed file (which now miss what it provided). An added file has no before-state, so all its symbols / classes appear.
107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/rigor/analysis/incremental_session.rb', line 107 def affected_closure(changed, added, removed) scan = changed + added new_fps = symbol_fingerprints_for(scan) new_class_decls = class_declarations_for(scan) changed_pairs = Incremental.changed_symbol_pairs(changed, @symbol_fingerprints, new_fps) base = if changed_pairs.any? || changed.any? { |f| @ancestry_dependents[f] } Incremental.affected_with_symbols(changed, changed_pairs, @symbol_dependents, @ancestry_dependents) else Incremental.affected(changed, @dependents) end closure = base | added.to_set | negative_affected(scan, new_fps, new_class_decls) removed.each { |path| closure |= @dependents[path] || Set.new } closure.freeze end |
#analyzed_files ⇒ Object
The project files analyzed at the last baseline / recheck — the set a verify pass partitions and the merge subtracts the affected closure from.
64 65 66 |
# File 'lib/rigor/analysis/incremental_session.rb', line 64 def analyzed_files @analyzed end |
#baseline ⇒ Object
Full baseline analysis with recording. Returns the run’s diagnostics; populates the in-process cache + dependency state.
70 71 72 73 74 75 76 77 78 |
# File 'lib/rigor/analysis/incremental_session.rb', line 70 def baseline runner = build_runner(record_dependencies: true) diagnostics = run_runner(runner).diagnostics @analyzed = runner.analyzed_files absorb_dependency_graph(runner) @cache = per_file(diagnostics) @digests = @analyzed.to_h { |path| [path, digest(path)] } diagnostics end |
#current_files ⇒ Object
The current project file set (cheap directory expansion, no analysis), used to detect files added / removed since the last run.
124 125 126 127 |
# File 'lib/rigor/analysis/incremental_session.rb', line 124 def current_files runner = build_runner @paths ? runner.analysis_file_set(@paths) : runner.analysis_file_set end |
#reanalyze_subset(subset) ⇒ Object
Verification engine (the ‘–verify-incremental` gate): with NO source edit, re-analyze `subset` fresh and serve every other analyzed file from the baseline cache. Because nothing on disk changed, the merged result MUST equal a full analysis — so this exercises the subset-analysis and cache-merge paths against a known-good oracle (a full `–no-cache` run) for an arbitrary partition, without mutating session state. Returns the merged diagnostics.
137 138 139 140 141 142 143 |
# File 'lib/rigor/analysis/incremental_session.rb', line 137 def reanalyze_subset(subset) affected = subset.to_set runner = build_runner(analyze_only: affected) fresh = run_runner(runner).diagnostics reused = @analyzed - affected.to_a fresh + reused.flat_map { |path| @cache[path] || [] } end |
#recheck ⇒ Object
Re-check after on-disk edits, including files added or removed since the last run (the structural tier). Re-analyzes only the affected closure and serves the rest from cache; refreshes the cache + dependency state so a subsequent #recheck sees the new world.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/rigor/analysis/incremental_session.rb', line 84 def recheck previous = @analyzed current = current_files added = current - previous removed = previous - current changed = (current & previous).reject { |path| digest(path) == @digests[path] } affected = affected_closure(changed, added, removed) analyze_set = affected & current runner = build_runner(analyze_only: analyze_set, record_dependencies: true) fresh = run_runner(runner).diagnostics reused = (current & previous) - affected.to_a merged = fresh + reused.flat_map { |path| @cache[path] || [] } absorb(runner, fresh, current, analyze_set, removed) Recheck.new(diagnostics: merged, changed: changed.to_set, affected: affected, reused: reused.to_set) end |
#run_incremental(snapshot:, fingerprint:) ⇒ Object
Cross-process incremental run (the ‘–incremental` flag’s engine). With a disk ‘snapshot` whose `fingerprint` matches, restore the prior per-file state and `#recheck` (re-analyze only the changed closure, serve the rest from the restored cache); otherwise run a full `#baseline`. Either way, persist the updated snapshot for the next process. Returns `[diagnostics, warm]` — `warm` is true when a snapshot was restored. A nil `fingerprint` (uncomputable inputs) disables persistence: a plain full run.
153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/rigor/analysis/incremental_session.rb', line 153 def run_incremental(snapshot:, fingerprint:) restored = fingerprint && snapshot.load(fingerprint: fingerprint) if restored restore(restored) diagnostics = recheck.diagnostics warm = true else diagnostics = baseline warm = false end snapshot.save(fingerprint: fingerprint, payload: to_payload) if fingerprint [diagnostics, warm] end |