source_monitor 0.3.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/.claude/skills/sm-architecture/SKILL.md +233 -0
- data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
- data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
- data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
- data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
- data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
- data/.claude/skills/sm-configure/SKILL.md +153 -0
- data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
- data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
- data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
- data/.claude/skills/sm-domain-model/SKILL.md +188 -0
- data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
- data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
- data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
- data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
- data/.claude/skills/sm-engine-test/SKILL.md +302 -0
- data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
- data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
- data/.claude/skills/sm-event-handler/SKILL.md +265 -0
- data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
- data/.claude/skills/sm-health-rule/SKILL.md +327 -0
- data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
- data/.claude/skills/sm-host-setup/SKILL.md +223 -0
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
- data/.claude/skills/sm-job/SKILL.md +263 -0
- data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
- data/.claude/skills/sm-model-extension/SKILL.md +287 -0
- data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
- data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
- data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
- data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
- data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
- data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
- data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
- data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
- data/.vbw-planning/.notification-log.jsonl +54 -0
- data/.vbw-planning/.session-log.jsonl +121 -0
- data/CHANGELOG.md +9 -0
- data/CLAUDE.md +43 -0
- data/Gemfile.lock +20 -21
- data/lib/source_monitor/setup/workflow.rb +17 -2
- data/lib/source_monitor/version.rb +1 -1
- data/lib/tasks/source_monitor_setup.rake +58 -0
- data/source_monitor.gemspec +1 -0
- metadata +37 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Example Scraper Adapter
|
|
2
|
+
|
|
3
|
+
A complete working example of a custom scraper adapter.
|
|
4
|
+
|
|
5
|
+
## Use Case
|
|
6
|
+
|
|
7
|
+
This adapter extracts content from pages that require API-based rendering (e.g., JavaScript-heavy sites that need a headless browser service).
|
|
8
|
+
|
|
9
|
+
## Implementation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# app/scrapers/my_app/scrapers/headless.rb
|
|
13
|
+
module MyApp
|
|
14
|
+
module Scrapers
|
|
15
|
+
class Headless < SourceMonitor::Scrapers::Base
|
|
16
|
+
# Default settings for this adapter.
|
|
17
|
+
# Overridable per-source via source.scrape_settings JSON column,
|
|
18
|
+
# or per-invocation via the settings parameter.
|
|
19
|
+
def self.default_settings
|
|
20
|
+
{
|
|
21
|
+
render_service_url: ENV.fetch("RENDER_SERVICE_URL", "http://localhost:3001/render"),
|
|
22
|
+
wait_for_selector: "body",
|
|
23
|
+
timeout: 30,
|
|
24
|
+
selectors: {
|
|
25
|
+
content: "article, main, .content",
|
|
26
|
+
title: "h1, title"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
url = preferred_url
|
|
33
|
+
return missing_url_result unless url.present?
|
|
34
|
+
|
|
35
|
+
# Step 1: Render the page via headless service
|
|
36
|
+
render_result = render_page(url)
|
|
37
|
+
return fetch_failure(render_result) unless render_result[:success]
|
|
38
|
+
|
|
39
|
+
html = render_result[:body]
|
|
40
|
+
|
|
41
|
+
# Step 2: Extract content using CSS selectors
|
|
42
|
+
content = extract_content(html)
|
|
43
|
+
title = extract_title(html)
|
|
44
|
+
|
|
45
|
+
if content.blank?
|
|
46
|
+
return Result.new(
|
|
47
|
+
status: :partial,
|
|
48
|
+
html: html,
|
|
49
|
+
content: nil,
|
|
50
|
+
metadata: build_metadata(url: url, title: title, note: "No content extracted")
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Result.new(
|
|
55
|
+
status: :success,
|
|
56
|
+
html: html,
|
|
57
|
+
content: content,
|
|
58
|
+
metadata: build_metadata(url: url, title: title)
|
|
59
|
+
)
|
|
60
|
+
rescue Faraday::TimeoutError => error
|
|
61
|
+
timeout_result(url, error)
|
|
62
|
+
rescue StandardError => error
|
|
63
|
+
error_result(url, error)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def preferred_url
|
|
69
|
+
item.canonical_url.presence || item.url
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_page(url)
|
|
73
|
+
conn = http.client(timeout: settings[:timeout])
|
|
74
|
+
response = conn.post(settings[:render_service_url]) do |req|
|
|
75
|
+
req.headers["Content-Type"] = "application/json"
|
|
76
|
+
req.body = {
|
|
77
|
+
url: url,
|
|
78
|
+
wait_for: settings[:wait_for_selector],
|
|
79
|
+
timeout: (settings[:timeout].to_i * 1000)
|
|
80
|
+
}.to_json
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if response.status >= 200 && response.status < 300
|
|
84
|
+
{ success: true, body: response.body, status: response.status }
|
|
85
|
+
else
|
|
86
|
+
{ success: false, status: response.status, error: "HTTP #{response.status}" }
|
|
87
|
+
end
|
|
88
|
+
rescue Faraday::Error => error
|
|
89
|
+
{ success: false, error: error.message }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_content(html)
|
|
93
|
+
return nil if html.blank?
|
|
94
|
+
|
|
95
|
+
doc = Nokogiri::HTML(html)
|
|
96
|
+
selector = settings.dig(:selectors, :content) || "body"
|
|
97
|
+
|
|
98
|
+
element = doc.at_css(selector)
|
|
99
|
+
return nil unless element
|
|
100
|
+
|
|
101
|
+
# Remove script and style tags
|
|
102
|
+
element.css("script, style, nav, footer, header").each(&:remove)
|
|
103
|
+
element.text.squeeze(" \n").strip
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_title(html)
|
|
107
|
+
return nil if html.blank?
|
|
108
|
+
|
|
109
|
+
doc = Nokogiri::HTML(html)
|
|
110
|
+
selector = settings.dig(:selectors, :title) || "title"
|
|
111
|
+
doc.at_css(selector)&.text&.strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_metadata(url:, title: nil, note: nil)
|
|
115
|
+
meta = {
|
|
116
|
+
url: url,
|
|
117
|
+
extraction_method: "headless",
|
|
118
|
+
title: title
|
|
119
|
+
}
|
|
120
|
+
meta[:note] = note if note
|
|
121
|
+
meta.compact
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def missing_url_result
|
|
125
|
+
Result.new(
|
|
126
|
+
status: :failed,
|
|
127
|
+
metadata: { error: "missing_url", message: "No URL available for scraping" }
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def fetch_failure(render_result)
|
|
132
|
+
Result.new(
|
|
133
|
+
status: :failed,
|
|
134
|
+
metadata: {
|
|
135
|
+
error: "render_failed",
|
|
136
|
+
message: render_result[:error] || "Render service returned error",
|
|
137
|
+
http_status: render_result[:status]
|
|
138
|
+
}.compact
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def timeout_result(url, error)
|
|
143
|
+
Result.new(
|
|
144
|
+
status: :failed,
|
|
145
|
+
metadata: {
|
|
146
|
+
error: "timeout",
|
|
147
|
+
message: error.message,
|
|
148
|
+
url: url
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def error_result(url, error)
|
|
154
|
+
Result.new(
|
|
155
|
+
status: :failed,
|
|
156
|
+
metadata: {
|
|
157
|
+
error: error.class.name,
|
|
158
|
+
message: error.message,
|
|
159
|
+
url: url
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Registration
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# config/initializers/source_monitor.rb
|
|
172
|
+
SourceMonitor.configure do |config|
|
|
173
|
+
config.scrapers.register(:headless, "MyApp::Scrapers::Headless")
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Per-Source Settings
|
|
178
|
+
|
|
179
|
+
Override adapter defaults via the source's `scrape_settings` JSON column:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
source = SourceMonitor::Source.find(1)
|
|
183
|
+
source.update!(scrape_settings: {
|
|
184
|
+
render_service_url: "https://render.example.com/api/render",
|
|
185
|
+
wait_for_selector: ".article-content",
|
|
186
|
+
timeout: 60,
|
|
187
|
+
selectors: {
|
|
188
|
+
content: ".article-body",
|
|
189
|
+
title: ".article-title h1"
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Tests
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
require "test_helper"
|
|
198
|
+
require "webmock/minitest"
|
|
199
|
+
|
|
200
|
+
class HeadlessScraperTest < ActiveSupport::TestCase
|
|
201
|
+
setup do
|
|
202
|
+
@source = create_source!
|
|
203
|
+
@item = @source.items.create!(
|
|
204
|
+
title: "Test Article",
|
|
205
|
+
url: "https://example.com/spa-article",
|
|
206
|
+
external_id: "headless-test-1"
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
test "successfully renders and extracts content" do
|
|
211
|
+
stub_request(:post, "http://localhost:3001/render")
|
|
212
|
+
.to_return(
|
|
213
|
+
status: 200,
|
|
214
|
+
body: <<~HTML
|
|
215
|
+
<html>
|
|
216
|
+
<head><title>Test Page</title></head>
|
|
217
|
+
<body>
|
|
218
|
+
<article>
|
|
219
|
+
<h1>Article Title</h1>
|
|
220
|
+
<p>This is the article content.</p>
|
|
221
|
+
</article>
|
|
222
|
+
</body>
|
|
223
|
+
</html>
|
|
224
|
+
HTML
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
|
|
228
|
+
|
|
229
|
+
assert_equal :success, result.status
|
|
230
|
+
assert_includes result.content, "article content"
|
|
231
|
+
assert_equal "headless", result.metadata[:extraction_method]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
test "returns failed when render service is down" do
|
|
235
|
+
stub_request(:post, "http://localhost:3001/render")
|
|
236
|
+
.to_return(status: 500, body: "Internal Server Error")
|
|
237
|
+
|
|
238
|
+
result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
|
|
239
|
+
|
|
240
|
+
assert_equal :failed, result.status
|
|
241
|
+
assert_equal "render_failed", result.metadata[:error]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
test "returns partial when no content found" do
|
|
245
|
+
stub_request(:post, "http://localhost:3001/render")
|
|
246
|
+
.to_return(status: 200, body: "<html><body><nav>Nav only</nav></body></html>")
|
|
247
|
+
|
|
248
|
+
result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
|
|
249
|
+
|
|
250
|
+
assert_equal :partial, result.status
|
|
251
|
+
assert_nil result.content
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
test "handles missing URL" do
|
|
255
|
+
@item.update!(url: nil)
|
|
256
|
+
|
|
257
|
+
result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
|
|
258
|
+
|
|
259
|
+
assert_equal :failed, result.status
|
|
260
|
+
assert_equal "missing_url", result.metadata[:error]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
test "merges source-level settings" do
|
|
264
|
+
@source.update!(scrape_settings: { timeout: 60 })
|
|
265
|
+
|
|
266
|
+
stub_request(:post, "http://localhost:3001/render")
|
|
267
|
+
.to_return(status: 200, body: "<html><body><article>Content</article></body></html>")
|
|
268
|
+
|
|
269
|
+
result = MyApp::Scrapers::Headless.call(item: @item, source: @source)
|
|
270
|
+
|
|
271
|
+
assert_equal :success, result.status
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
@@ -190,3 +190,57 @@
|
|
|
190
190
|
"title": "",
|
|
191
191
|
"message": "Claude Code needs your attention"
|
|
192
192
|
}
|
|
193
|
+
{
|
|
194
|
+
"timestamp": "2026-02-11T00:46:04Z",
|
|
195
|
+
"type": "idle_prompt",
|
|
196
|
+
"title": "",
|
|
197
|
+
"message": "Claude is waiting for your input"
|
|
198
|
+
}
|
|
199
|
+
{
|
|
200
|
+
"timestamp": "2026-02-11T00:59:14Z",
|
|
201
|
+
"type": "idle_prompt",
|
|
202
|
+
"title": "",
|
|
203
|
+
"message": "Claude is waiting for your input"
|
|
204
|
+
}
|
|
205
|
+
{
|
|
206
|
+
"timestamp": "2026-02-11T03:26:32Z",
|
|
207
|
+
"type": "idle_prompt",
|
|
208
|
+
"title": "",
|
|
209
|
+
"message": "Claude is waiting for your input"
|
|
210
|
+
}
|
|
211
|
+
{
|
|
212
|
+
"timestamp": "2026-02-11T03:46:35Z",
|
|
213
|
+
"type": "idle_prompt",
|
|
214
|
+
"title": "",
|
|
215
|
+
"message": "Claude is waiting for your input"
|
|
216
|
+
}
|
|
217
|
+
{
|
|
218
|
+
"timestamp": "2026-02-11T04:21:35Z",
|
|
219
|
+
"type": "permission_prompt",
|
|
220
|
+
"title": "",
|
|
221
|
+
"message": "Claude Code needs your attention"
|
|
222
|
+
}
|
|
223
|
+
{
|
|
224
|
+
"timestamp": "2026-02-11T04:28:53Z",
|
|
225
|
+
"type": "permission_prompt",
|
|
226
|
+
"title": "",
|
|
227
|
+
"message": "Claude Code needs your approval for the plan"
|
|
228
|
+
}
|
|
229
|
+
{
|
|
230
|
+
"timestamp": "2026-02-11T04:33:09Z",
|
|
231
|
+
"type": "permission_prompt",
|
|
232
|
+
"title": "",
|
|
233
|
+
"message": "Claude Code needs your approval for the plan"
|
|
234
|
+
}
|
|
235
|
+
{
|
|
236
|
+
"timestamp": "2026-02-11T04:46:56Z",
|
|
237
|
+
"type": "idle_prompt",
|
|
238
|
+
"title": "",
|
|
239
|
+
"message": "Claude is waiting for your input"
|
|
240
|
+
}
|
|
241
|
+
{
|
|
242
|
+
"timestamp": "2026-02-11T04:55:02Z",
|
|
243
|
+
"type": "permission_prompt",
|
|
244
|
+
"title": "",
|
|
245
|
+
"message": "Claude Code needs your approval for the plan"
|
|
246
|
+
}
|
|
@@ -869,3 +869,124 @@
|
|
|
869
869
|
"other": 439
|
|
870
870
|
}
|
|
871
871
|
}
|
|
872
|
+
{
|
|
873
|
+
"timestamp": "2026-02-11T00:45:04Z",
|
|
874
|
+
"duration_ms": 0,
|
|
875
|
+
"cost_usd": 0,
|
|
876
|
+
"tokens_in": 0,
|
|
877
|
+
"tokens_out": 0,
|
|
878
|
+
"model": "unknown",
|
|
879
|
+
"branch": "main"
|
|
880
|
+
}
|
|
881
|
+
{
|
|
882
|
+
"timestamp": "2026-02-11T00:45:04Z",
|
|
883
|
+
"type": "cost_summary",
|
|
884
|
+
"costs": {
|
|
885
|
+
"other": 69
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
{
|
|
889
|
+
"timestamp": "2026-02-11T03:41:32Z",
|
|
890
|
+
"duration_ms": 0,
|
|
891
|
+
"cost_usd": 0,
|
|
892
|
+
"tokens_in": 0,
|
|
893
|
+
"tokens_out": 0,
|
|
894
|
+
"model": "unknown",
|
|
895
|
+
"branch": "chore/dependency-updates"
|
|
896
|
+
}
|
|
897
|
+
{
|
|
898
|
+
"timestamp": "2026-02-11T03:41:32Z",
|
|
899
|
+
"type": "cost_summary",
|
|
900
|
+
"costs": {
|
|
901
|
+
"other": 191
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
{
|
|
905
|
+
"timestamp": "2026-02-11T03:41:59Z",
|
|
906
|
+
"duration_ms": 0,
|
|
907
|
+
"cost_usd": 0,
|
|
908
|
+
"tokens_in": 0,
|
|
909
|
+
"tokens_out": 0,
|
|
910
|
+
"model": "unknown",
|
|
911
|
+
"branch": "chore/dependency-updates"
|
|
912
|
+
}
|
|
913
|
+
{
|
|
914
|
+
"timestamp": "2026-02-11T03:41:59Z",
|
|
915
|
+
"type": "cost_summary",
|
|
916
|
+
"costs": {
|
|
917
|
+
"other": 47
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
{
|
|
921
|
+
"timestamp": "2026-02-11T03:42:02Z",
|
|
922
|
+
"duration_ms": 0,
|
|
923
|
+
"cost_usd": 0,
|
|
924
|
+
"tokens_in": 0,
|
|
925
|
+
"tokens_out": 0,
|
|
926
|
+
"model": "unknown",
|
|
927
|
+
"branch": "chore/dependency-updates"
|
|
928
|
+
}
|
|
929
|
+
{
|
|
930
|
+
"timestamp": "2026-02-11T03:42:02Z",
|
|
931
|
+
"type": "cost_summary",
|
|
932
|
+
"costs": {
|
|
933
|
+
"other": 27
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
{
|
|
937
|
+
"timestamp": "2026-02-11T03:45:34Z",
|
|
938
|
+
"duration_ms": 0,
|
|
939
|
+
"cost_usd": 0,
|
|
940
|
+
"tokens_in": 0,
|
|
941
|
+
"tokens_out": 0,
|
|
942
|
+
"model": "unknown",
|
|
943
|
+
"branch": "chore/dependency-updates"
|
|
944
|
+
}
|
|
945
|
+
{
|
|
946
|
+
"timestamp": "2026-02-11T03:45:34Z",
|
|
947
|
+
"type": "cost_summary",
|
|
948
|
+
"costs": {
|
|
949
|
+
"other": 83
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
{
|
|
953
|
+
"timestamp": "2026-02-11T04:45:55Z",
|
|
954
|
+
"duration_ms": 0,
|
|
955
|
+
"cost_usd": 0,
|
|
956
|
+
"tokens_in": 0,
|
|
957
|
+
"tokens_out": 0,
|
|
958
|
+
"model": "unknown",
|
|
959
|
+
"branch": "chore/dependency-updates"
|
|
960
|
+
}
|
|
961
|
+
{
|
|
962
|
+
"timestamp": "2026-02-11T04:45:55Z",
|
|
963
|
+
"type": "cost_summary",
|
|
964
|
+
"costs": {
|
|
965
|
+
"other": 46018
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
{
|
|
969
|
+
"timestamp": "2026-02-11T04:47:49Z",
|
|
970
|
+
"duration_ms": 0,
|
|
971
|
+
"cost_usd": 0,
|
|
972
|
+
"tokens_in": 0,
|
|
973
|
+
"tokens_out": 0,
|
|
974
|
+
"model": "unknown",
|
|
975
|
+
"branch": "chore/dependency-updates"
|
|
976
|
+
}
|
|
977
|
+
{
|
|
978
|
+
"timestamp": "2026-02-11T04:50:36Z",
|
|
979
|
+
"duration_ms": 0,
|
|
980
|
+
"cost_usd": 0,
|
|
981
|
+
"tokens_in": 0,
|
|
982
|
+
"tokens_out": 0,
|
|
983
|
+
"model": "unknown",
|
|
984
|
+
"branch": "chore/dependency-updates"
|
|
985
|
+
}
|
|
986
|
+
{
|
|
987
|
+
"timestamp": "2026-02-11T04:50:36Z",
|
|
988
|
+
"type": "cost_summary",
|
|
989
|
+
"costs": {
|
|
990
|
+
"other": 89
|
|
991
|
+
}
|
|
992
|
+
}
|
data/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,15 @@ All notable changes to this project are documented below. The format follows [Ke
|
|
|
15
15
|
|
|
16
16
|
- No unreleased changes yet.
|
|
17
17
|
|
|
18
|
+
## [0.3.1] - 2026-02-10
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- 14 engine-specific Claude Code skills (`sm-*` prefix) for contributors and consumers.
|
|
23
|
+
- Skills installer with consumer/contributor groups via rake tasks.
|
|
24
|
+
- Skills installation integrated into guided `bin/source_monitor install` workflow.
|
|
25
|
+
- Skills packaged in gem for distribution to host apps.
|
|
26
|
+
|
|
18
27
|
## [0.3.0] - 2026-02-10
|
|
19
28
|
|
|
20
29
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -177,3 +177,46 @@ These skills are available in `.claude/skills/`:
|
|
|
177
177
|
| `performance-optimization` | Performance tuning patterns |
|
|
178
178
|
| `i18n-patterns` | Internationalization patterns |
|
|
179
179
|
| `active-storage-setup` | Active Storage configuration |
|
|
180
|
+
|
|
181
|
+
## Source Monitor Skills
|
|
182
|
+
|
|
183
|
+
Engine-specific skills (`sm-*` prefix). Consumer skills install by default; contributor skills are opt-in.
|
|
184
|
+
|
|
185
|
+
### Consumer Skills (default install)
|
|
186
|
+
|
|
187
|
+
| Skill | Purpose |
|
|
188
|
+
|-------|---------|
|
|
189
|
+
| `sm-host-setup` | Full host app setup walkthrough |
|
|
190
|
+
| `sm-configure` | DSL configuration across all sub-sections |
|
|
191
|
+
| `sm-scraper-adapter` | Custom scraper inheriting `Scrapers::Base` |
|
|
192
|
+
| `sm-event-handler` | Lifecycle callbacks (after_item_created, etc.) |
|
|
193
|
+
| `sm-model-extension` | Extend engine models from host app |
|
|
194
|
+
| `sm-dashboard-widget` | Dashboard queries, presenters, Turbo broadcasts |
|
|
195
|
+
|
|
196
|
+
### Contributor Skills (opt-in)
|
|
197
|
+
|
|
198
|
+
| Skill | Purpose |
|
|
199
|
+
|-------|---------|
|
|
200
|
+
| `sm-domain-model` | Model graph, relationships, state values, scopes |
|
|
201
|
+
| `sm-architecture` | Module map, autoload tree, extraction patterns |
|
|
202
|
+
| `sm-engine-test` | Engine test helpers, VCR/WebMock, parallel caveats |
|
|
203
|
+
| `sm-configuration-setting` | Add settings to config sub-sections |
|
|
204
|
+
| `sm-pipeline-stage` | Add/modify fetch or scrape pipeline stages |
|
|
205
|
+
| `sm-engine-migration` | Migrations with `sourcemon_` prefix conventions |
|
|
206
|
+
| `sm-job` | Solid Queue jobs with shallow delegation |
|
|
207
|
+
| `sm-health-rule` | Health status rules, circuit breaker, auto-pause |
|
|
208
|
+
|
|
209
|
+
### Skills Distribution
|
|
210
|
+
|
|
211
|
+
Host apps can install `sm-*` skills via rake:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
bin/rails source_monitor:skills:install # Consumer skills (default)
|
|
215
|
+
bin/rails source_monitor:skills:contributor # Contributor skills
|
|
216
|
+
bin/rails source_monitor:skills:all # All skills
|
|
217
|
+
bin/rails source_monitor:skills:remove # Remove all sm-* skills
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Maintenance Rules
|
|
221
|
+
|
|
222
|
+
- **Skills & docs alignment**: Whenever engine code changes (models, configuration, pipeline, jobs, migrations, scrapers, events, health rules, or dashboard), the corresponding `sm-*` skill and its `reference/` files MUST be updated in the same PR to ensure skills always reflect current engine behavior.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
source_monitor (0.3.
|
|
4
|
+
source_monitor (0.3.1)
|
|
5
5
|
cssbundling-rails (~> 1.4)
|
|
6
6
|
faraday (~> 2.9)
|
|
7
7
|
faraday-follow_redirects (~> 0.4)
|
|
@@ -99,7 +99,7 @@ GEM
|
|
|
99
99
|
ast (2.4.3)
|
|
100
100
|
base64 (0.3.0)
|
|
101
101
|
bigdecimal (4.0.1)
|
|
102
|
-
brakeman (
|
|
102
|
+
brakeman (8.0.2)
|
|
103
103
|
racc
|
|
104
104
|
builder (3.3.0)
|
|
105
105
|
capybara (3.40.0)
|
|
@@ -128,17 +128,17 @@ GEM
|
|
|
128
128
|
erubi (1.13.1)
|
|
129
129
|
et-orbi (1.4.0)
|
|
130
130
|
tzinfo
|
|
131
|
-
faraday (2.14.
|
|
131
|
+
faraday (2.14.1)
|
|
132
132
|
faraday-net_http (>= 2.0, < 3.5)
|
|
133
133
|
json
|
|
134
134
|
logger
|
|
135
135
|
faraday-follow_redirects (0.5.0)
|
|
136
136
|
faraday (>= 1, < 3)
|
|
137
|
-
faraday-gzip (3.0
|
|
137
|
+
faraday-gzip (3.1.0)
|
|
138
138
|
faraday (>= 2.0, < 3)
|
|
139
139
|
zlib (~> 3.0)
|
|
140
|
-
faraday-net_http (3.4.
|
|
141
|
-
net-http (
|
|
140
|
+
faraday-net_http (3.4.2)
|
|
141
|
+
net-http (~> 0.5)
|
|
142
142
|
faraday-retry (2.4.0)
|
|
143
143
|
faraday (~> 2.0)
|
|
144
144
|
feedjira (4.0.1)
|
|
@@ -182,8 +182,8 @@ GEM
|
|
|
182
182
|
minitest (6.0.1)
|
|
183
183
|
prism (~> 1.5)
|
|
184
184
|
minitest-mock (5.27.0)
|
|
185
|
-
net-http (0.
|
|
186
|
-
uri
|
|
185
|
+
net-http (0.9.1)
|
|
186
|
+
uri (>= 0.11.1)
|
|
187
187
|
net-imap (0.6.2)
|
|
188
188
|
date
|
|
189
189
|
net-protocol
|
|
@@ -222,13 +222,13 @@ GEM
|
|
|
222
222
|
parser (3.3.10.1)
|
|
223
223
|
ast (~> 2.4.1)
|
|
224
224
|
racc
|
|
225
|
-
pg (1.6.
|
|
226
|
-
pg (1.6.
|
|
227
|
-
pg (1.6.
|
|
228
|
-
pg (1.6.
|
|
229
|
-
pg (1.6.
|
|
230
|
-
pg (1.6.
|
|
231
|
-
pg (1.6.
|
|
225
|
+
pg (1.6.3)
|
|
226
|
+
pg (1.6.3-aarch64-linux)
|
|
227
|
+
pg (1.6.3-aarch64-linux-musl)
|
|
228
|
+
pg (1.6.3-arm64-darwin)
|
|
229
|
+
pg (1.6.3-x86_64-darwin)
|
|
230
|
+
pg (1.6.3-x86_64-linux)
|
|
231
|
+
pg (1.6.3-x86_64-linux-musl)
|
|
232
232
|
pp (0.6.3)
|
|
233
233
|
prettyprint
|
|
234
234
|
prettyprint (0.2.0)
|
|
@@ -326,13 +326,13 @@ GEM
|
|
|
326
326
|
rubocop-performance (>= 1.24)
|
|
327
327
|
rubocop-rails (>= 2.30)
|
|
328
328
|
ruby-progressbar (1.13.0)
|
|
329
|
-
ruby-readability (0.7.
|
|
329
|
+
ruby-readability (0.7.3)
|
|
330
330
|
guess_html_encoding (>= 0.0.4)
|
|
331
331
|
nokogiri (>= 1.6.0)
|
|
332
332
|
rubyzip (3.2.2)
|
|
333
333
|
sax-machine (1.3.2)
|
|
334
334
|
securerandom (0.4.1)
|
|
335
|
-
selenium-webdriver (4.
|
|
335
|
+
selenium-webdriver (4.40.0)
|
|
336
336
|
base64 (~> 0.2)
|
|
337
337
|
logger (~> 1.4)
|
|
338
338
|
rexml (~> 3.2, >= 3.2.5)
|
|
@@ -358,7 +358,7 @@ GEM
|
|
|
358
358
|
thor (>= 1.3.1)
|
|
359
359
|
stackprof (0.2.27)
|
|
360
360
|
stringio (3.2.0)
|
|
361
|
-
test-prof (1.
|
|
361
|
+
test-prof (1.5.2)
|
|
362
362
|
thor (1.5.0)
|
|
363
363
|
timeout (0.6.0)
|
|
364
364
|
tsort (0.2.0)
|
|
@@ -372,8 +372,7 @@ GEM
|
|
|
372
372
|
unicode-emoji (4.2.0)
|
|
373
373
|
uri (1.1.1)
|
|
374
374
|
useragent (0.16.11)
|
|
375
|
-
vcr (6.
|
|
376
|
-
base64
|
|
375
|
+
vcr (6.4.0)
|
|
377
376
|
webmock (3.26.1)
|
|
378
377
|
addressable (>= 2.8.0)
|
|
379
378
|
crack (>= 0.3.2)
|
|
@@ -386,7 +385,7 @@ GEM
|
|
|
386
385
|
xpath (3.2.0)
|
|
387
386
|
nokogiri (~> 1.8)
|
|
388
387
|
zeitwerk (2.7.4)
|
|
389
|
-
zlib (3.2.
|
|
388
|
+
zlib (3.2.2)
|
|
390
389
|
|
|
391
390
|
PLATFORMS
|
|
392
391
|
aarch64-linux-gnu
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
|
+
require_relative "skills_installer"
|
|
4
5
|
|
|
5
6
|
module SourceMonitor
|
|
6
7
|
module Setup
|
|
@@ -35,7 +36,8 @@ module SourceMonitor
|
|
|
35
36
|
migration_installer: MigrationInstaller.new,
|
|
36
37
|
initializer_patcher: InitializerPatcher.new,
|
|
37
38
|
devise_detector: method(:default_devise_detector),
|
|
38
|
-
verifier: Verification::Runner.new
|
|
39
|
+
verifier: Verification::Runner.new,
|
|
40
|
+
skills_installer: SkillsInstaller.new
|
|
39
41
|
)
|
|
40
42
|
@dependency_checker = dependency_checker
|
|
41
43
|
@prompter = prompter
|
|
@@ -47,6 +49,7 @@ module SourceMonitor
|
|
|
47
49
|
@initializer_patcher = initializer_patcher
|
|
48
50
|
@devise_detector = devise_detector
|
|
49
51
|
@verifier = verifier
|
|
52
|
+
@skills_installer = skills_installer
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
def run
|
|
@@ -67,6 +70,13 @@ module SourceMonitor
|
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
verifier.call
|
|
73
|
+
|
|
74
|
+
if prompter.yes?("Install Claude Code skills for using SourceMonitor?", default: true)
|
|
75
|
+
skills_installer.install(target_dir: skills_target_dir, group: :consumer)
|
|
76
|
+
if prompter.yes?("Also install contributor skills for engine development?", default: false)
|
|
77
|
+
skills_installer.install(target_dir: skills_target_dir, group: :contributor)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
70
80
|
end
|
|
71
81
|
|
|
72
82
|
private
|
|
@@ -80,7 +90,12 @@ module SourceMonitor
|
|
|
80
90
|
:migration_installer,
|
|
81
91
|
:initializer_patcher,
|
|
82
92
|
:devise_detector,
|
|
83
|
-
:verifier
|
|
93
|
+
:verifier,
|
|
94
|
+
:skills_installer
|
|
95
|
+
|
|
96
|
+
def skills_target_dir
|
|
97
|
+
File.join(Dir.pwd, ".claude", "skills")
|
|
98
|
+
end
|
|
84
99
|
|
|
85
100
|
def devise_available?
|
|
86
101
|
!!devise_detector.call
|