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[ΔF], 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).
The class-length budget is relaxed: this is one cohesive orchestrator of the incremental state
(per-file diagnostics cache, the file-level / symbol-level / negative dependency graphs, and the ADR-85
seed bundles), clearer read together than split across micro-classes that would all share the same ivars.
Defined Under Namespace
Classes: Recheck
Instance Attribute Summary collapse
-
#opaque_plugin_ids ⇒ Object
readonly
ADR-88 WD1 — reporting hooks the CLI reads after #run_incremental.
Instance Method Summary collapse
-
#affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new) ⇒ 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-topleveltarget ordef.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.
-
#behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) ⇒ Object
ADR-89 WD2 —
changed_pairsminus the behaviourally-STABLE pairs whose symbol dependents may be skipped. - #comment_ingesting_plugin_loaded? ⇒ Boolean
-
#current_files ⇒ Object
The current project file set (cheap directory expansion, no analysis), used to detect files added / removed since the last run.
- #declaration_stable?(path, current_declaration_signature) ⇒ Boolean
-
#declaration_unstable(changed, declaration_signatures) ⇒ Object
ADR-89 WD1 — the changed files whose ancestry / file-level dependents must STILL be re-checked: those not provably declaration-stable.
-
#dependents_base(unstable, symbol_pairs) ⇒ Object
The dependents contributed by the declaration-unstable changed files and the behaviourally-unstable symbol pairs: the ADR-46 slice-4 symbol-granular fan-out when either is present (ancestry deps of the unstable files + symbol deps of the changed pairs), else the coarse file-level fan-out.
- #fact_surface_invalidated? ⇒ Boolean
-
#fresh_param_table(current, changed, added, removed) ⇒ Object
ADR-67 WD6c lift — the fresh whole-project inferred-param table for this recheck ({} when the gate is off).
-
#initialize(configuration:, paths: nil, environment: nil, cache_store: nil, plugin_requirer: nil, workers: 0, buffer: nil) ⇒ IncrementalSession
constructor
A new instance of IncrementalSession.
-
#param_seed_closure(closure, param_files) ⇒ Object
ADR-67 WD6c lift — the seed-invalidated callees' own contribution to the closure: the files themselves, plus — on a pre-slice-4 snapshot with no symbol edges, where the pairs' symbol fan-out found nothing — their file-level dependents (wider, always sound).
-
#param_seed_invalidation(fresh_params) ⇒ Object
ADR-67 WD6c lift — the
[files, pairs]the fresh table invalidates. -
#reanalyze_subset(subset) ⇒ Object
Verification engine (the
--verify-incrementalgate): with NO source edit, re-analyzesubsetfresh 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_buffer_recheck(snapshot:, fingerprint:) ⇒ Object
Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one file, for the CLI's
--incremental --tmp-file=X --instead-of=Y. -
#run_incremental(snapshot:, fingerprint:, persist: true) ⇒ Object
Cross-process incremental run (the
--incrementalflag's engine).
Constructor Details
#initialize(configuration:, paths: nil, environment: nil, cache_store: nil, plugin_requirer: nil, workers: 0, buffer: nil) ⇒ IncrementalSession
Returns a new instance of IncrementalSession.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/rigor/analysis/incremental_session.rb', line 64 def initialize(configuration:, paths: nil, environment: nil, cache_store: nil, plugin_requirer: nil, workers: 0, buffer: nil) @configuration = configuration @paths = paths # Editor mode option B (#146) — the in-flight buffer, threaded into every internal Runner so the # pre-passes and the closure re-analysis read the editor's bytes at the logical path. A session # holding one MUST NOT persist its snapshot: its `@digests` and `@cache` describe bytes that exist # only in the editor. {#run_buffer_recheck} is the only entry that honours that. @buffer = buffer @environment = environment @cache_store = cache_store @plugin_requirer = plugin_requirer @workers = workers # ADR-85 WD2 — per-file discovery seed bundles keyed by logical path. A cold baseline builds them; a # warm recheck folds them (re-walking only changed files) and refreshes the set. Ride the snapshot. @seed_bundles = {} @cache = {} # analyzed path => [Diagnostic] @sources = {} # analyzed path => Set<source path it read from> @digests = {} # analyzed path => ADR-87 packed stat-digest entry 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> # ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects: }. # Harvested from the ADR-84 return memo after each run; drives the behavioural-stability gate. @return_summaries = {} # ADR-67 WD6c lift — the `parameter_inference:` seed table the cached diagnostics were computed # under (`{}` when the gate is off — the gate state is constant across a snapshot's lifetime because # the configuration is part of the global fingerprint). A recheck recomputes the table fresh and # diffs it against this: the pre-pass is whole-project by design, so the fresh table is ground truth # and a missing invalidation edge is impossible by construction — the reason this is a table diff # and not the caller→callee edge recording #204 first sketched. @param_table = {} # ADR-88 WD1 — the plugin fact-surface digest computed for THIS invocation (nil until a # `#run_incremental` pass runs / a plugin-free project) and the reporting flags a caller (the CLI # banner + `--cache-stats`) reads after `#run_incremental`. `@last_runner` is the analysis runner the # post-hoc fingerprint reads from; `@plugin_fact_reusable` is the computed decision object. @plugin_fact_digest = nil @opaque_plugin_ids = [].freeze @fact_surface_invalidated = false @last_runner = nil @plugin_fact_reusable = nil end |
Instance Attribute Details
#opaque_plugin_ids ⇒ Object (readonly)
ADR-88 WD1 — reporting hooks the CLI reads after #run_incremental. fact_surface_invalidated? is true
when a valid snapshot was dropped for a fact-surface reason (a plugin sig/catalog edit, or an opaque
plugin) rather than a cold miss. opaque_plugin_ids names the contributing-but-surfaceless plugins that
force a full run every invocation.
454 455 456 |
# File 'lib/rigor/analysis/incremental_session.rb', line 454 def opaque_plugin_ids @opaque_plugin_ids end |
Instance Method Details
#affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new) ⇒ 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.
ADR-67 WD6c lift — param_files / param_pairs are the callee files (and their [file, symbol]
pairs) whose inferred-param seeds moved since the snapshot. Their text is unchanged, so they enter
the closure here: the files themselves re-analyse (their in-body diagnostics were computed under the
old seeds), and their pairs join the SYMBOL fan-out — a seed change shifts the callee's inferred
return exactly the way a body edit does, so it reuses the same audited dependents machinery. The
pairs join AFTER the ADR-89 WD2 behavioural-stability pruning: that gate re-evaluates returns under
the snapshot's OLD seeds, which is the wrong oracle for a pair whose seeds are the thing that moved.
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/rigor/analysis/incremental_session.rb', line 213 def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new) scan = changed + added # Parse the changed / added set ONCE for the per-symbol fingerprints, the class declarations, AND the # ADR-89 WD1 declaration signatures. They were separate `discovered_def_index_for_paths` passes over # the same `scan` set — a duplicate re-parse of every changed file each recheck (recon §2 / the P6 # recheck-floor audit). # `buffer:` is load-bearing for editor mode option B (#146), not an optimisation: this scan decides # the closure, so reading the buffer's logical path from DISK would compare the snapshot's symbol # fingerprints against bytes the user has already edited away — every dependent of the unsaved change # would then be served from cache, which is the stale answer whole-project editor scope exists to # avoid. `ScopeIndexer.scan_summary_for_paths` resolves each path through the binding. summary = scan.empty? ? nil : Inference::ScopeIndexer.scan_summary_for_paths(scan, buffer: @buffer) scan_index = summary && summary[:def_index] declaration_signatures = (summary && summary[:declaration_signatures]) || {} new_fps = symbol_fingerprints_from_index(scan_index) new_class_decls = class_declarations_from_index(scan_index) changed_pairs = Incremental.changed_symbol_pairs(changed, @symbol_fingerprints, new_fps) # ADR-89 WD1 — a changed file whose DECLARATION signature (per-def parameter shape / visibility / # ancestry / member layout / def line, bodies excluded) is byte-identical to the snapshot is # declaration-stable: every cross-file fact its ancestry / file-level dependents consume is # declaration-derived and therefore unchanged (a body edit is consumed only by SYMBOL dependents, # governed below by `changed_pairs`), so those dependents are skipped. Only the declaration-UNSTABLE # changed files contribute their ancestry / file-level dependents; the changed files themselves are # always re-analysed (`changed.to_set`) to learn their own diagnostics. This generalises the B1 # comment-only gate (code-stable ⟹ declaration-stable) to same-line body edits. unstable = declaration_unstable(changed, declaration_signatures) # ADR-89 WD2 — a declaration-stable changed def whose behavioural surface (return type at every # previously-observed call key + content-mutation effects) is unchanged is behaviourally stable: its # symbol dependents' cached diagnostics stay valid, so drop them. `symbol_pairs` is `changed_pairs` # minus those stable pairs, and only it (not `changed_pairs`) drives the symbol-dependent fan-out. symbol_pairs = behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) | param_pairs base = dependents_base(unstable, symbol_pairs) closure = base | changed.to_set | added.to_set | negative_affected(scan, new_fps, new_class_decls) closure = param_seed_closure(closure, param_files) 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.
121 122 123 |
# File 'lib/rigor/analysis/incremental_session.rb', line 121 def analyzed_files @analyzed end |
#baseline ⇒ Object
Full baseline analysis with recording. Returns the run's diagnostics; populates the in-process cache
- dependency state.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/rigor/analysis/incremental_session.rb', line 127 def baseline runner = build_runner(record_dependencies: true) diagnostics = run_runner(runner).diagnostics @last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry. @analyzed = runner.analyzed_files @seed_bundles = runner.seed_bundles # ADR-85 WD2 — the freshly built bundle set for the next run. absorb_dependency_graph(runner) @return_summaries = runner.return_summaries # ADR-89 WD2 — the full-run behavioural surface. # ADR-67 WD6c lift — the seed table the runner's own pre-pass computed ({} when the gate is off). # Reading it back, rather than computing it here, keeps the baseline single-collect. @param_table = runner.param_inferred_types @cache = per_file(diagnostics) @digests = @analyzed.to_h { |path| [path, pack_digest(path)] } diagnostics end |
#behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) ⇒ Object
ADR-89 WD2 — changed_pairs minus the behaviourally-STABLE pairs whose symbol dependents may be
skipped. A pair [path, "Class#method"] is a candidate when its file is declaration-stable (WD1), it
carries a persisted return summary, and the (edited) def is GATE-ELIGIBLE — its only cross-file body
surfaces are its return type and its content-mutation effects (no instance/class variable write, no
yield, no implicit-self call whose transitive effect a caller could observe). For a candidate the
session compares its content-mutation effects (pure AST) then re-evaluates its return at every
persisted key (a runner probe); a pair passes only when BOTH are unchanged. Any missing summary,
ineligible def, changed effect, changed return, or uncomputable key keeps the pair — the conservative
direction the --verify-incremental and byte-identical recheck specs backstop.
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/rigor/analysis/incremental_session.rb', line 321 def behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) return changed_pairs if @return_summaries.empty? || scan_index.nil? candidates = changed_pairs.select do |pair| !unstable.include?(pair.first) && @return_summaries.key?(pair) && gate_eligible_def?(scan_def_node(pair.last, scan_index)) end return changed_pairs if candidates.empty? effect_stable = candidates.select { |pair| effects_unchanged?(pair, scan_index) } return changed_pairs if effect_stable.empty? stable = return_stable_pairs(effect_stable, scan_index) stable.empty? ? changed_pairs : (changed_pairs - stable) end |
#comment_ingesting_plugin_loaded? ⇒ Boolean
361 362 363 364 365 366 367 368 |
# File 'lib/rigor/analysis/incremental_session.rb', line 361 def comment_ingesting_plugin_loaded? # Mirrors the plugin loader's gem-name resolution (`ProjectPrePasses#trusted_gem_name`): a String # entry IS the gem name; a Hash entry names it under `"gem"` (or the manifest `"id"`). @configuration.plugins.any? do |entry| name = entry.is_a?(Hash) ? (entry["gem"] || entry["id"]) : entry COMMENT_INGESTING_PLUGIN_IDS.include?(name.to_s) end end |
#current_files ⇒ Object
The current project file set (cheap directory expansion, no analysis), used to detect files added / removed since the last run.
372 373 374 375 |
# File 'lib/rigor/analysis/incremental_session.rb', line 372 def current_files runner = build_runner @paths ? runner.analysis_file_set(@paths) : runner.analysis_file_set end |
#declaration_stable?(path, current_declaration_signature) ⇒ Boolean
352 353 354 355 356 357 358 359 |
# File 'lib/rigor/analysis/incremental_session.rb', line 352 def declaration_stable?(path, current_declaration_signature) return false if current_declaration_signature.nil? bundle = @seed_bundles[path] return false if bundle.nil? bundle[:declaration_signature] == current_declaration_signature end |
#declaration_unstable(changed, declaration_signatures) ⇒ Object
ADR-89 WD1 — the changed files whose ancestry / file-level dependents must STILL be re-checked: those not provably declaration-stable. A changed file is declaration-STABLE (its ancestry / file-level dependents skippable) when its current ScopeIndexer.declaration_signature matches the one stored in the snapshot's seed bundle — i.e. its edit changed no method signature, visibility, ancestry, member layout, def line, or method existence, so every cross-file DECLARATION fact its dependents consume is unchanged. Falls back to treating EVERY changed file as unstable (today's full closure) when a comment-ingesting plugin is loaded — such a plugin reads the very comments the signature ignores, so a comment edit it treats as a no-op could change a cross-file type. Sorbet sigs / dry-types includes are CODE, captured by ADR-88's plugin-fact fingerprint (WD3), so only comment-as-input plugins escape here.
346 347 348 349 350 |
# File 'lib/rigor/analysis/incremental_session.rb', line 346 def declaration_unstable(changed, declaration_signatures) return changed if comment_ingesting_plugin_loaded? changed.reject { |path| declaration_stable?(path, declaration_signatures[path]) } end |
#dependents_base(unstable, symbol_pairs) ⇒ Object
The dependents contributed by the declaration-unstable changed files and the behaviourally-unstable symbol pairs: the ADR-46 slice-4 symbol-granular fan-out when either is present (ancestry deps of the unstable files + symbol deps of the changed pairs), else the coarse file-level fan-out.
263 264 265 266 267 268 269 |
# File 'lib/rigor/analysis/incremental_session.rb', line 263 def dependents_base(unstable, symbol_pairs) if symbol_pairs.any? || unstable.any? { |f| @ancestry_dependents[f] } Incremental.affected_with_symbols(unstable, symbol_pairs, @symbol_dependents, @ancestry_dependents) else Incremental.affected(unstable, @dependents) end end |
#fact_surface_invalidated? ⇒ Boolean
456 457 458 |
# File 'lib/rigor/analysis/incremental_session.rb', line 456 def fact_surface_invalidated? @fact_surface_invalidated end |
#fresh_param_table(current, changed, added, removed) ⇒ Object
ADR-67 WD6c lift — the fresh whole-project inferred-param table for this recheck ({} when the gate
is off). When NO file moved, the collector's inputs are unchanged — the project files are identical,
and the env-side inputs (configuration, sig/, the gem set, the engine version) are constant under
a matched snapshot fingerprint — so the stored table is provably identical and the whole-project
re-collect is skipped (the ADR-87 null-recheck fast path stays collect-free).
276 277 278 279 280 281 |
# File 'lib/rigor/analysis/incremental_session.rb', line 276 def fresh_param_table(current, changed, added, removed) return {} unless @configuration.parameter_inference return @param_table if changed.empty? && added.empty? && removed.empty? build_runner.collect_param_inference_table(current) end |
#param_seed_closure(closure, param_files) ⇒ Object
ADR-67 WD6c lift — the seed-invalidated callees' own contribution to the closure: the files themselves, plus — on a pre-slice-4 snapshot with no symbol edges, where the pairs' symbol fan-out found nothing — their file-level dependents (wider, always sound).
254 255 256 257 258 |
# File 'lib/rigor/analysis/incremental_session.rb', line 254 def param_seed_closure(closure, param_files) closure |= param_files param_files.each { |path| closure |= @dependents[path] || Set.new } if @symbol_sources.empty? closure end |
#param_seed_invalidation(fresh_params) ⇒ Object
ADR-67 WD6c lift — the [files, pairs] the fresh table invalidates. For every [class, method, kind] entry that differs from the snapshot's copy (added, removed, or value-changed — Type#==
structural equality, the same comparison the collector's own fixpoint termination uses, already
exercised across Marshal round-trips by its fork workers), every snapshot file defining that symbol
re-analyses and its [file, symbol] pair joins the symbol fan-out. An entry attributable to NO
snapshot file is a def that first appeared in this edit: its file is in the changed/added set (so it
re-analyses anyway) and its prior callers are the negative-dependency closure's job — nothing is
lost by skipping it here. The restored types are compared and then DISCARDED (the run seeds from the
fresh table), so a cache-carried stale memo ivar can never poison a live lookup.
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/rigor/analysis/incremental_session.rb', line 292 def param_seed_invalidation(fresh_params) return [Set.new, Set.new] if fresh_params.equal?(@param_table) || @param_table == fresh_params files = Set.new pairs = Set.new (@param_table.keys | fresh_params.keys).each do |key| next if @param_table[key] == fresh_params[key] class_name, method_name, kind = key symbol = "#{class_name}#{kind == :singleton ? '.' : '#'}#{method_name}" @symbol_fingerprints.each do |path, symbols| next unless symbols.key?(symbol) files << path pairs << [path, symbol] end end [files, pairs] 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.
382 383 384 385 386 387 388 389 390 |
# File 'lib/rigor/analysis/incremental_session.rb', line 382 def reanalyze_subset(subset) affected = subset.to_set # ADR-67 WD6c lift — seed the subset run from the baseline's own table so the verification engine # exercises the exact seeds the served cache entries were computed under (and skips a re-collect). runner = build_runner(analyze_only: affected, param_inferred_types: @param_table) 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.
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/rigor/analysis/incremental_session.rb', line 146 def recheck previous = @analyzed current = current_files added = current - previous removed = previous - current changed = changed_paths(current & previous) # ADR-67 WD6c lift — recompute the whole-project inferred-param table BEFORE deciding the closure, # and diff it against the snapshot's copy: an entry that moved (because a caller's argument type # changed, a caller appeared, or one vanished) invalidates the CALLEE's file and its symbol # dependents, none of which the file-digest tier can see (the callee's text is unchanged). fresh_params = fresh_param_table(current, changed, added, removed) param_files, param_pairs = param_seed_invalidation(fresh_params) affected = affected_closure(changed, added, removed, param_files, param_pairs) analyze_set = affected & current # The freshly collected table is handed to the runner so the run seeds from the SAME table the diff # was decided on (and the collector runs once per recheck, not twice). runner = build_runner(analyze_only: analyze_set, record_dependencies: true, param_inferred_types: fresh_params) fresh = run_runner(runner).diagnostics @last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry. reused = (current & previous) - affected.to_a merged = fresh + reused.flat_map { |path| @cache[path] || [] } absorb(runner, fresh, current, analyze_set, removed) @param_table = fresh_params Recheck.new(diagnostics: merged, changed: changed.to_set, added: added.to_set, removed: removed.to_set, affected: affected, reused: reused.to_set) end |
#run_buffer_recheck(snapshot:, fingerprint:) ⇒ Object
Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
file, for the CLI's --incremental --tmp-file=X --instead-of=Y. Returns the Recheck when the
snapshot could be reused, or nil when it could not — the caller then falls back to option A
(single-file scope) rather than paying a full baseline, because that baseline would repeat on every
keystroke: this session MUST NOT save, so nothing it computes can warm the next invocation.
Not saving is the whole safety story. @digests and @cache here describe the buffer's bytes, which
exist only in the editor; persisting them would make the next rigor check --incremental believe the
on-disk file was already analysed in a state it was never in.
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/rigor/analysis/incremental_session.rb', line 183 def run_buffer_recheck(snapshot:, fingerprint:) Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do restored = fingerprint && snapshot.load(fingerprint: fingerprint) break nil unless restored restore(restored) result = recheck adopt_plugin_fact_fingerprint # The ADR-88 gate applies unchanged: if the plugin fact surface moved, the cache-served files may be # stale. A full baseline is the sound answer for `--incremental`, but in editor mode it is also the # latency this mode exists to avoid, so decline and let the caller drop to single-file scope. break nil unless @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest) result end end |
#run_incremental(snapshot:, fingerprint:, persist: true) ⇒ 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.
persist: false runs the same restore / recheck / baseline decision without writing the snapshot
back. The language server (#246) needs exactly that: it keeps its session in memory for the life of
the process, and writing shared state would race the way its read-only cache store already declines
to. Every soundness gate below — the fingerprint, the ADR-88 fact surface — is unchanged, so the
decision to reuse is made identically whether or not the result is saved.
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 |
# File 'lib/rigor/analysis/incremental_session.rb', line 403 def run_incremental(snapshot:, fingerprint:, persist: true) # ADR-87 WD1 — install the per-run digest table + recording instant + strict flag for the whole # invocation so change-detection's stat-then-digest freshness (`#pack_digest` / `#stat_fresh?`) honours # `cache.validation: digest` (and `RIGOR_STRICT_VALIDATION`, which the env-only path already sees) and # shares one digest memo across change detection and the baseline/absorb re-pack. The inner # `Runner#run` nests its own `with_run` for the analysis descriptors; nesting is safe (each restores). Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do restored = fingerprint && snapshot.load(fingerprint: fingerprint) # ADR-88 WD1 — the plugin fact-surface fingerprint gates snapshot reuse the same way the global # fingerprint gates the load: a plugin sig/catalog edit outside `signature_paths:` (a Sorbet `.rbi`) # changes the types unchanged call sites resolve, without moving any analyzed file, so the global # fingerprint stays fresh and the recheck would serve stale diagnostics. The fingerprint is computed # POST-HOC from the analysis runner (which already ran `#prepare` and validated its producers) so the # warm path pays no second `#prepare`; the decision is applied after the recheck. Opaque plugins # (types with no fingerprint surface) make the snapshot un-reusable. if restored restore(restored) result = recheck adopt_plugin_fact_fingerprint reuse = @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest) if reuse diagnostics = result.diagnostics warm = true # ADR-87 WD3 — a warm recheck that changed nothing leaves the session state byte-equivalent to the # snapshot it restored, so skip the unconditional rewrite (209ms + 2 MB on gitlab per null recheck). # A cold baseline always persists — there was no valid snapshot to reuse. skip_save = result.no_change? else # The fact surface moved (a plugin sig/catalog edit) or a plugin is opaque: the cached-served # files the recheck merged may be stale, so re-analyze the whole tree. The current fact-surface # digest (from the recheck runner) is unchanged by the re-analysis, so it is kept for the save. @fact_surface_invalidated = true diagnostics = baseline warm = false skip_save = false end else diagnostics = baseline adopt_plugin_fact_fingerprint warm = false skip_save = false end snapshot.save(fingerprint: fingerprint, payload: to_payload) if persist && fingerprint && !skip_save [diagnostics, warm] end end |