rigortype 0.4.2 → 0.4.4
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -169,7 +169,10 @@ module Rigor
|
|
|
169
169
|
# file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
|
|
170
170
|
# 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
|
|
171
171
|
# carries empty.
|
|
172
|
-
|
|
172
|
+
# 38: issue #1898 — the payload gains `effect_plugin_digest`, the plugin effect-fact digest the collections were
|
|
173
|
+
# stamped under, which a recheck now checks before its served collections reach the envelope pass. A pre-38
|
|
174
|
+
# blob carries none, so the gate rejects it and the next run is cold.
|
|
175
|
+
SCHEMA = 38
|
|
173
176
|
|
|
174
177
|
# The persisted per-file state.
|
|
175
178
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -224,11 +227,22 @@ module Rigor
|
|
|
224
227
|
# Issues #796 / #794 — the run-level rows a narrowed run replays instead of re-deriving; see
|
|
225
228
|
# docs/internal-spec/cache.md § "Run-level rows the snapshot carries" for why exactly these two are
|
|
226
229
|
# fresh under the fingerprint and why no other `.rigor.yml`-level row may join them.
|
|
230
|
+
# Issue #1898 — `effect_plugin_digest` is {Rigor::Effects::PluginFacts#digest} at the run that collected
|
|
231
|
+
# `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
|
|
232
|
+
# collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
|
|
233
|
+
# compares this one after the recheck has loaded them.
|
|
227
234
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
228
235
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
229
236
|
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
230
237
|
:return_summaries, :param_table,
|
|
231
|
-
:effect_collections, :effects_identity, :run_level_rows
|
|
238
|
+
:effect_collections, :effects_identity, :run_level_rows,
|
|
239
|
+
:effect_plugin_digest) do
|
|
240
|
+
# Defaults to nil so a payload built without it (an older caller, a spec fixture) reads as "unknown",
|
|
241
|
+
# which the session treats as moved.
|
|
242
|
+
def initialize(effect_plugin_digest: nil, **rest)
|
|
243
|
+
super
|
|
244
|
+
end
|
|
245
|
+
end
|
|
232
246
|
|
|
233
247
|
# Issues #796 / #794 — the two run-level rows the snapshot carries. `definition_build_failures` is the
|
|
234
248
|
# reported per-class detail list ({Environment::RbsLoader#definition_build_failures}); `hkt_scan_failure`
|
|
@@ -260,7 +274,11 @@ module Rigor
|
|
|
260
274
|
# {EngineSource.process_identity} closes it, and answers nil for a version-pinned tree — which adds no
|
|
261
275
|
# part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
|
|
262
276
|
# survive the upgrade untouched.
|
|
263
|
-
|
|
277
|
+
#
|
|
278
|
+
# Issue #1533 — `explain:` is the run's `--explain` flag. It adds `:info` rows to the per-file diagnostics
|
|
279
|
+
# the snapshot caches, so a snapshot written without it holds none and one written with it holds rows a
|
|
280
|
+
# plain run must not print. No part for the default, so every plain run's fingerprint is as it was.
|
|
281
|
+
def self.fingerprint(configuration:, roots:, explain: false)
|
|
264
282
|
bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
|
|
265
283
|
parts = [
|
|
266
284
|
"engine:#{Rigor::VERSION}:#{SCHEMA}",
|
|
@@ -279,14 +297,11 @@ module Rigor
|
|
|
279
297
|
# dependency edge records, so the whole snapshot goes. Scanned over the configured paths plus the roots'
|
|
280
298
|
# files, the set the run's environment scans; no part when nothing is required, so every other
|
|
281
299
|
# project's fingerprint is as it was.
|
|
282
|
-
features =
|
|
283
|
-
configuration, Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns)
|
|
284
|
-
)
|
|
300
|
+
features = required_features(configuration, roots)
|
|
285
301
|
parts << "required-features:#{features.join(',')}" unless features.empty?
|
|
286
302
|
# ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
|
|
287
303
|
# wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
|
|
288
|
-
|
|
289
|
-
parts << "shadow-harness:#{shadow}" if shadow
|
|
304
|
+
parts.concat(run_mode_parts(explain))
|
|
290
305
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
291
306
|
rescue StandardError
|
|
292
307
|
# {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
|
|
@@ -297,6 +312,12 @@ module Rigor
|
|
|
297
312
|
nil
|
|
298
313
|
end
|
|
299
314
|
|
|
315
|
+
def self.required_features(configuration, roots)
|
|
316
|
+
files = Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns,
|
|
317
|
+
base: configuration.exclude_base)
|
|
318
|
+
Environment::RequiredFeatures.for_configuration(configuration, files)
|
|
319
|
+
end
|
|
320
|
+
|
|
300
321
|
# ADR-10 slice 3, issue #1814 — the walked gems' versions: their method catalogue, their refinement table and the
|
|
301
322
|
# constants they bind reach every file's checks, and the lockfile digest above does not cover a project with none.
|
|
302
323
|
# No part for a project that lists no gem, so its fingerprint is what it was.
|
|
@@ -314,6 +335,15 @@ module Rigor
|
|
|
314
335
|
end
|
|
315
336
|
private_class_method :digest_file_if_present
|
|
316
337
|
|
|
338
|
+
# The parts naming how the run was asked to behave rather than what it reads: the ADR-116 WD5 shadow harness
|
|
339
|
+
# (the snapshot's rows are only as checked as the run that wrote them; nil with the harness off) and
|
|
340
|
+
# `--explain` (#1533).
|
|
341
|
+
def self.run_mode_parts(explain)
|
|
342
|
+
shadow = Analysis::ShadowHarness.cache_identity
|
|
343
|
+
[("shadow-harness:#{shadow}" if shadow), ("explain:true" if explain)].compact
|
|
344
|
+
end
|
|
345
|
+
private_class_method :run_mode_parts
|
|
346
|
+
|
|
317
347
|
# Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
|
|
318
348
|
# even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
|
|
319
349
|
def self.digest_pre_eval(paths)
|
|
@@ -346,8 +376,20 @@ module Rigor
|
|
|
346
376
|
end
|
|
347
377
|
private_class_method :digest_signature_paths
|
|
348
378
|
|
|
349
|
-
|
|
350
|
-
|
|
379
|
+
# Issue #1867 — one snapshot FILE per fingerprint mode. A run whose fingerprint carries run-mode parts
|
|
380
|
+
# (`--explain`, the ADR-116 shadow harness) keeps its own file so it neither reads nor overwrites the
|
|
381
|
+
# plain run's warm snapshot, and the reverse. The plain file name is unchanged. The fingerprint still
|
|
382
|
+
# carries the mode parts, so the files never cross-serve.
|
|
383
|
+
def initialize(root:, explain: false)
|
|
384
|
+
@path = File.join(root.to_s, "incremental", self.class.file_name(explain: explain))
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# `snapshot.bin` for a plain run; `snapshot-<mode digest>.bin` when {run_mode_parts} is non-empty.
|
|
388
|
+
def self.file_name(explain: false)
|
|
389
|
+
parts = run_mode_parts(explain)
|
|
390
|
+
return "snapshot.bin" if parts.empty?
|
|
391
|
+
|
|
392
|
+
"snapshot-#{Digest::SHA256.hexdigest(parts.join("\x00"))[0, 12]}.bin"
|
|
351
393
|
end
|
|
352
394
|
|
|
353
395
|
attr_reader :path
|
|
@@ -406,7 +448,8 @@ module Rigor
|
|
|
406
448
|
param_table: data[:param_table] || {},
|
|
407
449
|
effect_collections: data[:effect_collections] || {},
|
|
408
450
|
effects_identity: data[:effects_identity],
|
|
409
|
-
run_level_rows: data[:run_level_rows]
|
|
451
|
+
run_level_rows: data[:run_level_rows],
|
|
452
|
+
effect_plugin_digest: data[:effect_plugin_digest]
|
|
410
453
|
)
|
|
411
454
|
end
|
|
412
455
|
private :payload_from
|
|
@@ -431,7 +474,8 @@ module Rigor
|
|
|
431
474
|
param_table: payload.param_table,
|
|
432
475
|
effect_collections: payload.effect_collections,
|
|
433
476
|
effects_identity: payload.effects_identity,
|
|
434
|
-
run_level_rows: payload.run_level_rows
|
|
477
|
+
run_level_rows: payload.run_level_rows,
|
|
478
|
+
effect_plugin_digest: payload.effect_plugin_digest
|
|
435
479
|
)
|
|
436
480
|
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
437
481
|
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
@@ -72,6 +72,7 @@ module Rigor
|
|
|
72
72
|
)
|
|
73
73
|
runner = invocation.runner
|
|
74
74
|
raw_result = invocation.result
|
|
75
|
+
settle_run_stats(raw_result.stats)
|
|
75
76
|
result = apply_baseline_filter(raw_result, configuration, options)
|
|
76
77
|
|
|
77
78
|
coverage = compute_coverage(runner, configuration, options)
|
|
@@ -180,9 +181,9 @@ module Rigor
|
|
|
180
181
|
# A plain `--incremental` run the session would answer as a warm recheck and write a slot for. Every other
|
|
181
182
|
# shape declines, and the full path handles it exactly as before: an editor buffer (never saved), `--no-cache`
|
|
182
183
|
# (no store to hold a slot), `--verify-incremental`, a worker pool (the session writes no slot for one: a
|
|
183
|
-
# worker's plugin reads never reach the process that records them), and `--explain
|
|
184
|
-
#
|
|
185
|
-
# its writer never saw. The rest mirror the plain probe: `--coverage`, `--cache-stats` and the
|
|
184
|
+
# worker's plugin reads never reach the process that records them), and `--explain` (#1533: the session
|
|
185
|
+
# writes no slot for it — the slot key has no explain slot — so the probe declines rather than answer for a
|
|
186
|
+
# flag its writer never saw). The rest mirror the plain probe: `--coverage`, `--cache-stats` and the
|
|
186
187
|
# `RIGOR_*_TRACE` probes each want the full path's own output.
|
|
187
188
|
def incremental_run_hit_eligible?(configuration, options, buffer)
|
|
188
189
|
options.fetch(:incremental) && buffer.nil? &&
|
|
@@ -195,6 +196,7 @@ module Rigor
|
|
|
195
196
|
# The tail {#run_incremental_check} gives a warm run, from the slot's answer: the same banner, baseline
|
|
196
197
|
# filter, output and exit code. No fact-surface note, since a warm run never has one.
|
|
197
198
|
def finalize_incremental_hit(hit, configuration, options)
|
|
199
|
+
# The slot is written only for a run whose every row still validates, so nothing was re-analyzed.
|
|
198
200
|
report_incremental_run(true, hit.file_count)
|
|
199
201
|
write_incremental_result(hit.result.diagnostics, configuration, options)
|
|
200
202
|
end
|
|
@@ -267,8 +269,8 @@ module Rigor
|
|
|
267
269
|
# Every other file forms the re-analyzed subset, so the run exercises BOTH the subset-analysis path and the
|
|
268
270
|
# cache-serving path.
|
|
269
271
|
subset = analyzed.each_with_index.select { |_, index| index.even? }.map(&:first)
|
|
270
|
-
incremental =
|
|
271
|
-
full =
|
|
272
|
+
incremental = session.reanalyze_subset(subset).map(&:to_h)
|
|
273
|
+
full = verify_full_diagnostics(configuration, paths, collect_stats: options.fetch(:stats)).map(&:to_h)
|
|
272
274
|
|
|
273
275
|
report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size,
|
|
274
276
|
format: options.fetch(:format))
|
|
@@ -288,10 +290,11 @@ module Rigor
|
|
|
288
290
|
nil
|
|
289
291
|
else
|
|
290
292
|
Cache::IncrementalSnapshot.fingerprint(
|
|
291
|
-
configuration: configuration, roots: paths || configuration.paths
|
|
293
|
+
configuration: configuration, roots: paths || configuration.paths,
|
|
294
|
+
explain: options.fetch(:explain)
|
|
292
295
|
)
|
|
293
296
|
end
|
|
294
|
-
snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
|
|
297
|
+
snapshot = Cache::IncrementalSnapshot.new(root: cache_root, explain: options.fetch(:explain))
|
|
295
298
|
store = incremental_cache_store(configuration, options, cache_root)
|
|
296
299
|
session = Analysis::IncrementalSession.new(
|
|
297
300
|
configuration: configuration, paths: paths,
|
|
@@ -300,7 +303,7 @@ module Rigor
|
|
|
300
303
|
# (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
|
|
301
304
|
# re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
|
|
302
305
|
workers: CheckRunnerFactory.resolve_workers(options, configuration),
|
|
303
|
-
buffer: buffer
|
|
306
|
+
buffer: buffer, explain: options.fetch(:explain)
|
|
304
307
|
)
|
|
305
308
|
|
|
306
309
|
return run_editor_mode_option_b(session, snapshot, fingerprint, configuration, options) if buffer
|
|
@@ -309,7 +312,8 @@ module Rigor
|
|
|
309
312
|
# The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
|
|
310
313
|
# `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
|
|
311
314
|
# built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
|
|
312
|
-
|
|
315
|
+
# A warm banner reports the recheck's own sets (#1526), which only the session knows.
|
|
316
|
+
report_incremental_run(warm, session.analyzed_files.size, recheck: session.last_recheck)
|
|
313
317
|
emit_incremental_fact_surface_notes(session)
|
|
314
318
|
write_incremental_cache_stats(session, cache_root, store, options.fetch(:format)) if
|
|
315
319
|
options.fetch(:cache_stats)
|
|
@@ -317,9 +321,47 @@ module Rigor
|
|
|
317
321
|
write_incremental_result(diagnostics, configuration, options)
|
|
318
322
|
end
|
|
319
323
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
# #1526 — a warm banner says how many of the analysed files the recheck re-analysed, and why: `changed` is the
|
|
325
|
+
# edited set, `added` / `removed` the structural delta, and the rest came from the snapshot. A run served from
|
|
326
|
+
# the run-result slot has no recheck, which means nothing changed: every file is reused.
|
|
327
|
+
def report_incremental_run(warm, file_count, recheck: nil)
|
|
328
|
+
body = if warm
|
|
329
|
+
"warm — #{warm_recheck_summary(file_count, recheck)}"
|
|
330
|
+
else
|
|
331
|
+
"cold — full analysis (#{pluralize(file_count, 'file')})"
|
|
332
|
+
end
|
|
333
|
+
@err.puts("rigor: --incremental #{body}")
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# The warm clause. `file_count` is the analysed set M (the subset under a path argument); re-analysed files
|
|
337
|
+
# are M minus the reused set, which includes files invalidated by inferred-parameter changes (ADR-67), not only
|
|
338
|
+
# the edited closure. View templates are not in M: the recheck re-runs every one, so they are appended as their
|
|
339
|
+
# own count rather than folded into the file counts.
|
|
340
|
+
def warm_recheck_summary(file_count, recheck)
|
|
341
|
+
reused = recheck ? recheck.reused.size : file_count
|
|
342
|
+
# Current files are either reused or re-analysed (the recheck's `analyze_set`), so the count is taken from
|
|
343
|
+
# the reused set rather than `affected`, which can also name files that were removed.
|
|
344
|
+
analyzed = file_count - reused
|
|
345
|
+
head = "re-analysed #{analyzed} of #{pluralize(file_count, 'file')}"
|
|
346
|
+
head += " (#{change_counts(recheck).join(', ')})" if recheck && !change_counts(recheck).empty?
|
|
347
|
+
head += reuse_clause(analyzed, reused)
|
|
348
|
+
templates = recheck ? recheck.templates : 0
|
|
349
|
+
templates.positive? ? "#{head}, plus #{pluralize(templates, 'view template')}" : head
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def change_counts(recheck)
|
|
353
|
+
{ "changed" => recheck.changed.size, "added" => recheck.added.size, "removed" => recheck.removed.size }
|
|
354
|
+
.filter_map { |label, count| "#{count} #{label}" if count.positive? }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def reuse_clause(analyzed, reused)
|
|
358
|
+
return ", reused all" if analyzed.zero?
|
|
359
|
+
|
|
360
|
+
reused.zero? ? "" : ", reused the rest"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def pluralize(count, noun)
|
|
364
|
+
"#{count} #{count == 1 ? noun : "#{noun}s"}"
|
|
323
365
|
end
|
|
324
366
|
|
|
325
367
|
# The incremental tail, shared by the full path and the ADR-45 WD2 slot hit so a served run prints exactly
|
|
@@ -366,17 +408,30 @@ module Rigor
|
|
|
366
408
|
|
|
367
409
|
# ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
|
|
368
410
|
# value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
|
|
369
|
-
# snapshot (a Sorbet sig edit, a schema change) or a
|
|
370
|
-
#
|
|
371
|
-
#
|
|
411
|
+
# snapshot (a Sorbet sig edit, a schema change) or a plugin is opaque to the fingerprint (a full run every
|
|
412
|
+
# invocation): it contributes types and declares NO fingerprint surface, or a value it declares cannot be
|
|
413
|
+
# digested. Each opaque plugin's reason is worded per {PluginFactFingerprint::OpaqueReason}, and an
|
|
414
|
+
# undigestible value is named. Both are the sound direction; the note explains why a recheck was not warm.
|
|
372
415
|
def emit_incremental_fact_surface_notes(session)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
416
|
+
reasons = session.opaque_reasons
|
|
417
|
+
surfaceless = reasons.keys.select { |id| reasons.fetch(id).kind == :no_surface }.sort
|
|
418
|
+
unless surfaceless.empty?
|
|
419
|
+
many = surfaceless.size > 1
|
|
420
|
+
@err.puts("rigor: --incremental plugin#{'s' if many} #{surfaceless.join(', ')} " \
|
|
421
|
+
"contribute#{'s' unless many} call-site types with no incremental fingerprint surface; " \
|
|
422
|
+
"the snapshot cannot be reused (full analysis every run).")
|
|
423
|
+
end
|
|
424
|
+
reasons.each do |id, reason|
|
|
425
|
+
next if reason.kind == :no_surface
|
|
426
|
+
|
|
427
|
+
@err.puts("rigor: --incremental #{reason.describe(id)}; the snapshot cannot be reused " \
|
|
428
|
+
"(full analysis every run).")
|
|
429
|
+
end
|
|
430
|
+
if session.fact_store_unreadable?
|
|
431
|
+
@err.puts("rigor: --incremental the plugin fact store could not be read; the snapshot cannot be reused " \
|
|
432
|
+
"(full analysis every run).")
|
|
378
433
|
end
|
|
379
|
-
return unless session.fact_surface_invalidated? &&
|
|
434
|
+
return unless session.fact_surface_invalidated? && !session.opaque_fact_surface?
|
|
380
435
|
|
|
381
436
|
@err.puts("rigor: --incremental plugin fact surface changed since the snapshot; ran a full analysis.")
|
|
382
437
|
end
|
|
@@ -388,8 +443,10 @@ module Rigor
|
|
|
388
443
|
def write_incremental_cache_stats(session, cache_root, store, format)
|
|
389
444
|
write_cache_stats(cache_root, store, format)
|
|
390
445
|
status =
|
|
391
|
-
if
|
|
392
|
-
|
|
446
|
+
if session.opaque_fact_surface?
|
|
447
|
+
verdicts = session.opaque_reasons.map { |id, reason| "#{id}: #{reason.summary}" }
|
|
448
|
+
verdicts << "fact store: could not be read" if session.fact_store_unreadable?
|
|
449
|
+
"opaque (#{verdicts.join('; ')})"
|
|
393
450
|
elsif session.fact_surface_invalidated?
|
|
394
451
|
"changed — snapshot invalidated"
|
|
395
452
|
else
|
|
@@ -398,38 +455,72 @@ module Rigor
|
|
|
398
455
|
side_output(format).puts(" plugin fact surface: #{status}")
|
|
399
456
|
end
|
|
400
457
|
|
|
401
|
-
def verify_full_diagnostics(configuration, paths)
|
|
402
|
-
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
458
|
+
def verify_full_diagnostics(configuration, paths, collect_stats: true)
|
|
459
|
+
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil, collect_stats: collect_stats)
|
|
403
460
|
(paths ? runner.run(paths) : runner.run).diagnostics
|
|
404
461
|
end
|
|
405
462
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
463
|
+
# Issue #1542 — the rows are compared as a multiset over every field each row carries, so two rows that tie on
|
|
464
|
+
# the sort key but differ in severity, or a duplicated row, are a set mismatch and never a silent 0/0. Only when
|
|
465
|
+
# the multisets agree is the order compared, and an order mismatch is reported as its own failure, with the first
|
|
466
|
+
# index at which the two lists diverge, so a set regression and an order regression stay distinguishable.
|
|
467
|
+
#
|
|
412
468
|
# The OK line follows the same rule as the other side outputs (#493) — stdout under `text`, stderr
|
|
413
469
|
# under a machine format. The FAILED branch below has always gone to stderr: it is a failure
|
|
414
470
|
# report, and a run that produced one has no valid document to protect anyway.
|
|
415
471
|
def report_verify_incremental(incremental, full, subset_size:, total:, format: "text")
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
472
|
+
return report_verify_set_mismatch(incremental, full) unless incremental.tally == full.tally
|
|
473
|
+
return report_verify_order_mismatch(incremental, full) unless incremental == full
|
|
474
|
+
|
|
475
|
+
side_output(format).puts("rigor: --verify-incremental OK — incremental " \
|
|
476
|
+
"(#{subset_size}/#{total} files re-analyzed, rest from cache) " \
|
|
477
|
+
"matches full (#{full.size} diagnostics)")
|
|
478
|
+
0
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# The rows one side has more of than the other, counted by multiplicity: [x, x, y] against [x, y, y] has one
|
|
482
|
+
# extra x on the left and one extra y on the right.
|
|
483
|
+
def multiset_difference(left, right)
|
|
484
|
+
budget = right.tally
|
|
485
|
+
left.reject do |row|
|
|
486
|
+
next false unless budget.fetch(row, 0).positive?
|
|
487
|
+
|
|
488
|
+
budget[row] -= 1
|
|
489
|
+
true
|
|
421
490
|
end
|
|
491
|
+
end
|
|
422
492
|
|
|
423
|
-
|
|
424
|
-
|
|
493
|
+
def report_verify_set_mismatch(incremental, full)
|
|
494
|
+
only_incremental = multiset_difference(incremental, full)
|
|
495
|
+
only_full = multiset_difference(full, incremental)
|
|
425
496
|
@err.puts("rigor: --verify-incremental FAILED — incremental and full diagnostics differ.")
|
|
426
497
|
@err.puts(" incremental-only: #{only_incremental.size}, full-only: #{only_full.size}")
|
|
427
498
|
(only_incremental + only_full).first(10).each do |hash|
|
|
428
|
-
@err.puts(" #{hash
|
|
499
|
+
@err.puts(" #{diagnostic_location(hash)}")
|
|
429
500
|
end
|
|
430
501
|
1
|
|
431
502
|
end
|
|
432
503
|
|
|
504
|
+
def report_verify_order_mismatch(incremental, full)
|
|
505
|
+
index = (0...incremental.size).find { |i| incremental[i] != full[i] }
|
|
506
|
+
@err.puts("rigor: --verify-incremental FAILED — same set, different order.")
|
|
507
|
+
@err.puts(" incremental and full report the same #{full.size} diagnostics in a different order; " \
|
|
508
|
+
"first divergence at index #{index}:")
|
|
509
|
+
@err.puts(" incremental: #{diagnostic_location(incremental[index])}")
|
|
510
|
+
@err.puts(" full: #{diagnostic_location(full[index])}")
|
|
511
|
+
1
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Severity, source family and the optional fields a row may carry, so two rows that differ only in one of them
|
|
515
|
+
# never print as the same line.
|
|
516
|
+
def diagnostic_location(hash)
|
|
517
|
+
extra = %w[receiver_type method_name project_definition_site].filter_map do |key|
|
|
518
|
+
" #{key}=#{hash[key]}" if hash.key?(key)
|
|
519
|
+
end
|
|
520
|
+
"#{hash['path']}:#{hash['line']}:#{hash['column']}: #{hash['severity']} [#{hash['rule']}] " \
|
|
521
|
+
"(#{hash['source_family']}) #{hash['message']}#{extra.join}"
|
|
522
|
+
end
|
|
523
|
+
|
|
433
524
|
# ADR-22 slice 5 — the `--baseline-strict` CI gate. When the flag is set, ANY baseline drift fails the run — not
|
|
434
525
|
# only excess drift (a bucket over threshold, which already fails via the surfaced diagnostics) but also DEFICIT
|
|
435
526
|
# drift (`actual < count`: the baseline has grown looser than the code and should be regenerated). A no-op, with a
|
|
@@ -689,6 +780,7 @@ module Rigor
|
|
|
689
780
|
data = data.dup
|
|
690
781
|
data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
|
|
691
782
|
Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present, target_ruby_key_present)
|
|
783
|
+
.with_exclude_base(path && File.exist?(path) ? File.dirname(path) : nil)
|
|
692
784
|
end
|
|
693
785
|
|
|
694
786
|
# ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge` CLI selection over the configured
|
|
@@ -725,6 +817,13 @@ module Rigor
|
|
|
725
817
|
end
|
|
726
818
|
end
|
|
727
819
|
|
|
820
|
+
# Issue #1529 — reads the run's peak RSS straight after the analysis, before coverage or any output, so the
|
|
821
|
+
# figure the summary prints (and the JSON `stats` block carries) covers the analysis alone. A nil `stats`
|
|
822
|
+
# (`--no-stats`, or a run served from a cache) reads nothing.
|
|
823
|
+
def settle_run_stats(stats)
|
|
824
|
+
stats&.peak_rss_bytes
|
|
825
|
+
end
|
|
826
|
+
|
|
728
827
|
# Emits the {Analysis::RunStats} summary to STDERR so it doesn't interleave with the diagnostic stream (text or
|
|
729
828
|
# JSON) on STDOUT. JSON consumers can pipe stdout cleanly; interactive users still see the summary on their tty.
|
|
730
829
|
def write_run_stats(stats)
|
|
@@ -78,7 +78,7 @@ module Rigor
|
|
|
78
78
|
|
|
79
79
|
def run_index
|
|
80
80
|
if File.file?(LLMS_INDEX)
|
|
81
|
-
@out.write(File.read(LLMS_INDEX))
|
|
81
|
+
@out.write(File.read(LLMS_INDEX, encoding: Encoding::UTF_8))
|
|
82
82
|
0
|
|
83
83
|
else
|
|
84
84
|
run_list(nil)
|
|
@@ -115,11 +115,12 @@ module Rigor
|
|
|
115
115
|
doc = resolve_doc(name)
|
|
116
116
|
return doc if doc.is_a?(Integer) # error status, already reported
|
|
117
117
|
|
|
118
|
-
#
|
|
119
|
-
#
|
|
118
|
+
# The bundled page is UTF-8 by contract, so it is read as UTF-8 explicitly: under a C or POSIX locale the
|
|
119
|
+
# process default external encoding is US-ASCII, and a non-ASCII body would otherwise raise in `gsub`.
|
|
120
|
+
# The provenance header stays ASCII-only so it never disagrees with the rest of the output.
|
|
120
121
|
@out.puts("<!-- rigor docs #{doc.fetch(:name)} (rigortype #{Rigor::VERSION}, offline) -->")
|
|
121
122
|
@out.puts
|
|
122
|
-
@out.write(DocLinks.rewrite(File.read(doc.fetch(:path)), from: doc.fetch(:path)))
|
|
123
|
+
@out.write(DocLinks.rewrite(File.read(doc.fetch(:path), encoding: Encoding::UTF_8), from: doc.fetch(:path)))
|
|
123
124
|
0
|
|
124
125
|
end
|
|
125
126
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "command"
|
|
7
|
+
require_relative "options"
|
|
8
|
+
require_relative "check_runner_factory"
|
|
9
|
+
require_relative "../configuration"
|
|
10
|
+
require_relative "../lens"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
class CLI
|
|
14
|
+
# `rigor lens FILE...` — a declaration map of the files with type provenance, computed as `rigor check`
|
|
15
|
+
# computes them (ADR-113; output contract docs/internal-spec/lens.md). Experimental until `lens/v1`.
|
|
16
|
+
class LensCommand < Command
|
|
17
|
+
USAGE = "Usage: rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE..."
|
|
18
|
+
FORMATS = %w[text json].freeze
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
options = parse_options
|
|
22
|
+
return CLI::EXIT_USAGE if options.nil?
|
|
23
|
+
|
|
24
|
+
config = options.fetch(:config)
|
|
25
|
+
if config && !File.file?(config)
|
|
26
|
+
usage_error("no configuration file at #{config}")
|
|
27
|
+
return CLI::EXIT_USAGE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
configuration = Configuration.load(config)
|
|
31
|
+
config_path = options.fetch(:config) || Configuration.discover
|
|
32
|
+
# The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
|
|
33
|
+
root = Dir.pwd
|
|
34
|
+
workers = CheckRunnerFactory.resolve_workers(options, configuration)
|
|
35
|
+
outcome = Lens::Run.new(configuration: configuration, root: root, arguments: @argv, workers: workers).call
|
|
36
|
+
document = Lens::Document.new(outcome: outcome, configuration: configuration, root: root,
|
|
37
|
+
config_path: config_path).to_h
|
|
38
|
+
@err.puts("rigor lens: #{outcome.warm ? 'warm' : 'cold'} — #{@argv.size} target(s)")
|
|
39
|
+
render(document, options.fetch(:format))
|
|
40
|
+
outcome.targets.any? { |target| target.status == "ok" } ? 0 : 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def parse_options
|
|
46
|
+
options = { format: "text", config: nil, workers: nil }
|
|
47
|
+
OptionParser.new do |opts|
|
|
48
|
+
opts.banner = USAGE
|
|
49
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
|
|
50
|
+
opts.on("--workers=N", Integer, "Worker processes for a cold run") { |value| options[:workers] = value }
|
|
51
|
+
Options.add_config(opts, options)
|
|
52
|
+
end.parse!(@argv)
|
|
53
|
+
return usage_error("--format must be text or json") unless FORMATS.include?(options[:format])
|
|
54
|
+
return usage_error("no FILE given") if @argv.empty?
|
|
55
|
+
|
|
56
|
+
options
|
|
57
|
+
rescue OptionParser::ParseError => e
|
|
58
|
+
usage_error(e.message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def usage_error(message)
|
|
62
|
+
@err.puts("rigor lens: #{message}")
|
|
63
|
+
@err.puts(USAGE)
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render(document, format)
|
|
68
|
+
if format == "json"
|
|
69
|
+
@out.puts(JSON.generate(document))
|
|
70
|
+
else
|
|
71
|
+
TextRenderer.new(document, @out).render
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The text rendering for agents and people. Its layout is not a contract; the `LINE:HASH` anchor token is.
|
|
76
|
+
class TextRenderer
|
|
77
|
+
def initialize(document, out)
|
|
78
|
+
@document = document
|
|
79
|
+
@out = out
|
|
80
|
+
@members = document["members"].each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |member, index|
|
|
81
|
+
member["sites"].each { |key| index[key] << member }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render
|
|
86
|
+
@document["files"].each { |file| render_file(file) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def render_file(file)
|
|
92
|
+
unless file["status"] == "ok"
|
|
93
|
+
@out.puts("#{file['argument']} #{file['status']}: #{file['reason']}")
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
omitted = file["omissions"].empty? ? "" : " (#{file['omissions'].size} omitted)"
|
|
98
|
+
@out.puts("#{file['path']} #{file['digest']}#{omitted}")
|
|
99
|
+
file["sites"].each { |site| render_site(site) }
|
|
100
|
+
file["omissions"].each do |omission|
|
|
101
|
+
@out.puts(" omitted #{omission['kind']} #{omission['name']} (line #{omission.dig('span', 'start_line')})")
|
|
102
|
+
end
|
|
103
|
+
file["diagnostics"].each do |diagnostic|
|
|
104
|
+
@out.puts(" #{diagnostic['line']}:#{diagnostic['column']} #{diagnostic['severity']} " \
|
|
105
|
+
"#{diagnostic['message']} [#{diagnostic['rule']}]")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_site(site)
|
|
110
|
+
case site["kind"]
|
|
111
|
+
when "class", "module" then @out.puts(" #{site['anchor']} #{site['kind']} #{site['name']}")
|
|
112
|
+
else
|
|
113
|
+
@members[site["key"]].each { |member| render_member(site, member) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_member(site, member)
|
|
118
|
+
separator = member["scope"] == "singleton" ? "." : "#"
|
|
119
|
+
if member["overloads"].empty?
|
|
120
|
+
elsewhere = member["also_defined_at"].map { |entry| "#{entry['path']}:#{entry['line']}" }
|
|
121
|
+
@out.puts(" #{site['anchor']} #{site['owner']}#{separator}#{member['name']} " \
|
|
122
|
+
"[#{member['basis']}#{"; also #{elsewhere.join(', ')}" unless elsewhere.empty?}]")
|
|
123
|
+
end
|
|
124
|
+
member["overloads"].each do |overload|
|
|
125
|
+
signature = overload["rbs"] || "(signature unavailable: #{unavailable_reasons(overload).join(', ')})"
|
|
126
|
+
@out.puts(" #{site['anchor']} #{site['owner']}#{separator}#{member['name']}: #{signature} " \
|
|
127
|
+
"[#{member['basis']}#{provenance_note(overload)}]")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def provenance_note(overload)
|
|
132
|
+
provenances = ([overload["return"]] + (overload["params"] || []).map { |param| param["type"] })
|
|
133
|
+
.filter_map { |slot| slot["provenance"] }.uniq
|
|
134
|
+
provenances.empty? ? "" : " #{provenances.join('+')}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def unavailable_reasons(overload)
|
|
138
|
+
([overload["return"]] + (overload["params"] || []).map { |param| param["type"] })
|
|
139
|
+
.filter_map { |slot| slot["reason"] }.uniq
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -53,8 +53,11 @@ module Rigor
|
|
|
53
53
|
def build_server(writer:, config_path:) # rubocop:disable Metrics/MethodLength
|
|
54
54
|
configuration = Configuration.load(config_path)
|
|
55
55
|
# ProjectContext caches Environment + Cache::Store across requests so hover / publish hit the warm path.
|
|
56
|
-
# Invalidated by `workspace/didChangeWatchedFiles` and `workspace/didChangeConfiguration
|
|
57
|
-
|
|
56
|
+
# Invalidated by `workspace/didChangeWatchedFiles` and `workspace/didChangeConfiguration`; every save round
|
|
57
|
+
# also re-reads the configuration (the loader below) and re-baselines when its inputs moved (#1613).
|
|
58
|
+
project_context = LanguageServer::ProjectContext.new(
|
|
59
|
+
configuration: configuration, configuration_loader: -> { Configuration.load(config_path) }
|
|
60
|
+
)
|
|
58
61
|
# Single source of truth for buffer state — threaded to Server + all three providers.
|
|
59
62
|
buffer_table = LanguageServer::BufferTable.new
|
|
60
63
|
debouncer = LanguageServer::Debouncer.new
|
|
@@ -12,7 +12,7 @@ module Rigor
|
|
|
12
12
|
# Starts a long-running MCP (Model Context Protocol) server over stdio. The server exposes Rigor's analysis tools as
|
|
13
13
|
# MCP tool calls over a newline-delimited JSON-RPC 2.0 stream. See ADR-33.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# The stdio transport serves eight read-only tools: rigor_check, rigor_type_of, rigor_triage, rigor_lens,
|
|
16
16
|
# rigor_annotate, rigor_sig_gen, rigor_explain, rigor_coverage.
|
|
17
17
|
class McpCommand < Command
|
|
18
18
|
USAGE = "Usage: rigor mcp [options]"
|
|
@@ -128,7 +128,7 @@ module Rigor
|
|
|
128
128
|
|
|
129
129
|
@out.puts(render_print_header(skill))
|
|
130
130
|
@out.puts
|
|
131
|
-
@out.write(File.read(skill.fetch(:path)))
|
|
131
|
+
@out.write(File.read(skill.fetch(:path), encoding: Encoding::UTF_8))
|
|
132
132
|
0
|
|
133
133
|
end
|
|
134
134
|
|
|
@@ -145,14 +145,14 @@ module Rigor
|
|
|
145
145
|
|
|
146
146
|
@out.puts(render_print_header(skill))
|
|
147
147
|
@out.puts
|
|
148
|
-
@out.write(File.read(skill.fetch(:path)))
|
|
148
|
+
@out.write(File.read(skill.fetch(:path), encoding: Encoding::UTF_8))
|
|
149
149
|
|
|
150
150
|
reference_files(skill).each do |ref|
|
|
151
151
|
@out.puts
|
|
152
152
|
@out.puts
|
|
153
153
|
@out.puts("<!-- ===== references/#{File.basename(ref)} (bundled with rigortype #{Rigor::VERSION}) ===== -->")
|
|
154
154
|
@out.puts
|
|
155
|
-
@out.write(File.read(ref))
|
|
155
|
+
@out.write(File.read(ref, encoding: Encoding::UTF_8))
|
|
156
156
|
end
|
|
157
157
|
0
|
|
158
158
|
end
|