Class: RailsAiBridge::ModelSemanticClassifier

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_ai_bridge/model_semantic_classifier.rb

Overview

Heuristic classification of ActiveRecord models for semantic context:

  • +core_entity+ — listed in Config::Introspection#core_models
  • +pure_join+ — +has_many :through+ join model whose columns are only metadata and +belongs_to+ foreign keys
  • +rich_join+ — same as pure join pattern but with extra payload columns
  • +supporting+ — everything else (typical domain models, STI, polymorphic owners, etc.)

STI +inheritance_column+, +lock_version+, and standard timestamp columns are treated as metadata. Counter caches and other extra columns disqualify a model from +pure_join+.

Constant Summary collapse

BASE_METADATA =
%w[id created_at updated_at created_on updated_on].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(core_model_names: [], through_model_names: Set.new) ⇒ ModelSemanticClassifier

Initialize the classifier with configured core and through-join model names. Initialize the classifier with configured core and through model names.

Parameters:

  • core_model_names (Array<String, Symbol>) (defaults to: [])

    from Configuration#core_models

  • core_model_names (Array<String>, Set<String>) (defaults to: [])
    • Model class names treated as core entities; entries are converted to strings and stored as a Set.
  • core_model_names (Array<String>) (defaults to: [])
    • Names of models considered core; entries are converted to strings and stored as a Set.
  • through_model_names (Set<String>, Array<String>) (defaults to: Set.new)
    • Model class names identified as through/join models (typically from .through_join_model_names); entries are converted to strings and stored as a Set.


50
51
52
53
# File 'lib/rails_ai_bridge/model_semantic_classifier.rb', line 50

def initialize(core_model_names: [], through_model_names: Set.new)
  @core = core_model_names.to_set(&:to_s)
  @through = through_model_names.to_set(&:to_s)
end

Class Method Details

.through_join_model_namesSet<String>

Collects model class names used as the join model in a +through:+ association.

Returns:

  • (Set<String>)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/rails_ai_bridge/model_semantic_classifier.rb', line 18

def self.through_join_model_names
  return Set.new unless defined?(ActiveRecord::Base)

  names = Set.new
  ActiveRecord::Base.descendants.each do |model|
    next if model.abstract_class? || model.name.blank?

    model.reflect_on_all_associations.each do |assoc|
      through = assoc.options[:through]
      next unless through

      join_assoc = model.reflect_on_association(through)
      join_klass = join_assoc&.klass
      names << join_klass.name if join_klass&.name.present?
    end
  rescue StandardError
    next
  end
  names
end

Instance Method Details

#call(model) ⇒ Hash{Symbol => String}, Hash

Classifies an ActiveRecord model into a semantic tier for downstream semantic context. Classifies an ActiveRecord model into a semantic tier and provides a machine-oriented reason code.

Parameters:

  • model (Class)

    ActiveRecord model

  • model (Class)
    • The ActiveRecord model class to classify.
  • model (Class)
    • The ActiveRecord model class to classify.

Returns:

  • (Hash{Symbol => String})

    A hash with keys:

    • :tier => one of "core_entity", "pure_join", "rich_join", or "supporting".
  • (Hash)

    A hash with :tier and :reason keys. On unexpected errors, returns a :tier of "supporting" and a :reason starting with "classification_error: ".



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/rails_ai_bridge/model_semantic_classifier.rb', line 67

def call(model)
  return tier(:supporting, 'unnamed_model') if model.name.blank?
  return tier(:core_entity, 'configured_core_model') if @core.include?(model.name)

  column_names = safe_column_names(model)
  return tier(:supporting, 'no_columns_loaded') if column_names.empty?

  belongs = model.reflect_on_all_associations.select { |a| a.macro == :belongs_to }
  fk_columns = belongs.filter_map { |a| a.foreign_key&.to_s }.uniq
  allowed = ((model, column_names) + fk_columns).uniq
  extra = column_names - allowed
  is_through = @through.include?(model.name)
  belongs_count = belongs.size

  if extra.empty?
    classify_without_payload(is_through, belongs_count)
  else
    classify_with_payload(is_through, belongs_count)
  end
rescue StandardError => error
  tier(:supporting, "classification_error: #{error.message}")
end