Module: RailsAiBridge::Serializers::ContextSummary

Defined in:
lib/rails_ai_bridge/serializers/context_summary.rb

Overview

Shared stack metrics so compact outputs stay consistent with split rule files (e.g. +# Controllers (N)+ uses ControllerIntrospector, not +routes[:by_controller]+ alone).

Constant Summary collapse

HOUSEKEEPING_COLUMNS =

Column names excluded from compact model output — primary key, timestamps, and foreign keys (matched via +_id+ suffix separately).

%w[id created_at updated_at].freeze

Class Method Summary collapse

Class Method Details

.compact_performance_security_sectionArray<String>

Short, copy-pastable baseline for compact serializers (performance, drift, MCP exposure).

Returns:

  • (Array<String>)

    markdown lines (including heading)



213
214
215
216
217
218
219
220
221
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 213

def compact_performance_security_section
  [
    '## Performance & security (baseline)',
    '- Large or hot tables: mind indexes and N+1s; use `includes`, batching, and bounded queries — validate with `rails_get_schema` and real load patterns.',
    '- Treat generated context as **snapshots** that can drift; prefer `rails_*` MCP tools for authoritative structure when in doubt.',
    '- Merge team-specific rules (performance, auth, compliance) into these files or companion rules — generated output is generic.',
    '- MCP is read-only but exposes app structure; avoid exposing the HTTP transport on untrusted networks.'
  ]
end

.database_size_bucket(row_count) ⇒ String?

Human-oriented approximate table size bucket for optional database stats.

Parameters:

  • row_count (Integer, nil)

    approximate rows

Returns:

  • (String, nil)

    +small+, +medium+, +large+, +hot+, or nil



197
198
199
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 197

def database_size_bucket(row_count)
  DatabaseSize.bucket(row_count)
end

.database_size_bucket_for_table(context, table_name) ⇒ String?

Optional size bucket for a table from +context[:database_stats]+.

Parameters:

  • context (Hash)

    full introspection hash

  • table_name (String, nil)

Returns:

  • (String, nil)


206
207
208
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 206

def database_size_bucket_for_table(context, table_name)
  DatabaseSize.bucket_for_table(context, table_name)
end

.introspected_controller_count(context) ⇒ Integer?

Returns number of Ruby controller classes under +app/controllers+.

Parameters:

  • context (Hash)

    full introspection hash

Returns:

  • (Integer, nil)

    number of Ruby controller classes under +app/controllers+



32
33
34
35
36
37
38
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 32

def introspected_controller_count(context)
  ctrl = context[:controllers]
  return nil unless ctrl.is_a?(Hash) && !ctrl[:error]

  count = (ctrl[:controllers] || {}).size
  count.positive? ? count : nil
end

.model_complexity_score(data) ⇒ Integer

Complexity score for a single model's introspection data. Used by compact serializers to surface the most architecturally significant models (high association/validation/callback/scope counts) first.

Parameters:

  • data (Hash)

    single-model entry from +context[:models]+

Returns:

  • (Integer)

    non-negative score; higher = more complex



93
94
95
96
97
98
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 93

def model_complexity_score(data)
  Array(data[:associations]).size +
    Array(data[:validations]).size +
    Array(data[:callbacks]).size +
    Array(data[:scopes]).size
end

.model_relevance_score(data, name: nil, context: {}) ⇒ Integer

Task-relevance score for passive context ordering.

Parameters:

  • data (Hash)

    single-model entry from +context[:models]+

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

    model class name

  • context (Hash) (defaults to: {})

    full introspection hash

Returns:

  • (Integer)

    non-negative score; higher = more relevant



106
107
108
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 106

def model_relevance_score(data, name: nil, context: {})
  ModelRelevance.new(data: data, name: name, context: context).score
end

.models_by_relevance(models, context: {}) ⇒ Array<Array(String, Hash)>

Valid model entries sorted by task relevance, then model name for stable deterministic output when scores tie.

Parameters:

  • models (Hash)

    model payloads keyed by model name

  • context (Hash) (defaults to: {})

    full introspection hash

Returns:

  • (Array<Array(String, Hash)>)


116
117
118
119
120
121
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 116

def models_by_relevance(models, context: {})
  return [] unless models.is_a?(Hash)

  models.select { |_name, data| data.is_a?(Hash) && !data[:error] }
        .sort_by { |name, data| [-model_relevance_score(data, name: name, context: context), name.to_s] }
end

.models_grouped_by_semantic_tier(models, context: {}) ⇒ Hash{String => Array<String>}

Valid model names grouped by semantic tier while preserving relevance ordering.

Parameters:

  • models (Hash)

    model payloads keyed by model name

  • context (Hash) (defaults to: {})

    full introspection hash

Returns:

  • (Hash{String => Array<String>})


128
129
130
131
132
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 128

def models_grouped_by_semantic_tier(models, context: {})
  models_by_relevance(models, context: context).each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(name, data), groups|
    groups[semantic_tier_for(data)] << name
  end
end

.recently_migrated?(table_name, migrations) ⇒ Boolean

Returns true when any migration within the last 30 days references +table_name+. Migration recency is derived from the YYYYMMDDHHMMSS timestamp prefix in +:version+. The table match is based on common Rails migration filename forms such as +create_users+ and +add_email_to_users+.

Parameters:

  • table_name (String, nil)

    snake_case table name (e.g. +"users"+)

  • migrations (Hash, nil)

    +context[:migrations]+ hash; must have a +:recent+ key

Returns:

  • (Boolean)


178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 178

def recently_migrated?(table_name, migrations)
  return false unless table_name && migrations.is_a?(Hash)

  cutoff = Time.zone.today - 30
  Array(migrations[:recent]).any? do |m|
    version = m[:version].to_s
    next false unless version.length >= 8

    migration_date = Date.strptime(version[0..7], '%Y%m%d')
    migration_date >= cutoff && migration_filename_matches_table?(m[:filename], table_name)
  rescue ArgumentError
    false
  end
end

.route_focus_lines(context, limit: 5) ⇒ Array<String>

Bounded route focus lines for passive context. Shows busiest endpoint areas without dumping the full route table.

Parameters:

  • context (Hash)

    full introspection hash

  • limit (Integer) (defaults to: 5)

    max controllers to render

Returns:

  • (Array<String>)

    markdown lines without heading



83
84
85
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 83

def route_focus_lines(context, limit: 5)
  RouteFocus.new(context, limit).lines
end

.route_target_controller_count(context) ⇒ Integer?

Returns distinct controller names referenced in the route set.

Parameters:

  • context (Hash)

Returns:

  • (Integer, nil)

    distinct controller names referenced in the route set



42
43
44
45
46
47
48
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 42

def route_target_controller_count(context)
  routes = context[:routes]
  return nil unless routes.is_a?(Hash) && !routes[:error]

  count = (routes[:by_controller] || {}).keys.size
  count.positive? ? count : nil
end

.routes_stack_line(context) ⇒ String?

One stack bullet for routes + controller inventory, aligned with +rails-controllers+ split files.

Parameters:

  • context (Hash)

Returns:

  • (String, nil)

    markdown line starting with "- Routes:" or +nil+ if no routes data



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 54

def routes_stack_line(context)
  routes = context[:routes]
  return nil unless routes.is_a?(Hash) && !routes[:error]

  total = routes[:total_routes]
  ic = introspected_controller_count(context)
  rt = route_target_controller_count(context)

  if ic
    suffix =
      if rt && rt != ic
        " (#{rt} names in routing — can exceed class count when routes reference engines or non-file controllers)"
      else
        ''
      end
    "- Routes: #{total} total — #{ic} controller classes#{suffix}"
  elsif rt
    "- Routes: #{total} total — #{rt} route targets (controller inventory unavailable)"
  else
    "- Routes: #{total} total"
  end
end

.safe_config_files(files, limit: nil) ⇒ Array<String>

Filters config file paths down to entries safe for generated assistant context.

Parameters:

  • files (Array<String>, nil)

    config file paths from convention detection

  • limit (Integer, nil) (defaults to: nil)

    optional maximum safe paths to return

Returns:

  • (Array<String>)

    safe config file paths, preserving input order



228
229
230
231
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 228

def safe_config_files(files, limit: nil)
  safe_files = Array(files).reject { |path| sensitive_config_file?(path) }
  limit ? safe_files.first(limit) : safe_files
end

.semantic_tier_for(data) ⇒ String

Returns semantic tier with a stable fallback.

Parameters:

  • data (Hash, Object)

    single-model entry

Returns:

  • (String)

    semantic tier with a stable fallback



136
137
138
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 136

def semantic_tier_for(data)
  (data.is_a?(Hash) && data[:semantic_tier].presence) || 'supporting'
end

.sensitive_config_file?(path) ⇒ Boolean

Returns true for config paths that are secret-bearing by name, even when only the path is exposed and file contents are never read.

Parameters:

  • path (String, nil)

    relative config path

Returns:

  • (Boolean)

    whether the path should be omitted from generated context



238
239
240
241
242
243
244
245
246
247
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 238

def sensitive_config_file?(path)
  normalized = normalized_config_path(path)
  basename = File.basename(normalized)

  dotenv_file?(basename) ||
    sensitive_config_basename?(basename) ||
    SENSITIVE_CONFIG_EXTENSIONS.include?(File.extname(basename)) ||
    sensitive_config_path_segment?(normalized) ||
    rails_environment_credentials?(normalized)
end

.test_command(context) ⇒ String

Returns the appropriate test command string for this app's test framework. Reads +context[:tests][:framework]+ (value returned by the tests introspector: "rspec" or "minitest"). Falls back to +"bundle exec rspec"+ when the key is absent, nil, or contains an unrecognised value.

Parameters:

  • context (Hash)

    full introspection hash

Returns:

  • (String)

    copy-pastable test command



147
148
149
150
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 147

def test_command(context)
  framework = context.dig(:tests, :framework).to_s.strip.downcase
  framework == 'minitest' ? 'bin/rails test' : 'bundle exec rspec'
end

.top_columns(table_data) ⇒ Array<Hash>

Top non-housekeeping columns for a model's table. Excludes primary key (+id+), timestamps (+created_at+, +updated_at+), and foreign key columns (names ending in +_id+). Returns at most 3 columns.

Parameters:

  • table_data (Hash, nil)

    entry from +context[:schema][:tables][table_name]+; must have a +:columns+ key with an array of +{ name:, type: }+ hashes.

Returns:

  • (Array<Hash>)

    up to 3 column hashes with +:name+ and +:type+ keys



159
160
161
162
163
164
165
166
167
168
# File 'lib/rails_ai_bridge/serializers/context_summary.rb', line 159

def top_columns(table_data)
  return [] unless table_data.is_a?(Hash)

  columns = Array(table_data[:columns])

  columns
    .select { |column| displayable_column?(column) }
    .first(3)
    .map { |column| { name: column[:name], type: column[:type] } }
end