Class: RailsConsoleAi::Skill

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/rails_console_ai/skill.rb

Constant Summary collapse

STATUS_PROPOSED =
'proposed'.freeze
STATUS_APPROVED =
'approved'.freeze
STATUSES =
[STATUS_PROPOSED, STATUS_APPROVED].freeze
CONTENT_ATTRIBUTES =

Attributes that, if changed, invalidate the current approval and revert the skill back to “proposed”. Status / approver columns are excluded so that an explicit approve! call doesn’t reset its own approval.

%w[name description body tags bypass_guards_for_methods].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.connectionObject



29
30
31
32
33
34
35
36
37
# File 'app/models/rails_console_ai/skill.rb', line 29

def self.connection
  klass = RailsConsoleAi.configuration.connection_class
  if klass
    klass = Object.const_get(klass) if klass.is_a?(String)
    klass.connection
  else
    super
  end
end

.decode_json_array(raw) ⇒ Object



121
122
123
124
125
126
127
# File 'app/models/rails_console_ai/skill.rb', line 121

def self.decode_json_array(raw)
  return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
  return raw if raw.is_a?(Array)
  JSON.parse(raw)
rescue JSON::ParserError
  []
end

.encode_json_array(value) ⇒ Object



129
130
131
# File 'app/models/rails_console_ai/skill.rb', line 129

def self.encode_json_array(value)
  JSON.dump(Array(value))
end

.record_use!(id) ⇒ Object

Atomically bump use_count + last_used_at without firing callbacks / validations / updated_at. Safe to call from concurrent AI tool calls. No-op (returns false) if the table doesn’t have the columns yet — that keeps older installs working until they run ai_db_migrate.



101
102
103
104
105
106
107
108
109
110
111
# File 'app/models/rails_console_ai/skill.rb', line 101

def self.record_use!(id)
  return false unless connection.column_exists?(table_name, :use_count)
  where(id: id).update_all([
    'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
    Time.now.utc
  ])
  true
rescue ::ActiveRecord::ActiveRecordError => e
  RailsConsoleAi.logger.warn("RailsConsoleAi::Skill.record_use!(#{id.inspect}) failed: #{e.message}")
  false
end

Instance Method Details

#approve!(approved_by:) ⇒ Object

Marks the current head as approved. Logs a version row with the approver name so the audit trail captures the approval moment.

Raises:

  • (ArgumentError)


176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'app/models/rails_console_ai/skill.rb', line 176

def approve!(approved_by:)
  raise ArgumentError, 'approved_by is required' if approved_by.to_s.strip.empty?

  update_with_version!(
    {
      status:      STATUS_APPROVED,
      approved_by: approved_by,
      approved_at: Time.now.utc
    },
    edited_by: approved_by,
    change_note: "Approved by #{approved_by}",
    preserve_approval: true
  )
end

#approved?Boolean

Returns:

  • (Boolean)


73
# File 'app/models/rails_console_ai/skill.rb', line 73

def approved?; status.to_s == STATUS_APPROVED; end

#approved_atObject



68
69
70
# File 'app/models/rails_console_ai/skill.rb', line 68

def approved_at
  has_attribute?(:approved_at) ? read_attribute(:approved_at) : nil
end

#approved_byObject



64
65
66
# File 'app/models/rails_console_ai/skill.rb', line 64

def approved_by
  has_attribute?(:approved_by) ? read_attribute(:approved_by) : nil
end

#bypass_guards_for_methodsObject



49
50
51
# File 'app/models/rails_console_ai/skill.rb', line 49

def bypass_guards_for_methods
  decode_json_array(read_attribute(:bypass_guards_for_methods))
end

#bypass_guards_for_methods=(value) ⇒ Object



53
54
55
# File 'app/models/rails_console_ai/skill.rb', line 53

def bypass_guards_for_methods=(value)
  write_attribute(:bypass_guards_for_methods, encode_json_array(value))
end

#decode_json_array(raw) ⇒ Object



133
134
135
# File 'app/models/rails_console_ai/skill.rb', line 133

def decode_json_array(raw)
  self.class.decode_json_array(raw)
end

#encode_json_array(value) ⇒ Object



137
138
139
# File 'app/models/rails_console_ai/skill.rb', line 137

def encode_json_array(value)
  self.class.encode_json_array(value)
end

#has_attribute_status?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'app/models/rails_console_ai/skill.rb', line 93

def has_attribute_status?
  has_attribute?(:status)
end

#last_used_atObject



117
118
119
# File 'app/models/rails_console_ai/skill.rb', line 117

def last_used_at
  has_attribute?(:last_used_at) ? read_attribute(:last_used_at) : nil
end

#proposed?Boolean

Returns:

  • (Boolean)


72
# File 'app/models/rails_console_ai/skill.rb', line 72

def proposed?; status.to_s == STATUS_PROPOSED; end

#statusObject

Defensive accessors — if ‘ai_db_migrate` hasn’t been run yet, the status / approval columns may be missing on an older table. Return safe defaults instead of blowing up with NameError.



60
61
62
# File 'app/models/rails_console_ai/skill.rb', line 60

def status
  has_attribute_status? ? read_attribute(:status) : STATUS_PROPOSED
end

#tagsObject

Manual JSON accessors keep us off Rails-version-specific ‘serialize` syntax (positional coder in Rails 5–6, keyword coder in Rails 7+).



41
42
43
# File 'app/models/rails_console_ai/skill.rb', line 41

def tags
  decode_json_array(read_attribute(:tags))
end

#tags=(value) ⇒ Object



45
46
47
# File 'app/models/rails_console_ai/skill.rb', line 45

def tags=(value)
  write_attribute(:tags, encode_json_array(value))
end

#to_hashObject



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/rails_console_ai/skill.rb', line 75

def to_hash
  {
    'id'                        => id,
    'name'                      => name,
    'description'               => description,
    'body'                      => body,
    'tags'                      => tags,
    'bypass_guards_for_methods' => bypass_guards_for_methods,
    'status'                    => status,
    'approved_by'               => approved_by,
    'approved_at'               => approved_at,
    'use_count'                 => use_count,
    'last_used_at'              => last_used_at,
    'source'                    => :db,
    'updated_at'                => updated_at
  }
end

#update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false) ⇒ Object

Assigns attrs, saves, and records one SkillVersion snapshot of the post-save state. Every save produces exactly one version row, so the version log is a complete history including the current state (the most recent version mirrors ‘self`).

If ‘preserve_approval` is false (the default), any change to a content attribute reverts the skill back to “proposed” and clears the approver. Pass true from the approve! flow so approval doesn’t reset itself.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'app/models/rails_console_ai/skill.rb', line 148

def update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false)
  transaction do
    assign_attributes(attrs)

    if !preserve_approval && approved? && content_dirty?
      self.status      = STATUS_PROPOSED
      self.approved_by = nil
      self.approved_at = nil
    end

    save!
    RailsConsoleAi::SkillVersion.create!(
      skill_id:                   id,
      name:                       name,
      description:                description,
      body:                       body,
      tags:                       tags,
      bypass_guards_for_methods:  bypass_guards_for_methods,
      status:                     status,
      edited_by:                  edited_by,
      change_note:                change_note
    )
  end
  self
end

#use_countObject



113
114
115
# File 'app/models/rails_console_ai/skill.rb', line 113

def use_count
  has_attribute?(:use_count) ? (read_attribute(:use_count) || 0) : 0
end