Skip to content

Wire PinCache into DocMap, ApiMap, Workspace, and Library - #36

Merged
apiology merged 1 commit into
pin-caching-3-pincache-corefrom
pin-caching-4-wire-caching
Aug 2, 2026
Merged

apiology merged 1 commit into
pin-caching-3-pincache-corefrom
pin-caching-4-wire-caching

Conversation

@apiology

@apiology apiology commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Replace DocMap's ad hoc gem-caching logic with delegation to PinCache
(introduced in the prior stacked PR), simplifying DocMap substantially.
Workspace gains a pin_cache accessor plus cache_gem/uncache_gem/
cache_all_for_workspace! entry points that drive PinCache for a given
workspace's gemspecs. ApiMap follows the renamed DocMap API
(cache_all! -> cache_doc_map_gems!, uncached_gemspecs.any? ->
any_uncached?) and dedupes resolved method aliases via
GemPins.combine_method_pins_by_path. Library exposes pin_cache
(delegating to workspace), uses it to check whether a gem's cache
build is already in progress, and fixes a subprocess chdir bug in its
background gem-caching thread.

Extracted from castwide#1006 (Improve pin caching) as the
second piece of that PR, stacked on top of the PinCache engine PR.
This depends on PinCache existing; the CLI updates that depend on this
wiring follow in a further stacked PR.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com


Stack created with GitHub Stacks CLI • Give Feedback 💬

Replace DocMap's ad hoc gem-caching logic with delegation to PinCache
(introduced in the prior stacked PR), simplifying DocMap substantially.
Workspace gains a pin_cache accessor plus cache_gem/uncache_gem/
cache_all_for_workspace! entry points that drive PinCache for a given
workspace's gemspecs. ApiMap follows the renamed DocMap API
(cache_all! -> cache_doc_map_gems!, uncached_gemspecs.any? ->
any_uncached?) and dedupes resolved method aliases via
GemPins.combine_method_pins_by_path. Library exposes pin_cache
(delegating to workspace), uses it to check whether a gem's cache
build is already in progress, and fixes a subprocess chdir bug in its
background gem-caching thread.

Extracted from castwide#1006 (Improve pin caching) as the
second piece of that PR, stacked on top of the PinCache engine PR.
This depends on PinCache existing; the CLI updates that depend on this
wiring follow in a further stacked PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@apiology
apiology merged commit 7309146 into pin-caching-3-pincache-core Aug 2, 2026
9 of 31 checks passed
@apiology

apiology commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

🤖 Filed by Claude, not the account owner — acting on their behalf via their GitHub credentials.

Bug: Workspace::Gemspecs#resolve_require fails to resolve gems whose require path differs from their package name

This PR introduces a regression that silently drops gems from DocMap/ApiMap cataloging whenever a project's require path doesn't textually match the gem's RubyGems package name (e.g. require 'active_support' for the activesupport gem). No error or warning is produced anywhere — the gem's pins simply never get built or cached, and any of its own transitive dependencies never get discovered either.

Root cause

lib/solargraph/workspace/gemspecs.rb, Gemspecs#resolve_require:

def resolve_require require
  ...
  # Determine gem name based on the require path
  file = "lib/#{require}.rb"
  spec_with_path = Gem::Specification.find_by_path(file)
  ...
  gem_names_to_try = [
    spec_with_path&.name,
    require.tr('/', '-'),
    require.split('/').first
  ].compact.uniq
  ...

Gem::Specification.find_by_path already resolves relative to each gem's own require_paths (normally ["lib"]) internally — it expects the path exactly as it would appear in a require statement (e.g. "active_support"), not pre-prefixed with "lib/". Prepending "lib/" here double-prefixes the lookup and it always returns nil:

Gem::Specification.find_by_path("active_support")        # => #<Gem::Specification activesupport-8.0.5>
Gem::Specification.find_by_path("lib/active_support.rb") # => nil

Because spec_with_path is always nil, resolve_require falls through to two name-guessing fallbacks (require.tr('/', '-') and require.split('/').first), both of which produce "active_support". Neither matches the real gem name "activesupport", so the lookup against all_gemspecs_from_bundle also fails. The require path is left unresolved.

This only becomes visible for gems where the conventional require path differs from the RubyGems package name — a well-known split in the Ruby ecosystem (activesupport/active_support, actionpack/action_pack, etc.). For the common case where they're identical (asana, json, faraday, ...) the fallback guess happens to work, masking the bug.

The pre-refactor code (before this PR, in DocMap#resolve_path_to_gemspecs) called Gem::Specification.find_by_path(path) directly with no "lib/" prefix, and worked correctly.

Downstream effect (how this was found)

In a real project (github.com/apiology/checkoff) this silently broke Minitest resolution entirely, with zero errors even at SOLARGRAPH_LOG=debug. The causal chain:

  1. activesupport never resolves as a required gemspec (this bug).
  2. activesupport has a genuine runtime dependency on minitest (it ships Minitest-based test helpers) — this transitive dependency is never discovered as a result.
  3. The project's only literal require 'minitest/autorun' lived in a file excluded from the workspace scan via .solargraph.yml.
  4. Both possible discovery paths for minitest failed simultaneously, so it vanished from the ApiMap with no diagnostic of any kind — bin/solargraph pin Minitest returned nothing, exit 1.

Minimal reproduction

No test framework needed — this reproduces with activesupport alone.

Gemfile:

# frozen_string_literal: true
source 'https://rubygems.org'

gem 'activesupport'
gem 'solargraph',
    github: 'apiology/solargraph',
    branch: '2026-08-04'

example.rb:

require 'active_support'
$ bundle install
$ bundle exec solargraph pin ActiveSupport
$ echo $?
1

No output at all — ActiveSupport fails to resolve. On the parent commit (dba36ec2b2d8b1e5c72dd80b6914d3589b81b044, right before this PR was merged), the same repro resolves correctly and exits 0.

Bisected with git bisect across the 72 commits between apiology/solargraph@8fda6338 (merge-base with castwide/solargraph master) and the 2026-08-04 integration branch tip (99cc5bf); this PR's merge commit (73091464) is the first bad commit, dba36ec2 (the immediately preceding "Add PinCache: unified gem pin caching engine" PR) is the last good one.

Suggested fix

Drop the manual "lib/" prefix and pass the require path directly, matching the pre-refactor behavior:

# Determine gem name based on the require path
spec_with_path = Gem::Specification.find_by_path(require)

(require_paths.rb's existing suffix-matching/.rb-appending behavior lives inside find_by_path itself, so no other logic here needs to change.)

I'd suggest also adding a regression spec covering a gem whose require path differs from its package name (e.g. activesupport/active_support) to spec/workspace/gemspecs_spec.rb, since the existing spec suite apparently didn't catch this — every currently-tested gem likely has matching require-path/package-name pairs.

apiology added a commit that referenced this pull request Sep 30, 2026
The fork branch existed because this branch needs spec-side adaptations: more precise Hash and Array literal inference, ComplexType#items in place of the removed #map, and an ActionMailer require path fix. All three are now on lekemula main, squashed as #36, #37 and castwide#38, so the pointer and the comment explaining it can both go.
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.

1 participant