Class: RubynCode::Skills::PackContext

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/skills/pack_context.rb

Overview

Builds additional skill context for PR reviews based on detected gems.

On GitHub App runs (or any run where a Gemfile is present in the repo):

  1. Parse the Gemfile for gem names

  2. Fetch matching packs from the registry API

  3. Format pack skills into a context block for the review agent

The GitHub App has access to ALL packs as a premium differentiator. No local ‘/install-skills` required — everything is fetched transparently.

Usage:

context = PackContext.for_repo(
  project_root: '/path/to/repo',
  registry_url: 'https://rubyn.ai'
)
review_prompt = context.build_review_context(diff_content)

Constant Summary collapse

SKILL_NAME_OVERRIDES =
{
  'stripe' => 'stripe/webhooks'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(gems:, registry_client:) ⇒ PackContext

Returns a new instance of PackContext.



55
56
57
58
59
# File 'lib/rubyn_code/skills/pack_context.rb', line 55

def initialize(gems:, registry_client:)
  @gems = gems
  @registry_client = registry_client
  @cache = {}
end

Instance Attribute Details

#gemsObject (readonly)

Returns the value of attribute gems.



53
54
55
# File 'lib/rubyn_code/skills/pack_context.rb', line 53

def gems
  @gems
end

Class Method Details

.for_packs(pack_names:, registry_client:) ⇒ String

Build context for an external/GitHub App context where we need to fetch the full pack content (not just local skills).

Parameters:

  • pack_names (Array<String>)

    names of packs to load

Returns:

  • (String)

    context block to prepend to the review prompt



49
50
51
# File 'lib/rubyn_code/skills/pack_context.rb', line 49

def self.for_packs(pack_names:, registry_client:)
  new(gems: pack_names, registry_client: registry_client).build_context_block
end

.for_repo(project_root:, registry_url: nil) ⇒ PackContext

Factory: build a PackContext for a given repo.

Parameters:

  • project_root (String)

    path to the repository

  • registry_url (String, nil) (defaults to: nil)

    override for registry URL

Returns:



36
37
38
39
40
41
42
# File 'lib/rubyn_code/skills/pack_context.rb', line 36

def self.for_repo(project_root:, registry_url: nil)
  gemfile_path = File.join(project_root, 'Gemfile')
  content = File.read(gemfile_path, encoding: 'UTF-8') if File.exist?(gemfile_path)
  gems = content ? GemfileParser.gems(content) : []
  client = RegistryClient.new(base_url: registry_url)
  new(gems: gems, registry_client: client)
end

Instance Method Details

#build_context_blockString

Build a context block listing all detected packs and their skills. This is prepended to the review prompt so the agent can apply them.

Returns:

  • (String)

    context block



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/rubyn_code/skills/pack_context.rb', line 100

def build_context_block
  return '' if matched_packs.empty?

  lines = []
  lines << "\n## Pack-Informed Review Context"
  intro = 'The following skill packs were detected from the Gemfile and ' \
          'are available for this review (via GitHub App access):'
  lines << intro
  lines << ''

  matched_packs.each do |pack_name|
    pack = fetch_pack(pack_name)
    if pack.nil?
      lines << "- **[#{pack_name}]** (pack not found in registry — skipped)"
      next
    end

    lines << "### Pack: #{pack_name}"
    lines << pack_description(pack)
    lines << pack_skills(pack)
    lines << ''
  rescue StandardError
    lines << "- **[#{pack_name}]** (failed to load — skipped)"
  end

  lines.join("\n")
end

#fetch_pack(pack_name) ⇒ Hash?

Fetch and cache pack content from registry.

RegistryClient#fetch_pack returns a { data:, etag:, not_modified: } wrapper. We cache and return only the :data payload so callers work with pack attributes directly (e.g. :description, :files) rather than the transport envelope.

Parameters:

  • pack_name (String)

Returns:

  • (Hash, nil)

    pack data or nil if not found



87
88
89
90
91
92
93
94
# File 'lib/rubyn_code/skills/pack_context.rb', line 87

def fetch_pack(pack_name)
  return @cache[pack_name] if @cache.key?(pack_name)

  result = @registry_client.fetch_pack(pack_name)
  @cache[pack_name] = result[:data]
rescue RegistryError
  @cache[pack_name] = nil
end

#matched_packsArray<String>

Returns the list of packs that matched detected gems. Some gems map to pack names (e.g. stripe → stripe/webhooks).

Tradeoff: every detected gem that doesn’t appear in SKILL_NAME_OVERRIDES is queried against the registry as a potential pack name. For a typical Rails app with 40-80 gems this means up to 80 sequential registry calls, each returning a 404 for unknown packs. This is intentional for now:

- Responses are cached in @cache so repeated calls within a session are free
- The GitHub App context is latency-tolerant (async review runs)
- A future batch endpoint on the registry API can reduce this to one call

If latency becomes a problem, add a KNOWN_PACKS allowlist and skip gems that are not in it before fetching.

Returns:

  • (Array<String>)

    pack names



75
76
77
# File 'lib/rubyn_code/skills/pack_context.rb', line 75

def matched_packs
  @matched_packs ||= gems.filter_map { |gem| pack_name_for(gem) }.uniq
end