source_monitor 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-architecture/SKILL.md +233 -0
  3. data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
  4. data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
  5. data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
  6. data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
  7. data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
  8. data/.claude/skills/sm-configure/SKILL.md +153 -0
  9. data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
  10. data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
  11. data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
  12. data/.claude/skills/sm-domain-model/SKILL.md +188 -0
  13. data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
  14. data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
  15. data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
  16. data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
  17. data/.claude/skills/sm-engine-test/SKILL.md +302 -0
  18. data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
  19. data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
  20. data/.claude/skills/sm-event-handler/SKILL.md +265 -0
  21. data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
  22. data/.claude/skills/sm-health-rule/SKILL.md +327 -0
  23. data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
  24. data/.claude/skills/sm-host-setup/SKILL.md +223 -0
  25. data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
  26. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
  27. data/.claude/skills/sm-job/SKILL.md +263 -0
  28. data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
  29. data/.claude/skills/sm-model-extension/SKILL.md +287 -0
  30. data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
  31. data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
  32. data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
  33. data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
  34. data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
  35. data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
  36. data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
  37. data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
  38. data/.vbw-planning/.notification-log.jsonl +102 -0
  39. data/.vbw-planning/.session-log.jsonl +505 -0
  40. data/AGENTS.md +20 -57
  41. data/CHANGELOG.md +19 -0
  42. data/CLAUDE.md +44 -1
  43. data/CONTRIBUTING.md +5 -5
  44. data/Gemfile.lock +20 -21
  45. data/README.md +18 -5
  46. data/VERSION +1 -0
  47. data/docs/deployment.md +1 -1
  48. data/docs/setup.md +4 -4
  49. data/lib/source_monitor/setup/skills_installer.rb +94 -0
  50. data/lib/source_monitor/setup/workflow.rb +17 -2
  51. data/lib/source_monitor/version.rb +1 -1
  52. data/lib/tasks/source_monitor_setup.rake +58 -0
  53. data/source_monitor.gemspec +1 -0
  54. metadata +39 -1
@@ -0,0 +1,287 @@
1
+ ---
2
+ name: sm-model-extension
3
+ description: Use when extending SourceMonitor engine models from a host app, including adding concerns, validations, scopes, associations, and customizing table name prefixes via ModelExtensions.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # sm-model-extension: Extend Engine Models from Host App
8
+
9
+ Add custom behavior to SourceMonitor engine models without monkey-patching.
10
+
11
+ ## When to Use
12
+
13
+ - Adding associations, scopes, or methods to `Source`, `Item`, or other engine models
14
+ - Adding custom validations to engine models
15
+ - Changing the database table name prefix
16
+ - Understanding how `ModelExtensions.register` works
17
+ - Debugging model extension issues
18
+
19
+ ## Extension Mechanism
20
+
21
+ SourceMonitor uses `ModelExtensions.register` to apply host-defined concerns and validations to engine models at load time. When `SourceMonitor.configure` runs, it calls `ModelExtensions.reload!` to re-apply all extensions.
22
+
23
+ ### Flow
24
+
25
+ ```
26
+ 1. Host app defines concern modules and validations
27
+ 2. config/initializers/source_monitor.rb registers them:
28
+ config.models.source.include_concern "MyApp::SourceExtension"
29
+ config.models.source.validate :custom_check
30
+ 3. SourceMonitor.configure { |c| ... } runs
31
+ 4. ModelExtensions.reload! applies all concerns and validations
32
+ 5. Engine models now have the extended behavior
33
+ ```
34
+
35
+ ## Available Extension Points
36
+
37
+ ### Extendable Models
38
+
39
+ | Config Accessor | Engine Model | DB Table |
40
+ |---|---|---|
41
+ | `config.models.source` | `SourceMonitor::Source` | `sourcemon_sources` |
42
+ | `config.models.item` | `SourceMonitor::Item` | `sourcemon_items` |
43
+ | `config.models.fetch_log` | `SourceMonitor::FetchLog` | `sourcemon_fetch_logs` |
44
+ | `config.models.scrape_log` | `SourceMonitor::ScrapeLog` | `sourcemon_scrape_logs` |
45
+ | `config.models.health_check_log` | `SourceMonitor::HealthCheckLog` | `sourcemon_health_check_logs` |
46
+ | `config.models.item_content` | `SourceMonitor::ItemContent` | `sourcemon_item_contents` |
47
+ | `config.models.log_entry` | `SourceMonitor::LogEntry` | `sourcemon_log_entries` |
48
+
49
+ ### Table Name Prefix
50
+
51
+ ```ruby
52
+ config.models.table_name_prefix = "sm_" # Changes all tables from sourcemon_* to sm_*
53
+ ```
54
+
55
+ Default: `"sourcemon_"`
56
+
57
+ ### Including Concerns
58
+
59
+ Three forms supported:
60
+
61
+ ```ruby
62
+ # 1. String (lazy constantization -- recommended for autoloaded modules)
63
+ config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
64
+
65
+ # 2. Module reference (immediate)
66
+ config.models.source.include_concern MyApp::SourceMonitor::SourceExtensions
67
+
68
+ # 3. Anonymous block (creates Module.new)
69
+ config.models.source.include_concern do
70
+ has_many :tags, dependent: :destroy, foreign_key: :source_id
71
+ scope :tagged, ->(tag) { joins(:tags).where(tags: { name: tag }) }
72
+ end
73
+ ```
74
+
75
+ Concerns are deduplicated by signature -- including the same concern twice is safe.
76
+
77
+ ### Adding Validations
78
+
79
+ Two forms:
80
+
81
+ ```ruby
82
+ # 1. Symbol -- method name (must be defined in a concern or the model)
83
+ config.models.source.validate :enforce_custom_rules
84
+
85
+ # 2. Callable (proc/lambda) -- receives the record
86
+ config.models.source.validate ->(record) {
87
+ record.errors.add(:url, "must be HTTPS") unless record.url&.start_with?("https://")
88
+ }
89
+
90
+ # With validation options
91
+ config.models.source.validate :check_plan_limits, on: :create
92
+ ```
93
+
94
+ ## Creating a Host Extension
95
+
96
+ ### Step 1: Define the Concern
97
+
98
+ ```ruby
99
+ # app/models/concerns/my_app/source_monitor/source_extensions.rb
100
+ module MyApp
101
+ module SourceMonitor
102
+ module SourceExtensions
103
+ extend ActiveSupport::Concern
104
+
105
+ included do
106
+ # Associations
107
+ has_many :source_tags, class_name: "MyApp::SourceTag",
108
+ foreign_key: :source_monitor_source_id, dependent: :destroy
109
+
110
+ # Scopes
111
+ scope :by_team, ->(team_id) { where(team_id: team_id) }
112
+ scope :premium, -> { where(premium: true) }
113
+
114
+ # Callbacks
115
+ after_create :notify_team
116
+ end
117
+
118
+ # Instance methods
119
+ def team_name
120
+ team&.name || "Unassigned"
121
+ end
122
+
123
+ private
124
+
125
+ def notify_team
126
+ TeamNotifier.source_added(self) if team_id.present?
127
+ end
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Step 2: Register in Configuration
134
+
135
+ ```ruby
136
+ # config/initializers/source_monitor.rb
137
+ SourceMonitor.configure do |config|
138
+ config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
139
+ config.models.source.validate :validate_team_assignment
140
+
141
+ config.models.item.include_concern "MyApp::SourceMonitor::ItemExtensions"
142
+ end
143
+ ```
144
+
145
+ ### Step 3: Add Database Columns (if needed)
146
+
147
+ If your extension requires new columns on engine tables, create a migration in the host app:
148
+
149
+ ```ruby
150
+ # db/migrate/YYYYMMDDHHMMSS_add_team_to_sourcemon_sources.rb
151
+ class AddTeamToSourcemonSources < ActiveRecord::Migration[8.0]
152
+ def change
153
+ add_column :sourcemon_sources, :team_id, :bigint
154
+ add_index :sourcemon_sources, :team_id
155
+ end
156
+ end
157
+ ```
158
+
159
+ ## How ModelExtensions Works Internally
160
+
161
+ ### Registration (`ModelExtensions.register`)
162
+
163
+ Called by each engine model during class loading:
164
+
165
+ ```ruby
166
+ # In SourceMonitor::Source (simplified)
167
+ SourceMonitor::ModelExtensions.register(self, :source)
168
+ ```
169
+
170
+ This:
171
+ 1. Looks up the `ModelDefinition` for the given key
172
+ 2. Sets `table_name` based on `table_name_prefix + base_table`
173
+ 3. Includes all registered concerns (deduped by signature)
174
+ 4. Applies all registered validations
175
+
176
+ ### Reload (`ModelExtensions.reload!`)
177
+
178
+ Called by `SourceMonitor.configure` after the block runs. Re-applies all extensions to all registered models. Safe to call multiple times.
179
+
180
+ ### Concern Deduplication
181
+
182
+ Concerns are tracked by signature:
183
+ - Named module: `[:module, object_id]`
184
+ - String constant: `[:constant, "MyApp::SourceExtensions"]`
185
+ - Anonymous block: `[:anonymous_module, block.object_id]`
186
+
187
+ ### Validation Management
188
+
189
+ Extension validations are tracked separately from model-native validations. On reload:
190
+ 1. Previous extension validations are removed
191
+ 2. New extension validations are applied
192
+ 3. Model-native validations are untouched
193
+
194
+ ## Limitations and Gotchas
195
+
196
+ 1. **Table name prefix is global** -- changing it affects all engine tables. Must match existing migration table names or you need to rename tables.
197
+
198
+ 2. **Concern order matters** -- concerns are included in registration order. If concern B depends on an association from concern A, register A first.
199
+
200
+ 3. **Anonymous blocks create new modules** -- each `configure` call with a block creates a new anonymous module. In development with code reloading, this is fine because `reload!` re-applies everything.
201
+
202
+ 4. **Validations with symbols** require the method to exist on the model. Define it in a concern and register the concern before the validation.
203
+
204
+ 5. **Foreign keys** -- when adding associations to engine models, use explicit `foreign_key` and `class_name` options to avoid namespace confusion.
205
+
206
+ 6. **Engine table names** -- always reference tables by their prefixed name (e.g., `sourcemon_sources`), not the model name.
207
+
208
+ ## Key Source Files
209
+
210
+ | File | Purpose |
211
+ |---|---|
212
+ | `lib/source_monitor/model_extensions.rb` | Registration, reload, apply logic |
213
+ | `lib/source_monitor/configuration/models.rb` | Models config with MODEL_KEYS |
214
+ | `lib/source_monitor/configuration/model_definition.rb` | Per-model concern + validation storage |
215
+ | `lib/source_monitor/configuration/validation_definition.rb` | Validation wrapper |
216
+ | `lib/source_monitor.rb` | `configure` and `reset_configuration!` |
217
+
218
+ ## References
219
+
220
+ - `reference/extension-api.md` -- Detailed API reference
221
+ - `docs/configuration.md` -- Configuration documentation (Model Extensions section)
222
+
223
+ ## Testing
224
+
225
+ ```ruby
226
+ require "test_helper"
227
+
228
+ module TestExtensions
229
+ extend ActiveSupport::Concern
230
+
231
+ included do
232
+ scope :test_scope, -> { where.not(url: nil) }
233
+ end
234
+
235
+ def test_method
236
+ "extended"
237
+ end
238
+ end
239
+
240
+ class ModelExtensionTest < ActiveSupport::TestCase
241
+ setup do
242
+ SourceMonitor.reset_configuration!
243
+ end
244
+
245
+ test "include_concern adds methods to source" do
246
+ SourceMonitor.configure do |config|
247
+ config.models.source.include_concern TestExtensions
248
+ end
249
+
250
+ source = create_source!
251
+ assert_equal "extended", source.test_method
252
+ assert_respond_to SourceMonitor::Source, :test_scope
253
+ end
254
+
255
+ test "validate adds custom validation" do
256
+ SourceMonitor.configure do |config|
257
+ config.models.source.validate ->(record) {
258
+ record.errors.add(:base, "test error")
259
+ }
260
+ end
261
+
262
+ source = SourceMonitor::Source.new
263
+ source.valid?
264
+ assert_includes source.errors[:base], "test error"
265
+ end
266
+
267
+ test "table_name_prefix changes table names" do
268
+ SourceMonitor.configure do |config|
269
+ config.models.table_name_prefix = "custom_"
270
+ end
271
+ SourceMonitor::ModelExtensions.reload!
272
+
273
+ assert_equal "custom_sources", SourceMonitor::Source.table_name
274
+ end
275
+ end
276
+ ```
277
+
278
+ ## Checklist
279
+
280
+ - [ ] Extension concern defined under `app/models/concerns/`
281
+ - [ ] Concern registered via `config.models.<model>.include_concern`
282
+ - [ ] Custom validations registered via `config.models.<model>.validate`
283
+ - [ ] Foreign keys use explicit `foreign_key:` option
284
+ - [ ] Host migration created for any new columns on engine tables
285
+ - [ ] Tests verify extension behavior
286
+ - [ ] Tests use `SourceMonitor.reset_configuration!` in setup
287
+ - [ ] Extension is idempotent (safe to apply multiple times)
@@ -0,0 +1,317 @@
1
+ # Model Extension API Reference
2
+
3
+ Detailed reference for extending SourceMonitor engine models from host applications.
4
+
5
+ Source: `lib/source_monitor/model_extensions.rb`, `lib/source_monitor/configuration/models.rb`, `lib/source_monitor/configuration/model_definition.rb`
6
+
7
+ ## Configuration API
8
+
9
+ ### `config.models`
10
+
11
+ Class: `SourceMonitor::Configuration::Models`
12
+
13
+ #### `table_name_prefix`
14
+
15
+ | Property | Type | Default |
16
+ |---|---|---|
17
+ | `table_name_prefix` | String | `"sourcemon_"` |
18
+
19
+ Changes the database table name prefix for all engine models.
20
+
21
+ ```ruby
22
+ config.models.table_name_prefix = "sm_"
23
+ # sourcemon_sources -> sm_sources
24
+ # sourcemon_items -> sm_items
25
+ # etc.
26
+ ```
27
+
28
+ #### Model Accessors
29
+
30
+ Each returns a `ModelDefinition` instance:
31
+
32
+ | Accessor | Key | Engine Model |
33
+ |---|---|---|
34
+ | `config.models.source` | `:source` | `SourceMonitor::Source` |
35
+ | `config.models.item` | `:item` | `SourceMonitor::Item` |
36
+ | `config.models.fetch_log` | `:fetch_log` | `SourceMonitor::FetchLog` |
37
+ | `config.models.scrape_log` | `:scrape_log` | `SourceMonitor::ScrapeLog` |
38
+ | `config.models.health_check_log` | `:health_check_log` | `SourceMonitor::HealthCheckLog` |
39
+ | `config.models.item_content` | `:item_content` | `SourceMonitor::ItemContent` |
40
+ | `config.models.log_entry` | `:log_entry` | `SourceMonitor::LogEntry` |
41
+
42
+ #### `for(name) -> ModelDefinition`
43
+
44
+ Look up a model definition by key. Raises `ArgumentError` for unknown models.
45
+
46
+ ---
47
+
48
+ ## ModelDefinition API
49
+
50
+ Class: `SourceMonitor::Configuration::ModelDefinition`
51
+
52
+ ### `include_concern(concern = nil, &block)`
53
+
54
+ Include a concern module into the engine model.
55
+
56
+ **Three forms:**
57
+
58
+ ```ruby
59
+ # 1. String constant (lazy -- recommended for autoloaded classes)
60
+ config.models.source.include_concern "MyApp::SourceExtension"
61
+
62
+ # 2. Module reference (immediate)
63
+ config.models.source.include_concern MyApp::SourceExtension
64
+
65
+ # 3. Anonymous block
66
+ config.models.source.include_concern do
67
+ has_many :tags, dependent: :destroy
68
+ scope :tagged, ->(t) { joins(:tags).where(tags: { name: t }) }
69
+ end
70
+ ```
71
+
72
+ **Deduplication:** Concerns are deduplicated by signature:
73
+
74
+ | Form | Signature |
75
+ |---|---|
76
+ | String | `[:constant, "MyApp::SourceExtension"]` |
77
+ | Module | `[:module, <object_id>]` |
78
+ | Block | `[:anonymous_module, <block_object_id>]` |
79
+
80
+ Including the same concern twice (by signature) is a no-op.
81
+
82
+ **Return value:**
83
+ - String: returns the string
84
+ - Module: returns the module
85
+ - Block: returns the anonymous module created from the block
86
+
87
+ ### `validate(handler = nil, **options, &block)`
88
+
89
+ Register a validation on the engine model.
90
+
91
+ **Forms:**
92
+
93
+ ```ruby
94
+ # 1. Symbol -- method name (must exist on the model, typically from a concern)
95
+ config.models.source.validate :check_custom_rules
96
+
97
+ # 2. Callable (proc/lambda)
98
+ config.models.source.validate ->(record) {
99
+ record.errors.add(:url, "must use HTTPS") unless record.url&.start_with?("https://")
100
+ }
101
+
102
+ # 3. Block
103
+ config.models.source.validate do |record|
104
+ record.errors.add(:base, "invalid") unless record.valid_for_host?
105
+ end
106
+
107
+ # With options (passed to ActiveModel::Validations.validate)
108
+ config.models.source.validate :check_limits, on: :create
109
+ config.models.source.validate :check_format, if: :needs_format_check?
110
+ ```
111
+
112
+ **Returns:** A `ValidationDefinition` instance.
113
+
114
+ ### `validations -> Array<ValidationDefinition>`
115
+
116
+ Read-only list of registered validations.
117
+
118
+ ### `each_concern { |signature, module| }`
119
+
120
+ Iterate over registered concerns. Yields signature and resolved module.
121
+
122
+ ---
123
+
124
+ ## ValidationDefinition
125
+
126
+ Class: `SourceMonitor::Configuration::ValidationDefinition`
127
+
128
+ | Method | Returns | Description |
129
+ |---|---|---|
130
+ | `handler` | Symbol/Proc | The validation handler |
131
+ | `options` | Hash | Options passed to `validate` (e.g., `on:`, `if:`) |
132
+ | `signature` | Array | Unique identifier for deduplication |
133
+ | `symbol?` | Boolean | True if handler is a Symbol or String |
134
+
135
+ ---
136
+
137
+ ## ModelExtensions Module
138
+
139
+ Class: `SourceMonitor::ModelExtensions`
140
+
141
+ ### `register(model_class, key)`
142
+
143
+ Called by each engine model during class loading to register itself:
144
+
145
+ ```ruby
146
+ # Inside SourceMonitor::Source (simplified)
147
+ SourceMonitor::ModelExtensions.register(self, :source)
148
+ ```
149
+
150
+ Actions:
151
+ 1. Adds the model to the internal registry
152
+ 2. Sets `table_name` using `table_name_prefix + base_table`
153
+ 3. Includes all registered concerns (via `apply_concerns`)
154
+ 4. Applies all registered validations (via `apply_validations`)
155
+
156
+ ### `reload!`
157
+
158
+ Re-applies all extensions to all registered models. Called by:
159
+ - `SourceMonitor.configure` (after the block runs)
160
+ - `SourceMonitor.reset_configuration!`
161
+
162
+ Safe to call multiple times. Idempotent for concerns (deduplicated). Validations are removed and re-applied on each reload.
163
+
164
+ ---
165
+
166
+ ## Internal Mechanics
167
+
168
+ ### Table Name Assignment
169
+
170
+ ```ruby
171
+ def assign_table_name(entry)
172
+ desired = "#{SourceMonitor.table_name_prefix}#{entry.base_table}"
173
+ model_class.table_name = desired
174
+ end
175
+ ```
176
+
177
+ `base_table` is derived from the model class name: `Source` -> `sources`, `FetchLog` -> `fetch_logs`.
178
+
179
+ ### Concern Application
180
+
181
+ ```ruby
182
+ def apply_concerns(model_class, definition)
183
+ applied = model_class.instance_variable_get(:@_source_monitor_extension_concerns) || []
184
+
185
+ definition.each_concern do |signature, mod|
186
+ next if applied.include?(signature)
187
+ model_class.include(mod) unless model_class < mod
188
+ applied << signature
189
+ end
190
+
191
+ model_class.instance_variable_set(:@_source_monitor_extension_concerns, applied)
192
+ end
193
+ ```
194
+
195
+ Concerns are tracked via an instance variable on the model class. Including the same module twice is prevented both by signature tracking and the `model_class < mod` check.
196
+
197
+ ### Validation Application
198
+
199
+ ```ruby
200
+ def apply_validations(model_class, definition)
201
+ remove_extension_validations(model_class) # Remove previous extensions
202
+
203
+ definition.validations.each do |validation|
204
+ if validation.symbol?
205
+ model_class.validate(validation.handler, **validation.options)
206
+ else
207
+ callback = proc { |record| validation.handler.call(record) }
208
+ model_class.validate(**validation.options, &callback)
209
+ end
210
+ end
211
+ end
212
+ ```
213
+
214
+ Extension validations are tracked separately. On reload:
215
+ 1. Previous extension validations are removed from `_validate_callbacks`
216
+ 2. New validations are applied fresh
217
+ 3. Model-native validations are never touched
218
+
219
+ ---
220
+
221
+ ## Concern Definition Internals
222
+
223
+ Class: `SourceMonitor::Configuration::ModelDefinition::ConcernDefinition` (private)
224
+
225
+ ### Resolver
226
+
227
+ | Form | Behavior |
228
+ |---|---|
229
+ | Block | Creates `Module.new(&block)`, wraps in lazy resolver |
230
+ | Module | Returns the module directly |
231
+ | String | Calls `constantize` lazily (raises `ArgumentError` on `NameError`) |
232
+
233
+ ### Lazy Resolution
234
+
235
+ String-based concerns are not constantized until `resolve` is called. This allows registering concerns for classes that haven't been loaded yet (common with autoloading).
236
+
237
+ ---
238
+
239
+ ## Patterns and Examples
240
+
241
+ ### Adding Associations
242
+
243
+ ```ruby
244
+ # app/models/concerns/my_app/source_monitor/source_extensions.rb
245
+ module MyApp::SourceMonitor::SourceExtensions
246
+ extend ActiveSupport::Concern
247
+
248
+ included do
249
+ has_many :source_tags,
250
+ class_name: "MyApp::SourceTag",
251
+ foreign_key: :sourcemon_source_id,
252
+ dependent: :destroy
253
+
254
+ has_many :tags, through: :source_tags, class_name: "MyApp::Tag"
255
+ end
256
+ end
257
+ ```
258
+
259
+ ### Adding Scopes
260
+
261
+ ```ruby
262
+ config.models.source.include_concern do
263
+ scope :active_in_team, ->(team_id) {
264
+ where(team_id: team_id).where.not(paused_at: nil)
265
+ }
266
+
267
+ scope :with_recent_items, -> {
268
+ where(id: SourceMonitor::Item.where("created_at > ?", 24.hours.ago).select(:source_id))
269
+ }
270
+ end
271
+ ```
272
+
273
+ ### Adding Callbacks
274
+
275
+ ```ruby
276
+ config.models.item.include_concern do
277
+ after_create :update_source_item_count
278
+
279
+ private
280
+
281
+ def update_source_item_count
282
+ source.update_column(:items_count, source.items.count)
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### Multi-Model Extensions
288
+
289
+ ```ruby
290
+ SourceMonitor.configure do |config|
291
+ # Extend sources
292
+ config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
293
+ config.models.source.validate :validate_team_assignment
294
+
295
+ # Extend items
296
+ config.models.item.include_concern "MyApp::SourceMonitor::ItemExtensions"
297
+ config.models.item.validate ->(record) {
298
+ record.errors.add(:title, "too short") if record.title&.length.to_i < 5
299
+ }
300
+
301
+ # Extend fetch logs
302
+ config.models.fetch_log.include_concern do
303
+ scope :for_team, ->(team_id) {
304
+ joins(:source).where(sourcemon_sources: { team_id: team_id })
305
+ }
306
+ end
307
+ end
308
+ ```
309
+
310
+ ### Custom Table Prefix
311
+
312
+ ```ruby
313
+ config.models.table_name_prefix = "feeds_"
314
+ # Tables: feeds_sources, feeds_items, feeds_fetch_logs, etc.
315
+ ```
316
+
317
+ Requires matching migration table names. If changing prefix on an existing install, you must rename tables.