automatic 26.08 → 26.09

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -46
  3. data/VERSION +1 -1
  4. data/assets/siteinfo/items_all.json +60300 -52138
  5. data/automatic.gemspec +4 -3
  6. data/bin/automatic +1 -1
  7. data/doc/AI_TUTORIAL.md +64 -40
  8. data/doc/BASIC_DESIGN.md +31 -0
  9. data/doc/DEPLOYMENT.md +53 -47
  10. data/doc/PLUGINS.md +231 -91
  11. data/doc/POLICY.md +149 -44
  12. data/doc/QUICKSTART.md +19 -15
  13. data/doc/RELEASING.md +20 -7
  14. data/doc/REQUIREMENTS.md +40 -10
  15. data/doc/VERSIONS +112 -54
  16. data/lib/automatic/cli.rb +40 -18
  17. data/lib/automatic/environment.rb +1 -1
  18. data/lib/automatic/feed_maker.rb +57 -18
  19. data/lib/automatic/feed_parser.rb +1 -1
  20. data/lib/automatic/http.rb +1 -1
  21. data/lib/automatic/log.rb +1 -1
  22. data/lib/automatic/pipeline.rb +15 -5
  23. data/lib/automatic/recipe.rb +45 -1
  24. data/lib/automatic/version.rb +3 -3
  25. data/lib/automatic.rb +18 -3
  26. data/plugins/custom_feed/web.rb +1 -1
  27. data/plugins/filter/absolute_uri.rb +1 -1
  28. data/plugins/filter/batch.rb +97 -0
  29. data/plugins/filter/claude.rb +1 -1
  30. data/plugins/filter/clear.rb +1 -1
  31. data/plugins/filter/description_link.rb +20 -3
  32. data/plugins/filter/full_feed.rb +28 -16
  33. data/plugins/filter/gemini.rb +1 -1
  34. data/plugins/filter/ignore.rb +1 -1
  35. data/plugins/filter/image.rb +1 -1
  36. data/plugins/filter/image_source.rb +20 -3
  37. data/plugins/filter/join.rb +4 -6
  38. data/plugins/filter/kimi.rb +216 -0
  39. data/plugins/filter/limit.rb +57 -0
  40. data/plugins/filter/open_ai.rb +1 -1
  41. data/plugins/filter/present.rb +75 -0
  42. data/plugins/filter/sakura_ai.rb +1 -1
  43. data/plugins/filter/sanitize.rb +1 -1
  44. data/plugins/filter/sort.rb +1 -1
  45. data/plugins/filter/tumblr_resize.rb +1 -1
  46. data/plugins/notify/ikachan.rb +1 -1
  47. data/plugins/provide/fluentd.rb +1 -1
  48. data/plugins/publish/amazon_s3.rb +9 -3
  49. data/plugins/publish/console.rb +1 -1
  50. data/plugins/publish/fluentd.rb +1 -1
  51. data/plugins/publish/hatena_bookmark.rb +1 -1
  52. data/plugins/publish/markdown.rb +1 -1
  53. data/plugins/publish/memcached.rb +1 -1
  54. data/plugins/store/digest.rb +1 -1
  55. data/plugins/store/file.rb +11 -3
  56. data/plugins/store/full_text.rb +9 -13
  57. data/plugins/store/permalink.rb +1 -1
  58. data/plugins/subscription/feed.rb +1 -1
  59. data/plugins/subscription/link.rb +1 -1
  60. data/plugins/subscription/text.rb +13 -4
  61. data/plugins/subscription/tumblr.rb +1 -1
  62. data/plugins/subscription/xml.rb +1 -1
  63. metadata +6 -2
data/doc/VERSIONS CHANGED
@@ -1,26 +1,64 @@
1
1
  automaticruby Repository Version History
2
2
  ========================================
3
3
 
4
+ v26.09 (2026-09-23)
5
+ -------------------
6
+ - Add FilterKimi for the Kimi API, using its OpenAI-compatible Chat Completions
7
+ interface.
8
+ - Add FilterLimit, FilterBatch, and FilterPresent for pipeline-wide limiting,
9
+ batching, and required-field filtering.
10
+ - Add per-fetch interval handling to FilterFullFeed, FilterImageSource, and
11
+ FilterDescriptionLink.
12
+ - Refresh the bundled LDRFullFeed siteinfo database from a newer upstream
13
+ snapshot.
14
+ - Make doc/PLUGINS.md section 6 the plugin catalogue's source of truth.
15
+ - Harden the framework's execution boundary: fix CLI contract drift, distinguish
16
+ dependency from load failures, and preflight-validate Recipe plugins.
17
+ - Preserve standard item metadata when FeedMaker rebuilds pipelines so filters
18
+ keep source, enclosure, and full content.
19
+ - Preserve nil-link pipeline items and remove SubscriptionText placeholder
20
+ title and link values.
21
+ - Preserve SubscriptionText TSV column positions and ignore empty input rows.
22
+ - Preserve local file paths when StoreFile hands file URIs to PublishAmazonS3.
23
+ - Stop StoreFullText from passing items downstream when database writes fail.
24
+
4
25
  v26.08 (2026-08-22)
5
26
  -------------------
6
- - Support Ruby 3.3 through 4.0 and modernize the codebase for current Ruby and maintained library APIs, validating representative versions in CI.
7
- - Harden Recipe loading with safe YAML parsing, structural validation and framework-specific errors.
8
- - Restructure the CLI with help and version options, predictable error reporting and documented exit statuses.
9
- - Report a clear CLI error when feed inspection discovers no feed instead of raising an internal exception.
27
+ - Support Ruby 3.3 through 4.0 and modernize the codebase for current Ruby and
28
+ maintained library APIs, validating representative versions in CI.
29
+ - Harden Recipe loading with safe YAML parsing, structural validation and
30
+ framework-specific errors.
31
+ - Restructure the CLI with help and version options, predictable error reporting
32
+ and documented exit statuses.
33
+ - Report a clear CLI error when feed inspection discovers no feed instead of
34
+ raising an internal exception.
10
35
  - Reject HTTP and HTTPS URLs without a host before attempting a network request.
11
- - Verify TLS certificates when publishing to Instapaper instead of accepting an unverified connection.
12
- - Modernize gem packaging and dependency policy, separating core requirements from optional plugin dependencies so that an installation can be minimal, complete or extended one plugin at a time, and excluding development and generated files.
13
- - Classify every shipped plugin by its current support status rather than simulating obsolete services in tests.
14
- - Modernize the bundled plugins for current Ruby, libraries and services, and remove the integrations that are no longer viable.
15
- - Remove the plugins for Twitter, Pocket, HipChat, Google Calendar, livedoor Weather, So-net G-Guide and Chan-Toru, and Google News link rewriting; their services or APIs no longer exist, and a Recipe naming one now fails at load.
16
- - Add the plugin CustomFeedWeb, which builds one feed per HTML index page from the article links it lists, selected with CSS selectors and filtered by host, pattern and count.
17
- - Add the plugin StoreDigest, which drops items by content rather than by URL, on a SHA-256 digest of the item fields a Recipe names.
18
- - Add the plugin FilterJoin, which joins the whole pipeline into one item carrying each item's title, link and body, and invents no permalink for it.
19
- - Add the plugins FilterOpenAI, FilterClaude, FilterGemini and FilterSakuraAI, one per AI service rather than one selectable by setting, each replacing an item's description with what that service answers to the Recipe's prompt under a token the Recipe holds.
20
- - Rebuild the test and CI strategy for current RSpec and Ruby, with deterministic isolation from user data and external services.
21
- - Add Markdown as the primary service-independent publication format, with a documented and tested first-run workflow.
22
- - Rebuild the maintained documentation around current usage, architecture, policy, plugins and deployment, and remove superseded historical documents.
23
- - Offer the project under either GPLv3 or LGPLv3, retaining third-party attribution and shipping both licence texts.
36
+ - Verify TLS certificates when publishing to Instapaper instead of accepting an
37
+ unverified connection.
38
+ - Modernize gem packaging: separate core from optional plugin dependencies so an
39
+ install can be minimal or extended one plugin at a time.
40
+ - Classify every shipped plugin by its current support status rather than
41
+ simulating obsolete services in tests.
42
+ - Modernize the bundled plugins for current Ruby, libraries and services, and
43
+ remove the integrations that are no longer viable.
44
+ - Remove the plugins for services that no longer exist: Twitter, Pocket, HipChat,
45
+ Google Calendar, livedoor Weather, So-net G-Guide and Chan-Toru.
46
+ - Add the plugin CustomFeedWeb, building one feed per HTML index page from its
47
+ article links, selected by CSS selector and filtered by host, pattern and count.
48
+ - Add the plugin StoreDigest, which drops items by content rather than by URL, on
49
+ a SHA-256 digest of the item fields a Recipe names.
50
+ - Add the plugin FilterJoin, which joins the whole pipeline into one item carrying
51
+ each item's title, link and body, and invents no permalink for it.
52
+ - Add one AI filter plugin per service (FilterOpenAI, FilterClaude, FilterGemini,
53
+ FilterSakuraAI), replacing an item's description with that service's answer.
54
+ - Rebuild the test and CI strategy for current RSpec and Ruby, with deterministic
55
+ isolation from user data and external services.
56
+ - Add Markdown as the primary service-independent publication format, with a
57
+ documented and tested first-run workflow.
58
+ - Rebuild the maintained documentation around current usage, architecture, policy,
59
+ plugins and deployment, and remove superseded historical documents.
60
+ - Offer the project under either GPLv3 or LGPLv3, retaining third-party
61
+ attribution and shipping both licence texts.
24
62
 
25
63
  v14.12.2 (2015-01-04)
26
64
  ---------------------
@@ -56,9 +94,12 @@ v14.3.0 (2014-03-23)
56
94
  v14.2.0 (2014-02-26)
57
95
  --------------------
58
96
  - Add the plugin Publish::AmazonS3.
59
- - Rename Store::TargetLink to Store::File, and have it rewrite the link to a file URI scheme.
60
- - Enable the file URI scheme in an RSS 2.0 feed, and let Store::File fetch a file from Amazon S3.
61
- - Read text from TSV formatted files in Subscription::Text, and add the description, author and comments fields to the RSS 2.0 feed.
97
+ - Rename Store::TargetLink to Store::File, and have it rewrite the link to a file
98
+ URI scheme.
99
+ - Enable the file URI scheme in an RSS 2.0 feed, and let Store::File fetch a file
100
+ from Amazon S3.
101
+ - Read text from TSV formatted files in Subscription::Text, and add the
102
+ description, author and comments fields to the RSS 2.0 feed.
62
103
  - Give the log messages more detail.
63
104
  - Use 'double' instead of 'mock' and 'stub' for rspec-mocks.
64
105
  - Improve CI with a network connection.
@@ -80,14 +121,17 @@ v13.7.0 (2013-07-28)
80
121
 
81
122
  v13.6.0 (2013-06-28)
82
123
  --------------------
83
- - Add the plugins Subscription::Pocket, Subscription::TwitterSearch, Subscription::GGuide, Subscription::ChanToru, Filter::Accept, Filter::Sanitize, Filter::GithubFeed, Publish::ConsoleLink, Publish::Hipchat, Publish::Memcached and Publish::Fluentd.
84
- - Correspond to activesupport and activerecord 4.0, temporarily removing the mail plugins for actionmailer 4.0.
124
+ - Add eleven plugins spanning Subscription, Filter and Publish, including Pocket,
125
+ GGuide, ChanToru, Accept, Sanitize, ConsoleLink, Hipchat, Memcached and Fluentd.
126
+ - Correspond to activesupport and activerecord 4.0, temporarily removing the mail
127
+ plugins for actionmailer 4.0.
85
128
  - Ruby 1.8 support was outdated.
86
129
  - And other improvements and bug fixes.
87
130
 
88
131
  v13.5.0 (2013-05-18)
89
132
  --------------------
90
- - Add the plugins Subscription::Text, Subscription::Weather, Filter::One, Filter::Rand, Publish::Twitter and Publish::Pocket.
133
+ - Add the plugins Subscription::Text, Subscription::Weather, Filter::One,
134
+ Filter::Rand, Publish::Twitter and Publish::Pocket.
91
135
  - Move the repository from id774/automaticruby to automaticruby/automaticruby.
92
136
 
93
137
  v13.4.1 (2013-04-10)
@@ -101,7 +145,8 @@ v13.2.0 (2013-02-13)
101
145
  - Implement the retry function in all subscription and download plugins.
102
146
  - Plugin Filter::Ignore has become an item-based filter.
103
147
  - Rename the plugin Filter::Reverse to Filter::Sort.
104
- - Add the plugins Subscription::GoogleReaderStar, Filter::FullFeed and Publish::Instapaper.
148
+ - Add the plugins Subscription::GoogleReaderStar, Filter::FullFeed and
149
+ Publish::Instapaper.
105
150
  - Improve the tests.
106
151
  - And other minor bug fixes.
107
152
 
@@ -122,18 +167,25 @@ v12.9.1 (2012-09-18)
122
167
 
123
168
  v12.9.0 (2012-09-18)
124
169
  --------------------
125
- - Unify the format of the pipeline to a feed object, an array of feeds; some plugins lost backward compatibility.
126
- - Add the plugins Subscription::Link, Subscription::Twitter, Filter::Image and Filter::AbsoluteURI.
127
- - Remove the obsolete plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link and Store::Target.
170
+ - Unify the format of the pipeline to a feed object, an array of feeds; some
171
+ plugins lost backward compatibility.
172
+ - Add the plugins Subscription::Link, Subscription::Twitter, Filter::Image and
173
+ Filter::AbsoluteURI.
174
+ - Remove the obsolete plugins Subscription::URI, Extract::Link, Filter::ImageLink,
175
+ Store::Link and Store::Target.
128
176
 
129
177
  v12.6.0 (2012-06-18)
130
178
  --------------------
131
- - Add the plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link and Store::Target.
132
- - Make scaffold create the config and db directories, and add the unscaffold subcommand to automatic-config.
133
- - Search the user directory's config when given -c with a simple filename, and use the user's db directory when it exists.
179
+ - Add the plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link
180
+ and Store::Target.
181
+ - Make scaffold create the config and db directories, and add the unscaffold
182
+ subcommand to automatic-config.
183
+ - Search the user directory's config when given -c with a simple filename, and use
184
+ the user's db directory when it exists.
134
185
  - Show the recipe and db name in the log.
135
186
  - Refactor the plug-in names and the header fields.
136
- - Add tests, taking RSpec code coverage over 99%, and add the all option to script/build.
187
+ - Add tests, taking RSpec code coverage over 99%, and add the all option to
188
+ script/build.
137
189
 
138
190
  v12.4.0 (2012-04-30)
139
191
  --------------------
@@ -144,40 +196,40 @@ v12.4.0 (2012-04-30)
144
196
  v12.3.1 (2012-03-16)
145
197
  --------------------
146
198
  - Write the English documents doc/README and doc/PLUGINS.
147
- - Let a user who installed the gem package create their own plugins, under ~/.automatic/plugins.
148
- - Merge the utilities into the automatic-config command, adding the subcommands scaffold, autodiscovery, opmlparser, feedparser and log.
199
+ - Let a user who installed the gem package create their own plugins, under
200
+ ~/.automatic/plugins.
201
+ - Merge the utilities into the automatic-config command, adding the subcommands
202
+ scaffold, autodiscovery, opmlparser, feedparser and log.
149
203
  - Add the integration test script script/build for CI.
150
204
 
151
205
  v12.03 (2012-03-07)
152
206
  -------------------
153
207
  - Distribute as a gem library.
154
- - Add the plugins CustomFeed::SVNLog, Notify::Ikachan, Filter::TumblrResize, Filter::Image and Store::TargetLink.
208
+ - Add the plugins CustomFeed::SVNLog, Notify::Ikachan, Filter::TumblrResize,
209
+ Filter::Image and Store::TargetLink.
155
210
  - Add the CI and build script script/bootstrap.
156
211
  - Significant refactoring.
157
212
 
158
213
  v12.02.1 (2012-02-25)
159
214
  ---------------------
160
- - Point release: a large refactoring of the core, a reorganization of the namespace, and a few plugins added.
215
+ - Point release: a large refactoring of the core, a reorganization of the
216
+ namespace, and a few plugins added.
161
217
 
162
218
  v12.02 (2012-02-18)
163
219
  -------------------
164
- - First release: the scratch of this script, with a plugin existing only for Hatena Bookmark.
220
+ - Initial release.
165
221
 
166
222
 
167
223
  Notes on this record
168
224
  ====================
169
- - Entries from v12.02 to v14.12.0 were moved here from doc/ChangeLog, which this
170
- file replaces. No entry and no date was changed in the move; the wording was
171
- brought to one bullet per change, and nothing was added or dropped.
172
- - v14.10.1, v14.12.1 and v14.12.2 were released and are recorded in the commit
173
- log, but were never written into doc/ChangeLog. Their entries above were
174
- reconstructed from the commits that bumped the version.
175
- - No Git tag exists for any release before v26.08. The version numbers above are
176
- those recorded in the VERSION file at the time, and the commit log is the only
177
- other record of them.
178
- - Between v14.12.2 and v26.08 the only committed change was the addition of the
179
- integration recipe test/integration/test_pdf2local.yml in April 2015. It is
180
- part of the v26.08 entry's repository state and took no version of its own.
225
+ - Entries from v12.02 to v14.12.0 were moved here from doc/ChangeLog, unchanged in
226
+ date or content, one bullet per change.
227
+ - v14.10.1, v14.12.1 and v14.12.2 were released and recorded in the commit log but
228
+ never in doc/ChangeLog; their entries above are reconstructed from it.
229
+ - No Git tag exists for any release before v26.08; the version numbers above are
230
+ those recorded in the VERSION file at the time.
231
+ - Between v14.12.2 and v26.08 the only committed change was adding
232
+ test/integration/test_pdf2local.yml (April 2015), folded into v26.08.
181
233
 
182
234
 
183
235
  Version History Guidelines
@@ -187,10 +239,11 @@ Version History Guidelines
187
239
  - Omit development-time corrections and intermediate states that were reverted
188
240
  or superseded before release; Git history retains that detail.
189
241
  - Summarize related low-level changes under their substantive outcome.
190
- - Write one coherent change per physical line, as a rule.
191
- - Keep entries near 100 columns where practical. Necessary file names, module
192
- names, plugin names, option names, and configuration names may extend an entry
193
- to approximately 120 columns.
242
+ - Write one coherent change per bullet, at most two physical lines.
243
+ - A single line at or under 80 columns is preferred whenever practical.
244
+ Necessary file names, module names, plugin names, option names, and
245
+ configuration names may pass 80 columns on the bullet's one or two lines
246
+ when they cannot be shortened without losing meaning.
194
247
  - When a version history already exists, balance a new entry against the length
195
248
  of the existing lines, so that the whole file stays consistent.
196
249
  - When an entry becomes substantially longer, omit or abstract implementation
@@ -201,8 +254,13 @@ Version History Guidelines
201
254
  same file or feature.
202
255
  - Place closely related entries near each other.
203
256
  - Append an independent new change to the end of the current version entry.
257
+ - Never create more than one repository version for the same calendar date.
258
+ - Independent changes may use separate bullets, but they stay in that day's
259
+ single version entry.
204
260
  - Past entries and their dates are a record. Do not rewrite, renumber or merge
205
- them, and do not merge two releases that actually existed because they share
206
- a date.
261
+ them. Past releases that share a date stay recorded as they were made; they
262
+ do not conform to the rule above and are not precedent for new releases.
207
263
  - This file records the history of the repository. A change that is not
208
264
  observable from outside does not need an entry here.
265
+ - The first entry, at the lowest version this file reaches, reads only
266
+ "Initial release." and nothing else.
data/lib/automatic/cli.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::CLI
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
7
7
  # Created:: Aug 14, 2026
8
- # Updated:: Aug 19, 2026
8
+ # Updated:: Sep 9, 2026
9
9
  # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
10
  #
11
11
  # Everything that belongs to being a command: option parsing, the subcommands,
@@ -103,11 +103,13 @@ module Automatic
103
103
  EXIT_FAILURE
104
104
  end
105
105
 
106
- # A LoadError is one of the failures reported as a message here: a Recipe
107
- # naming a plugin whose optional gem is not installed is an operator's
108
- # mistake with an answer, and the answer is in the message rather than in a
109
- # backtrace. What the plugin itself raises is left alone; see
110
- # doc/PLUGINS.md section 2.7.
106
+ # Automatic::OptionalDependencyError is one of the failures reported as a
107
+ # message here: a Recipe naming a plugin whose optional gem is not
108
+ # installed is an operator's mistake with an answer, and the answer is in
109
+ # the message rather than in a backtrace. A LoadError raised from inside a
110
+ # plugin's own load -- for a different, unrelated missing file -- is not
111
+ # this, and is left alone along with everything else the plugin itself
112
+ # raises; see doc/PLUGINS.md section 2.7.
111
113
  def run_recipe(path)
112
114
  unless File.exist?(resolve_recipe(path))
113
115
  @stderr.puts "automatic: no such recipe: #{path}"
@@ -116,7 +118,8 @@ module Automatic
116
118
 
117
119
  Automatic.run(recipe: Automatic::Recipe.new(path), root_dir: @root_dir)
118
120
  EXIT_SUCCESS
119
- rescue Automatic::Error, Psych::Exception, SystemCallError, IOError, LoadError => e
121
+ rescue Automatic::Error, Psych::Exception, SystemCallError, IOError,
122
+ Automatic::OptionalDependencyError => e
120
123
  @stderr.puts "automatic: #{e.message}"
121
124
  EXIT_FAILURE
122
125
  end
@@ -139,7 +142,7 @@ module Automatic
139
142
 
140
143
  handler.call(argv)
141
144
  EXIT_SUCCESS
142
- rescue Automatic::Error, SystemCallError, IOError, LoadError => e
145
+ rescue Automatic::Error, SystemCallError, IOError, Automatic::OptionalDependencyError => e
143
146
  @stderr.puts "automatic: #{e.message}"
144
147
  EXIT_FAILURE
145
148
  end
@@ -178,16 +181,12 @@ module Automatic
178
181
  create_dir(File.join(Automatic.user_dir, 'db'))
179
182
 
180
183
  assets = File.join(Automatic.user_dir, 'assets')
181
- if create_dir(assets)
182
- FileUtils.cp_r(File.join(@root_dir, 'assets', 'siteinfo'),
183
- File.join(assets, 'siteinfo'))
184
- end
184
+ create_dir(assets)
185
+ copy_bundled(File.join(@root_dir, 'assets', 'siteinfo'), File.join(assets, 'siteinfo'))
185
186
 
186
187
  config = Automatic.user_config_dir
187
- if create_dir(config)
188
- FileUtils.cp_r(File.join(@root_dir, 'config'),
189
- File.join(config, 'example'))
190
- end
188
+ create_dir(config)
189
+ copy_bundled(File.join(@root_dir, 'config'), File.join(config, 'example'))
191
190
  end
192
191
 
193
192
  def unscaffold(_argv)
@@ -209,9 +208,19 @@ module Automatic
209
208
  require 'automatic/feed_parser'
210
209
  require 'pp'
211
210
  url = argv.shift || missing_argument('feedparser')
211
+ validate_fetch_url(url)
212
212
  @stdout.puts Automatic::FeedParser.get_url(url).pretty_inspect
213
213
  end
214
214
 
215
+ # Converts only the URL validation failure `Automatic::Http.uri` already
216
+ # applies into the CLI's ordinary failure path; an unexpected error from
217
+ # inside FeedParser itself is left to propagate.
218
+ def validate_fetch_url(url)
219
+ Automatic::Http.uri(url)
220
+ rescue ArgumentError, URI::InvalidURIError => e
221
+ raise Automatic::Error, e.message
222
+ end
223
+
215
224
  def inspect_url(argv)
216
225
  require 'automatic/feed_parser'
217
226
  Automatic.require_optional('feedbag', needed_by: 'the inspect subcommand')
@@ -221,7 +230,7 @@ module Automatic
221
230
  raise Automatic::Error, "no feed found at #{url}" if feeds.empty?
222
231
 
223
232
  @stdout.puts feeds.pretty_inspect
224
- @stdout.puts Automatic::FeedParser.get_url(feeds.pop).pretty_inspect
233
+ @stdout.puts Automatic::FeedParser.get_url(feeds.first).pretty_inspect
225
234
  end
226
235
 
227
236
  def opmlparser(argv)
@@ -244,5 +253,18 @@ module Automatic
244
253
  @stdout.puts "Creating #{path}"
245
254
  true
246
255
  end
256
+
257
+ # Bundled initial data (siteinfo, example config) is copied whenever its
258
+ # own destination is missing, independent of whether the parent user
259
+ # directory already existed. That is what lets a re-run of scaffold heal a
260
+ # partially-scaffolded user directory instead of only ever populating a
261
+ # brand new one. See doc/DEPLOYMENT.md and doc/POLICY.md on scaffold only
262
+ # adding what is missing.
263
+ def copy_bundled(src, dest)
264
+ return if File.exist?(dest)
265
+
266
+ FileUtils.cp_r(src, dest)
267
+ @stdout.puts "Creating #{dest}"
268
+ end
247
269
  end
248
270
  end
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::Environment
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
@@ -1,11 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::FeedMaker
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
7
7
  # Created:: Feb 21, 2014
8
- # Updated:: Aug 14, 2026
8
+ # Updated:: Sep 7, 2026
9
9
  # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
10
 
11
11
  module Automatic
@@ -16,8 +16,8 @@ module Automatic
16
16
  class FeedObject
17
17
  attr_accessor :title, :link, :description, :author, :comments
18
18
  def initialize
19
- @link = 'http://dummy'
20
- @title = 'dummy'
19
+ @link = nil
20
+ @title = nil
21
21
  @description = ''
22
22
  @author = ''
23
23
  @comments = ''
@@ -34,6 +34,20 @@ module Automatic
34
34
  feed_object
35
35
  end
36
36
 
37
+ # Plain-value standard fields, copied onto the rebuilt item as-is when the
38
+ # item being rebuilt carries them. See doc/REQUIREMENTS.md and
39
+ # doc/PLUGINS.md for the standard item field contract this preserves.
40
+ REBUILD_SIMPLE_FIELDS = %i[title link description author comments content_encoded].freeze
41
+
42
+ # source and enclosure are RSS child elements. RSS::Maker exposes each of
43
+ # them on a new item as a builder with its own sub-attributes rather than
44
+ # accepting the finished element directly, so each is rebuilt attribute by
45
+ # attribute instead of by one assignment.
46
+ REBUILD_STRUCTURED_FIELDS = {
47
+ source: %i[url content],
48
+ enclosure: %i[url length type]
49
+ }.freeze
50
+
37
51
  def self.create_pipeline(feeds = [])
38
52
  RSS::Maker.make("2.0") {|maker|
39
53
  xss = maker.xml_stylesheets.new_xml_stylesheet
@@ -44,20 +58,12 @@ module Automatic
44
58
 
45
59
  unless feeds.nil?
46
60
  feeds.each {|feed|
47
- unless feed.link.nil?
48
- Automatic::Log.puts("info", "Create Pipeline: #{feed.link}")
49
- item = maker.items.new_item
50
- item.title = feed.title
51
- item.link = feed.link
52
- begin
53
- item.description = feed.description
54
- item.author = feed.author
55
- item.comments = feed.comments
56
- item.date = feed.pubDate || Time.now
57
- rescue NoMethodError
58
- Automatic::Log.puts("warn", "Undefined field detected in feed.")
59
- end
60
- end
61
+ Automatic::Log.puts("info", "Create Pipeline: #{feed.link}")
62
+ item = maker.items.new_item
63
+ item.date = (feed.pubDate if feed.respond_to?(:pubDate)) || Time.now
64
+
65
+ REBUILD_SIMPLE_FIELDS.each { |field| copy_rebuild_field(feed, item, field) }
66
+ REBUILD_STRUCTURED_FIELDS.each_key { |field| copy_rebuild_structured_field(feed, item, field) }
61
67
  }
62
68
  end
63
69
  }
@@ -77,5 +83,38 @@ module Automatic
77
83
  item.date = Time.now
78
84
  }
79
85
  end
86
+
87
+ # A field the item being rebuilt does not carry, or carries as nil, is
88
+ # left off the new item: that is the normal shape of an optional field,
89
+ # not a reason to skip copying anything else. See doc/POLICY.md on
90
+ # distinguishing a missing optional value from a programming error.
91
+ def self.copy_rebuild_field(feed, item, field)
92
+ return unless feed.respond_to?(field)
93
+
94
+ value = feed.public_send(field)
95
+ return if value.nil?
96
+
97
+ item.public_send("#{field}=", value)
98
+ end
99
+ private_class_method :copy_rebuild_field
100
+
101
+ # source and enclosure are rebuilt sub-attribute by sub-attribute, on the
102
+ # same missing-is-normal basis as copy_rebuild_field, so that (for
103
+ # example) an enclosure with no type recorded still keeps its url.
104
+ def self.copy_rebuild_structured_field(feed, item, field)
105
+ return unless feed.respond_to?(field)
106
+
107
+ value = feed.public_send(field)
108
+ return if value.nil?
109
+
110
+ target = item.public_send(field)
111
+ REBUILD_STRUCTURED_FIELDS.fetch(field).each do |sub_field|
112
+ next unless value.respond_to?(sub_field)
113
+
114
+ sub_value = value.public_send(sub_field)
115
+ target.public_send("#{sub_field}=", sub_value) unless sub_value.nil?
116
+ end
117
+ end
118
+ private_class_method :copy_rebuild_structured_field
80
119
  end
81
120
  end
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::FeedParser
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::Http
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
data/lib/automatic/log.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::Log
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
@@ -1,16 +1,23 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Name:: Automatic::Pipeline
3
- # Author: id774 (More info: http://id774.net)
3
+ # Author: id774 (More info: https://id774.net)
4
4
  # Source Code:: https://github.com/id774/automaticruby
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
7
7
  # Created:: Feb 22, 2012
8
- # Updated:: Aug 14, 2026
8
+ # Updated:: Sep 6, 2026
9
9
  # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
10
  #
11
11
  # The core: resolve a plugin's class name to a file, then run the Recipe's
12
12
  # plugins in order, each receiving the previous one's output. See
13
13
  # doc/BASIC_DESIGN.md section 4.6 and doc/PLUGINS.md section 3.
14
+ #
15
+ # Every module the Recipe names is discovered before any plugin runs, so that
16
+ # an unknown module later in the Recipe is refused before an earlier plugin's
17
+ # side effect, not after it. Discovery only registers an autoload; it does not
18
+ # read a plugin's own source, which stays lazy until that plugin's turn to
19
+ # run. See doc/PLUGINS.md on unknown plugin being refused before any plugin
20
+ # runs.
14
21
 
15
22
  require 'active_support/core_ext/string/inflections'
16
23
 
@@ -51,10 +58,13 @@ module Automatic
51
58
  def run(recipe)
52
59
  raise NoRecipeError, 'no recipe given' if recipe.nil?
53
60
 
61
+ entries = []
62
+ recipe.each_plugin { |plugin| entries << [plugin, plugin.module] }
63
+
64
+ entries.each { |_plugin, mod| load_plugin(mod) }
65
+
54
66
  pipeline = []
55
- recipe.each_plugin do |plugin|
56
- mod = plugin.module
57
- load_plugin(mod)
67
+ entries.each do |plugin, mod|
58
68
  klass = Automatic::Plugin.const_get(mod)
59
69
  pipeline = klass.new(plugin.config, pipeline).run
60
70
  end
@@ -5,11 +5,16 @@
5
5
  # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
6
  # Contact:: idnanashi@gmail.com
7
7
  # Created:: Feb 18, 2012
8
- # Updated:: Aug 14, 2026
8
+ # Updated:: Sep 6, 2026
9
9
  # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
10
  #
11
11
  # Turns a Recipe file into something the pipeline can iterate. The format is
12
12
  # specified in doc/PLUGINS.md section 2.
13
+ #
14
+ # Each plugin entry's documented shape -- a mapping naming a module, with an
15
+ # optional config mapping -- is validated here, at load time, rather than
16
+ # left to surface as whatever internal exception a malformed entry happens to
17
+ # raise once the pipeline reaches it.
13
18
 
14
19
  require 'date'
15
20
  require 'hashie'
@@ -45,6 +50,8 @@ module Automatic
45
50
  "recipe #{resolved} has no plugins sequence"
46
51
  end
47
52
 
53
+ validate_plugins(resolved)
54
+
48
55
  Automatic::Log.level(@procedure.global&.log&.level)
49
56
  Automatic::Log.puts('info', "Loading Recipe: #{resolved}")
50
57
  @procedure
@@ -72,5 +79,42 @@ module Automatic
72
79
  aliases: true
73
80
  )
74
81
  end
82
+
83
+ # Only the shape doc/PLUGINS.md documents is checked here: a mapping
84
+ # naming a module, with an optional config mapping. What a plugin does
85
+ # with its own config is that plugin's concern, not this one's.
86
+ def validate_plugins(resolved)
87
+ @procedure.plugins.each_with_index do |plugin, index|
88
+ validate_plugin_entry(resolved, plugin, index)
89
+ end
90
+ end
91
+
92
+ def validate_plugin_entry(resolved, plugin, index)
93
+ unless plugin.is_a?(Hash)
94
+ raise InvalidRecipeError,
95
+ "recipe #{resolved} plugins[#{index}] is not a mapping"
96
+ end
97
+
98
+ validate_module_name(resolved, plugin, index)
99
+ validate_plugin_config(resolved, plugin, index)
100
+ end
101
+
102
+ def validate_module_name(resolved, plugin, index)
103
+ name = plugin['module']
104
+
105
+ return if name.is_a?(String) && !name.strip.empty?
106
+
107
+ raise InvalidRecipeError,
108
+ "recipe #{resolved} plugins[#{index}] has no module name"
109
+ end
110
+
111
+ def validate_plugin_config(resolved, plugin, index)
112
+ config = plugin['config']
113
+
114
+ return if config.nil? || config.is_a?(Hash)
115
+
116
+ raise InvalidRecipeError,
117
+ "recipe #{resolved} plugins[#{index}] has a config that is not a mapping"
118
+ end
75
119
  end
76
120
  end