Skip to content

branch_gap_triage: AST-structural modality bucketing#63

Open
cuzzo wants to merge 72 commits into
masterfrom
decomplex
Open

branch_gap_triage: AST-structural modality bucketing#63
cuzzo wants to merge 72 commits into
masterfrom
decomplex

Conversation

@cuzzo
Copy link
Copy Markdown
Owner

@cuzzo cuzzo commented May 15, 2026

Evolves triage from "group dark arms by method" to "classify each dark arm by which testing modality can reach it". Four buckets:

fuzz_axis valid program, unseen shape (case-on-AST, &&/||,
live if/while body) -> one fuzz axis covers a
family + a mutant
negative_spec the arm raises/diagnoses -> invalid-program only;
fuzz cannot reach it by construction
ffi_integration extern/require/module boundary -> needs a real
external artifact a fuzzer can't synthesize
accept_defensive narrow inert residue (synthetic else, empty, nil)
-> annotate + accept; human-confirmed, never
auto-accepts a reachable arm

Classification is AST-structural, NOT a regex over the arm line (the rejected fake-value grep): the SimpleCov parent tuple gives the decision kind, and the arm's (line,col) span is matched to an AST node whose subtree is inspected for raise/FFI. Two PER-PROJECT LEXICON constants (FFI boundary methods, diagnostic message names) are the only project-specific knobs -- the algorithm generalizes, swap the lexicon per codebase.

Result over the 3 lowering files: fuzz_axis 590, accept_defensive 296, ffi_integration 53, negative_spec 16. This is the work plan: not one fuzz test, not tons of unit tests, not an integration suite -- overwhelmingly fuzz axes, a bounded FFI .cht set, a tiny negative-spec set, a human-confirmed accept residue.

Lives entirely in the coverage tool; decomplex untouched and stays static/zero-runtime (boundary preserved).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🐰 Bencher Report

Branchdecomplex
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/01_socket_throughput/bench📈 view plot
⚠️ NO THRESHOLD
6.63 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,370.04 units
benchmarks/concurrent/06_dynamic_spawn/bench📈 view plot
⚠️ NO THRESHOLD
5.40 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
3,784.26 units
benchmarks/concurrent/11_parallel_aggregation/bench📈 view plot
⚠️ NO THRESHOLD
5.19 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
7,209.46 units
benchmarks/concurrent/18_atomic_counter/bench📈 view plot
⚠️ NO THRESHOLD
5.19 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
50.69 units
benchmarks/inter-clear/04_concurrent_mvcc_fat_struct/bench📈 view plot
⚠️ NO THRESHOLD
5.31 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
304.09 units
benchmarks/sequential/03_alloc_throughput/bench📈 view plot
⚠️ NO THRESHOLD
5.25 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
11,536.05 units
benchmarks/sequential/13_soa_layout/bench📈 view plot
⚠️ NO THRESHOLD
5.32 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
758.16 units
🐰 View full continuous benchmarking report in Bencher

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 15, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.42857% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.74%. Comparing base (22a1f0d) to head (5875394).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
src/backends/pipeline_rewriter.rb 91.93% 5 Missing ⚠️
src/annotator-helpers/generic_analysis.rb 55.55% 4 Missing ⚠️
src/annotator-helpers/pipe_analysis.rb 88.23% 2 Missing ⚠️
src/annotator-helpers/capabilities.rb 87.50% 1 Missing ⚠️
src/annotator-helpers/function_analysis.rb 90.90% 1 Missing ⚠️
src/annotator.rb 98.46% 1 Missing ⚠️
src/mir/control_flow.rb 93.33% 1 Missing ⚠️
src/mir/escape_analysis.rb 90.90% 1 Missing ⚠️
src/tools/doctor.rb 88.88% 1 Missing ⚠️
src/tools/predicate_rewriter.rb 91.66% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff             @@
##           master      #63       +/-   ##
===========================================
- Coverage   92.71%   81.74%   -10.98%     
===========================================
  Files         208      214        +6     
  Lines       52716    52943      +227     
  Branches    12381    12451       +70     
===========================================
- Hits        48878    43280     -5598     
- Misses       3838     9663     +5825     
Flag Coverage Δ
bc-lower 80.77% <95.80%> (?)
fuzz 46.09% <70.86%> (-0.02%) ⬇️
ruby 73.41% <96.42%> (-17.21%) ⬇️
transpile-tests 82.11% <95.80%> (-0.01%) ⬇️
zig 96.38% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🐰 Bencher Report

Branchdecomplex
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/03_atomic_contention/bench📈 view plot
⚠️ NO THRESHOLD
6.15 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
82.97 units
benchmarks/concurrent/08_pubsub/bench📈 view plot
⚠️ NO THRESHOLD
5.34 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
2,913.91 units
benchmarks/concurrent/13_rwlock_starvation/bench📈 view plot
⚠️ NO THRESHOLD
5.39 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,247.61 units
benchmarks/inter-clear/06_concurrent_mvcc_writer_pressure/bench📈 view plot
⚠️ NO THRESHOLD
5.40 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,834.29 units
benchmarks/sequential/05_string_builder/bench📈 view plot
⚠️ NO THRESHOLD
5.28 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
28,063.65 units
benchmarks/sequential/10_pool_vs_multiowned/bench📈 view plot
⚠️ NO THRESHOLD
5.21 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
748.92 units
benchmarks/server/01_tcp_kvstore/server📈 view plot
⚠️ NO THRESHOLD
5.38 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,002.61 units
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🐰 Bencher Report

Branchdecomplex
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/02_concurrent_search/bench📈 view plot
⚠️ NO THRESHOLD
4.01 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
5.97 units
benchmarks/concurrent/07_stream_merge/bench📈 view plot
⚠️ NO THRESHOLD
4.02 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
31.05 units
benchmarks/concurrent/12_false_sharing/bench📈 view plot
⚠️ NO THRESHOLD
3.93 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,002.13 units
benchmarks/concurrent/19_atomic_ptr/bench📈 view plot
⚠️ NO THRESHOLD
3.95 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
91.51 units
benchmarks/inter-clear/05_concurrent_mvcc_pure_read/bench📈 view plot
⚠️ NO THRESHOLD
4.07 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
501.64 units
benchmarks/sequential/04_hashmap/bench📈 view plot
⚠️ NO THRESHOLD
4.02 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,689.84 units
benchmarks/sequential/09_frame_vs_heap/bench📈 view plot
⚠️ NO THRESHOLD
3.87 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,642.19 units
benchmarks/sequential/14_iterator/bench📈 view plot
⚠️ NO THRESHOLD
3.97 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
370.41 units
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🐰 Bencher Report

Branchdecomplex
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/05_backpressure/bench📈 view plot
⚠️ NO THRESHOLD
5.47 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,595.74 units
benchmarks/concurrent/10_shard_vs_locked/bench📈 view plot
⚠️ NO THRESHOLD
5.26 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
60,004.57 units
benchmarks/concurrent/16_observables/bench📈 view plot
⚠️ NO THRESHOLD
5.18 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
96.18 units
benchmarks/inter-clear/03_concurrent_mvcc_vs_rwlock/bench📈 view plot
⚠️ NO THRESHOLD
5.97 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
314.37 units
benchmarks/sequential/07_pointer_chase/bench📈 view plot
⚠️ NO THRESHOLD
5.18 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
515.82 units
benchmarks/sequential/12_weak_ref_graph/bench📈 view plot
⚠️ NO THRESHOLD
5.20 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
258.84 units
benchmarks/server/03_pathological/server📈 view plot
⚠️ NO THRESHOLD
5.38 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,002.74 units
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🐰 Bencher Report

Branchdecomplex
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/04_fanout_fanin/bench📈 view plot
⚠️ NO THRESHOLD
5.57 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
3,266.30 units
benchmarks/concurrent/09_kvstore/bench📈 view plot
⚠️ NO THRESHOLD
5.47 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
60,003.90 units
benchmarks/concurrent/14_nested_lock/bench📈 view plot
⚠️ NO THRESHOLD
5.43 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
388.92 units
benchmarks/inter-clear/02_concurrent_fsm_vs_stackful/bench_fsm📈 view plot
⚠️ NO THRESHOLD
5.55 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
154.99 units
benchmarks/inter-clear/02_concurrent_fsm_vs_stackful/bench_stackful📈 view plot
⚠️ NO THRESHOLD
5.29 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
198.60 units
benchmarks/sequential/11_pipeline_overhead/bench📈 view plot
⚠️ NO THRESHOLD
5.31 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
12,378.92 units
benchmarks/server/02_json_api/server📈 view plot
⚠️ NO THRESHOLD
5.42 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,002.42 units
🐰 View full continuous benchmarking report in Bencher

cuzzo and others added 24 commits May 15, 2026 20:58
Net effect of the 107 nil-kill-prod commits (auto-inferred Sorbet
type annotations + nil-kill tooling under gems/nil-kill) replayed as
a single commit onto origin/master (22a1f0d). 143 files; git 3-way
auto-merged 136, 7 resolved by hand.

Conflict resolution (all were comment+sig collisions): kept master's
updated method comment (authoritative for master's current logic) and
took nil-kill-prod's refined sig (.void / concrete return types --
the value this branch adds). sorbet/config unions all three gem
ignores (decomplex, fix-cache, nil-kill).

Backup of pre-replay nil-kill-prod: branch
nil-kill-prod-backup-20260515-205549 (pushed to origin).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Expanded collection workload (integration specs + fuzz matrix + full
builds + module/ffi) cut param NoEvidence 148 -> 120 (13.8%); the
"never executed by workload" bucket fell 324 -> 70. Remaining: 70
genuine no-test-coverage, 40 block/kwarg tracer blind-spot, 10
only-NilClass.

Gemfile.lock: the squash-replay merged master's tty-screen / wisper /
tty-reader deps; recorded after bundle install so bundle exec works.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reframes the section per the actual decomplexity goal:

1. Slot source widened past sig params: guard_collapse_rows iterates
   every (class,method,receiver) that type_normalizers guards, drawn
   from existing_sigs + unsigned_methods, not just declared-union sig
   params.
2. Ranking key is now N = the count of defensive `is_a?(Type)`
   normalizers on the slot (guards that collapse), not callsite volume
   / a synthetic score. callsite_count was the wrong axis.
3. Joins the two facts nil-kill already gathers: type_normalizers (N
   guards + their sites) x param_origins (dominant producer type + the
   K outlier producer sites) into one ranked, actionable row.

Old score-ranked union_decomplexity_rows / union_members /
canonical_member removed (superseded, not kept as dead code).

Also fixes the type_normalizer collector: it cleared current_method
on the FIRST nested `end` (any if/do/each), so every normalizer past
the first block in a method was mis-attributed to method=nil --
defeating producer attribution. Now indentation-aware: only the
`end` at the def's own column closes the method.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a guard receiver is a local (not a param), attribute its
producers without static points-to:

- Collector tags each type_normalizer with the receiver's one-hop
  intra-method origin: `recv = annotate(x)` -> (call, annotate);
  `recv = @cached` / `@cached.is_a?(Type)` -> (ivar, @cached);
  param if no in-method assignment.
- Report joins that origin to facts the tracer already gathers:
  runtime return classes per method name, runtime ivar/field classes.
  A singleton observed set => "always T, collapse, N guards die"; a
  multi-member set => "tighten that contract" with the members.

This is the cheaper path: no points-to, no new static ivar-write
index. It names the origin contract + its empirical type set ranked
by guard count; it does NOT pinpoint the producing return statement
(runtime is per-method aggregate) -- that residual stays deferred.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The `\Adef` match missed `private def x`, `private_class_method def
self.x`, `module_function def ...` -- pervasive in this codebase
(src/mir/promotion_plan.rb, escape_analysis.rb, ...). Every
normalizer in such a method got method="" , which broke param/call
producer attribution wholesale. Detect def with an optional
visibility-modifier prefix and `self.`, capturing the bare name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- An attr reader (node.type_info) is a zero-arg method; join its
  runtime return classes the same as a call (largest origin bucket).
- Add a program-wide "highest-leverage origin contracts" rollup:
  guards summed by the contract feeding them, so the single
  highest-leverage type-to-add is explicit.

Finding (data, not tooling): the is_a?(Type) guards are diffuse --
<=2 per receiver, each fed by an accessor whose own runtime return is
itself T.untyped (the transitive wall). No single contract collapses
many guards; Type-coercion is pervasively defensive vs T.untyped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix #1 of the review feedback (the one that made the ranking noise):
the section keyed on the per-method local (`attr ti in lower_get_index`)
so `.type_info`'s guards shattered into ~90 buckets of 2. Now resolve
each receiver to its canonical contract (accessor `.type_info`, hash
key `[:type]`, ivar, call) and SUM guards across every method that
reads it. Ranking is now real: .type_info > .full_type > .return_type
(matches ground-truth ordering), tens-to-hundreds not capped at 2.

Also extend the origin tracer to hash-key origins (`recv = h[:type]`)
-- one of the three contracts the feedback flagged as missing.

Producer attribution still shows "unattributed (no runtime trace for
this contract yet)" for accessor contracts -- that is feedback #2,
gated on a runtime collect with accessor-return tracing; deferred
until the in-flight autofix loop releases the tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The single-line `is_a?(Type)` + `Type.new(` scanner undercounted ~10x
and mis-tracked methods. Replace with an AST walk over each def body
(reusing @method_nodes for exact class/method/params): match every
recv.is_a?(Type)/kind_of?(Type) CallNode (multi-line, !-wrapped,
ternary, T.must forms all caught), resolve the receiver's one-hop
origin on the AST (accessor / hash-key / ivar / call / param; a local
resolved once through its in-method assignment node).

src/ measure: 170 -> 350 guards captured, 0 blank-method (was ~half).
.type_info 59/38 methods, .full_type 38/29, [:type] 31/22 -- correct
ordering and real magnitude; aggregation in report.rb now meaningful.
Also removes the spurious is_a?() call-origin noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Proven defect: Pprof::Profile#add_sample is `@samples << {...}`
(Array#<<, returns Array). propose_static_return_action's origin
heuristically guessed `String`, confidence strong, no blockers, and
high_confidence_static_return_origin? stamped it HIGH. It then failed
`srb tc` ("Expected String, got Array"). 17 of the loop's high-tier
rejects are this exact miscalibration -- a HIGH action that cannot
typecheck is a contradiction.

Gate HIGH on static guarantee:
  1. origin confidence strong (unchanged);
  2. NO blockers -- a blocker IS the static analysis reporting it
     could not cleanly determine the return; the candidate is then a
     guess, never HIGH;
  3. a BARE static source (kind=static, not RBI/stdlib, code not a
     self-evident literal/constructor) is a heuristic guess and
     requires runtime corroboration (runtime_contradicts? already
     rejected incompatible observed returns, so any observed return
     agrees; none at all => unverifiable => REVIEW).
Literal/RBI/stdlib-backed static returns stay HIGH (provable). The
demoted ones become REVIEW, where the verified loop filters them --
a review rejection is by-design, not a calibration failure.

Scope: fixes the static-return-origin HIGH path (the proven, cited
defect). add_sig@high / void / noreturn are separate lower-rate
paths, not addressed here (avoiding over-broad risk mid-loop).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 sig blocks across 26 files (src/mir/*, src/tools/*, src/lsp/server)
landed by the nil-kill verified loop. Each survived per-batch
srb tc + dependent-spec verification with bisection; the full
post-loop gate is srb tc clean and the entire prspec spec/ suite
passes (the lone fmt_verifier_spec.rb:116 failure is pre-existing
parallel-harness flakiness -- passes deterministically in isolation,
and src/tools/formatter.rb is untouched by this batch).

86 candidate actions were rejected by verification and not applied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Post-loop re-infer on the +88-sig tree, with the AST normalizer
collector, canonical-contract Union Decomplexity aggregation, and the
HIGH calibration gate all active.

Calibration validated: fix_sig_return@high from static_return_origin
17 -> 1; add_sig@high -> 0; fix_sig_param@high -> 0. Remaining
fix_sig_return@high (17) are the sound void/noreturn whole-program
analyses, deliberately left HIGH. Next-cycle high-tier rejection from
heuristic static origins projected ~0.

Union Decomplexity now AST-accurate: .type_info 59 guards/38 methods,
.full_type 38/29, .type 28/23 -- aggregated by canonical contract,
0 blank-method. Producer attribution still pending a runtime collect
with accessor-return tracing (fix #2, deferred).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Union Decomplexity producer attribution (feedback #2) needed runtime
types for accessor contracts (.type_info/.full_type/.type -- the top
guard clusters). attr_reader-backed accessors have no traced `def`, so
rt_returns was empty and every row read "unattributed".

collect now captures the backing store: record_ivar_assignment (which
already receives every instrumented ivar write) accumulates a
per-(declaring class, ivar) runtime class set, dumped to
ivars-*.jsonl, ingested as the `ivar_runtime` fact. The report joins
an attr/ivar origin to the like-named ivar's runtime classes,
aggregated globally by name (a `.type_info` accessor is one contract
across ~38 classes; its producers are the union of every @type_info
class set). attr origins try rt_returns first, then fall back to the
ivar store.

End-to-end spec added (attr contract -> @ivar runtime -> aggregated
"N guards collapse | .type_info ... via @type_info assignments
{Type, Symbol}"). 148 nil-kill specs green.

Hash-key origins ([:type], 31 guards) still need a hash-write hook --
separate, narrower follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picks up the collect-update join (f959f51). Even before a dedicated
collect populates ivar_runtime, the attr->ivar-by-name fallback
attributes accessor contracts from existing struct-field facts:
.type_info etc. now show "via @<ivar> assignments (runtime) {...}"
instead of "producers unattributed". A future full collect enriches
this with the per-(class,ivar) ivar_runtime set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause of the recurring garbage reports: running `infer`
standalone reused runtime evidence collected against an OLDER src/
(post-squash + post-autofix line shifts). Stale method records fail
to join the changed code -> 60% calls==0 -> NoEvidence ballooned to
~50%+. Partial evidence is useless; producing a report from it is
worse than no report.

Guards (default-on, single explicit override --allow-stale-runtime):
- `infer`: abort if RUNTIME_DIR empty, or newest src/ mtime > newest
  runtime jsonl mtime (src changed after the last collect).
- `report`: abort if evidence.json older than newest src/.
Both print the exact full-collect command. This makes FULL, fresh
evidence the structural default -- you can no longer silently infer
or report on stale/partial runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…moved)

Re-collected the full expanded workload on the current src/ (post
+88-autofix, post-squash), then infer+report through the new
freshness guards (all passed -- runtime newer than src/).

NoEvidence corrected: Param 33.4%->13.7%, Returns 56.8%->32.6%,
Arrays 75.9%->53.7% (Struct/ivar 72.3% unchanged -- genuine
no-static-no-runtime floor). ivar_runtime now populated, so accessor
producer attribution in Union Decomplexity is live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Measured against fresh evidence: 161 of 204 Heterogeneous param slots
(79%) have every observed class in ONE namespace -- 134 AST::*, 23
MIR::*, 4 FsmOps::*. They are not "untyped"; they are one node-union
each. New "### Node-Union Alias Candidates" Hygiene section lists,
per namespace alias (AstNode/MirNode/...), the count and EVERY param
location (sorted grab-bags-first by distinct node-type count), so a
single T.type_alias per namespace can type them all at once.

Pure report analysis (reuses the Heterogeneous classifier); no
proposer/codegen yet. 149 nil-kill specs green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Investigation of the "89 NoEvidence returns": only ~7 were truly
evidence-free. classify_return_untyped_cause had the same
runtime-evidence-discard bug already fixed for params:

1. Executed methods observed returning only nil fell through to
   NoEvidence. They are determinable (void/NilClass/T.nilable) ->
   Refused/Pending per the legend's "void/unused".
2. The `unless callees.empty?` transitive-wall branch returned
   NoEvidence WITHOUT consulting the runtime `returns` set, so methods
   observed returning {Array,Hash,nil} or 60 AST node types were
   labeled NoEvidence. Now it only short-circuits to NoEvidence when
   there is no runtime non-nil evidence; otherwise it falls through to
   the runtime Heterogeneous / WeakEvidence verdict.

Returns NoEvidence 89 -> 15 (the residual 15 are the genuine floor:
never-executed + void-bang methods with no traced return).
Refused/Pending 83, WeakEvidence 73, Heterogeneous 65.

Verified params do NOT have this bug (NoEvidence params bucket only as
calls==0 / block-kwarg-untraced / only-nil; the a4165b4 honest
classifier already consults runtime). No param change. 4 new
return-classifier specs; suite green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause of 654 (72%) struct/ivar NoEvidence (not coverage, not a
classifier discard like returns): record_struct_field stored the raw
caller path, which under source instrumentation is the instrumented
copy. record_call maps via abs_path; record_struct_field did not. The
separate `infer` process has no NIL_KILL_INSTRUMENTED_ROOT, so its
struct ingest `target_path?(obs["path"])` rejected EVERY struct row
-> struct_field_runtime stayed 0 (verified: scratch collect produced
130 struct rows, all with instrumented paths infer drops).

Fix 1 (tracer): record_struct_field maps the path via abs_path, same
as record_call -> infer ingests it (verified: path now src, target_path?
true).

Fix 2 (classifier): classify_struct_ivar_untyped! never consulted
struct_field_runtime/ivar_runtime -- it bucketed every untyped field
PropagationGap-or-NoEvidence. Now honest (same as returns/params):
single observed type or only-nil -> Refused/Pending; > MAX_UNION ->
Heterogeneous; >1 -> WeakEvidence; action-resolvable -> PropagationGap;
genuinely none -> NoEvidence.

Effect lands after a fresh full collect (struct_field_runtime is
empty in current evidence until re-collected). 149 specs green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fresh full collect with the abs_path fix: struct_field_runtime
0 -> 27,458 entries. Struct/class fields & ivars NoEvidence
654 (72.3%) -> 205 (22.7%); Refused/Pending 243 -> 604,
PropagationGap 7 -> 52, Heterogeneous 0 -> 37. The 72% "no evidence"
was the instrumented-path ingest bug, not test coverage -- now real.

Collections NoEvidence ~unchanged (350) -- that is the separate
sig-line vs mutation-site join-key bug, still outstanding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
classify_collection_untyped! joined collection_runtime by
[path, sig/decl line, name], but runtime records the OBSERVATION /
mutation-site line, so nearly every join missed -> false NoEvidence.
Join on owner identity instead: param name within the method's
line-range, method name for returns, class-qualified "Class.field"
for struct fields.

Collections NoEvidence 350 -> 302 (52% -> 45%); Heterogeneous
146 -> 189, Refused/Pending 107 -> 129 (~48 reclassified + others
corrected). The line-key bug is fixed. The residual 302 is NOT a join
bug: collection_runtime only captures collections seen through the
mutation/iteration hooks, so read-only params, build-and-return
values, and tlet collections genuinely have no element evidence -- a
deeper collection-coverage limit, separate and documented.

149 specs green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ence

The residual collection NoEvidence (302) was collections the
mutation/iteration hooks never see: read-only params, build-and-return
values, tlet collections. But the tracer ALREADY captures element
classes/shapes at the call/return boundary
(param_elem/return_elem/*_kv/*_shapes) and at struct construction
(struct_field_runtime.elem_classes) -- the classifier just ignored
them. Fold all of those into the per-slot element/shape evidence
alongside the owner-identity collection_runtime join.

No collect change (data already present). Collections NoEvidence
350 -> 302 (join fix) -> 186 (27.8%); Heterogeneous 189 -> 246,
WeakEvidence 44 -> 79, Refused/Pending 129 -> 131. Residual 186 are
collections with genuinely zero element observation on any path
(only-empty / never crossing an instrumented boundary).

150 specs green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New "### Untyped Evidence Gaps" Hygiene section turns the 526 residual
NoEvidence into a triage list grouped by root cause, each with file
locations:
  - 284 never run  -> dead code / missing test (72 param, 7 return,
    205 struct field never constructed)
  - 186 collection no-elements -> only-empty / off any instrumented path
  - 38 arg untraced -> block/kwarg/splat (tracer types positional only)
  - 10 only-nil -> likely unused/optional-dead
  - 8 discarded-return -> should likely be .void
Counts reconcile exactly with the cause table's NoEvidence column.

Refactor: collection slot+evidence building extracted to
collection_evidence_slots (single source of truth) so the classifier
and this breakout cannot drift. 149 specs green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A#dead's return is unused program-wide -> classify_return_untyped_cause
returns Refused/Pending (void), correctly NOT a NoEvidence gap. The
production code was right; the spec over-asserted. Assert only the
param is a never_run gap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
What NotImplemented was: return-expression AST forms analyze_return_origin
didn't model (the blocker "unknown return expression X"). In practice
28 returns: 25 were `@x = v` / `x = v` (assignment-as-return), 3 yield.

Fix: return_sources_for now recurses into a write node's RHS
(Instance/Local/Class/Global/Constant WriteNode) -- `@x = v` types
exactly like `return v`. That resolves the 25 via their RHS evidence
(static + the runtime/ivar fallback already in place).

The residual 3 are `return yield` -- block-return typing, which is the
observational wall, NOT single-file implementation debt. So the
"NotImplemented" cause is no longer meaningful: removed from
UNTYPED_CAUSES, the legend, the classifier branch, the
NOT_IMPLEMENTED_RETURN_NODES list, and the actionable/inherent summary.
Slots that were NotImplemented now classify by their real evidence
(typed via RHS, or honest NoEvidence/Heterogeneous for yield).

Cause table is now 5 columns. Effect needs a re-infer (return_origin
regenerated by the new recursion); no re-collect. 148 specs green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cuzzo and others added 27 commits May 16, 2026 17:01
#1 Loader gap: install_instrumented_require_hook only overrode
require_relative. Any src/ file reached via plain `require` (bare name
via $LOAD_PATH, or a path) ran the REAL un-wrapped source -> no record,
in every process. Now also override Kernel#require: resolve the name
via $LOAD_PATH and, if it maps to a target src file with an
instrumented copy, load the copy. Pristine loaders captured before
prepend so transitively required files are still redirected (no global
reentrancy flag, which would re-open the gap one level down).

#2 Return-in-block: methods with a `return` inside an iterator/`proc`
block were punted to the (fragile) TracePoint fallback. Such a return
is a non-local METHOD return; rewriting it to `throw
__nil_kill_return_tag` reproduces it exactly and records the value.
collect_return_edits now recurses into BlockNode; only LambdaNode /
`lambda {}` scopes (where `return` is lambda-local) stay punted
(contains_return_inside_lambda? replaces contains_return_inside_block?).

#3 Coverage join: a process that ran REAL src for a file that WAS
instrumented had its src-space line numbers mis-translated through the
instrumented line-map, fabricating "covered" lines. coverage_src_lines
now applies the map ONLY when the raw Coverage key is under the
instrumented root; real-src coverage is identity.

Adds end-to-end specs (#1 plain-require redirect, #2 block-return
recorded, #2 lambda-return still punted) and a unit spec (#3 line-map
gating). Full nil-kill suite green (150 + 14 runtime_trace).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The #1 require-redirect loaded the instrumented copy by its
instrumented path, so __FILE__/__dir__ pointed into the parallel
instrumented tree. Every __FILE__-relative resource read broke --
e.g. transpiler.rb:121 `File.read(File.dirname(__FILE__)/../../zig/
runtime/runtime-footer.zig)` raised Errno::ENOENT, failing every
ZigTranspiler.transpile under instrumentation (a latent bug the
require_relative redirect also had; widening to `require` exposed it
across the spec workload).

Fix: load_instrumented reads the instrumented SOURCE and compiles it
under the REAL path via RubyVM::InstructionSequence, so __FILE__ /
__dir__ resolve against real src while the executed code is the
instrumented transform. require idempotency preserved via
$LOADED_FEATURES + an instrumented_loaded set; on any load-strategy
error it falls back to the real file (workload stays sound).

Coverage join (#3) now keys off instrumented_loaded (Coverage
attributes lines to the real compile path) instead of an
instrumented-root path prefix.

Adds a regression spec proving __FILE__-relative resource reads work
through a redirected instrumented copy. Full nil-kill suite green
(150 + 15 runtime_trace).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
With the require-hook (#1, load instrumented under real path),
return-in-block rewrite (#2), and coverage-join hardening (#3) all in
effect, collect_ran_untraced 13 -> 0. No method's body provably
executes during collect without producing a record anymore: the
require loader gap is closed (every reached src file runs the wrapped
transform), block/proc returns are captured, and the coverage join no
longer fabricates "body-covered" from real-src line numbers.

Residual reclassified honestly: untraced_covered 44 (workload-input
gap -- file exercised by the aggregate suite, this method's body not
hit in THIS collect), arg_untraced 52, only_nil 11, struct_unobserved
23, collection_no_elements 162. Total NoEvidence 326 -> 292.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The bucket detected methods whose body provably ran during collect yet
produced no record ("real tracer/trace-plan defect"). Every cause was
fixed: def-line-vs-body classifier, the require loader gap (load
instrumented under real path), return-in-block capture, coverage-join
hardening, and __FILE__ resolution. The bucket has been 0 since; the
category, the collect-run-coverage split, and the now-dead
collect_ran? / collect_coverage_index helpers are removed.

never_run_reason is now a pure SimpleCov split (untraced_covered /
unseen / never_run). Deleted the two specs that exercised the removed
collect_ran mechanism; kept the SimpleCov-split spec. Report gap table
loses the column; totals unchanged (292).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Evolves triage from "group dark arms by method" to "classify each
dark arm by which testing modality can reach it". Four buckets:

  fuzz_axis        valid program, unseen shape (case-on-AST, &&/||,
                   live if/while body) -> one fuzz axis covers a
                   family + a mutant
  negative_spec    the arm raises/diagnoses -> invalid-program only;
                   fuzz cannot reach it by construction
  ffi_integration  extern/require/module boundary -> needs a real
                   external artifact a fuzzer can't synthesize
  accept_defensive narrow inert residue (synthetic else, empty, nil)
                   -> annotate + accept; human-confirmed, never
                   auto-accepts a reachable arm

Classification is AST-structural, NOT a regex over the arm line
(the rejected fake-value grep): the SimpleCov parent tuple gives the
decision kind, and the arm's (line,col) span is matched to an AST
node whose subtree is inspected for raise/FFI. Two PER-PROJECT
LEXICON constants (FFI boundary methods, diagnostic message names)
are the only project-specific knobs -- the algorithm generalizes,
swap the lexicon per codebase.

Result over the 3 lowering files: fuzz_axis 590, accept_defensive
296, ffi_integration 53, negative_spec 16. This is the work plan:
not one fuzz test, not tons of unit tests, not an integration suite
-- overwhelmingly fuzz axes, a bounded FFI .cht set, a tiny
negative-spec set, a human-confirmed accept residue.

Lives entirely in the coverage tool; decomplex untouched and stays
static/zero-runtime (boundary preserved).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 6 proposed fuzz_axis matrices, built and run. Result:

BUGS (3 real, all OPEN — deliberately not fixed):
  catch_allocator_matrix surfaces B1: `r = maybe("") OR fbv` (frame
    fallback, heap success) -> Invalid free (invariant #9).
  catch_reassign_matrix surfaces B2 (leak: reassign-through-OR on
    success) and B3 (segfault: struct field fallback reads itself
    mid-cleanup -> UAF).
  All three are the catch/OR-rescue allocator-identity family — the
  exact P0 cluster branch_gap_triage flagged (infer_catch_value_
  allocator 12/12 dark). The modality plan predicted it; the targeted
  matrices confirmed real memory-safety bugs there.
  Documented in docs/agents/fuzz-matrix-surfaced-bugs.md; the failing
  :pass cells are the live tickets.

CLEAN: capability_wrap_matrix (3/3, +3 in_dev), match_matrix (6/6),
  indexed_assignment_matrix (20/20), binary_op_matrix (21/21) — after
  fixing two template-correctness bugs of mine (off-by-one list index;
  inverted string lt/gte oracle). These were my noise, not CLEAR bugs.

COVERAGE: 68 cells moved mir_lowering branch coverage 673 -> 671 (2
  arms). Verified real (COVERAGE=1 fuzz run writes a transpile-tests
  resultset entry with mir_lowering data; branch_gap_triage merges
  it). This reproduces the "92 programs -> 50 arms" result more
  starkly: feature-level fuzzing finds bugs well but is NOT a
  branch-closure lever — the dark arms need exact triggering
  type_info, and/or the fuzz_axis bucket is over-assigned vs
  reachability. Full analysis in the forensic doc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Feasibility verified: the @target==:bc branches in mir_lowering fire
during MIRLowering#lower_program (Ruby), before the bytecode VM. The
incomplete _bc_runner is irrelevant -- we never execute, never require
BcEmitter to succeed; a program that hits `raise Unimplemented` in a
:bc arm still covered that arm. Per-file rescue, zero new programs.

Result: mir_lowering dark arms 671 -> 656 (15 closed) by re-lowering
the existing corpus with target: :bc. Cost comparison vs the 6
hand-written matrices: 15 arms / 0 new programs vs 2 arms / 68 new
programs (~250x more cost-efficient). But still only 15/671 -- which
is the decisive evidence, from a second direction, that the remaining
~581 fuzz_axis-bucketed arms are NOT closable by program generation in
any backend mode. They are internal-IR-state / defensive guards: the
fuzz_axis bucket is over-assigned and mir_lowering branch closure is a
re-triage problem, not a test-generation problem.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New `bc-lower-coverage` job mirroring the transpile-tests / tools-fuzz
coverage pattern: COVERAGE=1, run tools/bc_lower_coverage.rb, collate,
upload to Codecov with flags `ruby,bc-lower`. Pure Ruby -- no Zig, no
clear build (the @target==:bc arms are covered during MIRLowering,
before the bytecode VM; the incomplete _bc_runner is never executed),
so the job is minimal and fast. fail_ci_if_error: false, matching the
other coverage jobs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rebuild both from SAMPLED axes to EXHAUSTIVE enumeration of the
dispatch's own when-labels, with surface syntax confirmed from
lexer/transpile-tests (not guessed):
  binary_op: every comparison op incl. LTE/GT (was missing), POW
    int+float (** confirmed), MOD, concat, OR. Symbol-path EXCLUDED
    -- CLEAR has no surface symbol literal, so those {EQ,NEQ} arms
    are not source-reachable (accept, not fuzz). 21 -> 30 cells, all
    clean.
  capability_wrap: one cell per ft.sync x ownership label
    {locked, write_locked, always_mutable, versioned, atomic-ptr,
    multiowned, shared:locked} -- all forms confirmed from
    transpile-tests; zero in_dev. 6 pass.

Surfaces B4: @indirect:atomic + WITH EXCLUSIVE (both the compiler's
own directed forms) -> invalid Zig `no field 'ctrl' in AtomicPtr`.
The atomicPtrCreate dark arm of compose_capability_wrap is broken.
OPEN, not fixed.

Coverage delta from the provably-complete enumeration: mir_lowering
656 -> 653 (3 arms). Fourth independent confirmation that branch
coverage is not closable by test generation; documented in the
forensic. Fuzz's value here is bug-finding (4 bugs on dark arms),
not coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…riven

The project's primary goal made concrete: not "this decision is
duplicated N times" (scatter) but "THIS contract is the SOURCE of N
defensive type/nil decisions -- fix the contract once, the cluster
dies." Attributes every is_a?/kind_of?/instance_of?/nil?/respond_to?/
safe-nav guard to the canonical root contract of its subject,
resolving proximate locals through INTRA-procedural assignment
(reuses the derived-state def-use idea + semantic-alias-style
canonicalization). Cross-procedure pressure stays nil-kill's by the
recorded boundary -- not re-implemented (decomplex stays CFG-free).

Ranks contracts by decisions x methods; unresolved ~local bucket
sorts last (that residue needs cross-proc = nil-kill). New tier-1
report section. Self-tested: decision_pressure_test (5), full suite
44/124/0.

Verified on src/ (93 files): top contract `.type_info` drives 274
defensive decisions across 94 methods; the type-contract family
(.type_info 274, .value 110, .full_type 33, .type 28, .return_type
27, [:type] 29) dominates the head of the ranking -- exactly the
"one loose contract -> hundreds of conditionals" the user predicted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Promotes tools/branch_gap_triage from a one-off probe to a
first-class gem (named `prick` -- it pricks holes in your codebase).
A flat "673/2732 uncovered" is unactionable; prick categorizes every
dark branch arm and overlays fix-churn so the actionable slice
surfaces.

OWNS the gap-categorization analysis (AST-structural per-arm
classifier, dead/live decision split, categorical rollup). CONSUMES
the sibling fix-cache gem for churn (require_relative, not
re-derived) + an optional nil-kill verdict for type_norm. Boundary
held: it aggregates, it does not re-implement.

Categories: type_norm (confirm w/ nil-kill -> removable), dead
(delete, complexity down), defensive (accept), ffi, diagnostic
(negative spec), genuine (the real gap). New signal: genuine x
fix-churn = "bugs highly likely HERE".

Validated on src/mir/{mir_lowering,control_flow,escape_analysis}:
935 dark arms -> diagnostic 305, genuine 273, type_norm 229, dead
68, ffi 46, defensive 14. Bugs-likely #1 mir_lowering (187 genuine x
churn 1.0); top sites hoist_alloc / owned_value_temp_needs_cleanup?
-- the exact methods that produced B1-B4. The synthesis points at
real bugs.

Honest v0 caveats (documented in design.md): diagnostic over-greedy
(subtree-wide raise), type_norm under-counted (no intra-proc
local->accessor resolution yet). Shape + bug-likely join are sound;
percentages are candidates to tighten. Self-tested 6/30/0 incl. a
real stdlib-Coverage resultset integration + temp-git churn overlay.
sorbet: ignore gems/prick/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses three valid critiques:

1. Repo-relative + linked paths. Was absolute
   (/home/yahn/cheat/...); now [src/x.rb:226](src/x.rb#L226).

2. Report leads with the actionable artifact. Dropped the
   unhelpful per-file %-table; the headline is now "Top True Gaps
   (N) -- test these, ranked by fix-churn": every genuine reachable
   arm, linked, sorted by the file's fix-cache churn score. Compact
   category summary follows as context.

3. General gem, no baked-in repo lexicon/jargon. Category action
   text is now testing-strategy-neutral (no .cht / fuzz / nil-kill).
   The FFI/external-boundary lexicon ships EMPTY in the gem and is
   caller-supplied via --ffi (CLEAR's set lives in exe/prick, not
   the library). DIAGNOSTIC_MIDS is general Ruby. The engine
   (categorize uncovered branches, rank genuine by consumed
   fix-cache churn) is general to any Ruby project.

classifier: ffi_boundary injected (kwarg, default []); doc comment
de-jargoned + rename-mangled history ref removed. rollup: emits
top_gaps (genuine arms ranked by churn) instead of file-level
bug_likely. README/design.md rewritten generic + caveats kept.
Tests updated for new signatures/shape; 6/30/0. report.md
regenerated (Top True Gaps headline, linked paths).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
report.md lives at gems/prick/report.md, so `src/x.rb` resolved to
the nonexistent gems/prick/src/x.rb. Report now computes the href
relative to the OUTPUT file's directory (link_base = dirname of
--output; defaults to repo root for stdout). Link is now
../../src/mir/mir_lowering.rb#L226 from gems/prick/report.md; display
text stays the readable repo-relative path. Verified target resolves;
6/30/0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gems/prick -> gems/slopcop: directory, lib/, exe/, gemspec, module
Prick -> SlopCop, require paths, CLI name, sorbet ignore entry, and
README/design branding. Tests unchanged (6 runs / 30 assertions / 0
failures); CLI smoke-tested.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
other_type is sig-typed Type and nil-kill confirms the runtime
producer is always Type, so both `other_type.is_a?(Type) &&` guards
are provably dead. Removing them is behavior-preserving (nil-kill
Union Decomplexity: "always Type: collapse, all 2 die").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ti is sig-typed Type and nil-kill confirms the runtime producer is
always Type, so `ti = Type.new(ti) if ti && !ti.is_a?(Type)` and
`ti = nil unless ti.is_a?(Type)` are dead. Removing them is
behavior-preserving (nil-kill: "always Type: collapse, all 2 die").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
No CLEAR transpiler bugs encountered. Documents that only nil-kill
"always Type" verdicts are safe standalone guard collapses (#55,#56
done); the other 18 tracked contracts are legitimately nilable or
unattributed -- their is_a?(Type) checks are correct discriminators,
so they need the producer-side propagation typing program, not blind
guard deletion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ull_type=

62 sites set `.full_type = :Sym`, which full_type= silently launders
via Type.new -- the source of the nilable/non-Type pollution that
forces is_a?(Type) re-guards across 38 reader methods. Pass Type.new
at the producer instead (runtime-identical; the setter already did
exactly this). Step 1 of the source-tightening program for #45.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pipeline_rewriter producer conversion is safe (uniform Locatable
receivers, landed f29524a). The same transform on annotator.rb et
al. regressed 1799 specs: heterogeneous full_type receivers, some of
which genuinely store/read a raw Symbol. The source fix needs
per-receiver typing, not a blanket caller rewrite.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nil-kill-attributed Locatable#full_type= producer site. Wrap the
Symbol RHS in Type.new (runtime-identical to the setter's existing
launder). Validates the per-site approach for the 22-site producer
worklist nil-kill enumerates for this contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 10 nil-kill-attributed Locatable#full_type= producer sites
(303,307,368,445,503,611,645,911 + the 1623/1686 case exprs) wrapped
in Type.new -- runtime-identical to the setter's launder. Scoped
strictly to nil-kill's worklist (other :Void/:Bool sites untouched).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 11 nil-kill-attributed Locatable#full_type= producer sites in
annotator.rb wrapped in Type.new (3838 case fixed per-branch, no
double-wrap). Surfaced real slop: visit_Slice declared
`returns(Symbol)` but is a side-effecting annotator whose return is
unused -- only "satisfied" by the pre-launder Symbol. Corrected to
`.void`, matching its sibling visitors (visit_HashLit/_YieldExpr).
Completes nil-kill's 22-site producer worklist for this contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
With producers passing Type (prior commits) the Locatable#full_type=
setter guarantees the .type_info contract is strictly nil|Type, so
`x.type_info.is_a?(Type)` is a redundant nil-check. Collapsed to
nil-safe access (&. / truthiness / drop the dead Type.new branch)
across function_analysis(5), escape_analysis(3), generic_analysis(3),
mir_checker(1), mir_lowering(2). The 3 remaining sites
(annotator.rb:2793 final_type; 6522/6618 classify_og_kind param) are
different contracts, intentionally untouched. Completes #45.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
.full_type and .type_info return the same @type_object, so the
producer work in #45 already guarantees this contract is strictly
nil|Type. All 14 .full_type is_a?(Type) reader guards collapsed to
nil-safe access (&. / truthiness; dead Type.new branches dropped,
incl. the 5014 block guarded by an outer non-nil check). Gates green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Locals sourced from `x.type_info rescue nil` are provably nil|Type
post-#45, so the `Type.new(ti) if ti && !ti.is_a?(Type)` coercions
are dead and `if ti.is_a?(Type)` is a redundant nil-check. Collapsed:
escape_analysis per_fn_scan!(238), e2_loop_carry_names!(decl_ti,
outer_ti), e3_mark_carry_expr!(904,910); control_flow
_collect_share_moves; promotion_plan stamp_field_pre_cleanups!.
#52's 327/374 are .symbol.type (heterogeneous, = #47), left alone.
Gates green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Type#wrapped_type is structurally nil|Type (type.rb:1030), and both
promotion_plan sites are guarded by `next unless inner_ti`, so
`inner_ti = Type.new(inner_ti) unless inner_ti.is_a?(Type)` is dead.
Removed. (annotator.rb:1325 nil-kill grouped here is actually a
heterogeneous hash value b[:unwrapped_type], not wrapped_type --
left alone.) Gates green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 items landed (seam-backed nil|Type contracts -- safe
behavior-preserving collapses, all gate-verified). Remaining 9 are a
major blocker: genuinely heterogeneous contracts (.type holds
FunctionSignature/String, .return_type holds Hash/Proc, final_type
is Symbol|Type by design) where is_a?(Type) is a real discriminator
-- collapsing is not behavior-preserving and needs a deep
per-contract retype, not a deslop commit. Documented, not faked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

🐰 Bencher Report

Branchdecomplex
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/01_socket_throughput/bench📈 view plot
⚠️ NO THRESHOLD
5.49 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,558.84 units
benchmarks/concurrent/06_dynamic_spawn/bench📈 view plot
⚠️ NO THRESHOLD
5.23 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
3,689.04 units
benchmarks/concurrent/11_parallel_aggregation/bench📈 view plot
⚠️ NO THRESHOLD
5.19 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
7,221.62 units
benchmarks/concurrent/18_atomic_counter/bench📈 view plot
⚠️ NO THRESHOLD
5.21 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
45.75 units
benchmarks/inter-clear/04_concurrent_mvcc_fat_struct/bench📈 view plot
⚠️ NO THRESHOLD
5.46 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
332.10 units
benchmarks/sequential/03_alloc_throughput/bench📈 view plot
⚠️ NO THRESHOLD
5.33 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
11,550.72 units
benchmarks/sequential/13_soa_layout/bench📈 view plot
⚠️ NO THRESHOLD
5.40 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
757.18 units
🐰 View full continuous benchmarking report in Bencher

User's model validated (target nil|Type|FunctionSignature; String/
Symbol slop; sigs -> T.any(Type,FunctionSignature)). Carrier
disambiguated: Literal#type is a separate token-kind field, no
blast radius. Real obstacle is semantic, not mechanical: the 11
is_a?(Type) sites double as a resolved-vs-unresolved gate, so
normalizing the seam changes which decls get processed. Documented
as the spec for a focused reviewed PR; not auto-run (gates-green !=
provably-correct for semantic change). #48-#51,#53,#57,#59 same.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants